1package models
2
3import (
4 "fmt"
5 "sort"
6 "time"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "tangled.org/core/api/tangled"
10)
11
12type Issue struct {
13 Id int64
14 Did string
15 Rkey string
16 RepoAt syntax.ATURI
17 IssueId int
18 Created time.Time
19 Edited *time.Time
20 Deleted *time.Time
21 Title string
22 Body string
23 Open bool
24 Mentions []syntax.DID
25 References []syntax.ATURI
26
27 // optionally, populate this when querying for reverse mappings
28 // like comment counts, parent repo etc.
29 Comments []IssueComment
30 Labels LabelState
31 Repo *Repo
32}
33
34func (i *Issue) AtUri() syntax.ATURI {
35 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey))
36}
37
38func (i *Issue) AsRecord() tangled.RepoIssue {
39 mentions := make([]string, len(i.Mentions))
40 for i, did := range i.Mentions {
41 mentions[i] = string(did)
42 }
43 references := make([]string, len(i.References))
44 for i, uri := range i.References {
45 references[i] = string(uri)
46 }
47 return tangled.RepoIssue{
48 Repo: i.RepoAt.String(),
49 Title: i.Title,
50 Body: &i.Body,
51 Mentions: mentions,
52 References: references,
53 CreatedAt: i.Created.Format(time.RFC3339),
54 }
55}
56
57func (i *Issue) State() string {
58 if i.Open {
59 return "open"
60 }
61 return "closed"
62}
63
64type CommentListItem struct {
65 Self *IssueComment
66 Replies []*IssueComment
67}
68
69func (it *CommentListItem) Participants() []syntax.DID {
70 participantSet := make(map[syntax.DID]struct{})
71 participants := []syntax.DID{}
72
73 addParticipant := func(did syntax.DID) {
74 if _, exists := participantSet[did]; !exists {
75 participantSet[did] = struct{}{}
76 participants = append(participants, did)
77 }
78 }
79
80 addParticipant(syntax.DID(it.Self.Did))
81
82 for _, c := range it.Replies {
83 addParticipant(syntax.DID(c.Did))
84 }
85
86 return participants
87}
88
89func (i *Issue) CommentList() []CommentListItem {
90 // Create a map to quickly find comments by their aturi
91 toplevel := make(map[string]*CommentListItem)
92 var replies []*IssueComment
93
94 // collect top level comments into the map
95 for _, comment := range i.Comments {
96 if comment.IsTopLevel() {
97 toplevel[comment.AtUri().String()] = &CommentListItem{
98 Self: &comment,
99 }
100 } else {
101 replies = append(replies, &comment)
102 }
103 }
104
105 for _, r := range replies {
106 parentAt := *r.ReplyTo
107 if parent, exists := toplevel[parentAt]; exists {
108 parent.Replies = append(parent.Replies, r)
109 }
110 }
111
112 var listing []CommentListItem
113 for _, v := range toplevel {
114 listing = append(listing, *v)
115 }
116
117 // sort everything
118 sortFunc := func(a, b *IssueComment) bool {
119 return a.Created.Before(b.Created)
120 }
121 sort.Slice(listing, func(i, j int) bool {
122 return sortFunc(listing[i].Self, listing[j].Self)
123 })
124 for _, r := range listing {
125 sort.Slice(r.Replies, func(i, j int) bool {
126 return sortFunc(r.Replies[i], r.Replies[j])
127 })
128 }
129
130 return listing
131}
132
133func (i *Issue) Participants() []string {
134 participantSet := make(map[string]struct{})
135 participants := []string{}
136
137 addParticipant := func(did string) {
138 if _, exists := participantSet[did]; !exists {
139 participantSet[did] = struct{}{}
140 participants = append(participants, did)
141 }
142 }
143
144 addParticipant(i.Did)
145
146 for _, c := range i.Comments {
147 addParticipant(c.Did)
148 }
149
150 return participants
151}
152
153func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue {
154 created, err := time.Parse(time.RFC3339, record.CreatedAt)
155 if err != nil {
156 created = time.Now()
157 }
158
159 body := ""
160 if record.Body != nil {
161 body = *record.Body
162 }
163
164 return Issue{
165 RepoAt: syntax.ATURI(record.Repo),
166 Did: did,
167 Rkey: rkey,
168 Created: created,
169 Title: record.Title,
170 Body: body,
171 Open: true, // new issues are open by default
172 }
173}
174
175type IssueComment struct {
176 Id int64
177 Did string
178 Rkey string
179 IssueAt string
180 ReplyTo *string
181 Body string
182 Created time.Time
183 Edited *time.Time
184 Deleted *time.Time
185 Mentions []syntax.DID
186 References []syntax.ATURI
187}
188
189func (i *IssueComment) AtUri() syntax.ATURI {
190 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey))
191}
192
193func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
194 mentions := make([]string, len(i.Mentions))
195 for i, did := range i.Mentions {
196 mentions[i] = string(did)
197 }
198 references := make([]string, len(i.References))
199 for i, uri := range i.References {
200 references[i] = string(uri)
201 }
202 return tangled.RepoIssueComment{
203 Body: i.Body,
204 Issue: i.IssueAt,
205 CreatedAt: i.Created.Format(time.RFC3339),
206 ReplyTo: i.ReplyTo,
207 Mentions: mentions,
208 References: references,
209 }
210}
211
212func (i *IssueComment) IsTopLevel() bool {
213 return i.ReplyTo == nil
214}
215
216func (i *IssueComment) IsReply() bool {
217 return i.ReplyTo != nil
218}
219
220func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
221 created, err := time.Parse(time.RFC3339, record.CreatedAt)
222 if err != nil {
223 created = time.Now()
224 }
225
226 ownerDid := did
227
228 if _, err = syntax.ParseATURI(record.Issue); err != nil {
229 return nil, err
230 }
231
232 i := record
233 mentions := make([]syntax.DID, len(record.Mentions))
234 for i, did := range record.Mentions {
235 mentions[i] = syntax.DID(did)
236 }
237 references := make([]syntax.ATURI, len(record.References))
238 for i, uri := range i.References {
239 references[i] = syntax.ATURI(uri)
240 }
241
242 comment := IssueComment{
243 Did: ownerDid,
244 Rkey: rkey,
245 Body: record.Body,
246 IssueAt: record.Issue,
247 ReplyTo: record.ReplyTo,
248 Created: created,
249 Mentions: mentions,
250 References: references,
251 }
252
253 return &comment, nil
254}