Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}