Add NIP 86 support

This commit is contained in:
Jon Staab
2025-09-25 16:02:06 -07:00
parent 9259b5b0a1
commit 400afc140b
3 changed files with 289 additions and 19 deletions
+2
View File
@@ -99,5 +99,7 @@ See `justfile` for defined commands.
## TODO
- [ ] Sync claims to management db, pull directly from management db when checking access
- [ ] Add admin/owner/etc to list allowed pubkeys
- [ ] Watch configuration files and hot reload
- [ ] Free up resources after instance inactivity
+29 -18
View File
@@ -14,12 +14,13 @@ import (
)
type Instance struct {
Host string
Config *Config
Secret nostr.SecretKey
Events eventstore.Store
Access *AccessStore
Relay *khatru.Relay
Host string
Config *Config
Secret nostr.SecretKey
Events eventstore.Store
Access *AccessStore
Management *ManagementStore
Relay *khatru.Relay
}
func MakeInstance(hostname string) (*Instance, error) {
@@ -51,7 +52,13 @@ func MakeInstance(hostname string) (*Instance, error) {
Access: &AccessStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "__events",
Name: slug.Make(config.Self.Schema) + "__access",
},
},
Management: &ManagementStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "__management",
},
},
Relay: khatru.NewRelay(),
@@ -79,6 +86,20 @@ func MakeInstance(hostname string) (*Instance, error) {
instance.Relay.RejectConnection = instance.RejectConnection
instance.Relay.PreventBroadcast = instance.PreventBroadcast
// Initialize stuff
if err := instance.Events.Init(); err != nil {
log.Fatal("Failed to initialize event store:", err)
}
if err := instance.Access.Init(); err != nil {
log.Fatal("Failed to initialize access store:", err)
}
if err := instance.Management.Init(); err != nil {
log.Fatal("Failed to initialize management store:", err)
}
if config.Groups.Enabled {
EnableGroups(instance)
}
@@ -88,17 +109,7 @@ func MakeInstance(hostname string) (*Instance, error) {
}
if config.Management.Enabled {
EnableManagement(instance)
}
// Initialize stuff
if err := instance.Events.Init(); err != nil {
log.Fatal("Failed to initialize event store:", err)
}
if err := instance.Access.Init(); err != nil {
log.Fatal("Failed to initialize access store:", err)
instance.Management.Enable(instance)
}
return instance, nil
+258 -1
View File
@@ -1,4 +1,261 @@
package zooid
func EnableManagement(instance *Instance) {
import (
"context"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/khatru"
"fiatjaf.com/nostr/nip86"
"fmt"
"github.com/Masterminds/squirrel"
)
type ManagementStore struct {
Config *Config
Schema *Schema
}
func (m *ManagementStore) Init() error {
basicSchema := m.Schema.Render(`
CREATE TABLE IF NOT EXISTS {{.Name}}__pubkeys (
pubkey PRIMARY KEY NOT NULL,
status TEXT NOT NULL,
reason TEXT
);
CREATE INDEX IF NOT EXISTS {{.Name}}__idx_pubkeys_pubkey ON {{.Name}}__pubkeys(pubkey);
CREATE INDEX IF NOT EXISTS {{.Name}}__idx_pubkeys_status ON {{.Name}}__pubkeys(status);
CREATE TABLE IF NOT EXISTS {{.Name}}__events (
id PRIMARY KEY NOT NULL,
status TEXT NOT NULL,
reason TEXT
);
CREATE INDEX IF NOT EXISTS {{.Name}}__idx_events_id ON {{.Name}}__events(id);
CREATE INDEX IF NOT EXISTS {{.Name}}__idx_events_status ON {{.Name}}__events(status);
`)
if _, err := GetDb().Exec(basicSchema); err != nil {
return fmt.Errorf("failed to create schema: %w", err)
}
return nil
}
// Banned/allowed pubkeys
type Nip86PubkeyInfo struct {
Pubkey nostr.PubKey
Status string
Reason string
}
func (m *ManagementStore) SelectPubkeys() squirrel.SelectBuilder {
return squirrel.Select("pubkey", "status", "reason").From(m.Schema.Prefix("pubkeys"))
}
func (m *ManagementStore) QueryPubkeys(builder squirrel.SelectBuilder) []Nip86PubkeyInfo {
rows, err := builder.RunWith(GetDb()).Query()
if err != nil {
return []Nip86PubkeyInfo{}
}
defer rows.Close()
var items []Nip86PubkeyInfo
for rows.Next() {
var item Nip86PubkeyInfo
var pubkeyStr string
err := rows.Scan(&pubkeyStr, &item.Status)
if err != nil {
continue
}
if pubkey, err := nostr.PubKeyFromHex(pubkeyStr); err == nil {
item.Pubkey = pubkey
} else {
continue
}
items = append(items, item)
}
return items
}
func (m *ManagementStore) BanPubkey(pubkey nostr.PubKey, reason string) error {
_, err := squirrel.Insert(m.Schema.Prefix("pubkeys")).
Columns("pubkey", "status", "reason").
Values(pubkey.Hex(), "banned", reason).
Suffix("ON CONFLICT(pubkey) DO UPDATE SET status = excluded.status, reason = excluded.reason").
RunWith(GetDb()).Exec()
return err
}
func (m *ManagementStore) AllowPubkey(pubkey nostr.PubKey, reason string) error {
_, err := squirrel.Delete(m.Schema.Prefix("pubkeys")).
Where(squirrel.Eq{"pubkey": pubkey.Hex()}).
RunWith(GetDb()).Exec()
return err
}
func (m *ManagementStore) PubkeyHasStatus(pubkey nostr.PubKey, status string) bool {
builder := m.SelectPubkeys().Where(squirrel.Eq{"pubkey": pubkey.Hex()})
for _, item := range m.QueryPubkeys(builder) {
if item.Status == status {
return true
}
}
return false
}
// Banned/allowed events
type Nip86EventInfo struct {
ID nostr.ID
Status string
Reason string
}
func (m *ManagementStore) SelectEvents() squirrel.SelectBuilder {
return squirrel.Select("id", "status", "reason").From(m.Schema.Prefix("events"))
}
func (m *ManagementStore) QueryEvents(builder squirrel.SelectBuilder) []Nip86EventInfo {
rows, err := builder.RunWith(GetDb()).Query()
if err != nil {
return []Nip86EventInfo{}
}
defer rows.Close()
var items []Nip86EventInfo
for rows.Next() {
var item Nip86EventInfo
var idStr string
err := rows.Scan(&idStr, &item.Status, &item.Reason)
if err != nil {
continue
}
if id, err := nostr.IDFromHex(idStr); err == nil {
item.ID = id
} else {
continue
}
items = append(items, item)
}
return items
}
func (m *ManagementStore) BanEvent(id nostr.ID, reason string) error {
_, err := squirrel.Insert(m.Schema.Prefix("events")).
Columns("id", "status", "reason").
Values(id.Hex(), "banned", reason).
Suffix("ON CONFLICT(id) DO UPDATE SET status = excluded.status, reason = excluded.reason").
RunWith(GetDb()).Exec()
return err
}
func (m *ManagementStore) AllowEvent(id nostr.ID, reason string) error {
_, err := squirrel.Delete(m.Schema.Prefix("events")).
Where(squirrel.Eq{"id": id.Hex()}).
RunWith(GetDb()).Exec()
return err
}
func (m *ManagementStore) EventHasStatus(id nostr.ID, status string) bool {
builder := m.SelectEvents().Where(squirrel.Eq{"id": id.Hex()})
for _, item := range m.QueryEvents(builder) {
if item.Status == status {
return true
}
}
return false
}
// Handlers
// Middleware
func (m *ManagementStore) Enable(instance *Instance) {
instance.Relay.ManagementAPI.OnAPICall = func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) {
pubkey, ok := khatru.GetAuthed(ctx)
if ok && m.Config.CanManage(m.Config.GetRolesForPubkey(pubkey)) {
return true, "blocked: only relay admins can manage this relay."
}
return false, ""
}
instance.Relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
filter := nostr.Filter{
Authors: []nostr.PubKey{pubkey},
}
for event := range instance.Events.QueryEvents(filter, 1000000) {
instance.Events.DeleteEvent(event.ID)
}
return m.BanPubkey(pubkey, reason)
}
instance.Relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
return m.AllowPubkey(pubkey, reason)
}
instance.Relay.ManagementAPI.ListBannedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) {
items := m.QueryPubkeys(m.SelectPubkeys().Where(squirrel.Eq{"status": "banned"}))
reasons := make([]nip86.PubKeyReason, 0, len(items))
for _, item := range items {
reasons = append(
reasons,
nip86.PubKeyReason{
PubKey: item.Pubkey,
Reason: item.Reason,
},
)
}
return reasons, nil
}
instance.Relay.ManagementAPI.BanEvent = func(ctx context.Context, id nostr.ID, reason string) error {
filter := nostr.Filter{
IDs: []nostr.ID{id},
}
for event := range instance.Events.QueryEvents(filter, 1000000) {
instance.Events.DeleteEvent(event.ID)
}
return m.BanEvent(id, reason)
}
instance.Relay.ManagementAPI.AllowEvent = func(ctx context.Context, id nostr.ID, reason string) error {
return m.AllowEvent(id, reason)
}
instance.Relay.ManagementAPI.ListBannedEvents = func(ctx context.Context) ([]nip86.IDReason, error) {
items := m.QueryEvents(m.SelectEvents().Where(squirrel.Eq{"status": "banned"}))
reasons := make([]nip86.IDReason, 0, len(items))
for _, item := range items {
reasons = append(
reasons,
nip86.IDReason{
ID: item.ID.Hex(),
Reason: item.Reason,
},
)
}
return reasons, nil
}
}