Search Apps Documentation Source Content File Folder Download Copy Actions Download

public_invite.gno

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