This repository has no description
1package models
2
3import (
4 "fmt"
5 "sort"
6 "strings"
7 "time"
8
9 comatproto "github.com/bluesky-social/indigo/api/atproto"
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 typegen "github.com/whyrusleeping/cbor-gen"
12 "tangled.org/core/api/tangled"
13)
14
15type Comment struct {
16 Id int64
17
18 Did syntax.DID
19 Collection syntax.NSID
20 Rkey syntax.RecordKey
21 Cid syntax.CID
22
23 // record content
24 Subject comatproto.RepoStrongRef
25 Body tangled.MarkupMarkdown // markup body type. only markdown is supported right now
26 Created time.Time
27 ReplyTo *comatproto.RepoStrongRef // (optional) parent comment
28 PullRoundIdx *int // (optional) pull round number used when subject is sh.tangled.repo.pull
29
30 // store on db, but not on PDS
31 Edited *time.Time
32 Deleted *time.Time
33}
34
35func (c Comment) AtUri() syntax.ATURI {
36 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, c.Collection, c.Rkey))
37}
38
39func (c Comment) StrongRef() comatproto.RepoStrongRef {
40 return comatproto.RepoStrongRef{
41 Uri: c.AtUri().String(),
42 Cid: c.Cid.String(),
43 }
44}
45
46func (c Comment) AsRecord() typegen.CBORMarshaler {
47 // can't convert to record for legacy types
48 if c.Collection != tangled.FeedCommentNSID {
49 return nil
50 }
51 var pullRoundIdx *int64
52 if c.PullRoundIdx != nil {
53 pullRoundIdx = new(int64)
54 *pullRoundIdx = int64(*c.PullRoundIdx)
55 }
56 return &tangled.FeedComment{
57 Subject: &c.Subject,
58 Body: &tangled.FeedComment_Body{MarkupMarkdown: &c.Body},
59 CreatedAt: c.Created.Format(time.RFC3339),
60 ReplyTo: c.ReplyTo,
61 PullRoundIdx: pullRoundIdx,
62 }
63}
64
65func (c Comment) EditableBody() string {
66 if c.Body.Original != nil {
67 return *c.Body.Original
68 }
69 return c.Body.Text
70}
71
72func (c Comment) IsLegacy() bool {
73 return c.Collection != tangled.FeedCommentNSID
74}
75
76func (c *Comment) IsTopLevel() bool {
77 return c.ReplyTo == nil
78}
79
80func (c *Comment) IsReply() bool {
81 return c.ReplyTo != nil
82}
83
84func (c *Comment) Validate() error {
85 // TODO: sanitize the body and then trim space
86 if sb := strings.TrimSpace(c.Body.Text); sb == "" {
87 return fmt.Errorf("body is empty after HTML sanitization")
88 }
89
90 // if it's for PR, PullSubmissionId should not be nil
91 subjectAt, err := syntax.ParseATURI(c.Subject.Uri)
92 if err != nil {
93 return fmt.Errorf("subject.uri is not valid at-uri: %w", err)
94 }
95 if subjectAt.Collection().String() == tangled.RepoPullNSID {
96 if c.PullRoundIdx == nil {
97 return fmt.Errorf("pullSubmissionId should not be nil when subject is sh.tangled.repo.pull")
98 }
99 }
100 return nil
101}
102
103func CommentFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.FeedComment) (*Comment, error) {
104 created, err := time.Parse(time.RFC3339, record.CreatedAt)
105 if err != nil {
106 created = time.Now()
107 }
108
109 if record.Subject == nil {
110 return nil, fmt.Errorf("subject can't be nil")
111 }
112 subjectAt, err := syntax.ParseATURI(record.Subject.Uri)
113 if err != nil {
114 return nil, fmt.Errorf("invalid subject uri: %w", err)
115 }
116 if _, err = syntax.ParseCID(record.Subject.Cid); err != nil {
117 return nil, fmt.Errorf("invalid subject cid: %w", err)
118 }
119
120 if subjectAt.Collection() == tangled.RepoPullNSID {
121 if record.PullRoundIdx == nil {
122 return nil, fmt.Errorf("pullRoundIdx can't be nil when subject is sh.tangled.repo.pull")
123 }
124 }
125
126 if record.Body == nil {
127 return nil, fmt.Errorf("body can't be nil")
128 }
129 if record.Body.MarkupMarkdown == nil {
130 return nil, fmt.Errorf("body should be markdown type")
131 }
132
133 if record.ReplyTo != nil {
134 if _, err = syntax.ParseATURI(record.ReplyTo.Uri); err != nil {
135 return nil, fmt.Errorf("invalid replyTo uri: %w", err)
136 }
137 if _, err = syntax.ParseCID(record.ReplyTo.Cid); err != nil {
138 return nil, fmt.Errorf("invalid replyTo cid: %w", err)
139 }
140 }
141
142 var pullRoundIdx *int
143 if record.PullRoundIdx != nil {
144 pullRoundIdx = new(int)
145 *pullRoundIdx = int(*record.PullRoundIdx)
146 }
147
148 return &Comment{
149 Did: did,
150 Collection: tangled.FeedCommentNSID,
151 Rkey: rkey,
152 Cid: cid,
153
154 Subject: *record.Subject,
155 Body: *record.Body.MarkupMarkdown,
156 Created: created,
157 ReplyTo: record.ReplyTo,
158 PullRoundIdx: pullRoundIdx,
159 }, nil
160}
161
162type CommentListItem struct {
163 Self *Comment
164 Replies []*Comment
165}
166
167func (it *CommentListItem) Participants() []syntax.DID {
168 participantSet := make(map[syntax.DID]struct{})
169 participants := []syntax.DID{}
170
171 addParticipant := func(did syntax.DID) {
172 if _, exists := participantSet[did]; !exists {
173 participantSet[did] = struct{}{}
174 participants = append(participants, did)
175 }
176 }
177
178 addParticipant(syntax.DID(it.Self.Did))
179
180 for _, c := range it.Replies {
181 addParticipant(syntax.DID(c.Did))
182 }
183
184 return participants
185}
186
187func NewCommentList(comments []Comment) []CommentListItem {
188 // Create a map to quickly find comments by their aturi
189 toplevel := make(map[syntax.ATURI]*CommentListItem)
190 var replies []*Comment
191
192 // collect top level comments into the map
193 for _, comment := range comments {
194 if comment.IsTopLevel() {
195 toplevel[comment.AtUri()] = &CommentListItem{
196 Self: &comment,
197 }
198 } else {
199 replies = append(replies, &comment)
200 }
201 }
202
203 for _, r := range replies {
204 if r.ReplyTo == nil {
205 continue
206 }
207 if parent, exists := toplevel[syntax.ATURI(r.ReplyTo.Uri)]; exists {
208 parent.Replies = append(parent.Replies, r)
209 }
210 }
211
212 var listing []CommentListItem
213 for _, v := range toplevel {
214 listing = append(listing, *v)
215 }
216
217 // sort everything
218 sortFunc := func(a, b *Comment) bool {
219 return a.Created.Before(b.Created)
220 }
221 sort.Slice(listing, func(i, j int) bool {
222 return sortFunc(listing[i].Self, listing[j].Self)
223 })
224 for _, r := range listing {
225 sort.Slice(r.Replies, func(i, j int) bool {
226 return sortFunc(r.Replies[i], r.Replies[j])
227 })
228 }
229
230 return listing
231}