forked from coracle/zooid
Add handlers
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user