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}