board.gno

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