A vibe coded tangled fork which supports pijul.
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}