forked from coracle/zooid
Add access store
This commit is contained in:
@@ -60,6 +60,7 @@ Defines roles that can be assigned to different users and attendant privileges.
|
|||||||
|
|
||||||
- `pubkeys` - a list of nostr pubkeys this role is assigned to.
|
- `pubkeys` - a list of nostr pubkeys this role is assigned to.
|
||||||
- `can_invite` - a boolean indicating whether this role can invite new members to the relay by requesting a `kind 28935` claim. Defaults to `false`. See [access requests](https://github.com/nostr-protocol/nips/pull/1079) for more details.
|
- `can_invite` - a boolean indicating whether this role can invite new members to the relay by requesting a `kind 28935` claim. Defaults to `false`. See [access requests](https://github.com/nostr-protocol/nips/pull/1079) for more details.
|
||||||
|
- `can_manage` - a boolean indicating whether this role can use NIP 86 relay management and administer NIP 29 groups. Defaults to `false`.
|
||||||
|
|
||||||
A special `[roles.member]` heading may be used to configure policies for all relay users (that is, pubkeys assigned to other roles, or who have redeemed an invite code).
|
A special `[roles.member]` heading may be used to configure policies for all relay users (that is, pubkeys assigned to other roles, or who have redeemed an invite code).
|
||||||
|
|
||||||
@@ -89,6 +90,7 @@ can_invite = true
|
|||||||
|
|
||||||
[roles.admin]
|
[roles.admin]
|
||||||
pubkeys = ["d9254d9898fd4728f7e2b32b87520221a50f6b8b97d935d7da2de8923988aa6d"]
|
pubkeys = ["d9254d9898fd4728f7e2b32b87520221a50f6b8b97d935d7da2de8923988aa6d"]
|
||||||
|
can_manage = true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
package zooid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Invite struct {
|
||||||
|
ID string
|
||||||
|
CreatedAt int
|
||||||
|
Pubkey nostr.PubKey
|
||||||
|
Claim string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Redemption struct {
|
||||||
|
ID string
|
||||||
|
InviteID string
|
||||||
|
CreatedAt int
|
||||||
|
Pubkey nostr.PubKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessStore struct {
|
||||||
|
Config *Config
|
||||||
|
Schema *Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) Init() error {
|
||||||
|
schema := access.Schema.Render(`
|
||||||
|
CREATE TABLE IF NOT EXISTS {{.Prefix}}__invites (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
claim TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_invites_created_at ON {{.Prefix}}__invites(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_invites_pubkey ON {{.Prefix}}__invites(pubkey);
|
||||||
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_invites_claim ON {{.Prefix}}__invites(claim);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS {{.Prefix}}__redemptions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
invite_id TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (invite_id) REFERENCES {{.Prefix}}__invites(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_redemptions_invite_id ON {{.Prefix}}__redemptions(invite_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_redemptions_created_at ON {{.Prefix}}__redemptions(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_redemptions_pubkey ON {{.Prefix}}__redemptions(pubkey);
|
||||||
|
`)
|
||||||
|
|
||||||
|
if _, err := GetDb().Exec(schema); err != nil {
|
||||||
|
return fmt.Errorf("failed to create schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite utils
|
||||||
|
|
||||||
|
func (access *AccessStore) SelectInvites() squirrel.SelectBuilder {
|
||||||
|
return squirrel.Select("id", "created_at", "pubkey", "claim").From(access.Schema.Prefix("invites"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) QueryInvites(builder squirrel.SelectBuilder) []Invite {
|
||||||
|
rows, err := builder.RunWith(GetDb()).Query()
|
||||||
|
if err != nil {
|
||||||
|
return []Invite{}
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var invites []Invite
|
||||||
|
for rows.Next() {
|
||||||
|
var invite Invite
|
||||||
|
var pubkeyStr string
|
||||||
|
err := rows.Scan(&invite.ID, &invite.CreatedAt, &pubkeyStr, &invite.Claim)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubkey, err := nostr.PubKeyFromHex(pubkeyStr); err == nil {
|
||||||
|
invite.Pubkey = pubkey
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
invites = append(invites, invite)
|
||||||
|
}
|
||||||
|
|
||||||
|
return invites
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) AddInvite(pubkey nostr.PubKey, claim string) error {
|
||||||
|
id := RandomString(32)
|
||||||
|
createdAt := int(time.Now().Unix())
|
||||||
|
|
||||||
|
insertQb := squirrel.Insert(access.Schema.Prefix("invites")).
|
||||||
|
Columns("id", "created_at", "pubkey", "claim").
|
||||||
|
Values(id, createdAt, pubkey.Hex(), claim)
|
||||||
|
|
||||||
|
_, err := insertQb.RunWith(GetDb()).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add invite: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) GetInvitesByClaim(claim string) []Invite {
|
||||||
|
return access.QueryInvites(access.SelectInvites().Where(squirrel.Eq{"claim": claim}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) GetInvitesByPubkey(pubkey nostr.PubKey) []Invite {
|
||||||
|
return access.QueryInvites(access.SelectInvites().Where(squirrel.Eq{"pubkey": pubkey.Hex()}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redemption utils
|
||||||
|
|
||||||
|
func (access *AccessStore) SelectRedemptions() squirrel.SelectBuilder {
|
||||||
|
return squirrel.Select("id", "invite_id", "created_at", "pubkey").From(access.Schema.Prefix("redemptions"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) QueryRedemptions(builder squirrel.SelectBuilder) []Redemption {
|
||||||
|
rows, err := builder.RunWith(GetDb()).Query()
|
||||||
|
if err != nil {
|
||||||
|
return []Redemption{}
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var redemptions []Redemption
|
||||||
|
for rows.Next() {
|
||||||
|
var redemption Redemption
|
||||||
|
var pubkeyStr string
|
||||||
|
|
||||||
|
err := rows.Scan(&redemption.ID, &redemption.InviteID, &redemption.CreatedAt, &pubkeyStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubkey, err := nostr.PubKeyFromHex(pubkeyStr); err == nil {
|
||||||
|
redemption.Pubkey = pubkey
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
redemptions = append(redemptions, redemption)
|
||||||
|
}
|
||||||
|
|
||||||
|
return redemptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) AddRedemption(pubkey nostr.PubKey, invite Invite) error {
|
||||||
|
id := RandomString(32)
|
||||||
|
createdAt := int(time.Now().Unix())
|
||||||
|
|
||||||
|
insertQb := squirrel.Insert(access.Schema.Prefix("redemptions")).
|
||||||
|
Columns("id", "invite_id", "created_at", "pubkey").
|
||||||
|
Values(id, invite.ID, createdAt, pubkey.Hex())
|
||||||
|
|
||||||
|
_, err := insertQb.RunWith(GetDb()).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add invite: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (access *AccessStore) GetRedemptionsByPubkey(pubkey nostr.PubKey) []Invite {
|
||||||
|
return access.QueryInvites(access.SelectRedemptions().Where(squirrel.Eq{"pubkey": pubkey.Hex()}))
|
||||||
|
}
|
||||||
+10
-9
@@ -15,14 +15,15 @@ import (
|
|||||||
|
|
||||||
func EnableBlossom(instance *Instance) {
|
func EnableBlossom(instance *Instance) {
|
||||||
fs := afero.NewOsFs()
|
fs := afero.NewOsFs()
|
||||||
|
dir := Env("DATA") + "/media"
|
||||||
|
|
||||||
if err := fs.MkdirAll(Env("DATA"), 0755); err != nil {
|
if err := fs.MkdirAll(dir, 0755); err != nil {
|
||||||
log.Fatal("🚫 error creating blossom path:", err)
|
log.Fatal("🚫 error creating blossom path:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
store := &EventStore{
|
store := &EventStore{
|
||||||
Schema: &Schema{
|
Schema: &Schema{
|
||||||
Name: slug.Make(config.Self.Schema) + "__blossom",
|
Name: slug.Make(instance.Config.Self.Schema) + "__blossom",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ func EnableBlossom(instance *Instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error {
|
backend.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error {
|
||||||
file, err := fs.Create(instance.Config.Blossom.Directory + "/" + sha256)
|
file, err := fs.Create(dir + "/" + sha256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,7 @@ func EnableBlossom(instance *Instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) {
|
backend.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) {
|
||||||
file, err := fs.Open(instance.Config.Blossom.Directory + "/" + sha256)
|
file, err := fs.Open(dir + "/" + sha256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ func EnableBlossom(instance *Instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error {
|
backend.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error {
|
||||||
return fs.Remove(instance.Config.Blossom.Directory + "/" + sha256)
|
return fs.Remove(dir + "/" + sha256)
|
||||||
}
|
}
|
||||||
|
|
||||||
backend.RejectUpload = func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) {
|
backend.RejectUpload = func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) {
|
||||||
@@ -63,7 +64,7 @@ func EnableBlossom(instance *Instance) {
|
|||||||
return true, "file too large", 413
|
return true, "file too large", 413
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth == nil || !instance.IsMember(auth.PubKey) {
|
if auth == nil || !instance.HasAccess(auth.PubKey) {
|
||||||
return true, "unauthorized", 403
|
return true, "unauthorized", 403
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ func EnableBlossom(instance *Instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.RejectGet = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) {
|
backend.RejectGet = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) {
|
||||||
if auth == nil || !instance.IsMember(auth.PubKey) {
|
if auth == nil || !instance.HasAccess(auth.PubKey) {
|
||||||
return true, "unauthorized", 403
|
return true, "unauthorized", 403
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ func EnableBlossom(instance *Instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.RejectList = func(ctx context.Context, auth *nostr.Event, pubkey nostr.PubKey) (bool, string, int) {
|
backend.RejectList = func(ctx context.Context, auth *nostr.Event, pubkey nostr.PubKey) (bool, string, int) {
|
||||||
if auth == nil || !instance.IsMember(auth.PubKey) {
|
if auth == nil || !instance.HasAccess(auth.PubKey) {
|
||||||
return true, "unauthorized", 403
|
return true, "unauthorized", 403
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ func EnableBlossom(instance *Instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.RejectDelete = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) {
|
backend.RejectDelete = func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) {
|
||||||
if auth == nil || !instance.IsMember(auth.PubKey) {
|
if auth == nil || !instance.HasAccess(auth.PubKey) {
|
||||||
return true, "unauthorized", 403
|
return true, "unauthorized", 403
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-11
@@ -1,15 +1,24 @@
|
|||||||
package zooid
|
package zooid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
Pubkeys []string `toml:"pubkeys"`
|
||||||
|
CanInvite bool `toml:"can_invite"`
|
||||||
|
CanManage bool `toml:"can_manage"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Self struct {
|
Self struct {
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
Icon string `toml:"icon"`
|
Icon string `toml:"icon"`
|
||||||
|
Schema string `toml:"schema"`
|
||||||
Secret string `toml:"secret"`
|
Secret string `toml:"secret"`
|
||||||
Pubkey string `toml:"pubkey"`
|
Pubkey string `toml:"pubkey"`
|
||||||
Description string `toml:"description"`
|
Description string `toml:"description"`
|
||||||
@@ -27,19 +36,10 @@ type Config struct {
|
|||||||
} `toml:"management"`
|
} `toml:"management"`
|
||||||
|
|
||||||
Blossom struct {
|
Blossom struct {
|
||||||
Enabled bool `toml:"enabled"`
|
Enabled bool `toml:"enabled"`
|
||||||
Directory string `toml:"directory"`
|
|
||||||
} `toml:"blossom"`
|
} `toml:"blossom"`
|
||||||
|
|
||||||
Roles map[string]struct {
|
Roles map[string]Role `toml:"roles"`
|
||||||
Pubkeys []string `toml:"pubkeys"`
|
|
||||||
CanInvite bool `toml:"can_invite"`
|
|
||||||
} `toml:"roles"`
|
|
||||||
|
|
||||||
Data struct {
|
|
||||||
Events string `toml:"events"`
|
|
||||||
Blossom string `toml:"blossom"`
|
|
||||||
} `toml:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(hostname string) (*Config, error) {
|
func LoadConfig(hostname string) (*Config, error) {
|
||||||
@@ -52,3 +52,36 @@ func LoadConfig(hostname string) (*Config, error) {
|
|||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) IsSelf(pubkey nostr.PubKey) bool {
|
||||||
|
return pubkey == nostr.MustSecretKeyFromHex(config.Self.Secret).Public()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) IsOwner(pubkey nostr.PubKey) bool {
|
||||||
|
return pubkey == nostr.MustPubKeyFromHex(config.Self.Pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) GetRolesForPubkey(pubkey nostr.PubKey) []Role {
|
||||||
|
roles := make([]Role, 0)
|
||||||
|
for name, role := range config.Roles {
|
||||||
|
if name == "member" {
|
||||||
|
roles = append(roles, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(role.Pubkeys, pubkey.Hex()) {
|
||||||
|
roles = append(roles, role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) CanManage(roles []Role) bool {
|
||||||
|
for _, role := range roles {
|
||||||
|
if role.CanManage {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package zooid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
env map[string]string
|
||||||
|
envOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func Env(k string, fallback ...string) (v string) {
|
||||||
|
envOnce.Do(func() {
|
||||||
|
env = make(map[string]string)
|
||||||
|
|
||||||
|
env["PORT"] = "3334"
|
||||||
|
env["DATA"] = "./data"
|
||||||
|
|
||||||
|
for _, item := range os.Environ() {
|
||||||
|
parts := strings.SplitN(item, "=", 2)
|
||||||
|
env[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
v = env[k]
|
||||||
|
|
||||||
|
if v == "" && len(fallback) > 0 {
|
||||||
|
v = fallback[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type EventStore struct {
|
type EventStore struct {
|
||||||
|
Config *Config
|
||||||
Schema *Schema
|
Schema *Schema
|
||||||
FTSAvailable bool
|
FTSAvailable bool
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-17
@@ -5,7 +5,6 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
@@ -17,7 +16,9 @@ import (
|
|||||||
type Instance struct {
|
type Instance struct {
|
||||||
Host string
|
Host string
|
||||||
Config *Config
|
Config *Config
|
||||||
|
Secret nostr.SecretKey
|
||||||
Events eventstore.Store
|
Events eventstore.Store
|
||||||
|
Access *AccessStore
|
||||||
Relay *khatru.Relay
|
Relay *khatru.Relay
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,15 +33,23 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// secret, err := nostr.SecretKeyFromHex(config.Self.Secret)
|
secret, err := nostr.SecretKeyFromHex(config.Self.Secret)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return nil, err
|
return nil, err
|
||||||
// }
|
}
|
||||||
|
|
||||||
instance := &Instance{
|
instance := &Instance{
|
||||||
Host: hostname,
|
Host: hostname,
|
||||||
Config: config,
|
Config: config,
|
||||||
|
Secret: secret,
|
||||||
Events: &EventStore{
|
Events: &EventStore{
|
||||||
|
Config: config,
|
||||||
|
Schema: &Schema{
|
||||||
|
Name: slug.Make(config.Self.Schema) + "__events",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Access: &AccessStore{
|
||||||
|
Config: config,
|
||||||
Schema: &Schema{
|
Schema: &Schema{
|
||||||
Name: slug.Make(config.Self.Schema) + "__events",
|
Name: slug.Make(config.Self.Schema) + "__events",
|
||||||
},
|
},
|
||||||
@@ -84,14 +93,14 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
|
|
||||||
// Initialize stuff
|
// Initialize stuff
|
||||||
|
|
||||||
if err := os.MkdirAll(instance.Config.Data.Events, 0755); err != nil {
|
|
||||||
log.Fatal("Failed to create event store path:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := instance.Events.Init(); err != nil {
|
if err := instance.Events.Init(); err != nil {
|
||||||
log.Fatal("Failed to initialize event store:", err)
|
log.Fatal("Failed to initialize event store:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := instance.Access.Init(); err != nil {
|
||||||
|
log.Fatal("Failed to initialize access store:", err)
|
||||||
|
}
|
||||||
|
|
||||||
return instance, nil
|
return instance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,18 +130,59 @@ func GetInstance(hostname string) (*Instance, error) {
|
|||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
|
||||||
func (instance *Instance) IsMember(pubkey nostr.PubKey) bool {
|
func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool {
|
||||||
pubkeyStr := pubkey.String()
|
if instance.Config.IsOwner(pubkey) {
|
||||||
for _, role := range instance.Config.Roles {
|
return true
|
||||||
for _, pk := range role.Pubkeys {
|
|
||||||
if pk == pubkeyStr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if instance.Config.IsSelf(pubkey) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
roles := instance.Config.GetRolesForPubkey(pubkey)
|
||||||
|
|
||||||
|
if instance.Config.CanManage(roles) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instance.Access.GetRedemptionsByPubkey(pubkey)) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (instance *Instance) GenerateInviteEvents(ctx context.Context, filter nostr.Filter) []*nostr.Event {
|
||||||
|
pubkey, ok := khatru.GetAuthed(ctx)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return []*nostr.Event{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return []*nostr.Event{&event}
|
||||||
|
}
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
|
|
||||||
func (instance *Instance) OnConnect(ctx context.Context) {
|
func (instance *Instance) OnConnect(ctx context.Context) {
|
||||||
|
|||||||
+47
-25
@@ -1,9 +1,8 @@
|
|||||||
package zooid
|
package zooid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -11,29 +10,52 @@ const (
|
|||||||
AUTH_INVITE = 28935
|
AUTH_INVITE = 28935
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func First[T any](s []T) T {
|
||||||
env map[string]string
|
if len(s) == 0 {
|
||||||
envOnce sync.Once
|
var zero T
|
||||||
)
|
return zero
|
||||||
|
|
||||||
func Env(k string, fallback ...string) (v string) {
|
|
||||||
envOnce.Do(func() {
|
|
||||||
env = make(map[string]string)
|
|
||||||
|
|
||||||
env["PORT"] = "3334"
|
|
||||||
env["DATA"] = "./data"
|
|
||||||
|
|
||||||
for _, item := range os.Environ() {
|
|
||||||
parts := strings.SplitN(item, "=", 2)
|
|
||||||
env[parts[0]] = parts[1]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
v = env[k]
|
|
||||||
|
|
||||||
if v == "" && len(fallback) > 0 {
|
|
||||||
v = fallback[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return v
|
return s[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Keys[K comparable, V any](m map[K]V) []K {
|
||||||
|
ks := make([]K, len(m))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for k := range m {
|
||||||
|
ks[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
|
||||||
|
func Filter[T any](ss []T, test func(T) bool) (ret []T) {
|
||||||
|
for _, s := range ss {
|
||||||
|
if test(s) {
|
||||||
|
ret = append(ret, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
func RandomString(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Split(s string, delim string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return []string{}
|
||||||
|
} else {
|
||||||
|
return strings.Split(s, delim)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user