Antonio Mika
·
2026-10-25
1package db
2
3import (
4 "database/sql"
5 "database/sql/driver"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "regexp"
10 "strings"
11 "time"
12)
13
14var ErrNameTaken = errors.New("username has already been claimed")
15var ErrNameDenied = errors.New("username is on the denylist")
16var ErrNameInvalid = errors.New("username has invalid characters in it")
17var ErrPublicKeyTaken = errors.New("public key is already associated with another user")
18var ErrPostChanged = errors.New("post changed while it was being updated")
19
20// sqlite uses string to BLOB type and postgres uses []uint8 for JSONB.
21func tcast(value any) ([]byte, error) {
22 switch val := value.(type) {
23 // sqlite3 BLOB
24 case string:
25 return []byte(val), nil
26 // postgres JSONB: []uint8
27 default:
28 b, ok := val.([]byte)
29 if !ok {
30 return []byte{}, errors.New("type assertion to []byte failed")
31 }
32 return b, nil
33 }
34}
35
36type BlockSignups struct {
37 ID int64 `db:"id"`
38 IP string `db:"ip"`
39 Reason string `db:"reason"`
40 CreatedAt *time.Time `db:"created_at"`
41}
42
43type PublicKey struct {
44 ID string `json:"id" db:"id"`
45 UserID string `json:"user_id" db:"user_id"`
46 Name string `json:"name" db:"name"`
47 Key string `json:"public_key" db:"public_key"`
48 CreatedAt *time.Time `json:"created_at" db:"created_at"`
49}
50
51type User struct {
52 ID string `json:"id" db:"id"`
53 Name string `json:"name" db:"name"`
54 PublicKey *PublicKey `json:"public_key,omitempty" db:"public_key,omitempty"`
55 CreatedAt *time.Time `json:"created_at" db:"created_at"`
56}
57
58type PostData struct {
59 ImgPath string `json:"img_path"`
60 LastDigest *time.Time `json:"last_digest"`
61 Attempts int `json:"attempts"`
62}
63
64// Make the Attrs struct implement the driver.Valuer interface. This method
65// simply returns the JSON-encoded representation of the struct.
66func (p PostData) Value() (driver.Value, error) {
67 return json.Marshal(p)
68}
69
70// Make the Attrs struct implement the sql.Scanner interface. This method
71// simply decodes a JSON-encoded value into the struct fields.
72func (p *PostData) Scan(value any) error {
73 b, err := tcast(value)
74 if err != nil {
75 return err
76 }
77
78 return json.Unmarshal(b, &p)
79}
80
81type Project struct {
82 ID string `json:"id" db:"id"`
83 UserID string `json:"user_id" db:"user_id"`
84 Name string `json:"name" db:"name"`
85 ProjectDir string `json:"project_dir" db:"project_dir"`
86 Username string `json:"username" db:"username"`
87 Acl ProjectAcl `json:"acl" db:"acl"`
88 Blocked string `json:"blocked" db:"blocked"`
89 CreatedAt *time.Time `json:"created_at" db:"created_at"`
90 UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
91}
92
93type ProjectAcl struct {
94 Type string `json:"type" db:"type"` // public, pico, pubkeys, private, http-pass
95 Data []string `json:"data" db:"data"`
96}
97
98// Make the Attrs struct implement the driver.Valuer interface. This method
99// simply returns the JSON-encoded representation of the struct.
100func (p ProjectAcl) Value() (driver.Value, error) {
101 return json.Marshal(p)
102}
103
104// Make the Attrs struct implement the sql.Scanner interface. This method
105// simply decodes a JSON-encoded value into the struct fields.
106func (p *ProjectAcl) Scan(value any) error {
107 b, err := tcast(value)
108 if err != nil {
109 return err
110 }
111 return json.Unmarshal(b, &p)
112}
113
114type FeedItemData struct {
115 Title string `json:"title"`
116 Description string `json:"description"`
117 Content string `json:"content"`
118 Link string `json:"link"`
119 PublishedAt *time.Time `json:"published_at"`
120}
121
122// Make the Attrs struct implement the driver.Valuer interface. This method
123// simply returns the JSON-encoded representation of the struct.
124func (p FeedItemData) Value() (driver.Value, error) {
125 return json.Marshal(p)
126}
127
128// Make the Attrs struct implement the sql.Scanner interface. This method
129// simply decodes a JSON-encoded value into the struct fields.
130func (p *FeedItemData) Scan(value any) error {
131 b, err := tcast(value)
132 if err != nil {
133 return err
134 }
135
136 return json.Unmarshal(b, &p)
137}
138
139type Post struct {
140 ID string `json:"id" db:"id"`
141 UserID string `json:"user_id" db:"user_id"`
142 Filename string `json:"filename" db:"filename"`
143 Slug string `json:"slug" db:"slug"`
144 Title string `json:"title" db:"title"`
145 Text string `json:"text" db:"text"`
146 Description string `json:"description" db:"description"`
147 CreatedAt *time.Time `json:"created_at" db:"created_at"`
148 PublishAt *time.Time `json:"publish_at" db:"publish_at"`
149 Username string `json:"username" db:"name"`
150 UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
151 ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
152 Hidden bool `json:"hidden" db:"hidden"`
153 Views int `json:"views" db:"views"`
154 Space string `json:"space" db:"cur_space"`
155 Shasum string `json:"shasum" db:"shasum"`
156 FileSize int `json:"file_size" db:"file_size"`
157 MimeType string `json:"mime_type" db:"mime_type"`
158 Data PostData `json:"data" db:"data"`
159 Tags []string `json:"tags" db:"-"`
160
161 // computed
162 IsVirtual bool `db:"-"`
163 Score int `json:"score" db:"score"`
164}
165
166type Paginate[T any] struct {
167 Data []T
168}
169
170type VisitInterval struct {
171 Interval *time.Time `json:"interval" db:"interval"`
172 Visitors int `json:"visitors" db:"visitors"`
173 MobileVisitors int `json:"mobile_visitors" db:"mobile_visitors"`
174 DesktopVisitors int `json:"desktop_visitors" db:"desktop_visitors"`
175}
176
177type VisitUrl struct {
178 Url string `json:"url" db:"url"`
179 Count int `json:"count" db:"count"`
180}
181
182type SummaryOpts struct {
183 Interval string
184 Origin time.Time
185 Host string
186 Path string
187 UserID string
188 Limit int
189}
190
191type SummaryVisits struct {
192 Intervals []*VisitInterval `json:"intervals"`
193 TopUrls []*VisitUrl `json:"top_urls"`
194 NotFoundUrls []*VisitUrl `json:"not_found_urls"`
195 TopReferers []*VisitUrl `json:"top_referers"`
196}
197
198type PostAnalytics struct {
199 ID string `json:"id" db:"id"`
200 PostID string `json:"post_id" db:"post_id"`
201 Views int `json:"views" db:"views"`
202 UpdateAt *time.Time `json:"updated_at" db:"updated_at"`
203}
204
205type AnalyticsVisits struct {
206 ID string `json:"id" db:"id"`
207 UserID string `json:"user_id" db:"user_id"`
208 ProjectID string `json:"project_id" db:"project_id"`
209 PostID string `json:"post_id" db:"post_id"`
210 Namespace string `json:"namespace" db:"namespace"`
211 Host string `json:"host" db:"host"`
212 Path string `json:"path" db:"path"`
213 IpAddress string `json:"ip_address" db:"ip_address"`
214 UserAgent string `json:"user_agent" db:"user_agent"`
215 Referer string `json:"referer" db:"referer"`
216 Status int `json:"status" db:"status"`
217 ContentType string `json:"content_type" db:"content_type"`
218}
219
220// IsMobileUserAgent checks common mobile user-agent patterns.
221// TODO: replace with a proper UA parser library (e.g., mssola/useragent).
222func IsMobileUserAgent(userAgent string) bool {
223 ua := strings.ToLower(userAgent)
224 mobileKeywords := []string{
225 "mobile",
226 "android",
227 "iphone",
228 "ipad",
229 "ipod",
230 "blackberry",
231 "windows phone",
232 }
233 for _, kw := range mobileKeywords {
234 if strings.Contains(ua, kw) {
235 return true
236 }
237 }
238 return false
239}
240
241type AccessLogData struct{}
242
243func (p *AccessLogData) Scan(value any) error {
244 b, err := tcast(value)
245 if err != nil {
246 return err
247 }
248
249 return json.Unmarshal(b, &p)
250}
251
252type AccessLog struct {
253 ID string `json:"id" db:"id"`
254 UserID string `json:"user_id" db:"user_id"`
255 Service string `json:"service" db:"service"`
256 Pubkey string `json:"pubkey" db:"pubkey"`
257 Identity string `json:"identity" db:"identity"`
258 Data AccessLogData `json:"data" db:"data"`
259 CreatedAt *time.Time `json:"created_at" db:"created_at"`
260}
261
262type Pager struct {
263 Num int
264 Page int
265}
266
267type FeedItem struct {
268 ID string `json:"id" db:"id"`
269 PostID string `json:"post_id" db:"post_id"`
270 GUID string `json:"guid" db:"guid"`
271 Data FeedItemData `json:"data" db:"data"`
272 CreatedAt *time.Time `json:"created_at" db:"created_at"`
273}
274
275type Token struct {
276 ID string `json:"id" db:"id"`
277 UserID string `json:"user_id" db:"user_id"`
278 Name string `json:"name" db:"name"`
279 Token string `json:"token" db:"token"`
280 CreatedAt *time.Time `json:"created_at" db:"created_at"`
281 ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
282}
283
284type FormEntry struct {
285 ID string `json:"id" db:"id"`
286 UserID string `json:"-" db:"user_id"`
287 Name string `json:"-" db:"name"`
288 Data FormEntryData `json:"data" db:"data"`
289 CreatedAt *time.Time `json:"created_at" db:"created_at"`
290}
291
292type FormEntryData map[string]interface{}
293
294// Make the FormEntry struct implement the driver.Valuer interface.
295func (f FormEntryData) Value() (driver.Value, error) {
296 return json.Marshal(f)
297}
298
299// Make the FormEntry struct implement the sql.Scanner interface.
300func (f *FormEntryData) Scan(value any) error {
301 b, err := tcast(value)
302 if err != nil {
303 return err
304 }
305 return json.Unmarshal(b, &f)
306}
307
308type FeatureFlag struct {
309 ID string `json:"id" db:"id"`
310 UserID string `json:"user_id" db:"user_id"`
311 PaymentHistoryID sql.NullString `json:"payment_history_id" db:"payment_history_id"`
312 Name string `json:"name" db:"name"`
313 CreatedAt *time.Time `json:"created_at" db:"created_at"`
314 ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
315 Data FeatureFlagData `json:"data" db:"data"`
316}
317
318func NewFeatureFlag(userID, name string, storageMax uint64, fileMax int64, specialFileMax int64) *FeatureFlag {
319 return &FeatureFlag{
320 UserID: userID,
321 Name: name,
322 Data: FeatureFlagData{
323 StorageMax: storageMax,
324 FileMax: fileMax,
325 SpecialFileMax: specialFileMax,
326 },
327 }
328}
329
330func (ff *FeatureFlag) FindStorageMax(defaultSize uint64) uint64 {
331 if ff.Data.StorageMax == 0 {
332 return defaultSize
333 }
334 return ff.Data.StorageMax
335}
336
337func (ff *FeatureFlag) FindFileMax(defaultSize int64) int64 {
338 if ff.Data.FileMax == 0 {
339 return defaultSize
340 }
341 return ff.Data.FileMax
342}
343
344func (ff *FeatureFlag) FindSpecialFileMax(defaultSize int64) int64 {
345 if ff.Data.SpecialFileMax == 0 {
346 return defaultSize
347 }
348 return ff.Data.SpecialFileMax
349}
350
351func (ff *FeatureFlag) IsValid() bool {
352 if ff.ExpiresAt.IsZero() {
353 return false
354 }
355 return ff.ExpiresAt.After(time.Now())
356}
357
358type FeatureFlagData struct {
359 StorageMax uint64 `json:"storage_max" db:"storage_max"`
360 FileMax int64 `json:"file_max" db:"file_max"`
361 SpecialFileMax int64 `json:"special_file_max" db:"special_file_max"`
362}
363
364// Make the Attrs struct implement the driver.Valuer interface. This method
365// simply returns the JSON-encoded representation of the struct.
366func (p FeatureFlagData) Value() (driver.Value, error) {
367 return json.Marshal(p)
368}
369
370// Make the Attrs struct implement the sql.Scanner interface. This method
371// simply decodes a JSON-encoded value into the struct fields.
372func (p *FeatureFlagData) Scan(value any) error {
373 b, err := tcast(value)
374 if err != nil {
375 return err
376 }
377
378 return json.Unmarshal(b, &p)
379}
380
381type PaymentHistoryData struct {
382 Notes string `json:"notes"`
383 TxID string `json:"tx_id"`
384}
385
386// Make the Attrs struct implement the driver.Valuer interface. This method
387// simply returns the JSON-encoded representation of the struct.
388func (p PaymentHistoryData) Value() (driver.Value, error) {
389 return json.Marshal(p)
390}
391
392// Make the Attrs struct implement the sql.Scanner interface. This method
393// simply decodes a JSON-encoded value into the struct fields.
394func (p *PaymentHistoryData) Scan(value any) error {
395 b, err := tcast(value)
396 if err != nil {
397 return err
398 }
399
400 return json.Unmarshal(b, &p)
401}
402
403type ErrMultiplePublicKeys struct{}
404
405func (m *ErrMultiplePublicKeys) Error() string {
406 return "there are multiple users with this public key, you must provide the username when using SSH: `ssh <user>@<domain>`\n"
407}
408
409type UserStats struct {
410 Prose UserServiceStats
411 Pastes UserServiceStats
412 Feeds UserServiceStats
413 Pages UserServiceStats
414}
415
416type UserServiceStats struct {
417 Service string
418 Num int
419 FirstCreatedAt time.Time
420 LastestCreatedAt time.Time
421 LatestUpdatedAt time.Time
422}
423
424type TunsEventLog struct {
425 ID string `json:"id" db:"id"`
426 ServerID string `json:"server_id" db:"server_id"`
427 Time *time.Time `json:"time" db:"time"`
428 User string `json:"user" db:"user"`
429 UserId string `json:"user_id" db:"user_id"`
430 RemoteAddr string `json:"remote_addr" db:"remote_addr"`
431 EventType string `json:"event_type" db:"event_type"`
432 TunnelID string `json:"tunnel_id" db:"tunnel_id"`
433 TunnelType string `json:"tunnel_type" db:"tunnel_type"`
434 ConnectionType string `json:"connection_type" db:"connection_type"`
435 CreatedAt *time.Time `json:"created_at" db:"created_at"`
436}
437
438type PipeMonitor struct {
439 ID string `json:"id" db:"id"`
440 UserId string `json:"user_id" db:"user_id"`
441 Topic string `json:"topic" db:"topic"`
442 WindowDur time.Duration `json:"window_dur" db:"window_dur"`
443 WindowEnd *time.Time `json:"window_end" db:"window_end"`
444 LastPing *time.Time `json:"last_ping" db:"last_ping"`
445 CreatedAt *time.Time `json:"created_at" db:"created_at"`
446 UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
447}
448
449type PipeMonitorHistory struct {
450 ID string `json:"id" db:"id"`
451 MonitorID string `json:"monitor_id" db:"monitor_id"`
452 WindowDur time.Duration `json:"window_dur" db:"window_dur"`
453 WindowEnd *time.Time `json:"window_end" db:"window_end"`
454 LastPing *time.Time `json:"last_ping" db:"last_ping"`
455 CreatedAt *time.Time `json:"created_at" db:"created_at"`
456 UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
457}
458
459type UptimeResult struct {
460 TotalDuration time.Duration
461 UptimeDuration time.Duration
462 UptimePercent float64
463}
464
465type Invite struct {
466 ID string `db:"id"`
467 FromUserID string `db:"from_user_id"`
468 ToUserID string `db:"to_user_id"`
469 CreatedAt *time.Time `db:"created_at"`
470 FromUserName string `db:"from_user_name"`
471 ToUserName string `db:"to_user_name"`
472}
473
474func ComputeUptime(history []*PipeMonitorHistory, from, to time.Time) UptimeResult {
475 totalDuration := to.Sub(from)
476 if totalDuration <= 0 {
477 return UptimeResult{}
478 }
479
480 if len(history) == 0 {
481 return UptimeResult{TotalDuration: totalDuration}
482 }
483
484 type interval struct {
485 start, end time.Time
486 }
487
488 var intervals []interval
489 for _, h := range history {
490 if h.WindowEnd == nil {
491 continue
492 }
493 windowStart := h.WindowEnd.Add(-h.WindowDur)
494 windowEnd := *h.WindowEnd
495
496 if windowStart.Before(from) {
497 windowStart = from
498 }
499 if windowEnd.After(to) {
500 windowEnd = to
501 }
502
503 if windowStart.Before(windowEnd) {
504 intervals = append(intervals, interval{start: windowStart, end: windowEnd})
505 }
506 }
507
508 if len(intervals) == 0 {
509 return UptimeResult{TotalDuration: totalDuration}
510 }
511
512 // Sort by start time
513 for i := range intervals {
514 for j := i + 1; j < len(intervals); j++ {
515 if intervals[j].start.Before(intervals[i].start) {
516 intervals[i], intervals[j] = intervals[j], intervals[i]
517 }
518 }
519 }
520
521 // Merge overlapping intervals
522 merged := []interval{intervals[0]}
523 for _, curr := range intervals[1:] {
524 last := &merged[len(merged)-1]
525 if !curr.start.After(last.end) {
526 if curr.end.After(last.end) {
527 last.end = curr.end
528 }
529 } else {
530 merged = append(merged, curr)
531 }
532 }
533
534 var uptimeDuration time.Duration
535 for _, iv := range merged {
536 uptimeDuration += iv.end.Sub(iv.start)
537 }
538
539 uptimePercent := float64(uptimeDuration) / float64(totalDuration) * 100
540
541 return UptimeResult{
542 TotalDuration: totalDuration,
543 UptimeDuration: uptimeDuration,
544 UptimePercent: uptimePercent,
545 }
546}
547
548func (m *PipeMonitor) Status() error {
549 if m.LastPing == nil {
550 return fmt.Errorf("no ping received yet")
551 }
552 if m.WindowEnd == nil {
553 return fmt.Errorf("window end not set")
554 }
555 now := time.Now().UTC()
556 if now.After(*m.WindowEnd) {
557 return fmt.Errorf(
558 "window expired at %s",
559 m.WindowEnd.UTC().Format("2006-01-02 15:04:05Z"),
560 )
561 }
562 windowStart := m.WindowEnd.Add(-m.WindowDur)
563 lastPingAfterStart := !m.LastPing.Before(windowStart)
564 if !lastPingAfterStart {
565 return fmt.Errorf(
566 "last ping before window start: %s",
567 windowStart.UTC().Format("2006-01-02 15:04:05Z"),
568 )
569 }
570 return nil
571}
572
573func (m *PipeMonitor) GetNextWindow() *time.Time {
574 win := m.WindowEnd.Add(m.WindowDur)
575 return &win
576}
577
578var NameValidator = regexp.MustCompile("^[a-zA-Z0-9]{1,50}$")
579var DenyList = []string{
580 "admin",
581 "abuse",
582 "cgi",
583 "ops",
584 "help",
585 "spec",
586 "root",
587 "new",
588 "create",
589 "www",
590 "public",
591 "global",
592 "g",
593 "root",
594 "localhost",
595 "ams",
596 "ash",
597 "nue",
598}
599
600type UserDB interface {
601 RegisterUser(name, pubkey, comment, ip string) (*User, error)
602 FindUsers() ([]*User, error)
603 FindUserByName(name string) (*User, error)
604 FindUserByKey(name string, pubkey string) (*User, error)
605 FindUserByPubkey(pubkey string) (*User, error)
606 FindUser(userID string) (*User, error)
607 FindUserStats(userID string) (*UserStats, error)
608}
609
610// AuthDB is what an SSH server needs to authenticate and log its users.
611type AuthDB interface {
612 FindUser(userID string) (*User, error)
613 FindUserByPubkey(pubkey string) (*User, error)
614 FindUserByName(name string) (*User, error)
615 FindFeature(userID string, feature string) (*FeatureFlag, error)
616 InsertAccessLog(log *AccessLog) error
617}
618
619type KeyDB interface {
620 UpdatePublicKey(pubkeyID, name string) (*PublicKey, error)
621 InsertPublicKey(userID, pubkey, name string) error
622 FindKeysByUser(user *User) ([]*PublicKey, error)
623 RemoveKeys(pubkeyIDs []string) error
624}
625
626type TokenDB interface {
627 FindUserByToken(token string) (*User, error)
628 FindTokensByUser(userID string) ([]*Token, error)
629 InsertToken(userID, name string) (string, error)
630 UpsertToken(userID, name string) (string, error)
631 RemoveToken(tokenID string) error
632}
633
634// PostDB is the posts storage that prose, pastes and feeds share. Each
635// keeps its posts in its own space.
636type PostDB interface {
637 FindPostsByUser(pager *Pager, userID string, space string) (*Paginate[*Post], error)
638 FindAllPostsByUser(userID string, space string) ([]*Post, error)
639 FindPostWithFilename(filename string, userID string, space string) (*Post, error)
640 FindPostWithSlug(slug string, userID string, space string) (*Post, error)
641 FindPost(postID string) (*Post, error)
642 InsertPost(post *Post) (*Post, error)
643 UpdatePost(post *Post) (*Post, error)
644 // RenamePost moves cur to next's filename, with the slug, title,
645 // description, publish date, hidden flag, expiry, mime type, tags and
646 // aliases that name gives it, and removes replaced when it is set, all
647 // in one transaction. It returns ErrPostChanged when either post no
648 // longer matches what the caller read.
649 RenamePost(cur, next, replaced *Post, tags, aliases []string) (*Post, error)
650 RemovePosts(postIDs []string) error
651}
652
653type TagDB interface {
654 ReplaceTagsByPost(tags []string, postID string) error
655 ReplaceAliasesByPost(aliases []string, postID string) error
656}
657
658type AnalyticsDB interface {
659 InsertVisit(view *AnalyticsVisits) error
660 VisitSummary(opts *SummaryOpts) (*SummaryVisits, error)
661 FindVisitSiteList(opts *SummaryOpts) ([]*VisitUrl, error)
662 VisitUrlNotFound(opts *SummaryOpts) ([]*VisitUrl, error)
663}
664
665type FeatureDB interface {
666 AddPicoPlusUser(username, email, paymentType, txId string) error
667 AddFeatureUser(username, name string) error
668 FindFeature(userID string, feature string) (*FeatureFlag, error)
669 FindFeaturesByUser(userID string) ([]*FeatureFlag, error)
670 InsertFeature(userID, name string, expiresAt time.Time) (*FeatureFlag, error)
671 RemoveFeature(userID, names string) error
672}
673
674type ProjectDB interface {
675 UpsertProject(userID, name, projectDir string) (*Project, error)
676 FindProjectByName(userID, name string) (*Project, error)
677}
678
679type TunsDB interface {
680 InsertTunsEventLog(log *TunsEventLog) error
681 FindTunsEventLogs(userID string) ([]*TunsEventLog, error)
682 FindTunsEventLogsByAddr(userID, addr string) ([]*TunsEventLog, error)
683}
684
685type AccessLogDB interface {
686 InsertAccessLog(log *AccessLog) error
687 FindAccessLogs(userID string, fromDate *time.Time) ([]*AccessLog, error)
688 FindPubkeysInAccessLogs(userID string) ([]string, error)
689 FindAccessLogsByPubkey(pubkey string, fromDate *time.Time) ([]*AccessLog, error)
690}
691
692type InviteDB interface {
693 FindWhoInvitedUser(toUserID string) (string, error)
694 FindInvitesByUser(userID string) ([]*Invite, error)
695 InviteUser(fromUserID string, toUserID string) error
696}
697
698// DB is the storage apps share. Each app declares the subset it uses, and
699// keeps the queries only it makes in its own package.
700type DB interface {
701 UserDB
702 KeyDB
703 TokenDB
704 PostDB
705 TagDB
706 AnalyticsDB
707 FeatureDB
708 ProjectDB
709 TunsDB
710 AccessLogDB
711 InviteDB
712
713 FindPosts() ([]*Post, error)
714
715 Close() error
716}
717
718// SetLimits fills in the flag's storage, file and special file limits, using
719// the defaults where the flag sets none.
720func (ff *FeatureFlag) SetLimits(storageMax uint64, fileMax, specialFileMax int64) {
721 ff.Data.StorageMax = ff.FindStorageMax(storageMax)
722 ff.Data.FileMax = ff.FindFileMax(fileMax)
723 ff.Data.SpecialFileMax = ff.FindSpecialFileMax(specialFileMax)
724}
725
726// FindAccess returns the flag that lets a user use a service: pico+, or the
727// service's own feature. The error says why a user has neither.
728func FindAccess(dbpool interface {
729 FindFeature(userID string, feature string) (*FeatureFlag, error)
730}, userID, service string) (*FeatureFlag, error) {
731 ff, err := dbpool.FindFeature(userID, "plus")
732 if err == nil {
733 if ff.IsValid() {
734 return ff, nil
735 }
736 err = fmt.Errorf("ERROR: your pico+ has expired: https://blog.pico.sh/ann-038-pico-invite-system")
737 }
738
739 ffService, serviceErr := dbpool.FindFeature(userID, service)
740 if serviceErr == nil {
741 if ffService.IsValid() {
742 return ffService, nil
743 }
744 serviceErr = fmt.Errorf("ERROR: your %s access has expired: https://blog.pico.sh/ann-038-pico-invite-system", service)
745 }
746
747 if err != nil && strings.Contains(err.Error(), "expired") {
748 return nil, err
749 }
750 if serviceErr != nil && strings.Contains(serviceErr.Error(), "expired") {
751 return nil, serviceErr
752 }
753 return nil, fmt.Errorf("ERROR: uploading to %s requires an invitation or pico+: https://blog.pico.sh/ann-038-pico-invite-system", service)
754}
755
756// HasFeature reports whether a user has a feature that has not expired.
757func HasFeature(dbpool interface {
758 FindFeature(userID string, feature string) (*FeatureFlag, error)
759}, userID, feature string) bool {
760 ff, err := dbpool.FindFeature(userID, feature)
761 return err == nil && ff.IsValid()
762}
763
764func HasPlusOrSpace(dbpool interface {
765 FindFeature(userID string, feature string) (*FeatureFlag, error)
766}, userID, space string) bool {
767 return HasFeature(dbpool, userID, "plus") || HasFeature(dbpool, userID, space)
768}