tinyrelayfile
tinyrelay/internal/templates/templates.go/

internal/templates/templates.go raw
1package templates
2
3import (
4 "encoding/json"
5 "fmt"
6
7 "github.com/FelineStateMachine/tinyrelay/internal/policy"
8)
9
10// Template describes a built-in relay policy and its recommended connections.
11type Template struct {
12 Name string
13 Title string
14 About string
15 Source string
16 EveryHours int
17 Policy map[string]any
18 AllowKinds []int
19 BlockKinds []int
20 RetentionDays int
21 Connections []string
22}
23
24// Connection describes one client integration displayed for a relay.
25type Connection struct {
26 Name string
27 Title string
28 About string
29 App string
30 Where string
31 Icon string
32 Feature string
33 Visibility string
34 QR string
35 Inputs []Input
36 Links []Link
37}
38
39// Input describes a value a connection asks the user to supply.
40type Input struct{ Name, Label, Placeholder, Default, Pattern string }
41
42// Link describes an external URL or copyable value for a connection.
43type Link struct{ Label, Href, Copy string }
44
45var templateCatalog = []Template{
46 {Name: "default", Title: "Default", About: "Anyone writes, anyone reads, every kind, kept forever. Feature settings stay as they are.", Policy: map[string]any{"writes": "open", "reads": "open", "directoryPublic": true}},
47 {Name: "outbox", Title: "Outbox", About: "Only you write, anyone reads. Your public notes and articles, kept forever. Private kinds are refused.", Policy: map[string]any{"writes": "owner", "reads": "open", "directoryPublic": true, "delivery": map[string]any{"enabled": true}}, BlockKinds: []int{4, 14, 1059}, Connections: []string{"notes", "find-me"}},
48 {Name: "inbox", Title: "Inbox", About: "Anyone writes notes, replies, reactions and zaps meant for you; anyone reads. Everything is kept 90 days.", Policy: map[string]any{"writes": "open", "reads": "open", "directoryPublic": true, "inbox": map[string]any{"targeted": true}}, AllowKinds: []int{0, 1, 6, 7, 16, 1111, 1984, 9735, 9802}, RetentionDays: 90, Connections: []string{"find-me", "notes"}},
49 {Name: "private", Title: "Private", About: "Only you write, only members read. Drafts, wallets and app data, every kind, kept forever.", Policy: map[string]any{"writes": "owner", "reads": "members", "directoryPublic": false}},
50 {Name: "chat", Title: "Chat", About: "Members write, members read. Private messages and the group's chat, kept forever. The directory is hidden.", Policy: map[string]any{"writes": "allowlist", "reads": "members", "directoryPublic": false}, AllowKinds: []int{0, 7, 9, 11, 1059, 1111}, Connections: []string{"group"}},
51 {Name: "media", Title: "Media", About: "A media host. Members upload files; the only events accepted are profiles and Blossom server lists. Anyone reads.", Policy: map[string]any{"writes": "allowlist", "reads": "open", "directoryPublic": true}, AllowKinds: []int{0, 10063}, Connections: []string{"files", "photos"}},
52 {Name: "search", Title: "Search replica", About: "A read-only copy of another relay's notes, threads, comments, highlights, articles and wiki pages, refreshed every six hours. Anyone reads.", Source: "required", EveryHours: 6, Policy: map[string]any{"writes": "owner", "reads": "open", "directoryPublic": true}, AllowKinds: []int{0, 1, 11, 1111, 9802, 30023, 30818}, Connections: []string{"notes"}},
53 {Name: "articles", Title: "Articles", About: "Long-form articles and the profiles behind them, nothing else. Only you write, anyone reads. Give a source relay to mirror its articles daily.", Source: "optional", EveryHours: 24, Policy: map[string]any{"writes": "owner", "reads": "open", "directoryPublic": true}, AllowKinds: []int{0, 30023}, Connections: []string{"notes"}},
54 {Name: "social", Title: "Social", About: "A chronological social feed for members' notes, comments, reactions and long-form articles. Members write; anyone reads.", Policy: map[string]any{"writes": "allowlist", "reads": "open", "directoryPublic": true}, AllowKinds: []int{0, 1, 5, 6, 7, 1111, 30023}, Connections: []string{"notes", "blog"}},
55 {Name: "dm", Title: "DM inbox", About: "An inbox for private messages. Anyone drops gift wraps, only members read them. Nothing else is accepted.", Policy: map[string]any{"writes": "open", "reads": "members", "directoryPublic": false}, AllowKinds: []int{0, 1059}, Connections: []string{"dm"}},
56 {Name: "quiet", Title: "Quiet", About: "A small private relay with nothing that costs or announces. Members write and read; optional features are off.", Policy: map[string]any{"writes": "allowlist", "reads": "members", "directoryPublic": false, "features": map[string]any{"search": "off", "sync": false, "count": false, "discovery": false, "names": false, "files": false, "pages": false, "signer": false, "sites": false, "marmot": false, "grasp": false}}, Connections: []string{}},
57 {Name: "site", Title: "Sites", About: "A static website host. Members publish manifests and files; the relay mirrors missing files into its own store.", Policy: map[string]any{"writes": "allowlist", "reads": "open", "directoryPublic": true, "features": map[string]any{"files": true, "sites": map[string]any{"enabled": true, "mirror": true}}}, Connections: []string{"notes"}},
58 {Name: "marmot", Title: "Marmot", About: "Public Marmot transport. Anyone publishes KeyPackages and opaque group messages; gift wraps keep their recipient checks.", Policy: map[string]any{"writes": "open", "reads": "open", "directoryPublic": true, "features": map[string]any{"marmot": true}}, AllowKinds: []int{10002, 10050, 1059, 30443, 445}, Connections: []string{"marmot"}},
59 {Name: "grasp", Title: "Git repositories", About: "A GRASP Git host. Admitted members publish repository state and related events.", Policy: map[string]any{"writes": "allowlist", "reads": "open", "directoryPublic": true, "features": map[string]any{"grasp": true}}, Connections: []string{"repos", "notes"}},
60 {Name: "marmot-members", Title: "Marmot members", About: "Members publish Marmot KeyPackages and authenticate group message writes. Encrypted transport stays publicly readable.", Policy: map[string]any{"writes": "allowlist", "reads": "open", "directoryPublic": true, "features": map[string]any{"marmot": true}}, AllowKinds: []int{10002, 10050, 1059, 30443, 445}, Connections: []string{"marmot"}},
61 {Name: "home", Title: "Home", About: "Your relay as your home on nostr. Members write, anyone reads; sites, files and Git hosting on.", Policy: map[string]any{"writes": "allowlist", "reads": "open", "directoryPublic": true, "features": map[string]any{"files": true, "sites": map[string]any{"enabled": true, "mirror": true}, "grasp": true}}, Connections: []string{"notes", "repos", "photos", "find-me", "group"}},
62}
63
64var connectionCatalog = []Connection{
65 {Name: "notes", Title: "Notes", About: "Everything posted on this relay as one feed.", App: "Jumble", Where: "web", Icon: "notes", Visibility: "public", Links: []Link{{Label: "Open", Href: "https://jumble.social/?r={relay:url|enc}"}}},
66 {Name: "find-me", Title: "Find me here", About: "The owner's profile with this relay attached, for feed apps.", App: "Primal", Where: "web, phone", Icon: "person", Visibility: "public", QR: "nostr:{owner:nprofile}", Links: []Link{{Label: "Open", Href: "https://primal.net/p/{owner:nprofile}"}, {Label: "Open in app", Href: "nostr:{owner:nprofile}"}}},
67 {Name: "group", Title: "Group", About: "The relay as a space, with its group as a room.", App: "Flotilla", Where: "web, phone", Icon: "chat", Visibility: "public", QR: "nostr:{relay:naddr}", Links: []Link{{Label: "Open", Href: "https://app.flotilla.social/spaces/{relay:host|enc}"}, {Label: "Open group in app", Href: "nostr:{relay:naddr}"}, {Label: "Copy naddr", Copy: "{relay:naddr}"}}},
68 {Name: "blog", Title: "Blog", About: "The owner's articles, with this relay attached so a reader finds the rest of them.", App: "YakiHonne", Where: "web, phone", Icon: "blog", Visibility: "public", QR: "nostr:{owner:nprofile}", Links: []Link{{Label: "Open", Href: "https://yakihonne.com/profile/{owner:nprofile}"}, {Label: "Open in app", Href: "nostr:{owner:nprofile}"}}},
69 {Name: "repos", Title: "Repos", About: "This relay's Git repositories, and the clone command for yours.", App: "GitWorkshop", Where: "web, terminal", Icon: "git", Feature: "grasp", Visibility: "public", Inputs: []Input{{Name: "repo", Label: "Repository name", Placeholder: "my-project", Default: "<repo>", Pattern: "^[A-Za-z0-9][A-Za-z0-9._\\-]{0,63}$"}}, Links: []Link{{Label: "Open", Href: "https://gitworkshop.dev/relay/{relay:host|enc}"}, {Label: "Copy clone command", Copy: "git clone '{relay:web}/{user:npub}/{input:repo}.git'"}}},
70 {Name: "sites", Title: "Sites", About: "Static sites this relay hosts, and the command that publishes one here.", App: "nsyte", Where: "web, terminal", Icon: "site", Feature: "sites", Visibility: "public", Links: []Link{{Label: "Open the owner's site", Href: "https://{owner:npub}.{relay:domain}"}, {Label: "Open your site", Href: "https://{user:npub}.{relay:domain}"}, {Label: "Copy publish command", Copy: "nsyte deploy ./dist --relays {relay:url} --servers {relay:web}"}}},
71 {Name: "bookmarks", Title: "Bookmarks", About: "Your bookmarks and lists in a list manager; add this relay in its settings so they land here.", App: "Listr", Where: "web", Icon: "bookmark", Visibility: "auth", Links: []Link{{Label: "Open", Href: "https://listr.lol/{user:npub}"}, {Label: "Open in noStrudel", Href: "https://nostrudel.ninja/bookmarks/{user:nprofile}"}}},
72 {Name: "photos", Title: "Photo library", About: "The owner's photos on this relay's file store.", App: "bouquet", Where: "web", Icon: "photo", Feature: "files", Visibility: "owner", Links: []Link{{Label: "Open", Href: "https://bouquet.slidestr.net/"}, {Label: "Copy Blossom server URL", Copy: "{relay:web}"}}},
73 {Name: "files", Title: "Files", About: "This relay's file store, for members, in a media manager.", App: "bouquet", Where: "web", Icon: "files", Feature: "files", Visibility: "members", Links: []Link{{Label: "Open", Href: "https://bouquet.slidestr.net/"}, {Label: "Copy Blossom server URL", Copy: "{relay:web}"}, {Label: "Copy NIP-96 URL", Copy: "{relay:web}/.well-known/nostr/nip96.json"}}},
74 {Name: "dm", Title: "Messages", About: "Message the owner privately; put this relay in your DM inbox list.", App: "0xchat", Where: "phone, desktop", Icon: "chat", Visibility: "public", QR: "nostr:{owner:nprofile}", Links: []Link{{Label: "Open in app", Href: "nostr:{owner:nprofile}"}}},
75 {Name: "marmot", Title: "Encrypted groups", About: "End-to-end encrypted group chat over this relay.", App: "White Noise", Where: "phone", Icon: "lock", Feature: "marmot", Visibility: "public", Links: []Link{{Label: "Open", Href: "https://whitenoise.chat/"}, {Label: "Get the app", Href: "https://whitenoise.chat/download"}}},
76 {Name: "relay-page", Title: "Relay page", About: "This relay's page in Coracle: its feed and its people.", App: "Coracle", Where: "web", Icon: "feed", Visibility: "public", Links: []Link{{Label: "Open", Href: "https://coracle.social/relays/{relay:host|enc}"}}},
77}
78
79func Names() []string {
80 out := make([]string, 0, len(templateCatalog))
81 for _, t := range templateCatalog {
82 out = append(out, t.Name)
83 }
84 return out
85}
86
87// Connections returns the built-in connection catalog in display order.
88func Connections() []Connection {
89 out := make([]Connection, len(connectionCatalog))
90 for i, c := range connectionCatalog {
91 out[i] = cloneConnection(c)
92 }
93 return out
94}
95
96// Find returns a named template and a boolean indicating whether it exists.
97func Find(name string) (Template, bool) {
98 for _, t := range templateCatalog {
99 if t.Name == name {
100 return cloneTemplate(t), true
101 }
102 }
103 return Template{}, false
104}
105
106func cloneTemplate(t Template) Template {
107 t.Policy = cloneMap(t.Policy)
108 t.AllowKinds = append([]int(nil), t.AllowKinds...)
109 t.BlockKinds = append([]int(nil), t.BlockKinds...)
110 t.Connections = append([]string(nil), t.Connections...)
111 return t
112}
113
114func cloneConnection(c Connection) Connection {
115 c.Inputs = append([]Input(nil), c.Inputs...)
116 c.Links = append([]Link(nil), c.Links...)
117 return c
118}
119
120func cloneMap(in map[string]any) map[string]any {
121 out := make(map[string]any, len(in))
122 for k, v := range in {
123 if nested, ok := v.(map[string]any); ok {
124 out[k] = cloneMap(nested)
125 } else {
126 out[k] = v
127 }
128 }
129 return out
130}
131
132func ApplyTemplate(name, owner string) (policy.Policy, error) {
133 t, ok := Find(name)
134 if !ok {
135 return policy.Policy{}, fmt.Errorf("template %q not found", name)
136 }
137 p := policy.Defaults(owner)
138 b, err := jsonMarshal(t.Policy)
139 if err != nil {
140 return policy.Policy{}, err
141 }
142 p, err = policy.Patch(p, b)
143 if err != nil {
144 return policy.Policy{}, fmt.Errorf("template %q policy: %w", name, err)
145 }
146 if len(t.AllowKinds) > 0 {
147 p.AllowedKinds = append([]int(nil), t.AllowKinds...)
148 }
149 if len(t.BlockKinds) > 0 {
150 p.BlockedKinds = append([]int(nil), t.BlockKinds...)
151 }
152 if t.RetentionDays > 0 {
153 p.Retention = []policy.RetentionRule{{Kind: -1, Days: t.RetentionDays}}
154 }
155 return p, nil
156}
157
158func jsonMarshal(v map[string]any) (map[string]json.RawMessage, error) {
159 result := make(map[string]json.RawMessage, len(v))
160 for k, value := range v {
161 b, err := json.Marshal(value)
162 if err != nil {
163 return nil, fmt.Errorf("%s: %w", k, err)
164 }
165 result[k] = b
166 }
167 return result, nil
168}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs