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
5.8 kB 231 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 // 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}