this repo has no description
1package db
2
3import (
4 "database/sql"
5 "fmt"
6 "log"
7 "time"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "tangled.sh/tangled.sh/core/api/tangled"
11)
12
13type RepoEvent struct {
14 Repo *Repo
15 Source *Repo
16}
17
18type ProfileTimeline struct {
19 ByMonth []ByMonth
20}
21
22type ByMonth struct {
23 RepoEvents []RepoEvent
24 IssueEvents IssueEvents
25 PullEvents PullEvents
26}
27
28func (b ByMonth) IsEmpty() bool {
29 return len(b.RepoEvents) == 0 &&
30 len(b.IssueEvents.Items) == 0 &&
31 len(b.PullEvents.Items) == 0
32}
33
34type IssueEvents struct {
35 Items []*Issue
36}
37
38type IssueEventStats struct {
39 Open int
40 Closed int
41}
42
43func (i IssueEvents) Stats() IssueEventStats {
44 var open, closed int
45 for _, issue := range i.Items {
46 if issue.Open {
47 open += 1
48 } else {
49 closed += 1
50 }
51 }
52
53 return IssueEventStats{
54 Open: open,
55 Closed: closed,
56 }
57}
58
59type PullEvents struct {
60 Items []*Pull
61}
62
63func (p PullEvents) Stats() PullEventStats {
64 var open, merged, closed int
65 for _, pull := range p.Items {
66 switch pull.State {
67 case PullOpen:
68 open += 1
69 case PullMerged:
70 merged += 1
71 case PullClosed:
72 closed += 1
73 }
74 }
75
76 return PullEventStats{
77 Open: open,
78 Merged: merged,
79 Closed: closed,
80 }
81}
82
83type PullEventStats struct {
84 Closed int
85 Open int
86 Merged int
87}
88
89const TimeframeMonths = 7
90
91func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) {
92 timeline := ProfileTimeline{
93 ByMonth: make([]ByMonth, TimeframeMonths),
94 }
95 currentMonth := time.Now().Month()
96 timeframe := fmt.Sprintf("-%d months", TimeframeMonths)
97
98 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe)
99 if err != nil {
100 return nil, fmt.Errorf("error getting pulls by owner did: %w", err)
101 }
102
103 // group pulls by month
104 for _, pull := range pulls {
105 pullMonth := pull.Created.Month()
106
107 if currentMonth-pullMonth >= TimeframeMonths {
108 // shouldn't happen; but times are weird
109 continue
110 }
111
112 idx := currentMonth - pullMonth
113 items := &timeline.ByMonth[idx].PullEvents.Items
114
115 *items = append(*items, &pull)
116 }
117
118 issues, err := GetIssuesByOwnerDid(e, forDid, timeframe)
119 if err != nil {
120 return nil, fmt.Errorf("error getting issues by owner did: %w", err)
121 }
122
123 for _, issue := range issues {
124 issueMonth := issue.Created.Month()
125
126 if currentMonth-issueMonth >= TimeframeMonths {
127 // shouldn't happen; but times are weird
128 continue
129 }
130
131 idx := currentMonth - issueMonth
132 items := &timeline.ByMonth[idx].IssueEvents.Items
133
134 *items = append(*items, &issue)
135 }
136
137 repos, err := GetAllReposByDid(e, forDid)
138 if err != nil {
139 return nil, fmt.Errorf("error getting all repos by did: %w", err)
140 }
141
142 for _, repo := range repos {
143 // TODO: get this in the original query; requires COALESCE because nullable
144 var sourceRepo *Repo
145 if repo.Source != "" {
146 sourceRepo, err = GetRepoByAtUri(e, repo.Source)
147 if err != nil {
148 return nil, err
149 }
150 }
151
152 repoMonth := repo.Created.Month()
153
154 if currentMonth-repoMonth >= TimeframeMonths {
155 // shouldn't happen; but times are weird
156 continue
157 }
158
159 idx := currentMonth - repoMonth
160
161 items := &timeline.ByMonth[idx].RepoEvents
162 *items = append(*items, RepoEvent{
163 Repo: &repo,
164 Source: sourceRepo,
165 })
166 }
167
168 return &timeline, nil
169}
170
171type Profile struct {
172 // ids
173 ID int
174 Did string
175
176 // data
177 Description string
178 IncludeBluesky bool
179 Location string
180 Links [5]string
181 Stats [2]VanityStat
182 PinnedRepos [6]syntax.ATURI
183}
184
185func (p Profile) IsLinksEmpty() bool {
186 for _, l := range p.Links {
187 if l != "" {
188 return false
189 }
190 }
191 return true
192}
193
194func (p Profile) IsStatsEmpty() bool {
195 for _, s := range p.Stats {
196 if s.Kind != "" {
197 return false
198 }
199 }
200 return true
201}
202
203func (p Profile) IsPinnedReposEmpty() bool {
204 for _, r := range p.PinnedRepos {
205 if r != "" {
206 return false
207 }
208 }
209 return true
210}
211
212type VanityStatKind string
213
214const (
215 VanityStatMergedPRCount VanityStatKind = "merged-pull-request-count"
216 VanityStatClosedPRCount VanityStatKind = "closed-pull-request-count"
217 VanityStatOpenPRCount VanityStatKind = "open-pull-request-count"
218 VanityStatOpenIssueCount VanityStatKind = "open-issue-count"
219 VanityStatClosedIssueCount VanityStatKind = "closed-issue-count"
220 VanityStatRepositoryCount VanityStatKind = "repository-count"
221)
222
223func (v VanityStatKind) String() string {
224 switch v {
225 case VanityStatMergedPRCount:
226 return "Merged PRs"
227 case VanityStatClosedPRCount:
228 return "Closed PRs"
229 case VanityStatOpenPRCount:
230 return "Open PRs"
231 case VanityStatOpenIssueCount:
232 return "Open Issues"
233 case VanityStatClosedIssueCount:
234 return "Closed Issues"
235 case VanityStatRepositoryCount:
236 return "Repositories"
237 }
238 return ""
239}
240
241type VanityStat struct {
242 Kind VanityStatKind
243 Value uint64
244}
245
246func (p *Profile) ProfileAt() syntax.ATURI {
247 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.ActorProfileNSID, "self"))
248}
249
250func UpsertProfile(tx *sql.Tx, profile *Profile) error {
251 defer tx.Rollback()
252
253 // update links
254 _, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did)
255 if err != nil {
256 return err
257 }
258 // update vanity stats
259 _, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did)
260 if err != nil {
261 return err
262 }
263
264 // update pinned repos
265 _, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did)
266 if err != nil {
267 return err
268 }
269
270 includeBskyValue := 0
271 if profile.IncludeBluesky {
272 includeBskyValue = 1
273 }
274
275 _, err = tx.Exec(
276 `insert or replace into profile (
277 did,
278 description,
279 include_bluesky,
280 location
281 )
282 values (?, ?, ?, ?)`,
283 profile.Did,
284 profile.Description,
285 includeBskyValue,
286 profile.Location,
287 )
288
289 if err != nil {
290 log.Println("profile", "err", err)
291 return err
292 }
293
294 for _, link := range profile.Links {
295 if link == "" {
296 continue
297 }
298
299 _, err := tx.Exec(
300 `insert into profile_links (did, link) values (?, ?)`,
301 profile.Did,
302 link,
303 )
304
305 if err != nil {
306 log.Println("profile_links", "err", err)
307 return err
308 }
309 }
310
311 for _, v := range profile.Stats {
312 if v.Kind == "" {
313 continue
314 }
315
316 _, err := tx.Exec(
317 `insert into profile_stats (did, kind) values (?, ?)`,
318 profile.Did,
319 v.Kind,
320 )
321
322 if err != nil {
323 log.Println("profile_stats", "err", err)
324 return err
325 }
326 }
327
328 for _, pin := range profile.PinnedRepos {
329 if pin == "" {
330 continue
331 }
332
333 _, err := tx.Exec(
334 `insert into profile_pinned_repositories (did, at_uri) values (?, ?)`,
335 profile.Did,
336 pin,
337 )
338
339 if err != nil {
340 log.Println("profile_pinned_repositories")
341 return err
342 }
343 }
344
345 return tx.Commit()
346}
347
348func GetProfile(e Execer, did string) (*Profile, error) {
349 var profile Profile
350 profile.Did = did
351
352 includeBluesky := 0
353 err := e.QueryRow(
354 `select description, include_bluesky, location from profile where did = ?`,
355 did,
356 ).Scan(&profile.Description, &includeBluesky, &profile.Location)
357 if err == sql.ErrNoRows {
358 profile := Profile{}
359 profile.Did = did
360 return &profile, nil
361 }
362
363 if err != nil {
364 return nil, err
365 }
366
367 if includeBluesky != 0 {
368 profile.IncludeBluesky = true
369 }
370
371 rows, err := e.Query(`select link from profile_links where did = ?`, did)
372 if err != nil {
373 return nil, err
374 }
375 defer rows.Close()
376 i := 0
377 for rows.Next() {
378 if err := rows.Scan(&profile.Links[i]); err != nil {
379 return nil, err
380 }
381 i++
382 }
383
384 rows, err = e.Query(`select kind from profile_stats where did = ?`, did)
385 if err != nil {
386 return nil, err
387 }
388 defer rows.Close()
389 i = 0
390 for rows.Next() {
391 if err := rows.Scan(&profile.Stats[i].Kind); err != nil {
392 return nil, err
393 }
394 value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind)
395 if err != nil {
396 return nil, err
397 }
398 profile.Stats[i].Value = value
399 i++
400 }
401
402 rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did)
403 if err != nil {
404 return nil, err
405 }
406 defer rows.Close()
407 i = 0
408 for rows.Next() {
409 if err := rows.Scan(&profile.PinnedRepos[i]); err != nil {
410 return nil, err
411 }
412 i++
413 }
414
415 return &profile, nil
416}
417
418func GetVanityStat(e Execer, did string, stat VanityStatKind) (uint64, error) {
419 query := ""
420 var args []any
421 switch stat {
422 case VanityStatMergedPRCount:
423 query = `select count(id) from pulls where owner_did = ? and state = ?`
424 args = append(args, did, PullMerged)
425 case VanityStatClosedPRCount:
426 query = `select count(id) from pulls where owner_did = ? and state = ?`
427 args = append(args, did, PullClosed)
428 case VanityStatOpenPRCount:
429 query = `select count(id) from pulls where owner_did = ? and state = ?`
430 args = append(args, did, PullOpen)
431 case VanityStatOpenIssueCount:
432 query = `select count(id) from issues where owner_did = ? and open = 1`
433 args = append(args, did)
434 case VanityStatClosedIssueCount:
435 query = `select count(id) from issues where owner_did = ? and open = 0`
436 args = append(args, did)
437 case VanityStatRepositoryCount:
438 query = `select count(id) from repos where did = ?`
439 args = append(args, did)
440 }
441
442 var result uint64
443 err := e.QueryRow(query, args...).Scan(&result)
444 if err != nil {
445 return 0, err
446 }
447
448 return result, nil
449}