issue.gno
4.06 Kb · 172 lines
1package forge
2
3import "gno.land/p/nt/avl/v0"
4
5// Issue is a discussion thread bound to a repo. Anyone with an address may open
6// one: the spam gate is not a moderator, it is that the author pays gas and
7// locks the storage deposit for every byte they write.
8type Issue struct {
9 ID int64
10 Title string
11 Body string
12 Author address
13 Open bool
14 Labels []string
15 CreatedAt int64
16 UpdatedAt int64
17
18 comments *avl.Tree // padded id -> *Comment
19 nextComment int64
20}
21
22// Comment is one reply, on an issue or on a change request.
23type Comment struct {
24 ID int64
25 Author address
26 Body string
27 CreatedAt int64
28}
29
30// OpenIssue files an issue. Permissionless by design.
31func (r *Repo) OpenIssue(actor address, height int64, title, body string, labels []string) (*Issue, error) {
32 if r.Archived {
33 return nil, ErrRepoArchived
34 }
35 if title == "" || !ValidLine(title, MaxTitleLen) {
36 return nil, ErrInvalidText
37 }
38 if !ValidText(body, MaxBodyLen) {
39 return nil, ErrInvalidText
40 }
41 if err := checkLabels(labels); err != nil {
42 return nil, err
43 }
44 i := &Issue{
45 ID: r.nextIssue,
46 Title: title,
47 Body: body,
48 Author: actor,
49 Open: true,
50 Labels: append([]string{}, labels...),
51 CreatedAt: height,
52 UpdatedAt: height,
53 comments: avl.NewTree(),
54 }
55 r.issues.Set(seqKey(i.ID), i)
56 r.nextIssue++
57 return i, nil
58}
59
60// CommentIssue appends a reply. Closed issues still take comments (closing is a
61// triage state, not a gag); an archived repo takes none.
62func (r *Repo) CommentIssue(actor address, height, id int64, body string) (*Comment, error) {
63 if r.Archived {
64 return nil, ErrRepoArchived
65 }
66 i := r.Issue(id)
67 if i == nil {
68 return nil, ErrIssueNotFound
69 }
70 if body == "" || !ValidText(body, MaxCommentLen) {
71 return nil, ErrInvalidText
72 }
73 c := &Comment{ID: i.nextComment, Author: actor, Body: body, CreatedAt: height}
74 i.comments.Set(seqKey(c.ID), c)
75 i.nextComment++
76 i.UpdatedAt = height
77 return c, nil
78}
79
80// SetIssueOpen closes or reopens an issue. The author can always close their
81// own; maintainers can close anyone's.
82func (r *Repo) SetIssueOpen(actor address, height, id int64, open bool) error {
83 if r.Archived {
84 return ErrRepoArchived
85 }
86 i := r.Issue(id)
87 if i == nil {
88 return ErrIssueNotFound
89 }
90 if i.Author != actor && !r.Can(actor, RoleMaintainer) {
91 return ErrUnauthorized
92 }
93 i.Open = open
94 i.UpdatedAt = height
95 return nil
96}
97
98// SetIssueLabels replaces an issue's labels. Triage is a maintainer action.
99func (r *Repo) SetIssueLabels(actor address, height, id int64, labels []string) error {
100 if r.Archived {
101 return ErrRepoArchived
102 }
103 i := r.Issue(id)
104 if i == nil {
105 return ErrIssueNotFound
106 }
107 if !r.Can(actor, RoleMaintainer) {
108 return ErrUnauthorized
109 }
110 if err := checkLabels(labels); err != nil {
111 return err
112 }
113 i.Labels = append([]string{}, labels...)
114 i.UpdatedAt = height
115 return nil
116}
117
118// Issue returns an issue by id, or nil.
119func (r *Repo) Issue(id int64) *Issue {
120 v := r.issues.Get(seqKey(id))
121 if v == nil {
122 return nil
123 }
124 return v.(*Issue)
125}
126
127// IterateIssues walks issues newest-first.
128func (r *Repo) IterateIssues(offset, count int, cb func(*Issue) bool) {
129 if count <= 0 {
130 count = r.issues.Size()
131 }
132 r.issues.ReverseIterateByOffset(offset, count, func(_ string, value any) bool {
133 return cb(value.(*Issue))
134 })
135}
136
137// OpenIssueCount counts issues still open.
138func (r *Repo) OpenIssueCount() int {
139 n := 0
140 r.issues.Iterate("", "", func(_ string, value any) bool {
141 if value.(*Issue).Open {
142 n++
143 }
144 return false
145 })
146 return n
147}
148
149// CommentCount is the number of replies on the issue.
150func (i *Issue) CommentCount() int { return i.comments.Size() }
151
152// IterateComments walks replies oldest-first.
153func (i *Issue) IterateComments(offset, count int, cb func(*Comment) bool) {
154 if count <= 0 {
155 count = i.comments.Size()
156 }
157 i.comments.IterateByOffset(offset, count, func(_ string, value any) bool {
158 return cb(value.(*Comment))
159 })
160}
161
162func checkLabels(labels []string) error {
163 if len(labels) > MaxLabels {
164 return ErrTooMany
165 }
166 for _, l := range labels {
167 if !ValidLabel(l) {
168 return ErrInvalidText
169 }
170 }
171 return nil
172}