Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}