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