Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

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 39// force-return the feed.comment NSID 40func (c Comment) FeedCommentAtUri() syntax.ATURI { 41 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, tangled.FeedCommentNSID, c.Rkey)) 42} 43 44func (c Comment) StrongRef() comatproto.RepoStrongRef { 45 return comatproto.RepoStrongRef{ 46 Uri: c.AtUri().String(), 47 Cid: c.Cid.String(), 48 } 49} 50 51func (c Comment) AsRecord() typegen.CBORMarshaler { 52 var pullRoundIdx *int64 53 if c.PullRoundIdx != nil { 54 pullRoundIdx = new(int64) 55 *pullRoundIdx = int64(*c.PullRoundIdx) 56 } 57 return &tangled.FeedComment{ 58 Subject: &c.Subject, 59 Body: &tangled.FeedComment_Body{MarkupMarkdown: &c.Body}, 60 CreatedAt: c.Created.Format(time.RFC3339), 61 ReplyTo: c.ReplyTo, 62 PullRoundIdx: pullRoundIdx, 63 } 64} 65 66func (c Comment) EditableBody() string { 67 if c.Body.Original != nil { 68 return *c.Body.Original 69 } 70 return c.Body.Text 71} 72 73func (c Comment) IsLegacy() bool { 74 return c.Collection != tangled.FeedCommentNSID 75} 76 77func (c *Comment) IsTopLevel() bool { 78 return c.ReplyTo == nil 79} 80 81func (c *Comment) IsReply() bool { 82 return c.ReplyTo != nil 83} 84 85func (c *Comment) Validate() error { 86 // TODO: sanitize the body and then trim space 87 if sb := strings.TrimSpace(c.Body.Text); sb == "" { 88 return fmt.Errorf("body is empty after HTML sanitization") 89 } 90 91 // if it's for PR, PullSubmissionId should not be nil 92 subjectAt, err := syntax.ParseATURI(c.Subject.Uri) 93 if err != nil { 94 return fmt.Errorf("subject.uri is not valid at-uri: %w", err) 95 } 96 if subjectAt.Collection().String() == tangled.RepoPullNSID { 97 if c.PullRoundIdx == nil { 98 return fmt.Errorf("pullSubmissionId should not be nil when subject is sh.tangled.repo.pull") 99 } 100 } 101 return nil 102} 103 104func CommentFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.FeedComment) (*Comment, error) { 105 created, err := time.Parse(time.RFC3339, record.CreatedAt) 106 if err != nil { 107 created = time.Now() 108 } 109 110 if record.Subject == nil { 111 return nil, fmt.Errorf("subject can't be nil") 112 } 113 subjectAt, err := syntax.ParseATURI(record.Subject.Uri) 114 if err != nil { 115 return nil, fmt.Errorf("invalid subject uri: %w", err) 116 } 117 if _, err = syntax.ParseCID(record.Subject.Cid); err != nil { 118 return nil, fmt.Errorf("invalid subject cid: %w", err) 119 } 120 121 if subjectAt.Collection() == tangled.RepoPullNSID { 122 if record.PullRoundIdx == nil { 123 return nil, fmt.Errorf("pullRoundIdx can't be nil when subject is sh.tangled.repo.pull") 124 } 125 } 126 127 if record.Body == nil { 128 return nil, fmt.Errorf("body can't be nil") 129 } 130 if record.Body.MarkupMarkdown == nil { 131 return nil, fmt.Errorf("body should be markdown type") 132 } 133 134 if record.ReplyTo != nil { 135 if _, err = syntax.ParseATURI(record.ReplyTo.Uri); err != nil { 136 return nil, fmt.Errorf("invalid replyTo uri: %w", err) 137 } 138 if _, err = syntax.ParseCID(record.ReplyTo.Cid); err != nil { 139 return nil, fmt.Errorf("invalid replyTo cid: %w", err) 140 } 141 } 142 143 var pullRoundIdx *int 144 if record.PullRoundIdx != nil { 145 pullRoundIdx = new(int) 146 *pullRoundIdx = int(*record.PullRoundIdx) 147 } 148 149 return &Comment{ 150 Did: did, 151 Collection: tangled.FeedCommentNSID, 152 Rkey: rkey, 153 Cid: cid, 154 155 Subject: *record.Subject, 156 Body: *record.Body.MarkupMarkdown, 157 Created: created, 158 ReplyTo: record.ReplyTo, 159 PullRoundIdx: pullRoundIdx, 160 }, nil 161} 162 163type CommentListItem struct { 164 Self *Comment 165 Replies []*Comment 166} 167 168func (it *CommentListItem) Participants() []syntax.DID { 169 participantSet := make(map[syntax.DID]struct{}) 170 participants := []syntax.DID{} 171 172 addParticipant := func(did syntax.DID) { 173 if _, exists := participantSet[did]; !exists { 174 participantSet[did] = struct{}{} 175 participants = append(participants, did) 176 } 177 } 178 179 addParticipant(syntax.DID(it.Self.Did)) 180 181 for _, c := range it.Replies { 182 addParticipant(syntax.DID(c.Did)) 183 } 184 185 return participants 186} 187 188func NewCommentList(comments []Comment) []CommentListItem { 189 // Create a map to quickly find comments by their aturi 190 toplevel := make(map[syntax.ATURI]*CommentListItem) 191 var replies []*Comment 192 193 // collect top level comments into the map 194 for _, comment := range comments { 195 if comment.IsTopLevel() { 196 toplevel[comment.AtUri()] = &CommentListItem{ 197 Self: &comment, 198 } 199 } else { 200 replies = append(replies, &comment) 201 } 202 } 203 204 for _, r := range replies { 205 if r.ReplyTo == nil { 206 continue 207 } 208 uri := syntax.ATURI(r.ReplyTo.Uri) 209 if parent, exists := toplevel[uri]; exists { 210 parent.Replies = append(parent.Replies, r) 211 continue 212 } 213 // HACK: fallback to legacy comment collections 214 if parent, exists := toplevel[syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", uri.Authority(), tangled.RepoIssueCommentNSID, uri.RecordKey()))]; exists { 215 parent.Replies = append(parent.Replies, r) 216 continue 217 } 218 if parent, exists := toplevel[syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", uri.Authority(), tangled.RepoPullCommentNSID, uri.RecordKey()))]; exists { 219 parent.Replies = append(parent.Replies, r) 220 continue 221 } 222 } 223 224 var listing []CommentListItem 225 for _, v := range toplevel { 226 listing = append(listing, *v) 227 } 228 229 // sort everything 230 sortFunc := func(a, b *Comment) bool { 231 return a.Created.Before(b.Created) 232 } 233 sort.Slice(listing, func(i, j int) bool { 234 return sortFunc(listing[i].Self, listing[j].Self) 235 }) 236 for _, r := range listing { 237 sort.Slice(r.Replies, func(i, j int) bool { 238 return sortFunc(r.Replies[i], r.Replies[j]) 239 }) 240 } 241 242 return listing 243}