clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / internal / templates / templates.go
templates.go 13.0K
1package templates23import (4 "encoding/json"5 "fmt"67 "github.com/FelineStateMachine/tinyrelay/internal/policy"8)910// Template describes a built-in relay policy and its recommended connections.11type Template struct {12 Name string13 Title string14 About string15 Source string16 EveryHours int17 Policy map[string]any18 AllowKinds []int19 BlockKinds []int20 RetentionDays int21 Connections []string22}2324// Connection describes one client integration displayed for a relay.25type Connection struct {26 Name string27 Title string28 About string29 App string30 Where string31 Icon string32 Feature string33 Visibility string34 QR string35 Inputs []Input36 Links []Link37}3839// Input describes a value a connection asks the user to supply.40type Input struct{ Name, Label, Placeholder, Default, Pattern string }4142// Link describes an external URL or copyable value for a connection.43type Link struct{ Label, Href, Copy string }4445var 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, long-form articles and podcasts. Members write; anyone reads.", Policy: map[string]any{"writes": "allowlist", "reads": "open", "directoryPublic": true}, AllowKinds: []int{0, 1, 5, 6, 7, 54, 1111, 30023, 10054, 10064, 10154}, 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}6364var 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}7879func Names() []string {80 out := make([]string, 0, len(templateCatalog))81 for _, t := range templateCatalog {82 out = append(out, t.Name)83 }84 return out85}8687// 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 out94}9596// 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), true101 }102 }103 return Template{}, false104}105106func 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 t112}113114func cloneConnection(c Connection) Connection {115 c.Inputs = append([]Input(nil), c.Inputs...)116 c.Links = append([]Link(nil), c.Links...)117 return c118}119120func 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] = v127 }128 }129 return out130}131132func 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{}, err141 }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, nil156}157158func 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] = b166 }167 return result, nil168}