A vibe coded tangled fork which supports pijul.
at master 243 lines 5.8 kB view raw
1package models 2 3import ( 4 "fmt" 5 "sort" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9) 10 11// DiscussionState represents the state of a discussion 12type DiscussionState int 13 14const ( 15 DiscussionClosed DiscussionState = iota 16 DiscussionOpen 17 DiscussionMerged 18) 19 20func (s DiscussionState) String() string { 21 switch s { 22 case DiscussionOpen: 23 return "open" 24 case DiscussionMerged: 25 return "merged" 26 case DiscussionClosed: 27 return "closed" 28 default: 29 return "closed" 30 } 31} 32 33func (s DiscussionState) IsOpen() bool { return s == DiscussionOpen } 34func (s DiscussionState) IsMerged() bool { return s == DiscussionMerged } 35func (s DiscussionState) IsClosed() bool { return s == DiscussionClosed } 36 37// Discussion represents a discussion in a Pijul repository 38// Anyone can add patches to a discussion 39type Discussion struct { 40 // ids 41 Id int64 42 Did string 43 Rkey string 44 RepoAt syntax.ATURI 45 DiscussionId int 46 47 // content 48 Title string 49 Body string 50 TargetChannel string 51 State DiscussionState 52 53 // meta 54 Created time.Time 55 Edited *time.Time 56 57 // populated on query 58 Patches []*DiscussionPatch 59 Comments []DiscussionComment 60 Labels LabelState 61 Repo *Repo 62} 63 64const DiscussionNSID = "sh.tangled.repo.discussion" 65 66func (d *Discussion) AtUri() syntax.ATURI { 67 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", d.Did, DiscussionNSID, d.Rkey)) 68} 69 70// ActivePatches returns only the patches that haven't been removed 71func (d *Discussion) ActivePatches() []*DiscussionPatch { 72 var active []*DiscussionPatch 73 for _, p := range d.Patches { 74 if p.IsActive() { 75 active = append(active, p) 76 } 77 } 78 return active 79} 80 81// Participants returns all DIDs that have participated in this discussion 82// (creator + patch pushers + commenters) 83func (d *Discussion) Participants() []string { 84 participantSet := make(map[string]struct{}) 85 participants := []string{} 86 87 addParticipant := func(did string) { 88 if _, exists := participantSet[did]; !exists { 89 participantSet[did] = struct{}{} 90 participants = append(participants, did) 91 } 92 } 93 94 // Discussion creator 95 addParticipant(d.Did) 96 97 // Patch pushers 98 for _, p := range d.Patches { 99 addParticipant(p.PushedByDid) 100 } 101 102 // Commenters 103 for _, c := range d.Comments { 104 addParticipant(c.Did) 105 } 106 107 return participants 108} 109 110// CommentList returns a threaded comment list 111func (d *Discussion) CommentList() []DiscussionCommentListItem { 112 toplevel := make(map[string]*DiscussionCommentListItem) 113 var replies []*DiscussionComment 114 115 for i := range d.Comments { 116 comment := &d.Comments[i] 117 if comment.IsTopLevel() { 118 toplevel[comment.AtUri().String()] = &DiscussionCommentListItem{ 119 Self: comment, 120 } 121 } else { 122 replies = append(replies, comment) 123 } 124 } 125 126 for _, r := range replies { 127 parentAt := *r.ReplyTo 128 if parent, exists := toplevel[parentAt]; exists { 129 parent.Replies = append(parent.Replies, r) 130 } 131 } 132 133 var listing []DiscussionCommentListItem 134 for _, v := range toplevel { 135 listing = append(listing, *v) 136 } 137 138 // Sort by creation time 139 sortFunc := func(a, b *DiscussionComment) bool { 140 return a.Created.Before(b.Created) 141 } 142 sort.Slice(listing, func(i, j int) bool { 143 return sortFunc(listing[i].Self, listing[j].Self) 144 }) 145 for _, r := range listing { 146 sort.Slice(r.Replies, func(i, j int) bool { 147 return sortFunc(r.Replies[i], r.Replies[j]) 148 }) 149 } 150 151 return listing 152} 153 154// TotalComments returns the total number of comments 155func (d *Discussion) TotalComments() int { 156 return len(d.Comments) 157} 158 159// DiscussionPatch represents a patch added to a discussion 160// Key difference from PullSubmission: it has pushed_by_did 161type DiscussionPatch struct { 162 Id int64 163 DiscussionAt syntax.ATURI 164 PushedByDid string 165 PatchHash string 166 Patch string 167 Added time.Time 168 Removed *time.Time 169} 170 171// IsActive returns true if the patch hasn't been removed 172func (p *DiscussionPatch) IsActive() bool { 173 return p.Removed == nil 174} 175 176// CanRemove checks if the given user can remove this patch 177// A patch can be removed by: 178// 1. The person who pushed it 179// 2. Someone with edit permissions on the repo 180func (p *DiscussionPatch) CanRemove(userDid string, hasEditPerm bool) bool { 181 return p.PushedByDid == userDid || hasEditPerm 182} 183 184// DiscussionComment represents a comment on a discussion 185type DiscussionComment struct { 186 Id int64 187 Did string 188 Rkey string 189 DiscussionAt string 190 ReplyTo *string 191 Body string 192 Created time.Time 193 Edited *time.Time 194 Deleted *time.Time 195} 196 197const DiscussionCommentNSID = "sh.tangled.repo.discussion.comment" 198 199func (c *DiscussionComment) AtUri() syntax.ATURI { 200 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, DiscussionCommentNSID, c.Rkey)) 201} 202 203func (c *DiscussionComment) IsTopLevel() bool { 204 return c.ReplyTo == nil 205} 206 207func (c *DiscussionComment) IsReply() bool { 208 return c.ReplyTo != nil 209} 210 211// DiscussionCommentListItem represents a top-level comment with its replies 212type DiscussionCommentListItem struct { 213 Self *DiscussionComment 214 Replies []*DiscussionComment 215} 216 217// Participants returns all DIDs that participated in this comment thread 218func (item *DiscussionCommentListItem) Participants() []syntax.DID { 219 participantSet := make(map[syntax.DID]struct{}) 220 participants := []syntax.DID{} 221 222 addParticipant := func(did syntax.DID) { 223 if _, exists := participantSet[did]; !exists { 224 participantSet[did] = struct{}{} 225 participants = append(participants, did) 226 } 227 } 228 229 addParticipant(syntax.DID(item.Self.Did)) 230 231 for _, c := range item.Replies { 232 addParticipant(syntax.DID(c.Did)) 233 } 234 235 return participants 236} 237 238// DiscussionCount holds counts for different discussion states 239type DiscussionCount struct { 240 Open int 241 Merged int 242 Closed int 243}