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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}