A vibe coded tangled fork which supports pijul.
at sl/tap-appview 291 lines 5.9 kB view raw
1package models 2 3import ( 4 "fmt" 5 6 "github.com/bluesky-social/indigo/atproto/syntax" 7 "tangled.org/core/api/tangled" 8) 9 10type Profile struct { 11 // ids 12 ID int 13 Did string 14 15 // data 16 Avatar string // CID of the avatar blob 17 Description string 18 IncludeBluesky bool 19 Location string 20 Links [5]string 21 Stats [2]VanityStat 22 PinnedRepos [6]syntax.ATURI 23 Pronouns string 24} 25 26func (p Profile) IsLinksEmpty() bool { 27 for _, l := range p.Links { 28 if l != "" { 29 return false 30 } 31 } 32 return true 33} 34 35func (p Profile) IsStatsEmpty() bool { 36 for _, s := range p.Stats { 37 if s.Kind != "" { 38 return false 39 } 40 } 41 return true 42} 43 44func (p Profile) IsPinnedReposEmpty() bool { 45 for _, r := range p.PinnedRepos { 46 if r != "" { 47 return false 48 } 49 } 50 return true 51} 52 53// ProfileFromRecord will validate the atproto record and convert it to [Profile]. 54// It can return error for invalid records. 55func ProfileFromRecord(did syntax.DID, record tangled.ActorProfile) (Profile, error) { 56 // validate atproto record 57 if err := func(record tangled.ActorProfile) error { 58 // validate description 59 if record.Description != nil && len(*record.Description) > 256 { 60 return fmt.Errorf("bio is too long") 61 } 62 63 // validate links 64 if len(record.Links) > 5 { 65 return fmt.Errorf("links cannot be more than 5") 66 } 67 68 // validate location 69 if record.Location != nil && len(*record.Location) > 256 { 70 return fmt.Errorf("location is too long") 71 } 72 73 // validate pinnedRepositories 74 if len(record.PinnedRepositories) >= 5 { 75 return fmt.Errorf("pinnedRepositories cannot be more than 6") 76 } 77 for i, v := range record.PinnedRepositories { 78 if _, err := syntax.ParseATURI(v); err != nil { 79 return fmt.Errorf("invalid at-uri at pinnedRepositories[%d]: %w", i, err) 80 } 81 } 82 83 // validate pronouns 84 if record.Pronouns != nil && len(*record.Pronouns) > 40 { 85 return fmt.Errorf("pronouns are too long") 86 } 87 88 // validate stats 89 if len(record.Stats) > 2 { 90 return fmt.Errorf("stats cannot be more than 2") 91 } 92 for i, v := range record.Stats { 93 if VanityStatKind(v).String() == "" { 94 return fmt.Errorf("unknown stat kind '%s' at stats[%d]", v, i) 95 } 96 } 97 return nil 98 }(record); err != nil { 99 return Profile{}, fmt.Errorf("invalid record %T: %w", record, err) 100 } 101 102 p := Profile{Did: did.String()} 103 104 if record.Description != nil { 105 p.Description = *record.Description 106 } 107 108 p.IncludeBluesky = record.Bluesky 109 110 if record.Location != nil { 111 p.Location = *record.Location 112 } 113 114 copy(p.Links[:], record.Links) 115 116 for i, s := range record.Stats { 117 if i >= 2 { 118 break 119 } 120 p.Stats[i].Kind = VanityStatKind(s) 121 } 122 123 for i, r := range record.PinnedRepositories { 124 if i >= 6 { 125 break 126 } 127 p.PinnedRepos[i] = syntax.ATURI(r) 128 } 129 130 if record.Pronouns != nil { 131 p.Pronouns = *record.Pronouns 132 } 133 134 return p, nil 135} 136 137type VanityStatKind string 138 139const ( 140 VanityStatMergedPRCount VanityStatKind = "merged-pull-request-count" 141 VanityStatClosedPRCount VanityStatKind = "closed-pull-request-count" 142 VanityStatOpenPRCount VanityStatKind = "open-pull-request-count" 143 VanityStatOpenIssueCount VanityStatKind = "open-issue-count" 144 VanityStatClosedIssueCount VanityStatKind = "closed-issue-count" 145 VanityStatRepositoryCount VanityStatKind = "repository-count" 146 VanityStatStarCount VanityStatKind = "star-count" 147 VanityStatNone VanityStatKind = "" 148) 149 150func ParseVanityStatKind(s string) VanityStatKind { 151 switch s { 152 case "merged-pull-request-count": 153 return VanityStatMergedPRCount 154 case "closed-pull-request-count": 155 return VanityStatClosedPRCount 156 case "open-pull-request-count": 157 return VanityStatOpenPRCount 158 case "open-issue-count": 159 return VanityStatOpenIssueCount 160 case "closed-issue-count": 161 return VanityStatClosedIssueCount 162 case "repository-count": 163 return VanityStatRepositoryCount 164 case "star-count": 165 return VanityStatStarCount 166 default: 167 return VanityStatNone 168 } 169} 170 171func (v VanityStatKind) String() string { 172 switch v { 173 case VanityStatMergedPRCount: 174 return "Merged PRs" 175 case VanityStatClosedPRCount: 176 return "Closed PRs" 177 case VanityStatOpenPRCount: 178 return "Open PRs" 179 case VanityStatOpenIssueCount: 180 return "Open Issues" 181 case VanityStatClosedIssueCount: 182 return "Closed Issues" 183 case VanityStatRepositoryCount: 184 return "Repositories" 185 case VanityStatStarCount: 186 return "Stars Received" 187 default: 188 return "" 189 } 190} 191 192type VanityStat struct { 193 Kind VanityStatKind 194 Value uint64 195} 196 197func (p *Profile) ProfileAt() syntax.ATURI { 198 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.ActorProfileNSID, "self")) 199} 200 201type RepoEvent struct { 202 Repo *Repo 203 Source *Repo 204} 205 206type ProfileTimeline struct { 207 ByMonth []ByMonth 208} 209 210func (p *ProfileTimeline) IsEmpty() bool { 211 if p == nil { 212 return true 213 } 214 215 for _, m := range p.ByMonth { 216 if !m.IsEmpty() { 217 return false 218 } 219 } 220 221 return true 222} 223 224type ByMonth struct { 225 Commits int 226 RepoEvents []RepoEvent 227 IssueEvents IssueEvents 228 PullEvents PullEvents 229} 230 231func (b ByMonth) IsEmpty() bool { 232 return len(b.RepoEvents) == 0 && 233 len(b.IssueEvents.Items) == 0 && 234 len(b.PullEvents.Items) == 0 && 235 b.Commits == 0 236} 237 238type IssueEvents struct { 239 Items []*Issue 240} 241 242type IssueEventStats struct { 243 Open int 244 Closed int 245} 246 247func (i IssueEvents) Stats() IssueEventStats { 248 var open, closed int 249 for _, issue := range i.Items { 250 if issue.Open { 251 open += 1 252 } else { 253 closed += 1 254 } 255 } 256 257 return IssueEventStats{ 258 Open: open, 259 Closed: closed, 260 } 261} 262 263type PullEvents struct { 264 Items []*Pull 265} 266 267func (p PullEvents) Stats() PullEventStats { 268 var open, merged, closed int 269 for _, pull := range p.Items { 270 switch pull.State { 271 case PullOpen: 272 open += 1 273 case PullMerged: 274 merged += 1 275 case PullClosed: 276 closed += 1 277 } 278 } 279 280 return PullEventStats{ 281 Open: open, 282 Merged: merged, 283 Closed: closed, 284 } 285} 286 287type PullEventStats struct { 288 Closed int 289 Open int 290 Merged int 291}