| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package social |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | |
| 10 | "github.com/jackc/pgx/v5" |
| 11 | |
| 12 | "github.com/tenseleyFlow/shithub/internal/auth/audit" |
| 13 | socialdb "github.com/tenseleyFlow/shithub/internal/social/sqlc" |
| 14 | ) |
| 15 | |
| 16 | // WatchLevel mirrors the DB enum so handlers can pass typed values |
| 17 | // without importing socialdb. |
| 18 | type WatchLevel string |
| 19 | |
| 20 | const ( |
| 21 | WatchAll WatchLevel = "all" |
| 22 | WatchParticipating WatchLevel = "participating" |
| 23 | WatchIgnore WatchLevel = "ignore" |
| 24 | ) |
| 25 | |
| 26 | func (w WatchLevel) valid() bool { |
| 27 | switch w { |
| 28 | case WatchAll, WatchParticipating, WatchIgnore: |
| 29 | return true |
| 30 | } |
| 31 | return false |
| 32 | } |
| 33 | |
| 34 | // SetWatch upserts an explicit watch level. The handler authorizes |
| 35 | // via policy.Can(ActionWatchSet, repo) before calling. |
| 36 | func SetWatch(ctx context.Context, deps Deps, actorUserID, repoID int64, level WatchLevel) error { |
| 37 | if actorUserID == 0 { |
| 38 | return ErrNotLoggedIn |
| 39 | } |
| 40 | if !level.valid() { |
| 41 | return ErrInvalidWatchLevel |
| 42 | } |
| 43 | if err := socialdb.New().UpsertWatch(ctx, deps.Pool, socialdb.UpsertWatchParams{ |
| 44 | UserID: actorUserID, |
| 45 | RepoID: repoID, |
| 46 | Level: socialdb.WatchLevel(level), |
| 47 | }); err != nil { |
| 48 | return fmt.Errorf("upsert watch: %w", err) |
| 49 | } |
| 50 | if deps.Audit != nil { |
| 51 | _ = deps.Audit.Record(ctx, deps.Pool, actorUserID, |
| 52 | audit.ActionWatchSet, audit.TargetRepo, repoID, |
| 53 | map[string]any{"level": string(level)}) |
| 54 | } |
| 55 | return nil |
| 56 | } |
| 57 | |
| 58 | // UnsetWatch removes the explicit row, returning the user to the |
| 59 | // implicit `participating` default. |
| 60 | func UnsetWatch(ctx context.Context, deps Deps, actorUserID, repoID int64) error { |
| 61 | if actorUserID == 0 { |
| 62 | return ErrNotLoggedIn |
| 63 | } |
| 64 | if err := socialdb.New().DeleteWatch(ctx, deps.Pool, socialdb.DeleteWatchParams{ |
| 65 | UserID: actorUserID, RepoID: repoID, |
| 66 | }); err != nil { |
| 67 | return fmt.Errorf("delete watch: %w", err) |
| 68 | } |
| 69 | if deps.Audit != nil { |
| 70 | _ = deps.Audit.Record(ctx, deps.Pool, actorUserID, |
| 71 | audit.ActionWatchUnset, audit.TargetRepo, repoID, nil) |
| 72 | } |
| 73 | return nil |
| 74 | } |
| 75 | |
| 76 | // CurrentLevel returns the actor's current watch level for the repo, |
| 77 | // resolving the implicit `participating` default when no row exists. |
| 78 | func CurrentLevel(ctx context.Context, deps Deps, userID, repoID int64) (WatchLevel, error) { |
| 79 | if userID == 0 { |
| 80 | return WatchParticipating, nil |
| 81 | } |
| 82 | row, err := socialdb.New().GetWatch(ctx, deps.Pool, socialdb.GetWatchParams{ |
| 83 | UserID: userID, RepoID: repoID, |
| 84 | }) |
| 85 | if err != nil { |
| 86 | if errors.Is(err, pgx.ErrNoRows) { |
| 87 | return WatchParticipating, nil |
| 88 | } |
| 89 | return "", err |
| 90 | } |
| 91 | return WatchLevel(row.Level), nil |
| 92 | } |
| 93 |