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.
|
- `pubkey` - the public key of the relay owner. Does not affect access controls.
|
||||||
- `description` - your relay's description.
|
- `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]`
|
### `[groups]`
|
||||||
|
|
||||||
Configures NIP 29 support.
|
Configures NIP 29 support.
|
||||||
@@ -74,6 +80,9 @@ name = "My relay"
|
|||||||
schema = 'my_relay'
|
schema = 'my_relay'
|
||||||
secret = "ce30b1831a4551f4cb7a984033c34ab96d8cf56ff50df9d0c27d9fa5422f2278"
|
secret = "ce30b1831a4551f4cb7a984033c34ab96d8cf56ff50df9d0c27d9fa5422f2278"
|
||||||
|
|
||||||
|
[policy]
|
||||||
|
strip_signatures = false
|
||||||
|
|
||||||
[groups]
|
[groups]
|
||||||
enabled = true
|
enabled = true
|
||||||
auto_join = false
|
auto_join = false
|
||||||
@@ -100,6 +109,8 @@ See `justfile` for defined commands.
|
|||||||
## TODO
|
## 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 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
|
- [ ] Add admin/owner/etc to list allowed pubkeys
|
||||||
- [ ] Watch configuration files and hot reload
|
- [ ] Watch configuration files and hot reload
|
||||||
- [ ] Free up resources after instance inactivity
|
- [ ] Free up resources after instance inactivity
|
||||||
|
- [ ] Admins/members
|
||||||
|
|||||||
+6
-6
@@ -4,8 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type BlossomStore struct {
|
type BlossomStore struct {
|
||||||
Config *Config
|
Config *Config
|
||||||
Schema *Schema
|
Schema *Schema
|
||||||
Store eventstore.Store
|
Store eventstore.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bl *BlossomStore) Init() error {
|
func (bl *BlossomStore) Init() error {
|
||||||
@@ -26,11 +26,11 @@ func (bl *BlossomStore) Init() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blossom uses a wrapped event store for metadata
|
// Blossom uses a wrapped event store for metadata
|
||||||
bl.Store = &EventStore{Schema: bl.Schema}
|
bl.Store = &EventStore{Schema: bl.Schema}
|
||||||
|
|
||||||
if err := bl.Store.Init(); err != nil {
|
if err := bl.Store.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+17
-3
@@ -15,7 +15,7 @@ type Role struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Host string
|
Host string
|
||||||
Self struct {
|
Self struct {
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
Icon string `toml:"icon"`
|
Icon string `toml:"icon"`
|
||||||
@@ -25,6 +25,10 @@ type Config struct {
|
|||||||
Description string `toml:"description"`
|
Description string `toml:"description"`
|
||||||
} `toml:"self"`
|
} `toml:"self"`
|
||||||
|
|
||||||
|
Policy struct {
|
||||||
|
StripSignatures bool `toml:"strip_signatures"`
|
||||||
|
} `toml:"policy"`
|
||||||
|
|
||||||
Groups struct {
|
Groups struct {
|
||||||
Enabled bool `toml:"enabled"`
|
Enabled bool `toml:"enabled"`
|
||||||
AutoJoin bool `toml:"auto_join"`
|
AutoJoin bool `toml:"auto_join"`
|
||||||
@@ -79,8 +83,8 @@ func (config *Config) GetRolesForPubkey(pubkey nostr.PubKey) []Role {
|
|||||||
return roles
|
return roles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) CanManage(roles []Role) bool {
|
func (config *Config) CanManage(pubkey nostr.PubKey) bool {
|
||||||
for _, role := range roles {
|
for _, role := range config.GetRolesForPubkey(pubkey) {
|
||||||
if role.CanManage {
|
if role.CanManage {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -88,3 +92,13 @@ func (config *Config) CanManage(roles []Role) bool {
|
|||||||
|
|
||||||
return false
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := maxLimit
|
if maxLimit > 0 && maxLimit < filter.Limit {
|
||||||
if filter.Limit > 0 && filter.Limit < limit {
|
filter.Limit = maxLimit
|
||||||
limit = filter.Limit
|
}
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := events.buildSelectQuery(filter, limit).RunWith(GetDb()).Query()
|
rows, err := events.buildSelectQuery(filter).RunWith(GetDb()).Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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").
|
qb := squirrel.Select("id", "created_at", "kind", "pubkey", "content", "tags", "sig").
|
||||||
From(events.Schema.Prefix("events")).
|
From(events.Schema.Prefix("events")).
|
||||||
OrderBy("created_at DESC")
|
OrderBy("created_at DESC")
|
||||||
@@ -206,25 +205,26 @@ func (events *EventStore) buildSelectQuery(filter nostr.Filter, limit int) squir
|
|||||||
}
|
}
|
||||||
|
|
||||||
for tagKey, tagValues := range filter.Tags {
|
for tagKey, tagValues := range filter.Tags {
|
||||||
if len(tagValues) > 0 && len(tagKey) == 1 {
|
if len(tagValues) == 0 {
|
||||||
tagValueInterfaces := make([]interface{}, len(tagValues))
|
continue
|
||||||
for i, tagValue := range tagValues {
|
}
|
||||||
tagValueInterfaces[i] = tagValue
|
|
||||||
}
|
|
||||||
|
|
||||||
subQuery := squirrel.Select("event_id").
|
tagValueInterfaces := make([]interface{}, len(tagValues))
|
||||||
From(events.Schema.Prefix("event_tags")).
|
for i, tagValue := range tagValues {
|
||||||
Where(squirrel.Eq{"key": tagKey}).
|
tagValueInterfaces[i] = tagValue
|
||||||
Where(squirrel.Eq{"value": tagValueInterfaces})
|
|
||||||
|
|
||||||
subQuerySql, subQueryArgs, _ := subQuery.ToSql()
|
|
||||||
qb = qb.Where("id IN ("+subQuerySql+")", subQueryArgs...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 filter.Limit > 0 {
|
||||||
if limit > 0 {
|
qb = qb.Limit(uint64(filter.Limit))
|
||||||
qb = qb.Limit(uint64(limit))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return qb
|
return qb
|
||||||
@@ -316,7 +316,7 @@ func (events *EventStore) ReplaceEvent(evt nostr.Event) error {
|
|||||||
|
|
||||||
func (events *EventStore) CountEvents(filter nostr.Filter) (uint32, error) {
|
func (events *EventStore) CountEvents(filter nostr.Filter) (uint32, error) {
|
||||||
// Build a count query based on the select query but with COUNT(*) instead
|
// 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
|
// Convert the select query to a count query
|
||||||
countQb := squirrel.Select("COUNT(*)").FromSelect(qb, "subquery")
|
countQb := squirrel.Select("COUNT(*)").FromSelect(qb, "subquery")
|
||||||
|
|||||||
+51
-276
@@ -1,152 +1,56 @@
|
|||||||
package zooid
|
package zooid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"iter"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip29"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GroupsStore struct {
|
func GetGroupIDFromEvent(event nostr.Event) string {
|
||||||
Host string
|
tag := event.Tags.Find("h")
|
||||||
Config *Config
|
|
||||||
Schema *Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
func (groups *GroupsStore) Init() error {
|
if tag != nil {
|
||||||
schema := groups.Schema.Render(`
|
return tag[1]
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group CRUD
|
func MakeGroupMetadataFilter(h string) nostr.Filter {
|
||||||
|
return nostr.Filter{
|
||||||
func (groups *GroupsStore) SelectGroups() squirrel.SelectBuilder {
|
Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata},
|
||||||
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},
|
|
||||||
Tags: nostr.TagMap{
|
Tags: nostr.TagMap{
|
||||||
"p": []string{pubkey},
|
"a": []string{h},
|
||||||
"h": []string{id},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
events, err := GetBackend().QueryEvents(ctx, filter)
|
func MakeGroupEventFilters(h string) []nostr.Filter {
|
||||||
|
return []nostr.Filter{
|
||||||
if err != nil {
|
{
|
||||||
log.Println(err)
|
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 {
|
for event := range events {
|
||||||
if event.Kind == nostr.KindSimpleGroupPutUser {
|
if event.Kind == nostr.KindSimpleGroupPutUser {
|
||||||
return true
|
return true
|
||||||
@@ -160,161 +64,32 @@ func (groups *GroupStore) IsGroupMember(ctx context.Context, id string, pubkey s
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleCreateGroup(event *nostr.Event) {
|
func MakePutUserEvent(h string, pubkey nostr.PubKey) nostr.Event {
|
||||||
group := MakeGroup(GetGroupIDFromEvent(event))
|
return nostr.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{
|
|
||||||
Kind: nostr.KindSimpleGroupPutUser,
|
Kind: nostr.KindSimpleGroupPutUser,
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
Tags: nostr.Tags{
|
Tags: nostr.Tags{
|
||||||
nostr.Tag{"p", event.PubKey},
|
nostr.Tag{"p", pubkey.Hex()},
|
||||||
nostr.Tag{"h", GetGroupIDFromEvent(event)},
|
nostr.Tag{"h", h},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := putUser.Sign(RELAY_SECRET); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &putUser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeRemoveUserEvent(event *nostr.Event) *nostr.Event {
|
func MakeRemoveUserEvent(h string, pubkey nostr.PubKey) nostr.Event {
|
||||||
removeUser := nostr.Event{
|
return nostr.Event{
|
||||||
Kind: nostr.KindSimpleGroupRemoveUser,
|
Kind: nostr.KindSimpleGroupRemoveUser,
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
Tags: nostr.Tags{
|
Tags: nostr.Tags{
|
||||||
nostr.Tag{"p", event.PubKey},
|
nostr.Tag{"p", pubkey.Hex()},
|
||||||
nostr.Tag{"h", GetGroupIDFromEvent(event)},
|
nostr.Tag{"h", h},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err := removeUser.Sign(RELAY_SECRET); err != nil {
|
|
||||||
log.Println(err)
|
func MakeMetadataEvent(event nostr.Event) nostr.Event {
|
||||||
}
|
return nostr.Event{
|
||||||
|
Kind: nostr.KindSimpleGroupMetadata,
|
||||||
return &removeUser
|
CreatedAt: event.CreatedAt,
|
||||||
|
Tags: event.Tags,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+259
-39
@@ -2,12 +2,14 @@ package zooid
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
"iter"
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip29"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/khatru"
|
"fiatjaf.com/nostr/khatru"
|
||||||
"github.com/gosimple/slug"
|
"github.com/gosimple/slug"
|
||||||
@@ -19,7 +21,6 @@ type Instance struct {
|
|||||||
Secret nostr.SecretKey
|
Secret nostr.SecretKey
|
||||||
Events eventstore.Store
|
Events eventstore.Store
|
||||||
Access *AccessStore
|
Access *AccessStore
|
||||||
Groups *GroupsStore
|
|
||||||
Blossom *BlossomStore
|
Blossom *BlossomStore
|
||||||
Management *ManagementStore
|
Management *ManagementStore
|
||||||
Relay *khatru.Relay
|
Relay *khatru.Relay
|
||||||
@@ -57,12 +58,6 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
Name: slug.Make(config.Self.Schema) + "_access",
|
Name: slug.Make(config.Self.Schema) + "_access",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Groups: &GroupsStore{
|
|
||||||
Config: config,
|
|
||||||
Schema: &Schema{
|
|
||||||
Name: slug.Make(config.Self.Schema) + "_groups",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Blossom: &BlossomStore{
|
Blossom: &BlossomStore{
|
||||||
Config: config,
|
Config: config,
|
||||||
Schema: &Schema{
|
Schema: &Schema{
|
||||||
@@ -110,10 +105,6 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
log.Fatal("Failed to initialize access store:", err)
|
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 {
|
if err := instance.Blossom.Init(); err != nil {
|
||||||
log.Fatal("Failed to initialize blossom store:", err)
|
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)
|
log.Fatal("Failed to initialize management store:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Groups.Enabled {
|
|
||||||
instance.Groups.Enable(instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Blossom.Enabled {
|
if config.Blossom.Enabled {
|
||||||
instance.Blossom.Enable(instance)
|
instance.Blossom.Enable(instance)
|
||||||
}
|
}
|
||||||
@@ -163,7 +150,7 @@ func GetInstance(hostname string) (*Instance, error) {
|
|||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
|
||||||
func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
|
func (instance *Instance) IsAdmin(pubkey nostr.PubKey) bool {
|
||||||
if instance.Config.IsOwner(pubkey) {
|
if instance.Config.IsOwner(pubkey) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -172,9 +159,15 @@ func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
|
|||||||
return true
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,35 +178,76 @@ func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *Instance) GenerateInviteEvents(ctx context.Context, filter nostr.Filter) []*nostr.Event {
|
func (instance *Instance) IsGroupMember(id string, pubkey nostr.PubKey) bool {
|
||||||
pubkey, ok := khatru.GetAuthed(ctx)
|
filter := MakeGroupMembershipCheckFilter(id, pubkey)
|
||||||
|
events := instance.Events.QueryEvents(filter, 0)
|
||||||
|
isMember := CheckGroupMembership(events)
|
||||||
|
|
||||||
if !ok {
|
return isMember
|
||||||
return []*nostr.Event{}
|
}
|
||||||
|
|
||||||
|
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)
|
func (instance *Instance) AllowRecipientEvent(event nostr.Event) bool {
|
||||||
|
// For zap receipts and gift wraps, authorize the recipient instead of the author.
|
||||||
if len(invites) > 0 {
|
// For everything else, make sure the authenticated user is the same as the event author
|
||||||
claim = First(invites).Claim
|
recipientAuthKinds := []nostr.Kind{
|
||||||
} else {
|
nostr.KindZap,
|
||||||
claim = RandomString(8)
|
nostr.KindGiftWrap,
|
||||||
instance.Access.AddInvite(pubkey, claim)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
event := nostr.Event{
|
if slices.Contains(recipientAuthKinds, event.Kind) {
|
||||||
Kind: AUTH_INVITE,
|
recipientTag := event.Tags.Find("p")
|
||||||
CreatedAt: nostr.Now(),
|
|
||||||
Tags: nostr.Tags{
|
if recipientTag != nil {
|
||||||
nostr.Tag{"claim", claim},
|
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
|
// 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) {
|
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, ""
|
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) {
|
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) OnEphemeralEvent(ctx context.Context, event nostr.Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *Instance) OnRequest(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
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, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
|
func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
|
||||||
return func(yield func(nostr.Event) bool) {
|
return func(yield func(nostr.Event) bool) {
|
||||||
for evt := range instance.Events.QueryEvents(filter, 400) {
|
pubkey, ok := khatru.GetAuthed(ctx)
|
||||||
if !yield(evt) {
|
|
||||||
|
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
|
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) {
|
instance.Relay.ManagementAPI.OnAPICall = func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) {
|
||||||
pubkey, ok := khatru.GetAuthed(ctx)
|
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."
|
return true, "blocked: only relay admins can manage this relay."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ func (m *ManagementStore) Enable(instance *Instance) {
|
|||||||
Authors: []nostr.PubKey{pubkey},
|
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)
|
instance.Events.DeleteEvent(event.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ func (m *ManagementStore) Enable(instance *Instance) {
|
|||||||
IDs: []nostr.ID{id},
|
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)
|
instance.Events.DeleteEvent(event.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package zooid
|
|||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -59,3 +60,18 @@ func Split(s string, delim string) []string {
|
|||||||
return strings.Split(s, delim)
|
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