/
issues.go
176 lines (152 loc) · 5.16 KB
/
issues.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// Package issues provides an issues service definition.
package issues
import (
"context"
"fmt"
"strings"
"time"
"dmitri.shuralyov.com/state"
"github.com/shurcooL/reactions"
"github.com/shurcooL/users"
)
// RepoSpec is a specification for a repository.
type RepoSpec struct {
URI string // URI is clean '/'-separated URI. E.g., "example.com/user/repo".
}
// String implements fmt.Stringer.
func (rs RepoSpec) String() string {
return rs.URI
}
// Service defines methods of an issue tracking service.
type Service interface {
// List issues.
List(ctx context.Context, repo RepoSpec, opt IssueListOptions) ([]Issue, error)
// Count issues.
Count(ctx context.Context, repo RepoSpec, opt IssueListOptions) (uint64, error)
// Get an issue.
Get(ctx context.Context, repo RepoSpec, id uint64) (Issue, error)
// ListTimeline lists timeline items (Comment, Event) for specified issue id
// in chronological order. The issue description comes first in a timeline.
ListTimeline(ctx context.Context, repo RepoSpec, id uint64, opt *ListOptions) ([]interface{}, error)
// Create a new issue.
Create(ctx context.Context, repo RepoSpec, issue Issue) (Issue, error)
// CreateComment creates a new comment for specified issue id.
CreateComment(ctx context.Context, repo RepoSpec, id uint64, comment Comment) (Comment, error)
// Edit the specified issue id.
Edit(ctx context.Context, repo RepoSpec, id uint64, ir IssueRequest) (Issue, []Event, error)
// EditComment edits comment of specified issue id.
EditComment(ctx context.Context, repo RepoSpec, id uint64, cr CommentRequest) (Comment, error)
// ThreadType reports the notification thread type for this service in repo.
ThreadType(ctx context.Context, repo RepoSpec) (string, error)
}
// Issue represents an issue on a repository.
type Issue struct {
ID uint64
State state.Issue
Title string
Labels []Label
Comment
Replies int // Number of replies to this issue (not counting the mandatory issue description comment).
}
// Label represents a label.
type Label struct {
Name string
Color RGB
}
// TODO: Dedup.
//
// RGB represents a 24-bit color without alpha channel.
type RGB struct {
R, G, B uint8
}
func (c RGB) RGBA() (r, g, b, a uint32) {
r = uint32(c.R)
r |= r << 8
g = uint32(c.G)
g |= g << 8
b = uint32(c.B)
b |= b << 8
a = uint32(255)
a |= a << 8
return
}
// HexString returns a hexadecimal color string. For example, "#ff0000" for red.
func (c RGB) HexString() string {
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}
// Comment represents a comment left on an issue.
type Comment struct {
ID uint64
User users.User
CreatedAt time.Time
Edited *Edited // Edited is nil if the comment hasn't been edited.
Body string
Reactions []reactions.Reaction
Editable bool // Editable represents whether the current user (if any) can perform edit operations on this comment (or the encompassing issue).
}
// Edited provides the actor and timing information for an edited item.
type Edited struct {
By users.User
At time.Time
}
// IssueRequest is a request to edit an issue.
// To edit the body, use EditComment with comment ID 0.
type IssueRequest struct {
State *state.Issue
Title *string
// TODO: Labels *[]Label
}
// CommentRequest is a request to edit a comment.
type CommentRequest struct {
ID uint64
Body *string // If not nil, set the body.
Reaction *reactions.EmojiID // If not nil, toggle this reaction.
}
// Validate returns non-nil error if the issue is invalid.
func (i Issue) Validate() error {
if strings.TrimSpace(i.Title) == "" {
return fmt.Errorf("title can't be blank or all whitespace")
}
return nil
}
// Validate returns non-nil error if the issue request is invalid.
func (ir IssueRequest) Validate() error {
if ir.State != nil {
switch *ir.State {
case state.IssueOpen, state.IssueClosed:
default:
return fmt.Errorf("bad state")
}
}
if ir.Title != nil {
if strings.TrimSpace(*ir.Title) == "" {
return fmt.Errorf("title can't be blank or all whitespace")
}
}
return nil
}
// Validate returns non-nil error if the comment is invalid.
func (c Comment) Validate() error {
// TODO: Issue descriptions can have blank bodies, support that (primarily for editing comments).
if strings.TrimSpace(c.Body) == "" {
return fmt.Errorf("comment body can't be blank or all whitespace")
}
return nil
}
// Validate validates the comment edit request, returning an non-nil error if it's invalid.
// requiresEdit reports if the edit request needs edit rights or if it can be done by anyone that can react.
func (cr CommentRequest) Validate() (requiresEdit bool, err error) {
if cr.Body != nil {
requiresEdit = true
// TODO: Issue descriptions can have blank bodies, support that (primarily for editing comments).
if strings.TrimSpace(*cr.Body) == "" {
return requiresEdit, fmt.Errorf("comment body can't be blank or all whitespace")
}
}
/*if cr.Reaction != nil {
// TODO: Maybe validate that the emojiID is one of supported ones.
// Or maybe not (unsupported ones can be handled by frontend component).
// That way custom emoji can be added/removed, etc. Figure out what the best thing to do is and do it.
}*/
return requiresEdit, nil
}