A vibe coded tangled fork which supports pijul.
at sl/tap-appview 286 lines 6.4 kB view raw
1package models 2 3import ( 4 "fmt" 5 "sort" 6 "strings" 7 "time" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/appview/pages/markup/sanitizer" 12) 13 14type Issue struct { 15 Id int64 16 Did string 17 Rkey string 18 RepoAt syntax.ATURI 19 IssueId int 20 Created time.Time 21 Edited *time.Time 22 Deleted *time.Time 23 Title string 24 Body string 25 Open bool 26 Mentions []syntax.DID 27 References []syntax.ATURI 28 29 // optionally, populate this when querying for reverse mappings 30 // like comment counts, parent repo etc. 31 Comments []IssueComment 32 Labels LabelState 33 Repo *Repo 34} 35 36func (i *Issue) AtUri() syntax.ATURI { 37 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey)) 38} 39 40func (i *Issue) AsRecord() tangled.RepoIssue { 41 mentions := make([]string, len(i.Mentions)) 42 for i, did := range i.Mentions { 43 mentions[i] = string(did) 44 } 45 references := make([]string, len(i.References)) 46 for i, uri := range i.References { 47 references[i] = string(uri) 48 } 49 return tangled.RepoIssue{ 50 Repo: i.RepoAt.String(), 51 Title: i.Title, 52 Body: &i.Body, 53 Mentions: mentions, 54 References: references, 55 CreatedAt: i.Created.Format(time.RFC3339), 56 } 57} 58 59func (i *Issue) State() string { 60 if i.Open { 61 return "open" 62 } 63 return "closed" 64} 65 66var _ Validator = new(Issue) 67 68func (i *Issue) Validate() error { 69 if i.Title == "" { 70 return fmt.Errorf("issue title is empty") 71 } 72 if i.Body == "" { 73 return fmt.Errorf("issue body is empty") 74 } 75 76 if st := strings.TrimSpace(sanitizer.SanitizeDescription(i.Title)); st == "" { 77 return fmt.Errorf("title is empty after HTML sanitization") 78 } 79 80 if st := strings.TrimSpace(sanitizer.SanitizeDefault(i.Body)); st == "" { 81 return fmt.Errorf("body is empty after HTML sanitization") 82 } 83 return nil 84} 85 86type CommentListItem struct { 87 Self *IssueComment 88 Replies []*IssueComment 89} 90 91func (it *CommentListItem) Participants() []syntax.DID { 92 participantSet := make(map[syntax.DID]struct{}) 93 participants := []syntax.DID{} 94 95 addParticipant := func(did syntax.DID) { 96 if _, exists := participantSet[did]; !exists { 97 participantSet[did] = struct{}{} 98 participants = append(participants, did) 99 } 100 } 101 102 addParticipant(syntax.DID(it.Self.Did)) 103 104 for _, c := range it.Replies { 105 addParticipant(syntax.DID(c.Did)) 106 } 107 108 return participants 109} 110 111func (i *Issue) CommentList() []CommentListItem { 112 // Create a map to quickly find comments by their aturi 113 toplevel := make(map[string]*CommentListItem) 114 var replies []*IssueComment 115 116 // collect top level comments into the map 117 for _, comment := range i.Comments { 118 if comment.IsTopLevel() { 119 toplevel[comment.AtUri().String()] = &CommentListItem{ 120 Self: &comment, 121 } 122 } else { 123 replies = append(replies, &comment) 124 } 125 } 126 127 for _, r := range replies { 128 parentAt := *r.ReplyTo 129 if parent, exists := toplevel[parentAt]; exists { 130 parent.Replies = append(parent.Replies, r) 131 } 132 } 133 134 var listing []CommentListItem 135 for _, v := range toplevel { 136 listing = append(listing, *v) 137 } 138 139 // sort everything 140 sortFunc := func(a, b *IssueComment) bool { 141 return a.Created.Before(b.Created) 142 } 143 sort.Slice(listing, func(i, j int) bool { 144 return sortFunc(listing[i].Self, listing[j].Self) 145 }) 146 for _, r := range listing { 147 sort.Slice(r.Replies, func(i, j int) bool { 148 return sortFunc(r.Replies[i], r.Replies[j]) 149 }) 150 } 151 152 return listing 153} 154 155func (i *Issue) Participants() []string { 156 participantSet := make(map[string]struct{}) 157 participants := []string{} 158 159 addParticipant := func(did string) { 160 if _, exists := participantSet[did]; !exists { 161 participantSet[did] = struct{}{} 162 participants = append(participants, did) 163 } 164 } 165 166 addParticipant(i.Did) 167 168 for _, c := range i.Comments { 169 addParticipant(c.Did) 170 } 171 172 return participants 173} 174 175func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { 176 created, err := time.Parse(time.RFC3339, record.CreatedAt) 177 if err != nil { 178 created = time.Now() 179 } 180 181 body := "" 182 if record.Body != nil { 183 body = *record.Body 184 } 185 186 return Issue{ 187 RepoAt: syntax.ATURI(record.Repo), 188 Did: did, 189 Rkey: rkey, 190 Created: created, 191 Title: record.Title, 192 Body: body, 193 Open: true, // new issues are open by default 194 } 195} 196 197type IssueComment struct { 198 Id int64 199 Did string 200 Rkey string 201 IssueAt string 202 ReplyTo *string 203 Body string 204 Created time.Time 205 Edited *time.Time 206 Deleted *time.Time 207 Mentions []syntax.DID 208 References []syntax.ATURI 209} 210 211func (i *IssueComment) AtUri() syntax.ATURI { 212 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey)) 213} 214 215func (i *IssueComment) AsRecord() tangled.RepoIssueComment { 216 mentions := make([]string, len(i.Mentions)) 217 for i, did := range i.Mentions { 218 mentions[i] = string(did) 219 } 220 references := make([]string, len(i.References)) 221 for i, uri := range i.References { 222 references[i] = string(uri) 223 } 224 return tangled.RepoIssueComment{ 225 Body: i.Body, 226 Issue: i.IssueAt, 227 CreatedAt: i.Created.Format(time.RFC3339), 228 ReplyTo: i.ReplyTo, 229 Mentions: mentions, 230 References: references, 231 } 232} 233 234func (i *IssueComment) IsTopLevel() bool { 235 return i.ReplyTo == nil 236} 237 238func (i *IssueComment) IsReply() bool { 239 return i.ReplyTo != nil 240} 241 242var _ Validator = new(IssueComment) 243 244func (i *IssueComment) Validate() error { 245 if sb := strings.TrimSpace(sanitizer.SanitizeDefault(i.Body)); sb == "" { 246 return fmt.Errorf("body is empty after HTML sanitization") 247 } 248 249 return nil 250} 251 252func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 253 created, err := time.Parse(time.RFC3339, record.CreatedAt) 254 if err != nil { 255 created = time.Now() 256 } 257 258 ownerDid := did 259 260 if _, err = syntax.ParseATURI(record.Issue); err != nil { 261 return nil, err 262 } 263 264 i := record 265 mentions := make([]syntax.DID, len(record.Mentions)) 266 for i, did := range record.Mentions { 267 mentions[i] = syntax.DID(did) 268 } 269 references := make([]syntax.ATURI, len(record.References)) 270 for i, uri := range i.References { 271 references[i] = syntax.ATURI(uri) 272 } 273 274 comment := IssueComment{ 275 Did: ownerDid, 276 Rkey: rkey, 277 Body: record.Body, 278 IssueAt: record.Issue, 279 ReplyTo: record.ReplyTo, 280 Created: created, 281 Mentions: mentions, 282 References: references, 283 } 284 285 return &comment, nil 286}