This repository has no description
0

Configure Feed

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

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