tinyrelayfile
tinyrelay/internal/daemon/creation.go/

internal/daemon/creation.go raw
1package daemon
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "net/url"
10 "strings"
11
12 "github.com/FelineStateMachine/tinyrelay/internal/catalog"
13 "github.com/FelineStateMachine/tinyrelay/internal/community"
14 "github.com/FelineStateMachine/tinyrelay/internal/replication"
15 "github.com/FelineStateMachine/tinyrelay/internal/storage"
16 "github.com/FelineStateMachine/tinyrelay/internal/templates"
17)
18
19// AfterCreate finishes the durable work for a catalog row still in creating.
20// It is intentionally separate from Catalog.Create so callers can retry after
21// a crash without creating a second tenant or losing the assigned owner.
22func (a *App) AfterCreate(ctx context.Context, meta catalog.Tenant, opts CreateOptions) error {
23 template, ok := templates.Find(meta.Template)
24 if !ok {
25 return fmt.Errorf("creation: template %q not found", meta.Template)
26 }
27 source := strings.TrimSpace(opts.Source)
28 if source == "" {
29 source = meta.Source
30 }
31 if template.Source == "required" && source == "" {
32 return fmt.Errorf("creation: template %q requires a source relay", template.Name)
33 }
34 if source != "" && !validSource(source) {
35 return errors.New("creation: source must be an http, https, ws, or wss relay")
36 }
37 store, err := storage.Open(ctx, meta.Paths.Database)
38 if err != nil {
39 return fmt.Errorf("creation: open tenant store: %w", err)
40 }
41 defer store.Close()
42 if err := initializeTenantRetryable(ctx, store, meta, template); err != nil {
43 return err
44 }
45 if source == "" && template.Name == "inbox" {
46 return seedInboxDiscoveryJob(ctx, store, meta, template)
47 }
48 if source == "" || !acceptsSource(template.Name) {
49 return nil
50 }
51 return seedSourceJob(ctx, store, meta, template, source)
52}
53
54func seedInboxDiscoveryJob(ctx context.Context, store *storage.Store, meta catalog.Tenant, template templates.Template) error {
55 service, err := replication.NewService(replication.Config{Store: store, DataDir: meta.Paths.Root})
56 if err != nil {
57 return fmt.Errorf("creation: initialize replication: %w", err)
58 }
59 filterObject := map[string]any{"kinds": template.AllowKinds, "#p": []string{meta.Owner}}
60 raw, err := json.Marshal(filterObject)
61 if err != nil {
62 return err
63 }
64 spec := replication.JobSpec{ID: "source-" + meta.ID, Kind: replication.JobPull, Filter: string(raw), Every: 1, DiscoverPubKey: meta.Owner}
65 if err := service.AddJob(ctx, spec); err != nil {
66 return fmt.Errorf("creation: seed inbox discovery job: %w", err)
67 }
68 return nil
69}
70
71// RecoverCreating retries every interrupted creation and makes only tenants
72// whose policy and source jobs were durably initialized ready.
73func (a *App) RecoverCreating(ctx context.Context) error {
74 items, err := a.catalog.List(ctx)
75 if err != nil {
76 return err
77 }
78 var failures []error
79 for _, meta := range items {
80 if meta.Status != catalog.StatusCreating {
81 continue
82 }
83 if err := a.AfterCreate(ctx, meta, CreateOptions{Source: meta.Source, Owner: meta.Owner, Template: meta.Template}); err != nil {
84 failures = append(failures, fmt.Errorf("%s: %w", meta.Name, err))
85 continue
86 }
87 if err := a.catalog.MarkReady(ctx, meta.ID); err != nil {
88 failures = append(failures, fmt.Errorf("%s: mark ready: %w", meta.Name, err))
89 }
90 }
91 return errors.Join(failures...)
92}
93
94func initializeTenantRetryable(ctx context.Context, store *storage.Store, meta catalog.Tenant, template templates.Template) error {
95 p, err := templates.ApplyTemplate(template.Name, meta.Owner)
96 if err != nil {
97 return err
98 }
99 if _, err := community.New(ctx, store, p.Owner); err != nil {
100 return err
101 }
102 err = store.WithTx(ctx, func(tx *sql.Tx) error {
103 if err := storage.PutSetting(ctx, tx, "policy", p); err != nil {
104 return err
105 }
106 for _, rule := range []struct {
107 kinds []int
108 name string
109 }{{p.AllowedKinds, "allow"}, {p.BlockedKinds, "block"}} {
110 for _, kind := range rule.kinds {
111 if _, err := tx.ExecContext(ctx, "INSERT OR IGNORE INTO community_kind_rules(kind,rule) VALUES(?,?)", kind, rule.name); err != nil {
112 return err
113 }
114 }
115 }
116 for _, retention := range p.Retention {
117 if _, err := tx.ExecContext(ctx, "INSERT OR IGNORE INTO community_retention(kind,days) VALUES(?,?)", retention.Kind, retention.Days); err != nil {
118 return err
119 }
120 }
121 if _, err := tx.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS config_connections(position INTEGER PRIMARY KEY,value TEXT NOT NULL)"); err != nil {
122 return err
123 }
124 for index, connection := range template.Connections {
125 raw, err := json.Marshal(map[string]string{"template": connection, "visibility": "public"})
126 if err != nil {
127 return err
128 }
129 if _, err := tx.ExecContext(ctx, "INSERT OR IGNORE INTO config_connections(position,value) VALUES(?,?)", index, string(raw)); err != nil {
130 return err
131 }
132 }
133 return nil
134 })
135 if err != nil {
136 return fmt.Errorf("creation: initialize policy: %w", err)
137 }
138 return nil
139}
140
141func seedSourceJob(ctx context.Context, store *storage.Store, meta catalog.Tenant, template templates.Template, source string) error {
142 service, err := replication.NewService(replication.Config{Store: store, DataDir: meta.Paths.Root})
143 if err != nil {
144 return fmt.Errorf("creation: initialize replication: %w", err)
145 }
146 filter := ""
147 if len(template.AllowKinds) > 0 {
148 filterObject := map[string]any{"kinds": template.AllowKinds}
149 if template.Name == "inbox" {
150 filterObject["#p"] = []string{meta.Owner}
151 }
152 raw, err := json.Marshal(filterObject)
153 if err != nil {
154 return err
155 }
156 filter = string(raw)
157 }
158 kind := replication.JobPull
159 every := template.EveryHours
160 if template.Name == "inbox" {
161 every = 1
162 }
163 if template.Name == "outbox" {
164 kind = replication.JobMirror
165 if every == 0 {
166 every = 24
167 }
168 }
169 spec := replication.JobSpec{ID: "source-" + meta.ID, Kind: kind, Relays: []string{sourceRelayURL(source)}, Filter: filter, Every: every}
170 if err := service.AddJob(ctx, spec); err != nil {
171 return fmt.Errorf("creation: seed source job: %w", err)
172 }
173 return nil
174}
175
176func validSource(source string) bool {
177 u, err := url.Parse(source)
178 return err == nil && u.Host != "" && (u.Scheme == "http" || u.Scheme == "https" || u.Scheme == "ws" || u.Scheme == "wss") && u.User == nil
179}
180
181func acceptsSource(name string) bool {
182 return name == "search" || name == "articles" || name == "inbox" || name == "outbox"
183}
184
185func sourceRelayURL(raw string) string {
186 u, err := url.Parse(raw)
187 if err != nil {
188 return raw
189 }
190 switch u.Scheme {
191 case "http":
192 u.Scheme = "ws"
193 case "https":
194 u.Scheme = "wss"
195 }
196 return u.String()
197}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs