A vibe coded tangled fork which supports pijul.
1package db
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "time"
8
9 "tangled.org/core/appview/models"
10)
11
12var (
13 ErrDomainTaken = errors.New("domain is already claimed by another user")
14 ErrDomainCooldown = errors.New("domain is in a 30-day cooldown period after being released")
15 ErrAlreadyClaimed = errors.New("you already have an active domain claim; release it before claiming a new one")
16)
17
18func scanClaim(row *sql.Row) (*models.DomainClaim, error) {
19 var claim models.DomainClaim
20 var deletedStr sql.NullString
21
22 if err := row.Scan(&claim.ID, &claim.Did, &claim.Domain, &deletedStr); err != nil {
23 return nil, err
24 }
25
26 if deletedStr.Valid {
27 t, err := time.Parse(time.RFC3339, deletedStr.String)
28 if err != nil {
29 return nil, fmt.Errorf("parsing deleted timestamp: %w", err)
30 }
31 claim.Deleted = &t
32 }
33
34 return &claim, nil
35}
36
37func GetDomainClaimByDomain(e Execer, domain string) (*models.DomainClaim, error) {
38 row := e.QueryRow(`
39 select id, did, domain, deleted
40 from domain_claims
41 where domain = ?
42 `, domain)
43 return scanClaim(row)
44}
45
46func GetActiveDomainClaimForDid(e Execer, did string) (*models.DomainClaim, error) {
47 row := e.QueryRow(`
48 select id, did, domain, deleted
49 from domain_claims
50 where did = ? and deleted is null
51 `, did)
52 return scanClaim(row)
53}
54
55func GetDomainClaimForDid(e Execer, did string) (*models.DomainClaim, error) {
56 row := e.QueryRow(`
57 select id, did, domain, deleted
58 from domain_claims
59 where did = ?
60 `, did)
61 return scanClaim(row)
62}
63
64func ClaimDomain(e Execer, did, domain string) error {
65 const cooldown = 30 * 24 * time.Hour
66
67 domainRow, err := GetDomainClaimByDomain(e, domain)
68 if err != nil && !errors.Is(err, sql.ErrNoRows) {
69 return fmt.Errorf("looking up domain: %w", err)
70 }
71
72 if domainRow != nil {
73 if domainRow.Did == did {
74 if domainRow.Deleted == nil {
75 return nil
76 }
77 if time.Since(*domainRow.Deleted) < cooldown {
78 return ErrDomainCooldown
79 }
80 _, err = e.Exec(`
81 update domain_claims set deleted = null where did = ? and domain = ?
82 `, did, domain)
83 return err
84 }
85
86 if domainRow.Deleted == nil {
87 return ErrDomainTaken
88 }
89 if time.Since(*domainRow.Deleted) < cooldown {
90 return ErrDomainCooldown
91 }
92
93 if _, err = e.Exec(`delete from domain_claims where domain = ?`, domain); err != nil {
94 return fmt.Errorf("clearing expired domain row: %w", err)
95 }
96 }
97
98 didRow, err := GetDomainClaimForDid(e, did)
99 if err != nil && !errors.Is(err, sql.ErrNoRows) {
100 return fmt.Errorf("looking up DID claim: %w", err)
101 }
102
103 if didRow == nil {
104 _, err = e.Exec(`
105 insert into domain_claims (did, domain) values (?, ?)
106 `, did, domain)
107 return err
108 }
109
110 if didRow.Deleted == nil {
111 return ErrAlreadyClaimed
112 }
113
114 _, err = e.Exec(`
115 update domain_claims set domain = ?, deleted = null where did = ?
116 `, domain, did)
117 return err
118}
119
120func ReleaseDomain(e Execer, did, domain string) error {
121 result, err := e.Exec(`
122 update domain_claims
123 set deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
124 where did = ? and domain = ? and deleted is null
125 `, did, domain)
126 if err != nil {
127 return err
128 }
129
130 n, err := result.RowsAffected()
131 if err != nil {
132 return err
133 }
134 if n == 0 {
135 return errors.New("domain not found or not actively claimed by this account")
136 }
137 return nil
138}
139
140// GetRepoSiteConfig returns the site configuration for a repo, or nil if not configured.
141func GetRepoSiteConfig(e Execer, repoAt string) (*models.RepoSite, error) {
142 row := e.QueryRow(`
143 select id, repo_at, branch, dir, is_index, created, updated
144 from repo_sites
145 where repo_at = ?
146 `, repoAt)
147
148 var s models.RepoSite
149 var isIndex int
150 var createdStr, updatedStr string
151
152 err := row.Scan(&s.ID, &s.RepoAt, &s.Branch, &s.Dir, &isIndex, &createdStr, &updatedStr)
153 if errors.Is(err, sql.ErrNoRows) {
154 return nil, nil
155 }
156 if err != nil {
157 return nil, err
158 }
159
160 s.IsIndex = isIndex != 0
161
162 s.Created, err = time.Parse(time.RFC3339, createdStr)
163 if err != nil {
164 return nil, fmt.Errorf("parsing created timestamp: %w", err)
165 }
166
167 s.Updated, err = time.Parse(time.RFC3339, updatedStr)
168 if err != nil {
169 return nil, fmt.Errorf("parsing updated timestamp: %w", err)
170 }
171
172 return &s, nil
173}
174
175// SetRepoSiteConfig inserts or replaces the site configuration for a repo.
176func SetRepoSiteConfig(e Execer, repoAt, branch, dir string, isIndex bool) error {
177 isIndexInt := 0
178 if isIndex {
179 isIndexInt = 1
180 }
181
182 _, err := e.Exec(`
183 insert into repo_sites (repo_at, branch, dir, is_index, updated)
184 values (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
185 on conflict(repo_at) do update set
186 branch = excluded.branch,
187 dir = excluded.dir,
188 is_index = excluded.is_index,
189 updated = excluded.updated
190 `, repoAt, branch, dir, isIndexInt)
191 return err
192}
193
194// DeleteRepoSiteConfig removes the site configuration for a repo.
195func DeleteRepoSiteConfig(e Execer, repoAt string) error {
196 _, err := e.Exec(`delete from repo_sites where repo_at = ?`, repoAt)
197 return err
198}