Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}