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}