Add handlers

This commit is contained in:
Jon Staab
2025-09-26 11:38:08 -07:00
parent 174e1a361f
commit 86a6eec127
8 changed files with 385 additions and 349 deletions
+11
View File
@@ -33,6 +33,12 @@ Optional:
- `pubkey` - the public key of the relay owner. Does not affect access controls.
- `description` - your relay's description.
### `[policy]`
Contains policy and access related configuration.
- `strip_signatures` - whether to remove signatures when serving events. This requires clients/users to trust the relay to properly authenticate signatures. Be cautious about using this; a malicious relay will be able to execute all kinds of attacks, including potentially serving events unrelated to a community use case.
### `[groups]`
Configures NIP 29 support.
@@ -74,6 +80,9 @@ name = "My relay"
schema = 'my_relay'
secret = "ce30b1831a4551f4cb7a984033c34ab96d8cf56ff50df9d0c27d9fa5422f2278"
[policy]
strip_signatures = false
[groups]
enabled = true
auto_join = false
@@ -100,6 +109,8 @@ See `justfile` for defined commands.
## TODO
- [ ] See if we can build groups directly on top of the event store by generating events eagerly rather than lazily
- [ ] See if we can implement invites/redemptions directly on top of the event store by storing generated claims and redemptions. Avoid serving these to other people.
- [ ] Add admin/owner/etc to list allowed pubkeys
- [ ] Watch configuration files and hot reload
- [ ] Free up resources after instance inactivity
- [ ] Admins/members
+6 -6
View File
@@ -4,8 +4,8 @@ import (
"bytes"
"context"
"io"
"os"
"net/url"
"os"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
@@ -14,9 +14,9 @@ import (
)
type BlossomStore struct {
Config *Config
Schema *Schema
Store eventstore.Store
Config *Config
Schema *Schema
Store eventstore.Store
}
func (bl *BlossomStore) Init() error {
@@ -26,11 +26,11 @@ func (bl *BlossomStore) Init() error {
return err
}
// Blossom uses a wrapped event store for metadata
// Blossom uses a wrapped event store for metadata
bl.Store = &EventStore{Schema: bl.Schema}
if err := bl.Store.Init(); err != nil {
return err
return err
}
return nil
+17 -3
View File
@@ -15,7 +15,7 @@ type Role struct {
}
type Config struct {
Host string
Host string
Self struct {
Name string `toml:"name"`
Icon string `toml:"icon"`
@@ -25,6 +25,10 @@ type Config struct {
Description string `toml:"description"`
} `toml:"self"`
Policy struct {
StripSignatures bool `toml:"strip_signatures"`
} `toml:"policy"`
Groups struct {
Enabled bool `toml:"enabled"`
AutoJoin bool `toml:"auto_join"`
@@ -79,8 +83,8 @@ func (config *Config) GetRolesForPubkey(pubkey nostr.PubKey) []Role {
return roles
}
func (config *Config) CanManage(roles []Role) bool {
for _, role := range roles {
func (config *Config) CanManage(pubkey nostr.PubKey) bool {
for _, role := range config.GetRolesForPubkey(pubkey) {
if role.CanManage {
return true
}
@@ -88,3 +92,13 @@ func (config *Config) CanManage(roles []Role) bool {
return false
}
func (config *Config) CanInvite(pubkey nostr.PubKey) bool {
for _, role := range config.GetRolesForPubkey(pubkey) {
if role.CanInvite {
return true
}
}
return false
}
+22 -22
View File
@@ -100,12 +100,11 @@ func (events *EventStore) QueryEvents(filter nostr.Filter, maxLimit int) iter.Se
return
}
limit := maxLimit
if filter.Limit > 0 && filter.Limit < limit {
limit = filter.Limit
}
if maxLimit > 0 && maxLimit < filter.Limit {
filter.Limit = maxLimit
}
rows, err := events.buildSelectQuery(filter, limit).RunWith(GetDb()).Query()
rows, err := events.buildSelectQuery(filter).RunWith(GetDb()).Query()
if err != nil {
return
}
@@ -159,7 +158,7 @@ func (events *EventStore) QueryEvents(filter nostr.Filter, maxLimit int) iter.Se
}
}
func (events *EventStore) buildSelectQuery(filter nostr.Filter, limit int) squirrel.SelectBuilder {
func (events *EventStore) buildSelectQuery(filter nostr.Filter) squirrel.SelectBuilder {
qb := squirrel.Select("id", "created_at", "kind", "pubkey", "content", "tags", "sig").
From(events.Schema.Prefix("events")).
OrderBy("created_at DESC")
@@ -206,25 +205,26 @@ func (events *EventStore) buildSelectQuery(filter nostr.Filter, limit int) squir
}
for tagKey, tagValues := range filter.Tags {
if len(tagValues) > 0 && len(tagKey) == 1 {
tagValueInterfaces := make([]interface{}, len(tagValues))
for i, tagValue := range tagValues {
tagValueInterfaces[i] = tagValue
}
if len(tagValues) == 0 {
continue
}
subQuery := squirrel.Select("event_id").
From(events.Schema.Prefix("event_tags")).
Where(squirrel.Eq{"key": tagKey}).
Where(squirrel.Eq{"value": tagValueInterfaces})
subQuerySql, subQueryArgs, _ := subQuery.ToSql()
qb = qb.Where("id IN ("+subQuerySql+")", subQueryArgs...)
tagValueInterfaces := make([]interface{}, len(tagValues))
for i, tagValue := range tagValues {
tagValueInterfaces[i] = tagValue
}
subQuery := squirrel.Select("event_id").
From(events.Schema.Prefix("event_tags")).
Where(squirrel.Eq{"key": tagKey}).
Where(squirrel.Eq{"value": tagValueInterfaces})
subQuerySql, subQueryArgs, _ := subQuery.ToSql()
qb = qb.Where("id IN ("+subQuerySql+")", subQueryArgs...)
}
// Add limit
if limit > 0 {
qb = qb.Limit(uint64(limit))
if filter.Limit > 0 {
qb = qb.Limit(uint64(filter.Limit))
}
return qb
@@ -316,7 +316,7 @@ func (events *EventStore) ReplaceEvent(evt nostr.Event) error {
func (events *EventStore) CountEvents(filter nostr.Filter) (uint32, error) {
// Build a count query based on the select query but with COUNT(*) instead
qb := events.buildSelectQuery(filter, 0)
qb := events.buildSelectQuery(filter)
// Convert the select query to a count query
countQb := squirrel.Select("COUNT(*)").FromSelect(qb, "subquery")
+51 -276
View File
@@ -1,152 +1,56 @@
package zooid
import (
"context"
"encoding/json"
"fmt"
"log"
"iter"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip29"
)
type GroupsStore struct {
Host string
Config *Config
Schema *Schema
}
func GetGroupIDFromEvent(event nostr.Event) string {
tag := event.Tags.Find("h")
func (groups *GroupsStore) Init() error {
schema := groups.Schema.Render(`
CREATE TABLE IF NOT EXISTS {{.Prefix}}__groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
about TEXT NOT NULL,
closed BOOLEAN NOT NULL,
private BOOLEAN NOT NULL,
last_metadata_update INTEGER,
last_admins_update INTEGER,
last_members_update INTEGER
);
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_groups_id ON {{.Prefix}}__groups(id);
CREATE TABLE IF NOT EXISTS {{.Prefix}}__group_members (
id TEXT PRIMARY KEY,
group_id TEXT NOT NULL,
pubkey TEXT NOT NULL,
FOREIGN KEY (group_id) REFERENCES {{.Prefix}}__groups(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_group_members_group_id ON {{.Prefix}}__group_members(group_id);
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_group_members_pubkey ON {{.Prefix}}__group_members(pubkey);
`)
if _, err := GetDb().Exec(schema); err != nil {
return fmt.Errorf("failed to create schema: %w", err)
if tag != nil {
return tag[1]
}
return nil
return ""
}
// Group CRUD
func (groups *GroupsStore) SelectGroups() squirrel.SelectBuilder {
return squirrel.Select("id", "name", "about", "closed", "private", "last_metadata_update", "last_admins_update", "last_members_update").From(groups.Schema.Prefix("groups"))
}
func (groups *GroupsStore) QueryGroups(builder squirrel.SelectBuilder) []Group {
rows, err := builder.RunWith(GetDb()).Query()
if err != nil {
return []Group{}
}
defer rows.Close()
var groups []Group
for rows.Next() {
var group Group
var id string
err := rows.Scan(&id, &group.Name, &group.About, &group.Closed, &group.Private, &group.LastMetadataUpdate, &group.LastAdminsUpdate, &group.LastMembersUpdate)
if err != nil {
continue
}
group.Address = nip29.GroupAddress{
ID: id
Relay: groups.Config.Host
}
groups = append(groups, group)
}
return groups
}
func (groups *GroupStore) PutGroup(group *nip29.Group) {
// Insert, on duplicate update
}
func (groups *GroupStore) DeleteGroup(id string) {
// Delete group
}
func (groups *GroupsStore) GetGroups() []nip29.Group {
return groups.QueryGroups(groups.SelectGroups())
}
func (groups *GroupsStore) GetGroupByID(id string) (nip29.Group, bool) {
groupList := groups.QueryGroups(groups.SelectGroups().Where(squirrel.Eq{"id": id}))
return First(groupList), len(groupList) > 0
}
// Group Utils
func (groups *GroupStore) MakeGroup(h string) *nip29.Group {
qualifiedID := fmt.Sprintf("%s'%s", groups.Config.Host, h)
group, err := nip29.NewGroup(qualifiedID)
if err != nil {
log.Printf("Failed to create group with qualified ID %s", qualifiedID)
return nil
}
return &group
}
func (groups *GroupStore) GetGroupIDFromEvent(event *nostr.Event) string {
hTag := event.Tags.GetFirst([]string{"h"})
if hTag == nil {
return ""
}
return hTag.Value()
}
func (groups *GroupStore) GetGroupFromEvent(event *nostr.Event) *nip29.Group {
id = GetGroupIDFromEvent(event)
if id == "" {
return nil
}
return GetGroupByID(id)
}
func (groups *GroupStore) IsGroupMember(ctx context.Context, id string, pubkey string) bool {
filter := nostr.Filter{
Kinds: []int{nostr.KindSimpleGroupPutUser, nostr.KindSimpleGroupRemoveUser},
func MakeGroupMetadataFilter(h string) nostr.Filter {
return nostr.Filter{
Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata},
Tags: nostr.TagMap{
"p": []string{pubkey},
"h": []string{id},
"a": []string{h},
},
}
}
events, err := GetBackend().QueryEvents(ctx, filter)
if err != nil {
log.Println(err)
func MakeGroupEventFilters(h string) []nostr.Filter {
return []nostr.Filter{
{
Tags: nostr.TagMap{
"a": []string{h},
},
},
{
Tags: nostr.TagMap{
"h": []string{h},
},
},
}
}
func MakeGroupMembershipCheckFilter(h string, pubkey nostr.PubKey) nostr.Filter {
return nostr.Filter{
Kinds: []nostr.Kind{nostr.KindSimpleGroupPutUser, nostr.KindSimpleGroupRemoveUser},
Tags: nostr.TagMap{
"p": []string{pubkey.Hex()},
"h": []string{h},
},
}
}
func CheckGroupMembership(events iter.Seq[nostr.Event]) bool {
for event := range events {
if event.Kind == nostr.KindSimpleGroupPutUser {
return true
@@ -160,161 +64,32 @@ func (groups *GroupStore) IsGroupMember(ctx context.Context, id string, pubkey s
return false
}
func HandleCreateGroup(event *nostr.Event) {
group := MakeGroup(GetGroupIDFromEvent(event))
if group != nil {
PutGroup(group)
}
}
func HandleEditMetadata(event *nostr.Event) {
group := GetGroupFromEvent(event)
if group == nil {
group = MakeGroup(GetGroupIDFromEvent(event))
}
group.LastMetadataUpdate = event.CreatedAt
group.Name = group.Address.ID
if tag := event.Tags.GetFirst([]string{"name", ""}); tag != nil {
group.Name = (*tag)[1]
}
if tag := event.Tags.GetFirst([]string{"about", ""}); tag != nil {
group.About = (*tag)[1]
}
if tag := event.Tags.GetFirst([]string{"picture", ""}); tag != nil {
group.Picture = (*tag)[1]
}
if tag := event.Tags.GetFirst([]string{"private"}); tag != nil {
group.Private = true
}
if tag := event.Tags.GetFirst([]string{"closed"}); tag != nil {
group.Closed = true
}
PutGroup(group)
}
func HandleDeleteGroup(event *nostr.Event) {
ctx := context.Background()
id := GetGroupIDFromEvent(event)
DeleteGroup(id)
hFilter := nostr.Filter{
Tags: nostr.TagMap{
"h": []string{id},
},
}
hCh, err := GetBackend().QueryEvents(ctx, hFilter)
if err != nil {
log.Println(err)
} else {
for event := range hCh {
DeleteEvent(ctx, event)
}
}
dFilter := nostr.Filter{
Tags: nostr.TagMap{
"d": []string{id},
},
}
dCh, err := GetBackend().QueryEvents(ctx, dFilter)
if err != nil {
log.Println(err)
} else {
for event := range dCh {
DeleteEvent(ctx, event)
}
}
}
func GenerateGroupMetadataEvents(ctx context.Context, filter nostr.Filter) []*nostr.Event {
result := make([]*nostr.Event, 0)
for _, group := range ListGroups() {
event := group.ToMetadataEvent()
if !filter.Matches(event) {
continue
}
if err := event.Sign(RELAY_SECRET); err != nil {
log.Println("Failed to sign metadata event", err)
} else {
result = append(result, event)
}
}
return result
}
func GenerateGroupAdminsEvents(ctx context.Context, filter nostr.Filter) []*nostr.Event {
result := make([]*nostr.Event, 0)
for _, group := range ListGroups() {
event := nostr.Event{
Kind: nostr.KindSimpleGroupAdmins,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"d", group.Address.ID},
},
}
for _, pubkey := range RELAY_ADMINS {
event.Tags = append(event.Tags, nostr.Tag{"p", pubkey})
}
if !filter.Matches(&event) {
continue
}
if err := event.Sign(RELAY_SECRET); err != nil {
log.Println("Failed to sign admins event", err)
} else {
result = append(result, &event)
}
}
return result
}
func MakePutUserEvent(event *nostr.Event) *nostr.Event {
putUser := nostr.Event{
func MakePutUserEvent(h string, pubkey nostr.PubKey) nostr.Event {
return nostr.Event{
Kind: nostr.KindSimpleGroupPutUser,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"p", event.PubKey},
nostr.Tag{"h", GetGroupIDFromEvent(event)},
nostr.Tag{"p", pubkey.Hex()},
nostr.Tag{"h", h},
},
}
if err := putUser.Sign(RELAY_SECRET); err != nil {
log.Println(err)
}
return &putUser
}
func MakeRemoveUserEvent(event *nostr.Event) *nostr.Event {
removeUser := nostr.Event{
func MakeRemoveUserEvent(h string, pubkey nostr.PubKey) nostr.Event {
return nostr.Event{
Kind: nostr.KindSimpleGroupRemoveUser,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"p", event.PubKey},
nostr.Tag{"h", GetGroupIDFromEvent(event)},
nostr.Tag{"p", pubkey.Hex()},
nostr.Tag{"h", h},
},
}
if err := removeUser.Sign(RELAY_SECRET); err != nil {
log.Println(err)
}
return &removeUser
}
func MakeMetadataEvent(event nostr.Event) nostr.Event {
return nostr.Event{
Kind: nostr.KindSimpleGroupMetadata,
CreatedAt: event.CreatedAt,
Tags: event.Tags,
}
}
+259 -39
View File
@@ -2,12 +2,14 @@ package zooid
import (
"context"
"slices"
"iter"
"log"
"net/http"
"sync"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip29"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/khatru"
"github.com/gosimple/slug"
@@ -19,7 +21,6 @@ type Instance struct {
Secret nostr.SecretKey
Events eventstore.Store
Access *AccessStore
Groups *GroupsStore
Blossom *BlossomStore
Management *ManagementStore
Relay *khatru.Relay
@@ -57,12 +58,6 @@ func MakeInstance(hostname string) (*Instance, error) {
Name: slug.Make(config.Self.Schema) + "_access",
},
},
Groups: &GroupsStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "_groups",
},
},
Blossom: &BlossomStore{
Config: config,
Schema: &Schema{
@@ -110,10 +105,6 @@ func MakeInstance(hostname string) (*Instance, error) {
log.Fatal("Failed to initialize access store:", err)
}
if err := instance.Groups.Init(); err != nil {
log.Fatal("Failed to initialize groups store:", err)
}
if err := instance.Blossom.Init(); err != nil {
log.Fatal("Failed to initialize blossom store:", err)
}
@@ -122,10 +113,6 @@ func MakeInstance(hostname string) (*Instance, error) {
log.Fatal("Failed to initialize management store:", err)
}
if config.Groups.Enabled {
instance.Groups.Enable(instance)
}
if config.Blossom.Enabled {
instance.Blossom.Enable(instance)
}
@@ -163,7 +150,7 @@ func GetInstance(hostname string) (*Instance, error) {
// Utility methods
func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
func (instance *Instance) IsAdmin(pubkey nostr.PubKey) bool {
if instance.Config.IsOwner(pubkey) {
return true
}
@@ -172,9 +159,15 @@ func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
return true
}
roles := instance.Config.GetRolesForPubkey(pubkey)
if instance.Config.CanManage(pubkey) {
return true
}
if instance.Config.CanManage(roles) {
return false
}
func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
if instance.IsAdmin(pubkey) {
return true
}
@@ -185,35 +178,76 @@ func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
return false
}
func (instance *Instance) GenerateInviteEvents(ctx context.Context, filter nostr.Filter) []*nostr.Event {
pubkey, ok := khatru.GetAuthed(ctx)
func (instance *Instance) IsGroupMember(id string, pubkey nostr.PubKey) bool {
filter := MakeGroupMembershipCheckFilter(id, pubkey)
events := instance.Events.QueryEvents(filter, 0)
isMember := CheckGroupMembership(events)
if !ok {
return []*nostr.Event{}
return isMember
}
func (instance *Instance) HasGroupAccess(id string, pubkey nostr.PubKey) bool {
filter := MakeGroupMetadataFilter(id)
for event := range instance.Events.QueryEvents(filter, 1) {
if !HasTag(event.Tags, "closed") {
return true
}
}
var claim string
return instance.IsGroupMember(id, pubkey)
}
invites := instance.Access.GetInvitesByPubkey(pubkey)
if len(invites) > 0 {
claim = First(invites).Claim
} else {
claim = RandomString(8)
instance.Access.AddInvite(pubkey, claim)
func (instance *Instance) AllowRecipientEvent(event nostr.Event) bool {
// For zap receipts and gift wraps, authorize the recipient instead of the author.
// For everything else, make sure the authenticated user is the same as the event author
recipientAuthKinds := []nostr.Kind{
nostr.KindZap,
nostr.KindGiftWrap,
}
event := nostr.Event{
Kind: AUTH_INVITE,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"claim", claim},
if slices.Contains(recipientAuthKinds, event.Kind) {
recipientTag := event.Tags.Find("p")
if recipientTag != nil {
pubkey, err := nostr.PubKeyFromHex(recipientTag[1])
if err == nil && instance.HasAccess(pubkey) {
return true
}
}
}
return false
}
func (instance *Instance) OnJoinEvent(event nostr.Event) (reject bool, msg string) {
claimTag := event.Tags.Find("claim")
if claimTag == nil {
return true, "invalid: no claim tag"
}
filter := nostr.Filter{
Kinds: []nostr.Kind{AUTH_INVITE},
Tags: nostr.TagMap{
"claim": []string{claimTag[1]},
},
}
event.Sign(instance.Secret)
for range instance.Events.QueryEvents(filter, 1) {
return false, ""
}
return []*nostr.Event{&event}
return true, "invalid: failed to validate invite code"
}
func (instance *Instance) GetGroupMetadataEvent(h string) nostr.Event {
for event := range instance.Events.QueryEvents(MakeGroupMetadataFilter(h), 1) {
return event
}
return nostr.Event{}
}
// Handlers
@@ -223,6 +257,78 @@ func (instance *Instance) OnConnect(ctx context.Context) {
}
func (instance *Instance) OnEvent(ctx context.Context, event nostr.Event) (reject bool, msg string) {
if instance.AllowRecipientEvent(event) {
return false, ""
}
pubkey, isAuthenticated := khatru.GetAuthed(ctx)
if !isAuthenticated {
return true, "auth-required: authentication is required for access"
} else if pubkey != event.PubKey {
return true, "restricted: you cannot publish events on behalf of others"
}
if event.Kind == AUTH_JOIN {
return instance.OnJoinEvent(event)
}
if !instance.HasAccess(pubkey) {
return true, "restricted: you are not a member of this relay"
}
if slices.Contains(nip29.MetadataEventKinds, event.Kind) {
return true, "invalid: group metadata cannot be set directly"
}
if slices.Contains(nip29.ModerationEventKinds, event.Kind) && !instance.IsAdmin(event.PubKey) {
return true, "restricted: you are not authorized to manage groups"
}
allGroupKinds := append(
nip29.ModerationEventKinds,
nostr.KindSimpleGroupJoinRequest,
nostr.KindSimpleGroupLeaveRequest,
)
h := GetGroupIDFromEvent(event)
if slices.Contains(allGroupKinds, event.Kind) {
if !instance.Config.Groups.Enabled {
return true, "invalid: group events not accepted on this relay"
}
if h == "" {
return true, "invalid: h tag is required"
}
meta := instance.GetGroupMetadataEvent(h)
if event.Kind == nostr.KindSimpleGroupCreateGroup && !IsEmptyEvent(meta) {
return true, "invalid: that group already exists"
} else if IsEmptyEvent(meta) {
return true, "invalid: no such group exists"
}
if event.Kind == nostr.KindSimpleGroupJoinRequest && instance.IsGroupMember(h, event.PubKey) {
return true, "duplicate: already a member"
}
if event.Kind == nostr.KindSimpleGroupLeaveRequest && !instance.IsGroupMember(h, event.PubKey) {
return true, "duplicate: not currently a member"
}
} else if h != "" {
meta := instance.GetGroupMetadataEvent(h)
if IsEmptyEvent(meta) {
return true, "invalid: no such group exists"
}
if HasTag(meta.Tags, "closed") && !instance.IsGroupMember(h, pubkey) {
return true, "restricted: you are not a member of that group"
}
}
return false, ""
}
@@ -239,19 +345,133 @@ func (instance *Instance) DeleteEvent(ctx context.Context, id nostr.ID) error {
}
func (instance *Instance) OnEventSaved(ctx context.Context, event nostr.Event) {
addEvent := func(newEvent nostr.Event) {
if err := newEvent.Sign(instance.Secret); err != nil {
log.Println(err)
} else {
if err := instance.Events.SaveEvent(newEvent); err != nil {
log.Println(err)
} else {
instance.Relay.BroadcastEvent(newEvent)
}
}
}
if event.Kind == nostr.KindSimpleGroupJoinRequest && instance.Config.Groups.AutoJoin {
h := GetGroupIDFromEvent(event)
meta := instance.GetGroupMetadataEvent(h)
if !HasTag(meta.Tags, "closed") {
addEvent(MakePutUserEvent(h, event.PubKey))
}
}
if event.Kind == nostr.KindSimpleGroupLeaveRequest && instance.Config.Groups.AutoLeave {
addEvent(MakeRemoveUserEvent(GetGroupIDFromEvent(event), event.PubKey))
}
if event.Kind == nostr.KindSimpleGroupCreateGroup {
addEvent(MakeMetadataEvent(event))
}
if event.Kind == nostr.KindSimpleGroupEditMetadata {
addEvent(MakeMetadataEvent(event))
}
if event.Kind == nostr.KindSimpleGroupDeleteGroup {
for _, filter := range MakeGroupEventFilters(GetGroupIDFromEvent(event)) {
for event := range instance.Events.QueryEvents(filter, 0) {
instance.Events.DeleteEvent(event.ID)
}
}
}
}
func (instance *Instance) OnEphemeralEvent(ctx context.Context, event nostr.Event) {
}
func (instance *Instance) OnRequest(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
pubkey, ok := khatru.GetAuthed(ctx)
if !ok {
return true, "auth-required: authentication is required for access"
}
if !instance.HasAccess(pubkey) {
return true, "restricted: you are not a member of this relay"
}
return false, ""
}
func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
return func(yield func(nostr.Event) bool) {
for evt := range instance.Events.QueryEvents(filter, 400) {
if !yield(evt) {
pubkey, ok := khatru.GetAuthed(ctx)
if !ok {
log.Fatal("Unauthenticated user was allowed to query events")
}
stripSignature := func(event nostr.Event) nostr.Event {
if instance.Config.Policy.StripSignatures && !instance.IsAdmin(pubkey) {
var zeroSig [64]byte
event.Sig = zeroSig
}
return event
}
if slices.Contains(filter.Kinds, AUTH_INVITE) && instance.Config.CanInvite(pubkey) {
var claim string
invites := instance.Access.GetInvitesByPubkey(pubkey)
if len(invites) > 0 {
claim = First(invites).Claim
} else {
claim = RandomString(8)
instance.Access.AddInvite(pubkey, claim)
}
event := nostr.Event{
Kind: AUTH_INVITE,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"claim", claim},
},
}
event.Sign(instance.Secret)
if !yield(stripSignature(event)) {
return
}
}
for event := range instance.Events.QueryEvents(filter, 1000) {
hTag := event.Tags.Find("h")
// Prune group related events if groups are disabled
if !instance.Config.Groups.Enabled {
if slices.Contains(nip29.ModerationEventKinds, event.Kind) {
continue
}
if slices.Contains(nip29.MetadataEventKinds, event.Kind) {
continue
}
if hTag != nil {
continue
}
}
// Prune events that the user doesn't have access to
if hTag != nil && !instance.HasGroupAccess(hTag[1], pubkey) {
continue
}
if !yield(event) {
return
}
}
+3 -3
View File
@@ -184,7 +184,7 @@ 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)) {
if ok && m.Config.CanManage(pubkey) {
return true, "blocked: only relay admins can manage this relay."
}
@@ -196,7 +196,7 @@ func (m *ManagementStore) Enable(instance *Instance) {
Authors: []nostr.PubKey{pubkey},
}
for event := range instance.Events.QueryEvents(filter, 1000000) {
for event := range instance.Events.QueryEvents(filter, 0) {
instance.Events.DeleteEvent(event.ID)
}
@@ -229,7 +229,7 @@ func (m *ManagementStore) Enable(instance *Instance) {
IDs: []nostr.ID{id},
}
for event := range instance.Events.QueryEvents(filter, 1000000) {
for event := range instance.Events.QueryEvents(filter, 0) {
instance.Events.DeleteEvent(event.ID)
}
+16
View File
@@ -3,6 +3,7 @@ package zooid
import (
"math/rand"
"strings"
"fiatjaf.com/nostr"
)
const (
@@ -59,3 +60,18 @@ func Split(s string, delim string) []string {
return strings.Split(s, delim)
}
}
func HasTag(tags nostr.Tags, key string) bool {
for _, v := range tags {
if len(v) >= 1 && v[0] == key {
return true
}
}
return false
}
func IsEmptyEvent(event nostr.Event) bool {
var zeroID nostr.ID
return event.ID == zeroID
}