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