main pico / pkg / db / db.go
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}