board.gno

6.21 Kb · 242 lines
  1package boards2
  2
  3import (
  4	"std"
  5	"strconv"
  6	"strings"
  7	"time"
  8
  9	"gno.land/p/jeronimoalbi/pager"
 10	"gno.land/p/moul/md"
 11	"gno.land/p/nt/avl"
 12	"gno.land/p/nt/commondao"
 13	"gno.land/p/nt/seqid"
 14)
 15
 16type (
 17	// PostIterFn defines a function type to iterate posts.
 18	PostIterFn func(*Post) bool
 19
 20	// BoardID defines a type for board identifiers.
 21	BoardID uint64
 22)
 23
 24// String returns the ID as a string.
 25func (id BoardID) String() string {
 26	return strconv.Itoa(int(id))
 27}
 28
 29// Key returns the ID as a string which can be used to index by ID.
 30func (id BoardID) Key() string {
 31	return seqid.ID(id).String()
 32}
 33
 34// Board defines a type for boards.
 35type Board struct {
 36	ID       BoardID
 37	Name     string
 38	Aliases  []string
 39	Creator  std.Address
 40	Readonly bool
 41
 42	perms     Permissions
 43	postsCtr  uint64   // Increments Post.ID
 44	threads   avl.Tree // Post.ID -> *Post
 45	createdAt time.Time
 46}
 47
 48func newBoard(id BoardID, name string, creator std.Address, p Permissions) *Board {
 49	return &Board{
 50		ID:        id,
 51		Name:      name,
 52		Creator:   creator,
 53		perms:     p,
 54		threads:   avl.Tree{},
 55		createdAt: time.Now(),
 56	}
 57}
 58
 59// CreatedAt returns the time when board was created.
 60func (board *Board) CreatedAt() time.Time {
 61	return board.createdAt
 62}
 63
 64// MembersCount returns the total number of board members.
 65func (board *Board) MembersCount() int {
 66	return board.perms.UsersCount()
 67}
 68
 69// IterateMembers iterates board members.
 70func (board *Board) IterateMembers(start, count int, fn func(std.Address, []Role)) {
 71	board.perms.IterateUsers(start, count, func(u User) bool {
 72		fn(u.Address, u.Roles)
 73		return false
 74	})
 75}
 76
 77// ThreadsCount returns the total number of board threads.
 78func (board *Board) ThreadsCount() int {
 79	return board.threads.Size()
 80}
 81
 82// IterateThreads iterates board threads.
 83func (board *Board) IterateThreads(start, count int, fn PostIterFn) bool {
 84	return board.threads.IterateByOffset(start, count, func(_ string, v any) bool {
 85		p := v.(*Post)
 86		return fn(p)
 87	})
 88}
 89
 90// ReverseIterateThreads iterates board threads in reverse order.
 91func (board *Board) ReverseIterateThreads(start, count int, fn PostIterFn) bool {
 92	return board.threads.ReverseIterateByOffset(start, count, func(_ string, v any) bool {
 93		p := v.(*Post)
 94		return fn(p)
 95	})
 96}
 97
 98// GetThread returns board thread.
 99func (board *Board) GetThread(threadID PostID) (_ *Post, found bool) {
100	v, found := board.threads.Get(threadID.Key())
101	if !found {
102		return nil, false
103	}
104	return v.(*Post), true
105}
106
107// AddThread adds a new thread to the board.
108func (board *Board) AddThread(creator std.Address, title, body string) *Post {
109	pid := board.generateNextPostID()
110	thread := newPost(board, pid, pid, creator, title, body)
111	board.threads.Set(pid.Key(), thread)
112	return thread
113}
114
115// DeleteThread deletes a thread from the board.
116// NOTE: this can be potentially very expensive for threads with many replies.
117// TODO: implement optional fast-delete where thread is simply moved.
118func (board *Board) DeleteThread(pid PostID) {
119	_, removed := board.threads.Remove(pid.Key())
120	if !removed {
121		panic("thread does not exist with ID " + pid.String())
122	}
123}
124
125// Render renders a board into Markdown.
126func (board *Board) Render(path, menu string) string {
127	var (
128		sb          strings.Builder
129		creatorLink = md.UserLink(board.Creator.String())
130		date        = board.CreatedAt().Format(dateFormat)
131	)
132
133	sb.WriteString(md.H1(board.Name))
134	sb.WriteString("Board created by " + creatorLink + " on " + date + ", #" + board.ID.String())
135	if board.Readonly {
136		sb.WriteString("  \n_" + md.Bold("Starting new threads and commenting is disabled") + "_")
137	}
138
139	sb.WriteString("\n")
140
141	// XXX: Menu is rendered by the caller to deal with links and sub-menus
142	// TODO: We should have the render logic separated from boards so avoid sending menu as argument
143	if menu != "" {
144		sb.WriteString("\n" + menu + "\n")
145	}
146
147	sb.WriteString(md.HorizontalRule())
148
149	if board.ThreadsCount() == 0 {
150		sb.WriteString(md.H3("This board doesn't have any threads"))
151		if !board.Readonly {
152			startConversationLink := md.Link("start a new conversation", makeCreateThreadURI(board))
153			sb.WriteString("Do you want to " + startConversationLink + " in this board ?")
154		}
155		return sb.String()
156	}
157
158	p, err := pager.New(path, board.ThreadsCount(), pager.WithPageSize(pageSizeDefault))
159	if err != nil {
160		panic(err)
161	}
162
163	render := func(thread *Post) bool {
164		if !thread.Hidden {
165			sb.WriteString(thread.RenderSummary() + "\n")
166		}
167		return false
168	}
169
170	sb.WriteString("Sort by: ")
171	r := parseRealmPath(path)
172	if r.Query.Get("order") == "desc" {
173		r.Query.Set("order", "asc")
174		sb.WriteString(md.Link("newest first", r.String()) + "\n\n")
175		board.ReverseIterateThreads(p.Offset(), p.PageSize(), render)
176	} else {
177		r.Query.Set("order", "desc")
178		sb.WriteString(md.Link("oldest first", r.String()) + "\n\n")
179		board.IterateThreads(p.Offset(), p.PageSize(), render)
180	}
181
182	if p.HasPages() {
183		sb.WriteString(md.HorizontalRule())
184		sb.WriteString(pager.Picker(p))
185	}
186
187	return sb.String()
188}
189
190func (board *Board) generateNextPostID() PostID {
191	board.postsCtr++
192	return PostID(board.postsCtr)
193}
194
195func createBasicBoardPermissions(owner std.Address) *BasicPermissions {
196	dao := commondao.New(commondao.WithMember(owner))
197	perms := NewBasicPermissions(dao)
198	perms.SetSuperRole(RoleOwner)
199	perms.AddRole(
200		RoleAdmin,
201		PermissionBoardRename,
202		PermissionBoardFlaggingUpdate,
203		PermissionMemberInvite,
204		PermissionMemberInviteRevoke,
205		PermissionMemberRemove,
206		PermissionThreadCreate,
207		PermissionThreadEdit,
208		PermissionThreadDelete,
209		PermissionThreadRepost,
210		PermissionThreadFlag,
211		PermissionThreadFreeze,
212		PermissionReplyCreate,
213		PermissionReplyDelete,
214		PermissionReplyFlag,
215		PermissionReplyFreeze,
216		PermissionRoleChange,
217		PermissionUserBan,
218		PermissionUserUnban,
219	)
220	perms.AddRole(
221		RoleModerator,
222		PermissionThreadCreate,
223		PermissionThreadEdit,
224		PermissionThreadRepost,
225		PermissionThreadFlag,
226		PermissionReplyCreate,
227		PermissionReplyFlag,
228		PermissionUserBan,
229		PermissionUserUnban,
230	)
231	perms.AddRole(
232		RoleGuest,
233		PermissionThreadCreate,
234		PermissionThreadRepost,
235		PermissionReplyCreate,
236	)
237	perms.SetUserRoles(cross, owner, RoleOwner)
238	perms.ValidateFunc(PermissionBoardRename, validateBoardRename)
239	perms.ValidateFunc(PermissionMemberInvite, validateMemberInvite)
240	perms.ValidateFunc(PermissionRoleChange, validateRoleChange)
241	return perms
242}