public_invite.gno
4.85 Kb · 195 lines
1package boards2
2
3import (
4 "chain"
5 "chain/runtime"
6 "strings"
7 "time"
8
9 "gno.land/p/gnoland/boards"
10 "gno.land/p/nt/avl/v0"
11)
12
13// Invite contains a user invitation.
14type Invite struct {
15 // User is the user to invite.
16 User address
17
18 // Role is the optional role to assign to the user.
19 Role boards.Role
20}
21
22// InviteMember adds a member to the realm or to a board.
23//
24// A role can optionally be specified to be assigned to the new member.
25func InviteMember(_ realm, boardID boards.ID, user address, role boards.Role) {
26 inviteMembers(boardID, Invite{
27 User: user,
28 Role: role,
29 })
30}
31
32// InviteMembers adds one or more members to the realm or to a board.
33//
34// Board ID is only required when inviting a member to a specific board.
35func InviteMembers(_ realm, boardID boards.ID, invites ...Invite) {
36 inviteMembers(boardID, invites...)
37}
38
39// RequestInvite request to be invited to a board.
40func RequestInvite(_ realm, boardID boards.ID) {
41 assertMembersUpdateIsEnabled(boardID)
42
43 if !runtime.PreviousRealm().IsUser() {
44 panic("caller must be user")
45 }
46
47 // TODO: Request a fee (returned on accept) or registered user to avoid spam?
48 // TODO: Make open invite requests optional (per board)
49
50 board := mustGetBoard(boardID)
51 user := runtime.PreviousRealm().Address()
52 if board.Permissions.HasUser(user) {
53 panic("caller is already a member")
54 }
55
56 invitee := user.String()
57 requests, found := getInviteRequests(boardID)
58 if !found {
59 requests = avl.NewTree()
60 requests.Set(invitee, time.Now())
61 gInviteRequests.Set(boardID.Key(), requests)
62 return
63 }
64
65 if requests.Has(invitee) {
66 panic("invite request already exists")
67 }
68
69 requests.Set(invitee, time.Now())
70}
71
72// AcceptInvite accepts a board invite request.
73func AcceptInvite(_ realm, boardID boards.ID, user address) {
74 assertMembersUpdateIsEnabled(boardID)
75 assertInviteRequestExists(boardID, user)
76
77 board := mustGetBoard(boardID)
78 if board.Permissions.HasUser(user) {
79 panic("user is already a member")
80 }
81
82 caller := runtime.PreviousRealm().Address()
83 invite := Invite{
84 User: user,
85 Role: RoleGuest,
86 }
87 args := boards.Args{caller, boardID, []Invite{invite}}
88 board.Permissions.WithPermission(caller, PermissionMemberInvite, args, crossingFn(func() {
89 assertMembersUpdateIsEnabled(boardID)
90
91 invitee := user.String()
92 requests, found := getInviteRequests(boardID)
93 if !found || !requests.Has(invitee) {
94 panic("invite request not found")
95 }
96
97 if board.Permissions.HasUser(user) {
98 panic("user is already a member")
99 }
100
101 board.Permissions.SetUserRoles(user)
102 requests.Remove(invitee)
103
104 chain.Emit(
105 "MembersInvited",
106 "invitedBy", caller.String(),
107 "boardID", board.ID.String(),
108 "members", user.String()+":"+string(RoleGuest), // TODO: Support optional role assign
109 )
110 }))
111}
112
113// RevokeInvite revokes a board invite request.
114func RevokeInvite(_ realm, boardID boards.ID, user address) {
115 assertInviteRequestExists(boardID, user)
116
117 board := mustGetBoard(boardID)
118 caller := runtime.PreviousRealm().Address()
119 args := boards.Args{boardID, user, RoleGuest}
120 board.Permissions.WithPermission(caller, PermissionMemberInviteRevoke, args, crossingFn(func() {
121 invitee := user.String()
122 requests, found := getInviteRequests(boardID)
123 if !found || !requests.Has(invitee) {
124 panic("invite request not found")
125 }
126
127 requests.Remove(invitee)
128
129 chain.Emit(
130 "InviteRevoked",
131 "revokedBy", caller.String(),
132 "boardID", board.ID.String(),
133 "user", user.String(),
134 )
135 }))
136}
137
138func inviteMembers(boardID boards.ID, invites ...Invite) {
139 if len(invites) == 0 {
140 panic("one or more user invites are required")
141 }
142
143 assertMembersUpdateIsEnabled(boardID)
144 assertNoDuplicatedInvites(invites)
145
146 perms := mustGetPermissions(boardID)
147 caller := runtime.PreviousRealm().Address()
148 args := boards.Args{caller, boardID, invites}
149 perms.WithPermission(caller, PermissionMemberInvite, args, crossingFn(func() {
150 assertMembersUpdateIsEnabled(boardID)
151
152 users := make([]string, len(invites))
153 for _, v := range invites {
154 assertMemberAddressIsValid(v.User)
155
156 if perms.HasUser(v.User) {
157 panic("user is already a member: " + v.User.String())
158 }
159
160 // NOTE: Permissions implementation should check that role is valid
161 perms.SetUserRoles(v.User, v.Role)
162 users = append(users, v.User.String()+":"+string(v.Role))
163 }
164
165 chain.Emit(
166 "MembersInvited",
167 "invitedBy", caller.String(),
168 "boardID", boardID.String(),
169 "members", strings.Join(users, ","),
170 )
171 }))
172}
173
174func assertInviteRequestExists(boardID boards.ID, user address) {
175 invitee := user.String()
176 requests, found := getInviteRequests(boardID)
177 if !found || !requests.Has(invitee) {
178 panic("invite request not found")
179 }
180}
181
182func assertNoDuplicatedInvites(invites []Invite) {
183 if len(invites) == 1 {
184 return
185 }
186
187 seen := make(map[address]struct{}, len(invites))
188 for _, v := range invites {
189 if _, found := seen[v.User]; found {
190 panic("duplicated invite: " + v.User.String())
191 }
192
193 seen[v.User] = struct{}{}
194 }
195}