merge from github.com/fiatjaf/relay29
This commit is contained in:
parent
4033a871c4
commit
9cd490bd84
94
README.adoc
94
README.adoc
@ -1,35 +1,79 @@
|
||||
### Ensure Go is installed on your server
|
||||
= relay29 image:https://pkg.go.dev/badge/github.com/fiatjaf/relay29.svg[link=https://pkg.go.dev/github.com/fiatjaf/relay29]
|
||||
|
||||
### Pull the code from Git
|
||||
NIP-29 requires the relays to have more of an active role in making groups work with the rules, so this is a library for creating NIP-29 relays, works with https://github.com/fiatjaf/khatru[khatru] using the https://pkg.go.dev/github.com/fiatjaf/relay29/khatru29[khatru29] wrapper, https://github.com/hoytech/strfry[strfry] with link:strfry29[strfry29] and https://github.com/fiatjaf/relayer[relayer] with link:relayer29[relayer29].
|
||||
|
||||
```sh
|
||||
git clone https://github.com/0xchat-app/relay29
|
||||
```
|
||||
CAUTION: This is probably broken so please don't trust it for anything serious and be prepared to delete your database.
|
||||
|
||||
### Set environment variables
|
||||
[source,go]
|
||||
----
|
||||
package main
|
||||
|
||||
```sh
|
||||
export PORT=
|
||||
export DOMAIN=
|
||||
export RELAY_NAME=
|
||||
export RELAY_PRIVKEY=
|
||||
```
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
### Build the project
|
||||
"github.com/fiatjaf/eventstore/slicestore"
|
||||
"github.com/fiatjaf/khatru/policies"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/fiatjaf/relay29/khatru29"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
```sh
|
||||
cd relay29/groups.0xchat.com
|
||||
go build -o groupRelay
|
||||
```
|
||||
func main() {
|
||||
relayPrivateKey := nostr.GeneratePrivateKey()
|
||||
|
||||
### Execute the binary
|
||||
db := &slicestore.SliceStore{} // this only keeps things in memory, use a different eventstore in production
|
||||
db.Init()
|
||||
|
||||
```sh
|
||||
./groupRelay
|
||||
```
|
||||
relay, _ := khatru29.Init(relay29.Options{
|
||||
Domain: "localhost:2929",
|
||||
DB: db,
|
||||
SecretKey: relayPrivateKey,
|
||||
})
|
||||
|
||||
### Connect to the server
|
||||
// init relay
|
||||
relay.Info.Name = "very ephemeral chat relay"
|
||||
relay.Info.Description = "everything will be deleted as soon as I turn off my computer"
|
||||
|
||||
```sh
|
||||
ws://127.0.0.1:5577 (default port)
|
||||
```
|
||||
// extra policies
|
||||
relay.RejectEvent = append(relay.RejectEvent,
|
||||
policies.PreventLargeTags(64),
|
||||
policies.PreventTooManyIndexableTags(6, []int{9005}, nil),
|
||||
policies.RestrictToSpecifiedKinds(
|
||||
9, 10, 11, 12,
|
||||
30023, 31922, 31923, 9802,
|
||||
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007,
|
||||
9021,
|
||||
),
|
||||
policies.PreventTimestampsInThePast(60 * time.Second),
|
||||
policies.PreventTimestampsInTheFuture(30 * time.Second),
|
||||
)
|
||||
|
||||
// http routes
|
||||
relay.Router().HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "nothing to see here, you must use a nip-29 powered client")
|
||||
})
|
||||
|
||||
fmt.Println("running on http://0.0.0.0:2929")
|
||||
if err := http.ListenAndServe(":2929", relay); err != nil {
|
||||
log.Fatal("failed to serve")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
== How to use
|
||||
|
||||
Basically you just call `khatru29.Init()` and then you get back a `khatru.Relay` and a `relay29.State` instances. The state has inside it also a map of `Group` objects that you can read but you should not modify manually. To modify these groups you must write moderation events with the `.AddEvent()` method of the `Relay`. This API may be improved later.
|
||||
|
||||
See link:examples/groups.fiatjaf.com/main.go[] for a (not very much) more complex example.
|
||||
|
||||
== How it works
|
||||
|
||||
What this library does is basically:
|
||||
- it keeps a list of of groups with metadata in memory (not the messages);
|
||||
- it checks a bunch of stuff for every event and filter received;
|
||||
- it acts on moderation events and on join-request events received and modify the group state;
|
||||
- it generates group metadata events (39000, 39001, 39002) events on the fly (these are not stored) and returns them to whoever queries them;
|
||||
- on startup it loads all the moderation events (9000, 9001, etc) from the database and rebuilds the group state from that (so if you want to modify the group state permanently you must publish one of these events to the relay — but of course you can also monkey-patch the map of groups in memory like an animal if you want);
|
||||
|
@ -6,13 +6,12 @@ import (
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip29"
|
||||
nip29_relay "github.com/nbd-wtf/go-nostr/nip29/relay"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const tooOld = 60 // seconds
|
||||
|
||||
func (s *State) requireHTagForExistingGroup(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
func (s *State) RequireHTagForExistingGroup(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
// this allows us to never check again in any of the other rules if the tag exists and just assume it exists always
|
||||
gtag := event.Tags.GetFirst([]string{"h", ""})
|
||||
if gtag == nil {
|
||||
@ -32,7 +31,7 @@ func (s *State) requireHTagForExistingGroup(ctx context.Context, event *nostr.Ev
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (s *State) restrictWritesBasedOnGroupRules(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
func (s *State) RestrictWritesBasedOnGroupRules(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
group := s.GetGroupFromEvent(event)
|
||||
|
||||
if event.Kind == nostr.KindSimpleGroupJoinRequest {
|
||||
@ -70,14 +69,14 @@ func (s *State) restrictWritesBasedOnGroupRules(ctx context.Context, event *nost
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (s *State) preventWritingOfEventsJustDeleted(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
func (s *State) PreventWritingOfEventsJustDeleted(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if s.deletedCache.Has(event.ID) {
|
||||
return true, "this was deleted"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (s *State) requireModerationEventsToBeRecent(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
func (s *State) RequireModerationEventsToBeRecent(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
// moderation action events must be new and not reused
|
||||
if nip29.ModerationEventKinds.Includes(event.Kind) && event.CreatedAt < nostr.Now()-tooOld {
|
||||
return true, "moderation action is too old (older than 1 minute ago)"
|
||||
@ -85,7 +84,7 @@ func (s *State) requireModerationEventsToBeRecent(ctx context.Context, event *no
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (s *State) restrictInvalidModerationActions(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
func (s *State) RestrictInvalidModerationActions(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if !nip29.ModerationEventKinds.Includes(event.Kind) {
|
||||
return false, ""
|
||||
}
|
||||
@ -102,17 +101,13 @@ func (s *State) restrictInvalidModerationActions(ctx context.Context, event *nos
|
||||
return false, ""
|
||||
}
|
||||
|
||||
action, err := nip29_relay.GetModerationAction(event)
|
||||
action, err := PrepareModerationAction(event)
|
||||
if err != nil {
|
||||
return true, "invalid moderation action: " + err.Error()
|
||||
}
|
||||
|
||||
// remove self from group
|
||||
if event.Kind == nostr.KindSimpleGroupRemoveUser {
|
||||
users := GetUsersFromEvent(event)
|
||||
if len(users) == 1 && event.PubKey == users[0] {
|
||||
return false, ""
|
||||
}
|
||||
if egs, ok := action.(EditGroupStatus); ok && egs.Private && !s.AllowPrivateGroups {
|
||||
return true, "groups cannot be private"
|
||||
}
|
||||
|
||||
group.mu.RLock()
|
||||
@ -127,9 +122,9 @@ func (s *State) restrictInvalidModerationActions(ctx context.Context, event *nos
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (s *State) applyModerationAction(ctx context.Context, event *nostr.Event) {
|
||||
func (s *State) ApplyModerationAction(ctx context.Context, event *nostr.Event) {
|
||||
// turn event into a moderation action processor
|
||||
action, err := nip29_relay.GetModerationAction(event)
|
||||
action, err := PrepareModerationAction(event)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -144,6 +139,7 @@ func (s *State) applyModerationAction(ctx context.Context, event *nostr.Event) {
|
||||
} else {
|
||||
group = s.GetGroupFromEvent(event)
|
||||
}
|
||||
|
||||
// apply the moderation action
|
||||
group.mu.Lock()
|
||||
action.Apply(&group.Group)
|
||||
@ -176,6 +172,9 @@ func (s *State) applyModerationAction(ctx context.Context, event *nostr.Event) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if event.Kind == nostr.KindSimpleGroupDeleteGroup {
|
||||
// when the group was deleted we just remove it
|
||||
s.Groups.Delete(group.Address.ID)
|
||||
}
|
||||
|
||||
// propagate new replaceable events to listeners depending on what changed happened
|
||||
@ -206,16 +205,17 @@ func (s *State) applyModerationAction(ctx context.Context, event *nostr.Event) {
|
||||
},
|
||||
}[event.Kind] {
|
||||
evt := toBroadcast()
|
||||
evt.Sign(s.privateKey)
|
||||
evt.Sign(s.secretKey)
|
||||
s.Relay.BroadcastEvent(evt)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) reactToJoinRequest(ctx context.Context, event *nostr.Event) {
|
||||
func (s *State) ReactToJoinRequest(ctx context.Context, event *nostr.Event) {
|
||||
if event.Kind != nostr.KindSimpleGroupJoinRequest {
|
||||
return
|
||||
}
|
||||
|
||||
// if the group is open, anyone requesting to join will be allowed
|
||||
group := s.GetGroupFromEvent(event)
|
||||
if !group.Closed {
|
||||
// immediately add the requester
|
||||
@ -227,7 +227,7 @@ func (s *State) reactToJoinRequest(ctx context.Context, event *nostr.Event) {
|
||||
nostr.Tag{"p", event.PubKey},
|
||||
},
|
||||
}
|
||||
if err := addUser.Sign(s.privateKey); err != nil {
|
||||
if err := addUser.Sign(s.secretKey); err != nil {
|
||||
log.Error().Err(err).Msg("failed to sign add-user event")
|
||||
return
|
||||
}
|
||||
@ -238,3 +238,32 @@ func (s *State) reactToJoinRequest(ctx context.Context, event *nostr.Event) {
|
||||
s.Relay.BroadcastEvent(addUser)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) ReactToLeaveRequest(ctx context.Context, event *nostr.Event) {
|
||||
if event.Kind != nostr.KindSimpleGroupLeaveRequest {
|
||||
return
|
||||
}
|
||||
|
||||
group := s.GetGroupFromEvent(event)
|
||||
|
||||
if _, isMember := group.Members[event.PubKey]; isMember {
|
||||
// immediately remove the requester
|
||||
removeUser := &nostr.Event{
|
||||
CreatedAt: nostr.Now(),
|
||||
Kind: nostr.KindSimpleGroupRemoveUser,
|
||||
Tags: nostr.Tags{
|
||||
nostr.Tag{"h", group.Address.ID},
|
||||
nostr.Tag{"p", event.PubKey},
|
||||
},
|
||||
}
|
||||
if err := removeUser.Sign(s.secretKey); err != nil {
|
||||
log.Error().Err(err).Msg("failed to sign remove-user event")
|
||||
return
|
||||
}
|
||||
if _, err := s.Relay.AddEvent(ctx, removeUser); err != nil {
|
||||
log.Error().Err(err).Msg("failed to remove user who requested to leave")
|
||||
return
|
||||
}
|
||||
s.Relay.BroadcastEvent(removeUser)
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/eventstore/slicestore"
|
||||
"github.com/fiatjaf/khatru/policies"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/fiatjaf/relay29/khatru29"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -18,18 +19,18 @@ func main() {
|
||||
db := &slicestore.SliceStore{}
|
||||
db.Init()
|
||||
|
||||
state := relay29.Init(relay29.Options{
|
||||
relay, _ := khatru29.Init(relay29.Options{
|
||||
Domain: "localhost:2929",
|
||||
DB: db,
|
||||
SecretKey: relayPrivateKey,
|
||||
})
|
||||
|
||||
// init relay
|
||||
state.Relay.Info.Name = "very ephemeral chat relay"
|
||||
state.Relay.Info.Description = "everything will be deleted as soon as I turn off my computer"
|
||||
relay.Info.Name = "very ephemeral chat relay"
|
||||
relay.Info.Description = "everything will be deleted as soon as I turn off my computer"
|
||||
|
||||
// extra policies
|
||||
state.Relay.RejectEvent = slices.Insert(state.Relay.RejectEvent, 0,
|
||||
relay.RejectEvent = append(relay.RejectEvent,
|
||||
policies.PreventLargeTags(64),
|
||||
policies.PreventTooManyIndexableTags(6, []int{9005}, nil),
|
||||
policies.RestrictToSpecifiedKinds(
|
||||
@ -38,17 +39,17 @@ func main() {
|
||||
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007,
|
||||
9021,
|
||||
),
|
||||
policies.PreventTimestampsInThePast(60),
|
||||
policies.PreventTimestampsInTheFuture(30),
|
||||
policies.PreventTimestampsInThePast(60*time.Second),
|
||||
policies.PreventTimestampsInTheFuture(30*time.Second),
|
||||
)
|
||||
|
||||
// http routes
|
||||
state.Relay.Router().HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
relay.Router().HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "nothing to see here, you must use a nip-29 powered client")
|
||||
})
|
||||
|
||||
fmt.Println("running on http://0.0.0.0:2929")
|
||||
if err := http.ListenAndServe(":2929", state.Relay); err != nil {
|
||||
if err := http.ListenAndServe(":2929", relay); err != nil {
|
||||
log.Fatal("failed to serve")
|
||||
}
|
||||
}
|
80
examples/basic-relayer/main.go
Normal file
80
examples/basic-relayer/main.go
Normal file
@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/fiatjaf/eventstore/slicestore"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/fiatjaf/relay29/relayer29"
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relayPrivateKey := nostr.GeneratePrivateKey()
|
||||
|
||||
db := &slicestore.SliceStore{}
|
||||
db.Init()
|
||||
|
||||
host := "0.0.0.0"
|
||||
port := 2929
|
||||
|
||||
opts := relay29.Options{
|
||||
Domain: fmt.Sprintf("%s:%d", host, port),
|
||||
DB: db,
|
||||
SecretKey: relayPrivateKey,
|
||||
}
|
||||
relay, _ := relayer29.Init(opts)
|
||||
|
||||
relay.(*relayer29.Relay).RejectFunc = func(ev *nostr.Event) (bool, string) {
|
||||
for _, tag := range ev.Tags {
|
||||
if len(tag) > 1 && len(tag[0]) == 1 {
|
||||
if len(tag[1]) > 64 {
|
||||
return true, "event contains too large tags"
|
||||
}
|
||||
}
|
||||
}
|
||||
if ev.Kind == 9005 {
|
||||
ntags := 0
|
||||
for _, tag := range ev.Tags {
|
||||
if len(tag) > 0 && len(tag[0]) == 1 {
|
||||
ntags++
|
||||
if ntags > 6 {
|
||||
return true, "too many indexable tags"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !slices.Contains([]int{9, 10, 11, 12, 30023, 31922, 31923, 9802, 9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9021}, ev.Kind) {
|
||||
return true, fmt.Sprintf("received event kind %d not allowed", ev.Kind)
|
||||
}
|
||||
if nostr.Now()-ev.CreatedAt > 60 {
|
||||
return true, "event too old"
|
||||
}
|
||||
if ev.CreatedAt-nostr.Now() > 30 {
|
||||
return true, "event too much in the future"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
server, err := relayer.NewServer(
|
||||
relay,
|
||||
relayer.WithPerConnectionLimiter(5.0, 1),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
|
||||
// http routes
|
||||
server.Router().HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "nothing to see here, you must use a nip-29 powered client")
|
||||
})
|
||||
|
||||
fmt.Printf("running on http://%v\n", opts.Domain)
|
||||
if err := server.Start(host, port); err != nil {
|
||||
log.Fatal("failed to serve")
|
||||
}
|
||||
}
|
@ -34,9 +34,17 @@ func handleCreateGroup(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
question := r.PostFormValue("captcha-id")
|
||||
solution := r.PostFormValue("captcha-solution")
|
||||
if !captcha.Verify(question, solution, true) {
|
||||
log.Info().Str("solution", solution).Msg("invalid captcha")
|
||||
http.Error(w, "captcha solution is wrong", 400)
|
||||
return
|
||||
}
|
||||
|
||||
id := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(id, uint64(time.Now().Unix()))
|
||||
groupId := hex.EncodeToString(id[0:4])
|
||||
groupId := hex.EncodeToString(id[0:3])
|
||||
|
||||
log.Info().Str("id", groupId).Str("owner", pubkey).Msg("making group")
|
||||
|
||||
@ -46,10 +54,6 @@ func handleCreateGroup(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// create group right here
|
||||
group = state.NewGroup(groupId)
|
||||
state.Groups.Store(groupId, group)
|
||||
|
||||
foundingEvents := []*nostr.Event{
|
||||
{
|
||||
CreatedAt: nostr.Now(),
|
||||
@ -98,6 +102,7 @@ func handleCreateGroup(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
group, _ = state.Groups.Load(groupId)
|
||||
naddr, _ := nip19.EncodeEntity(s.RelayPubkey, 39000, groupId, []string{"wss://" + s.Domain})
|
||||
fmt.Fprintf(w, "group created!\n\n%s\naddress: %s", naddr, group.Address)
|
||||
}
|
||||
|
@ -4,10 +4,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/eventstore/bolt"
|
||||
"github.com/fiatjaf/eventstore/lmdb"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/policies"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/fiatjaf/relay29/khatru29"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/rs/zerolog"
|
||||
@ -28,8 +31,9 @@ type Settings struct {
|
||||
|
||||
var (
|
||||
s Settings
|
||||
db = &bolt.BoltBackend{}
|
||||
db = &lmdb.LMDBBackend{}
|
||||
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
|
||||
relay *khatru.Relay
|
||||
state *relay29.State
|
||||
)
|
||||
|
||||
@ -50,42 +54,42 @@ func main() {
|
||||
log.Debug().Str("path", db.Path).Msg("initialized database")
|
||||
|
||||
// init relay29 stuff
|
||||
state = relay29.Init(relay29.Options{
|
||||
relay, state = khatru29.Init(relay29.Options{
|
||||
Domain: s.Domain,
|
||||
DB: db,
|
||||
SecretKey: s.RelayPrivkey,
|
||||
})
|
||||
|
||||
// init relay
|
||||
state.Relay.Info.Name = s.RelayName
|
||||
state.Relay.Info.Description = s.RelayDescription
|
||||
state.Relay.Info.Contact = s.RelayContact
|
||||
state.Relay.Info.Icon = s.RelayIcon
|
||||
relay.Info.Name = s.RelayName
|
||||
relay.Info.Description = s.RelayDescription
|
||||
relay.Info.Contact = s.RelayContact
|
||||
relay.Info.Icon = s.RelayIcon
|
||||
|
||||
state.Relay.OverwriteDeletionOutcome = append(state.Relay.OverwriteDeletionOutcome,
|
||||
relay.OverwriteDeletionOutcome = append(relay.OverwriteDeletionOutcome,
|
||||
blockDeletesOfOldMessages,
|
||||
)
|
||||
state.Relay.RejectEvent = slices.Insert(state.Relay.RejectEvent, 0,
|
||||
relay.RejectEvent = slices.Insert(relay.RejectEvent, 2,
|
||||
policies.PreventLargeTags(64),
|
||||
policies.PreventTooManyIndexableTags(6, []int{9005}, nil),
|
||||
policies.RestrictToSpecifiedKinds(
|
||||
9, 10, 11, 12,
|
||||
30023, 31922, 31923, 9802,
|
||||
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007,
|
||||
9021,
|
||||
9021, 9022,
|
||||
),
|
||||
policies.PreventTimestampsInThePast(60),
|
||||
policies.PreventTimestampsInTheFuture(30),
|
||||
policies.PreventTimestampsInThePast(60*time.Second),
|
||||
policies.PreventTimestampsInTheFuture(30*time.Second),
|
||||
rateLimit,
|
||||
preventGroupCreation,
|
||||
)
|
||||
|
||||
// http routes
|
||||
state.Relay.Router().HandleFunc("/create", handleCreateGroup)
|
||||
state.Relay.Router().HandleFunc("/", handleHomepage)
|
||||
relay.Router().HandleFunc("/create", handleCreateGroup)
|
||||
relay.Router().HandleFunc("/", handleHomepage)
|
||||
|
||||
log.Info().Str("relay-pubkey", s.RelayPubkey).Msg("running on http://0.0.0.0:" + s.Port)
|
||||
if err := http.ListenAndServe(":"+s.Port, state.Relay); err != nil {
|
||||
if err := http.ListenAndServe(":"+s.Port, relay); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to serve")
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mojocn/base64Captcha"
|
||||
. "github.com/theplant/htmlgo"
|
||||
)
|
||||
|
||||
var captcha = base64Captcha.NewCaptcha(base64Captcha.DefaultDriverDigit, base64Captcha.DefaultMemStore)
|
||||
|
||||
func homepageHTML() HTMLComponent {
|
||||
id, b64s, _, err := captcha.Generate()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to draw captcha")
|
||||
return HTML()
|
||||
}
|
||||
|
||||
return HTML(
|
||||
Head(
|
||||
Meta().Charset("utf-8"),
|
||||
@ -19,6 +28,10 @@ func homepageHTML() HTMLComponent {
|
||||
Input("").Id("npub").Placeholder("npub1...").Name("pubkey").Class("w-96 px-4 py-2 outline-0 bg-stone-100"),
|
||||
Label("group name:").For("name").Class("mr-1 mt-4 block"),
|
||||
Input("").Id("name").Placeholder("my little group").Name("name").Class("w-96 px-4 py-2 outline-0 bg-stone-100"),
|
||||
Label("solve this:").For("captcha").Class("mr-1 mt-4 block"),
|
||||
Img(b64s),
|
||||
Input("").Name("captcha-id").Value(id).Type("hidden"),
|
||||
Input("").Id("captcha").Placeholder("some number").Name("captcha-solution").Class("w-96 px-4 py-2 outline-0 bg-stone-100"),
|
||||
Button("create").Class("block rounded mt-4 px-4 py-2 bg-emerald-500 text-white hover:bg-emerald-300 transition-colors"),
|
||||
).Action("/create").Method("POST"),
|
||||
).Class("mx-4 my-6"),
|
||||
|
@ -4,12 +4,11 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip29"
|
||||
)
|
||||
|
||||
func (s *State) requireKindAndSingleGroupIDOrSpecificEventReference(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
func (s *State) RequireKindAndSingleGroupIDOrSpecificEventReference(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
isMeta := false
|
||||
isNormal := false
|
||||
isReference := false
|
||||
@ -36,7 +35,7 @@ func (s *State) requireKindAndSingleGroupIDOrSpecificEventReference(ctx context.
|
||||
}
|
||||
}
|
||||
|
||||
authed := khatru.GetAuthed(ctx)
|
||||
authed := s.GetAuthed(ctx)
|
||||
|
||||
switch {
|
||||
case isNormal:
|
||||
@ -80,30 +79,7 @@ func (s *State) requireKindAndSingleGroupIDOrSpecificEventReference(ctx context.
|
||||
return true, "invalid query, must have 'h', 'e' or 'a' tag"
|
||||
}
|
||||
case isMeta:
|
||||
// access depends on whether a user is logged in and the groups are public or private
|
||||
if tags, ok := filter.Tags["d"]; ok && len(tags) > 0 {
|
||||
// "h" tags specified
|
||||
for _, tag := range tags {
|
||||
if group, _ := s.Groups.Load(tag); group != nil {
|
||||
if !group.Private {
|
||||
continue // fine, this is public
|
||||
}
|
||||
|
||||
// private,
|
||||
if authed == "" {
|
||||
return true, "auth-required: trying to read from a private group"
|
||||
}
|
||||
// check membership
|
||||
group.mu.RLock()
|
||||
if _, isMember := group.Members[authed]; isMember {
|
||||
group.mu.RUnlock()
|
||||
continue // fine, this user is a member
|
||||
}
|
||||
group.mu.RUnlock()
|
||||
return true, "restricted: not a member"
|
||||
}
|
||||
}
|
||||
}
|
||||
// should be fine
|
||||
}
|
||||
|
||||
return false, ""
|
||||
|
18
go.mod
18
go.mod
@ -1,19 +1,21 @@
|
||||
module github.com/fiatjaf/relay29
|
||||
|
||||
go 1.22.2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/fiatjaf/eventstore v0.7.0
|
||||
github.com/fiatjaf/khatru v0.6.1
|
||||
github.com/fiatjaf/eventstore v0.8.2
|
||||
github.com/fiatjaf/khatru v0.8.1
|
||||
github.com/fiatjaf/relayer/v2 v2.2.1
|
||||
github.com/fiatjaf/set v0.0.3
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/nbd-wtf/go-nostr v0.34.3
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0
|
||||
github.com/mojocn/base64Captcha v1.3.6
|
||||
github.com/nbd-wtf/go-nostr v0.35.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/theplant/htmlgo v1.0.3
|
||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8
|
||||
golang.org/x/time v0.4.0
|
||||
golang.org/x/time v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -29,9 +31,9 @@ require (
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
@ -43,7 +45,7 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.54.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.9 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
64
go.sum
64
go.sum
@ -28,7 +28,6 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -42,10 +41,12 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/fasthttp/websocket v1.5.9 h1:9deGuzYcCRKjk940kNwSN6Hd14hk4zYwropm4UsUIUQ=
|
||||
github.com/fasthttp/websocket v1.5.9/go.mod h1:NLzHBFur260OMuZHohOfYQwMTpR7sfSpUnuqKxMpgKA=
|
||||
github.com/fiatjaf/eventstore v0.7.0 h1:etBN0jYodP4VCqhfglIk7VZ+tXAfL1FlDz3c6VygMLE=
|
||||
github.com/fiatjaf/eventstore v0.7.0/go.mod h1:r5yCFmrVNT2b1xUOuMnDVS3xPGh97y8IgTcLyY2rYP8=
|
||||
github.com/fiatjaf/khatru v0.6.1 h1:bH9GKhsGY83rTJSYZznXUf7iOFpNKg1zV8vzG0Jrr/4=
|
||||
github.com/fiatjaf/khatru v0.6.1/go.mod h1:dYTEXVwYQ2tB+whuL9DGg/GLV3y//io/FsQP7qY1jCs=
|
||||
github.com/fiatjaf/eventstore v0.8.2 h1:nCa3UuJNV5Y5t+SDoPQe7PBmKJ6dhm9TQ/WyR4SCbIM=
|
||||
github.com/fiatjaf/eventstore v0.8.2/go.mod h1:ck3RxufitHUBjID1RLcRxfX+NMywQzMsdfNpSt6m+9U=
|
||||
github.com/fiatjaf/khatru v0.8.1 h1:BWAZqwuT0272ZlyzPkuqAA0eGBOs5G3u0Dn1tlWrm6Q=
|
||||
github.com/fiatjaf/khatru v0.8.1/go.mod h1:jRmqbbIbEH+y0unt3wMUBwqY/btVussqx5SmBoGhXtg=
|
||||
github.com/fiatjaf/relayer/v2 v2.2.1 h1:BO9Za6kFB2n0aCAv5BCWfIijrSF3dGLZDoCC/8zgb0Y=
|
||||
github.com/fiatjaf/relayer/v2 v2.2.1/go.mod h1:5HASI/J7g/8h7KAGg1mw7scM3tJy5kggtyrdMyxmLao=
|
||||
github.com/fiatjaf/set v0.0.3 h1:LoawhcGoD504baBw0TwVJTQemzt15QYuxutzzWl2kgE=
|
||||
github.com/fiatjaf/set v0.0.3/go.mod h1:hdSwBrO+CwMEbYQAMaHtsib30KQLDtVjbX/1OgDK3tY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@ -57,6 +58,8 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@ -91,8 +94,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/nbd-wtf/go-nostr v0.34.3 h1:JfDOHOje7gzUhisbZD0v2Y9b9vh2PmP6eHsU/GfU8QE=
|
||||
github.com/nbd-wtf/go-nostr v0.34.3/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
|
||||
github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw=
|
||||
github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E=
|
||||
github.com/nbd-wtf/go-nostr v0.35.0 h1:oINIBr5XE1kowkaz7NXC5vLvj2jUWH6xlzJjChpgV6Q=
|
||||
github.com/nbd-wtf/go-nostr v0.35.0/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@ -102,14 +107,13 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@ -137,23 +141,32 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.54.0 h1:cCL+ZZR3z3HPLMVfEYVUMtJqVaui0+gu7Lx63unHwS0=
|
||||
github.com/valyala/fasthttp v1.54.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
|
||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -163,17 +176,32 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
|
||||
golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
|
@ -4,10 +4,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/eventstore/lmdb"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/policies"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/fiatjaf/relay29/khatru29"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/rs/zerolog"
|
||||
@ -30,6 +33,7 @@ var (
|
||||
s Settings
|
||||
db = &lmdb.LMDBBackend{}
|
||||
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
|
||||
relay *khatru.Relay
|
||||
state *relay29.State
|
||||
)
|
||||
|
||||
@ -50,37 +54,36 @@ func main() {
|
||||
log.Debug().Str("path", db.Path).Msg("initialized database")
|
||||
|
||||
// init relay29 stuff
|
||||
state = relay29.Init(relay29.Options{
|
||||
relay, state = khatru29.Init(relay29.Options{
|
||||
Domain: s.Domain,
|
||||
DB: db,
|
||||
SecretKey: s.RelayPrivkey,
|
||||
})
|
||||
|
||||
// init relay
|
||||
state.Relay.Info.Name = s.RelayName
|
||||
state.Relay.Info.Description = s.RelayDescription
|
||||
state.Relay.Info.Contact = s.RelayContact
|
||||
state.Relay.Info.Icon = s.RelayIcon
|
||||
relay.Info.Name = s.RelayName
|
||||
relay.Info.Description = s.RelayDescription
|
||||
relay.Info.Contact = s.RelayContact
|
||||
relay.Info.Icon = s.RelayIcon
|
||||
|
||||
state.Relay.OverwriteDeletionOutcome = append(state.Relay.OverwriteDeletionOutcome,
|
||||
relay.OverwriteDeletionOutcome = append(relay.OverwriteDeletionOutcome,
|
||||
blockDeletesOfOldMessages,
|
||||
)
|
||||
state.Relay.RejectEvent = slices.Insert(state.Relay.RejectEvent, 2,
|
||||
relay.RejectEvent = slices.Insert(relay.RejectEvent, 2,
|
||||
policies.PreventLargeTags(64),
|
||||
policies.PreventTooManyIndexableTags(6, []int{9005}, nil),
|
||||
policies.PreventTooManyIndexableTags(6, []int{9000, 9001, 9005}, nil),
|
||||
policies.RestrictToSpecifiedKinds(
|
||||
7, 9, 10, 11, 12,
|
||||
30023, 31922, 31923, 9802,
|
||||
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007,
|
||||
9021, 9735,
|
||||
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008,
|
||||
9021, 9022, 9735,
|
||||
),
|
||||
policies.PreventTimestampsInThePast(60),
|
||||
policies.PreventTimestampsInTheFuture(30),
|
||||
// rateLimit,
|
||||
policies.PreventTimestampsInThePast(60*time.Second),
|
||||
policies.PreventTimestampsInTheFuture(30*time.Second),
|
||||
)
|
||||
|
||||
log.Info().Str("relay-pubkey", s.RelayPubkey).Msg("running on http://0.0.0.0:" + s.Port)
|
||||
if err := http.ListenAndServe(":"+s.Port, state.Relay); err != nil {
|
||||
if err := http.ListenAndServe(":"+s.Port, relay); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to serve")
|
||||
}
|
||||
}
|
||||
|
70
groups.go
Normal file
70
groups.go
Normal file
@ -0,0 +1,70 @@
|
||||
package relay29
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip29"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
nip29.Group
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *State) NewGroup(id string) *Group {
|
||||
return &Group{
|
||||
Group: nip29.Group{
|
||||
Address: nip29.GroupAddress{
|
||||
ID: id,
|
||||
Relay: "wss://" + s.Domain,
|
||||
},
|
||||
Members: map[string]*nip29.Role{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// loadGroups loads all the group metadata from all the past action messages.
|
||||
func (s *State) loadGroups(ctx context.Context) {
|
||||
groupMetadataEvents, _ := s.DB.QueryEvents(ctx, nostr.Filter{Kinds: []int{nostr.KindSimpleGroupCreateGroup}})
|
||||
for evt := range groupMetadataEvents {
|
||||
gtag := evt.Tags.GetFirst([]string{"h", ""})
|
||||
id := (*gtag)[1]
|
||||
|
||||
group := s.NewGroup(id)
|
||||
f := nostr.Filter{
|
||||
Limit: 5000, Kinds: nip29.ModerationEventKinds, Tags: nostr.TagMap{"h": []string{id}},
|
||||
}
|
||||
ch, _ := s.DB.QueryEvents(ctx, f)
|
||||
|
||||
events := make([]*nostr.Event, 0, 5000)
|
||||
for event := range ch {
|
||||
events = append(events, event)
|
||||
}
|
||||
for i := len(events) - 1; i >= 0; i-- {
|
||||
evt := events[i]
|
||||
act, _ := PrepareModerationAction(evt)
|
||||
act.Apply(&group.Group)
|
||||
}
|
||||
|
||||
// if the group was deleted there will be no actions after the delete
|
||||
if len(events) > 0 && events[0].Kind == nostr.KindSimpleGroupDeleteGroup {
|
||||
// we don't keep track of this if it was deleted
|
||||
continue
|
||||
}
|
||||
|
||||
s.Groups.Store(group.Address.ID, group)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) GetGroupFromEvent(event *nostr.Event) *Group {
|
||||
group, _ := s.Groups.Load(GetGroupIDFromEvent(event))
|
||||
return group
|
||||
}
|
||||
|
||||
func GetGroupIDFromEvent(event *nostr.Event) string {
|
||||
gtag := event.Tags.GetFirst([]string{"h", ""})
|
||||
groupId := (*gtag)[1]
|
||||
return groupId
|
||||
}
|
53
khatru29/khatru29.go
Normal file
53
khatru29/khatru29.go
Normal file
@ -0,0 +1,53 @@
|
||||
package khatru29
|
||||
|
||||
import (
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func Init(opts relay29.Options) (*khatru.Relay, *relay29.State) {
|
||||
pubkey, _ := nostr.GetPublicKey(opts.SecretKey)
|
||||
|
||||
// create a new relay29.State
|
||||
state := relay29.New(opts)
|
||||
|
||||
// create a new khatru relay
|
||||
relay := khatru.NewRelay()
|
||||
relay.Info.PubKey = pubkey
|
||||
relay.Info.SupportedNIPs = append(relay.Info.SupportedNIPs, 29)
|
||||
|
||||
// assign khatru relay to relay29.State
|
||||
state.Relay = relay
|
||||
|
||||
// provide GetAuthed function
|
||||
state.GetAuthed = khatru.GetAuthed
|
||||
|
||||
// apply basic relay policies
|
||||
relay.StoreEvent = append(relay.StoreEvent, state.DB.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents,
|
||||
state.NormalEventQuery,
|
||||
state.MetadataQueryHandler,
|
||||
state.AdminsQueryHandler,
|
||||
state.MembersQueryHandler,
|
||||
)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, state.DB.DeleteEvent)
|
||||
relay.RejectFilter = append(relay.RejectFilter,
|
||||
state.RequireKindAndSingleGroupIDOrSpecificEventReference,
|
||||
)
|
||||
relay.RejectEvent = append(relay.RejectEvent,
|
||||
state.RequireHTagForExistingGroup,
|
||||
state.RequireModerationEventsToBeRecent,
|
||||
state.RestrictWritesBasedOnGroupRules,
|
||||
state.RestrictInvalidModerationActions,
|
||||
state.PreventWritingOfEventsJustDeleted,
|
||||
)
|
||||
relay.OnEventSaved = append(relay.OnEventSaved,
|
||||
state.ApplyModerationAction,
|
||||
state.ReactToJoinRequest,
|
||||
state.ReactToLeaveRequest,
|
||||
)
|
||||
relay.OnConnect = append(relay.OnConnect, khatru.RequestAuth)
|
||||
|
||||
return relay, state
|
||||
}
|
468
khatru29/relay_test.go
Normal file
468
khatru29/relay_test.go
Normal file
@ -0,0 +1,468 @@
|
||||
package khatru29
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/eventstore/slicestore"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var relayPrivateKey = nostr.GeneratePrivateKey()
|
||||
|
||||
func startTestRelay() func() {
|
||||
db := &slicestore.SliceStore{}
|
||||
db.Init()
|
||||
|
||||
relay, state := Init(relay29.Options{
|
||||
Domain: "localhost:29292",
|
||||
DB: db,
|
||||
SecretKey: relayPrivateKey,
|
||||
})
|
||||
|
||||
relay.Info.Name = "very testy relay"
|
||||
relay.Info.Description = "this is just for testing"
|
||||
|
||||
// don't do this at home -- we're going to remove one requirement to make tests simpler
|
||||
relay.RejectEvent = slices.DeleteFunc(relay.RejectEvent, func(f func(ctx context.Context, event *nostr.Event) (reject bool, msg string)) bool {
|
||||
return fmt.Sprintf("%v", []any{f}) == fmt.Sprintf("%v", []any{state.RequireModerationEventsToBeRecent})
|
||||
})
|
||||
|
||||
server := &http.Server{Addr: ":29292", Handler: relay}
|
||||
|
||||
go func() {
|
||||
server.ListenAndServe()
|
||||
}()
|
||||
|
||||
return func() {
|
||||
server.Shutdown(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupStuffABunch(t *testing.T) {
|
||||
defer startTestRelay()()
|
||||
ctx := context.Background()
|
||||
|
||||
user1 := "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
user1pk, _ := nostr.GetPublicKey(user1)
|
||||
|
||||
user2 := "0000000000000000000000000000000000000000000000000000000000000002"
|
||||
user2pk, _ := nostr.GetPublicKey(user2)
|
||||
|
||||
user3 := "0000000000000000000000000000000000000000000000000000000000000003"
|
||||
user3pk, _ := nostr.GetPublicKey(user3)
|
||||
|
||||
// simple open group
|
||||
{
|
||||
r, err := nostr.RelayConnect(ctx, "ws://localhost:29292")
|
||||
require.NoError(t, err, "failed to connect to relay")
|
||||
|
||||
metaSub, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{39000}, Tags: nostr.TagMap{"d": []string{"a"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to group metadata")
|
||||
|
||||
membersSub, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{39002}, Tags: nostr.TagMap{"d": []string{"a"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to group members")
|
||||
|
||||
// create group
|
||||
createGroup := nostr.Event{
|
||||
CreatedAt: 1,
|
||||
Kind: 9007,
|
||||
Tags: nostr.Tags{{"h", "a"}},
|
||||
}
|
||||
createGroup.Sign(user1)
|
||||
require.NoError(t, r.Publish(ctx, createGroup), "failed to publish kind 9007")
|
||||
|
||||
// see if we get notified about that
|
||||
select {
|
||||
case evt := <-metaSub.Events:
|
||||
require.Equal(t, "a", evt.Tags.GetD())
|
||||
require.Nil(t, evt.Tags.GetFirst([]string{"private"}))
|
||||
require.NotNil(t, evt.Tags.GetFirst([]string{"public"}))
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case evt := <-membersSub.Events:
|
||||
require.Equal(t, "a", evt.Tags.GetD())
|
||||
require.NotNil(t,
|
||||
evt.Tags.GetFirst([]string{"p", user1pk}),
|
||||
)
|
||||
require.Len(t, evt.Tags, 2)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
// invite another member
|
||||
inviteMember := nostr.Event{
|
||||
CreatedAt: 2,
|
||||
Kind: 9000,
|
||||
Tags: nostr.Tags{{"h", "a"}, {"p", user2pk}},
|
||||
}
|
||||
inviteMember.Sign(user1)
|
||||
require.NoError(t, r.Publish(ctx, inviteMember), "failed to publish kind 9000")
|
||||
|
||||
// see if we get notified about that
|
||||
select {
|
||||
case evt := <-membersSub.Events:
|
||||
require.Equal(t, "a", evt.Tags.GetD())
|
||||
require.NotNil(t,
|
||||
evt.Tags.GetFirst([]string{"p", user1pk}),
|
||||
evt.Tags.GetFirst([]string{"p", user2pk}),
|
||||
)
|
||||
require.Len(t, evt.Tags, 3)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
// update metadata
|
||||
updateMetadata := nostr.Event{
|
||||
CreatedAt: 3,
|
||||
Kind: 9002,
|
||||
Tags: nostr.Tags{{"h", "a"}, {"name", "alface"}},
|
||||
}
|
||||
updateMetadata.Sign(user1)
|
||||
require.NoError(t, r.Publish(ctx, updateMetadata), "failed to publish kind 9002")
|
||||
|
||||
// see if we get notified about that
|
||||
select {
|
||||
case evt := <-metaSub.Events:
|
||||
require.Equal(t, "a", evt.Tags.GetD())
|
||||
require.Equal(t, &nostr.Tag{"name", "alface"}, evt.Tags.GetFirst([]string{"name"}))
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
msgSub, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{9, 10}, Tags: nostr.TagMap{"h": []string{"a"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to group messages")
|
||||
|
||||
// publish some messages
|
||||
for i := 4; i < 10; i++ {
|
||||
message := nostr.Event{
|
||||
CreatedAt: nostr.Timestamp(i),
|
||||
Content: fmt.Sprintf("hello %d", i),
|
||||
Kind: 9,
|
||||
Tags: nostr.Tags{{"h", "a"}},
|
||||
}
|
||||
signer := user1
|
||||
if i%2 == 1 {
|
||||
signer = user2
|
||||
}
|
||||
message.Sign(signer)
|
||||
require.NoError(t, r.Publish(ctx, message), "failed to publish kind 9")
|
||||
}
|
||||
|
||||
// check if we have received messages correctly from the subscription
|
||||
for i := 4; i < 10; i++ {
|
||||
publisher := user1pk
|
||||
if i%2 == 1 {
|
||||
publisher = user2pk
|
||||
}
|
||||
message := <-msgSub.Events
|
||||
require.Equal(t, fmt.Sprintf("hello %d", i), message.Content)
|
||||
require.Equal(t, publisher, message.PubKey)
|
||||
}
|
||||
|
||||
// events that should be rejected
|
||||
failedNoHTag := nostr.Event{
|
||||
CreatedAt: 11,
|
||||
Content: "failed",
|
||||
Kind: 9,
|
||||
}
|
||||
failedNoHTag.Sign(user1)
|
||||
require.Error(t, r.Publish(ctx, failedNoHTag), "should fail to publish kind 9 with no h tag")
|
||||
|
||||
failedWrongHTag := nostr.Event{
|
||||
CreatedAt: 11,
|
||||
Content: "failed",
|
||||
Kind: 9,
|
||||
Tags: nostr.Tags{{"h", "b"}},
|
||||
}
|
||||
failedWrongHTag.Sign(user1)
|
||||
require.Error(t, r.Publish(ctx, failedWrongHTag), "should fail to publish kind 9 with wrong h tag")
|
||||
|
||||
failedFromNonMember := nostr.Event{
|
||||
CreatedAt: 11,
|
||||
Content: "failed",
|
||||
Kind: 9,
|
||||
Tags: nostr.Tags{{"h", "a"}},
|
||||
}
|
||||
failedWrongHTag.Sign(user3)
|
||||
require.Error(t, r.Publish(ctx, failedFromNonMember), "should fail to publish kind 9 from non-member")
|
||||
|
||||
// get stored messages
|
||||
ext, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{9, 10, 11, 12}, Tags: nostr.TagMap{"h": []string{"a"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to messages again")
|
||||
count := 0
|
||||
for {
|
||||
select {
|
||||
case message := <-ext.Events:
|
||||
require.Equal(t, 9, message.Kind)
|
||||
require.Equal(t, fmt.Sprintf("hello %d", message.CreatedAt), message.Content)
|
||||
count++
|
||||
case <-ext.EndOfStoredEvents:
|
||||
require.Equal(t, 6, count, "must have 6 messages")
|
||||
goto end1_1
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
}
|
||||
end1_1:
|
||||
}
|
||||
|
||||
// adding now a private group
|
||||
{
|
||||
r, err := nostr.RelayConnect(ctx, "ws://localhost:29292")
|
||||
require.NoError(t, err, "failed to connect to relay")
|
||||
|
||||
createGroupFail := nostr.Event{
|
||||
CreatedAt: 1,
|
||||
Kind: 9007,
|
||||
Tags: nostr.Tags{{"h", "a"}},
|
||||
}
|
||||
createGroupFail.Sign(user3)
|
||||
require.Error(t, r.Publish(ctx, createGroupFail), "should fail to publish kind 9007 for existing group")
|
||||
|
||||
metaSub, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{39000}, Tags: nostr.TagMap{"d": []string{"b"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to group metadata")
|
||||
|
||||
membersSub, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{39002}, Tags: nostr.TagMap{"d": []string{"b"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to group members")
|
||||
|
||||
createGroup := nostr.Event{
|
||||
CreatedAt: 1,
|
||||
Kind: 9007,
|
||||
Tags: nostr.Tags{{"h", "b"}},
|
||||
}
|
||||
createGroup.Sign(user3)
|
||||
require.NoError(t, r.Publish(ctx, createGroup), "failed to publish kind 9007")
|
||||
|
||||
select {
|
||||
case evt := <-metaSub.Events:
|
||||
require.Equal(t, "b", evt.Tags.GetD())
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case evt := <-membersSub.Events:
|
||||
require.Equal(t, "b", evt.Tags.GetD())
|
||||
require.NotNil(t,
|
||||
evt.Tags.GetFirst([]string{"p", user3pk}),
|
||||
)
|
||||
require.Len(t, evt.Tags, 2)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
inviteMember := nostr.Event{
|
||||
CreatedAt: 2,
|
||||
Kind: 9000,
|
||||
Tags: nostr.Tags{{"h", "b"}, {"p", user2pk}},
|
||||
}
|
||||
inviteMember.Sign(user3)
|
||||
require.NoError(t, r.Publish(ctx, inviteMember), "failed to publish kind 9000")
|
||||
|
||||
select {
|
||||
case evt := <-membersSub.Events:
|
||||
require.Equal(t, "b", evt.Tags.GetD())
|
||||
require.NotNil(t,
|
||||
evt.Tags.GetFirst([]string{"p", user3pk}),
|
||||
evt.Tags.GetFirst([]string{"p", user2pk}),
|
||||
)
|
||||
require.Len(t, evt.Tags, 3)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
setGroupPrivate := nostr.Event{
|
||||
CreatedAt: 3,
|
||||
Kind: 9006,
|
||||
Tags: nostr.Tags{
|
||||
{"h", "b"},
|
||||
{"private"},
|
||||
},
|
||||
}
|
||||
setGroupPrivate.Sign(user2)
|
||||
require.Error(t, r.Publish(ctx, setGroupPrivate), "should fail to accept moderation from non-mod")
|
||||
|
||||
setGroupPrivate.Sign(user3)
|
||||
require.NoError(t, r.Publish(ctx, setGroupPrivate), "failed to publish kind 9006")
|
||||
|
||||
select {
|
||||
case evt := <-metaSub.Events:
|
||||
require.Equal(t, "b", evt.Tags.GetD())
|
||||
require.Nil(t, evt.Tags.GetFirst([]string{"public"}))
|
||||
require.NotNil(t, evt.Tags.GetFirst([]string{"private"}))
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
for i := 4; i < 10; i++ {
|
||||
message := nostr.Event{
|
||||
CreatedAt: nostr.Timestamp(i),
|
||||
Content: fmt.Sprintf("hello %d", i),
|
||||
Kind: 9,
|
||||
Tags: nostr.Tags{{"h", "b"}},
|
||||
}
|
||||
signer := user3
|
||||
if i%2 == 1 {
|
||||
signer = user2
|
||||
}
|
||||
message.Sign(signer)
|
||||
require.NoError(t, r.Publish(ctx, message), "failed to publish kind 9")
|
||||
}
|
||||
|
||||
failedSub, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{9}, Tags: nostr.TagMap{"h": []string{"b"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to private messages")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-failedSub.Events:
|
||||
t.Fatal("should not have received events")
|
||||
return
|
||||
case <-failedSub.EndOfStoredEvents:
|
||||
t.Fatal("should not have received EOSE")
|
||||
return
|
||||
case closed := <-failedSub.ClosedReason:
|
||||
require.Contains(t, closed, "auth-required:")
|
||||
goto end2_1
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
}
|
||||
end2_1:
|
||||
r2, err := nostr.RelayConnect(ctx, "ws://localhost:29292")
|
||||
require.NoError(t, err, "failed to connect to relay")
|
||||
|
||||
time.Sleep(time.Millisecond * 20) // wait until auth is received
|
||||
|
||||
err = r2.Auth(ctx, func(authEvent *nostr.Event) error {
|
||||
authEvent.Sign(user2)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err, "auth should have worked")
|
||||
|
||||
goodSub, err := r2.Subscribe(ctx, nostr.Filters{{Kinds: []int{9}, Tags: nostr.TagMap{"h": []string{"b"}}}})
|
||||
require.NoError(t, err, "failed to subscribe to private messages")
|
||||
|
||||
count := 0
|
||||
for {
|
||||
select {
|
||||
case message := <-goodSub.Events:
|
||||
require.Equal(t, 9, message.Kind)
|
||||
require.Equal(t, fmt.Sprintf("hello %d", message.CreatedAt), message.Content)
|
||||
count++
|
||||
case <-goodSub.EndOfStoredEvents:
|
||||
require.Equal(t, 6, count, "must have 6 messages")
|
||||
goto end2_2
|
||||
case <-failedSub.ClosedReason:
|
||||
t.Fatal("should not have received CLOSED")
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
}
|
||||
end2_2:
|
||||
|
||||
anotherMessage := nostr.Event{
|
||||
CreatedAt: 11,
|
||||
Content: "last",
|
||||
Kind: 9,
|
||||
Tags: nostr.Tags{{"h", "b"}},
|
||||
}
|
||||
anotherMessage.Sign(user3)
|
||||
require.NoError(t, r.Publish(ctx, anotherMessage), "failed to publish last kind 9")
|
||||
|
||||
select {
|
||||
case message := <-goodSub.Events:
|
||||
// good sub should receive it
|
||||
require.Equal(t, "last", message.Content)
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Fatal("select took too long")
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-failedSub.Events:
|
||||
t.Fatal("unauthed sub should not receive it")
|
||||
case <-time.After(time.Millisecond * 200):
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// query members list filtering by "#p"
|
||||
for i, s := range []struct {
|
||||
key string
|
||||
groupcount int
|
||||
groupcountwhenauthedasuser2 int
|
||||
}{
|
||||
{user1pk, 1, 1}, {user2pk, 1, 2}, {user3pk, 0, 1},
|
||||
} {
|
||||
r, err := nostr.RelayConnect(ctx, "ws://localhost:29292")
|
||||
require.NoError(t, err, "failed to connect to relay")
|
||||
|
||||
ms, err := r.Subscribe(ctx, nostr.Filters{{Kinds: []int{39002}, Tags: nostr.TagMap{"p": []string{s.key}}}})
|
||||
require.NoError(t, err, "failed to subscribe to group members")
|
||||
|
||||
count := 0
|
||||
for {
|
||||
select {
|
||||
case message := <-ms.Events:
|
||||
require.Equal(t, 39002, message.Kind)
|
||||
count++
|
||||
case <-ms.EndOfStoredEvents:
|
||||
require.Equal(t, s.groupcount, count,
|
||||
"when unauthed for key%d expected %d groups but got %d",
|
||||
i+1, s.groupcount, count)
|
||||
goto end3_1
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("select took too long for key%d", i+1)
|
||||
return
|
||||
}
|
||||
}
|
||||
end3_1:
|
||||
|
||||
// perform auth and try again
|
||||
err = r.Auth(ctx, func(authEvent *nostr.Event) error {
|
||||
authEvent.Sign(user2)
|
||||
return nil
|
||||
})
|
||||
ms, err = r.Subscribe(ctx, nostr.Filters{{Kinds: []int{39002}, Tags: nostr.TagMap{"p": []string{s.key}}}})
|
||||
require.NoError(t, err, "failed to subscribe to group members")
|
||||
|
||||
count = 0
|
||||
for {
|
||||
select {
|
||||
case message := <-ms.Events:
|
||||
require.Equal(t, 39002, message.Kind)
|
||||
count++
|
||||
case <-ms.EndOfStoredEvents:
|
||||
require.Equal(t, s.groupcountwhenauthedasuser2, count,
|
||||
"when authed for key%d expected %d groups but got %d",
|
||||
i+1, s.groupcountwhenauthedasuser2, count)
|
||||
goto end3_2
|
||||
case <-time.After(time.Second):
|
||||
return
|
||||
}
|
||||
}
|
||||
end3_2:
|
||||
}
|
||||
}
|
||||
}
|
352
moderation_actions.go
Normal file
352
moderation_actions.go
Normal file
@ -0,0 +1,352 @@
|
||||
package relay29
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip29"
|
||||
)
|
||||
|
||||
var PTagNotValidPublicKey = fmt.Errorf("'p' tag value is not a valid public key")
|
||||
|
||||
type Action interface {
|
||||
Apply(group *nip29.Group)
|
||||
PermissionName() nip29.Permission
|
||||
}
|
||||
|
||||
func PrepareModerationAction(evt *nostr.Event) (Action, error) {
|
||||
factory, ok := moderationActionFactories[evt.Kind]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("event kind %d is not a supported moderation action", evt.Kind)
|
||||
}
|
||||
return factory(evt)
|
||||
}
|
||||
|
||||
var moderationActionFactories = map[int]func(*nostr.Event) (Action, error){
|
||||
nostr.KindSimpleGroupAddUser: func(evt *nostr.Event) (Action, error) {
|
||||
targets := make([]string, 0, len(evt.Tags))
|
||||
for _, tag := range evt.Tags.GetAll([]string{"p", ""}) {
|
||||
if !nostr.IsValidPublicKey(tag[1]) {
|
||||
return nil, PTagNotValidPublicKey
|
||||
}
|
||||
targets = append(targets, tag[1])
|
||||
}
|
||||
if len(targets) > 0 {
|
||||
return &AddUser{Targets: targets, When: evt.CreatedAt}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("missing 'p' tags")
|
||||
},
|
||||
nostr.KindSimpleGroupRemoveUser: func(evt *nostr.Event) (Action, error) {
|
||||
targets := make([]string, 0, len(evt.Tags))
|
||||
for _, tag := range evt.Tags.GetAll([]string{"p", ""}) {
|
||||
if !nostr.IsValidPublicKey(tag[1]) {
|
||||
return nil, PTagNotValidPublicKey
|
||||
}
|
||||
targets = append(targets, tag[1])
|
||||
}
|
||||
if len(targets) > 0 {
|
||||
return &RemoveUser{Targets: targets, When: evt.CreatedAt}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("missing 'p' tags")
|
||||
},
|
||||
nostr.KindSimpleGroupEditMetadata: func(evt *nostr.Event) (Action, error) {
|
||||
ok := false
|
||||
edit := EditMetadata{When: evt.CreatedAt}
|
||||
if t := evt.Tags.GetFirst([]string{"name", ""}); t != nil {
|
||||
edit.NameValue = (*t)[1]
|
||||
ok = true
|
||||
}
|
||||
if t := evt.Tags.GetFirst([]string{"picture", ""}); t != nil {
|
||||
edit.PictureValue = (*t)[1]
|
||||
ok = true
|
||||
}
|
||||
if t := evt.Tags.GetFirst([]string{"about", ""}); t != nil {
|
||||
edit.AboutValue = (*t)[1]
|
||||
ok = true
|
||||
}
|
||||
if ok {
|
||||
return &edit, nil
|
||||
}
|
||||
return nil, fmt.Errorf("missing metadata tags")
|
||||
},
|
||||
nostr.KindSimpleGroupAddPermission: func(evt *nostr.Event) (Action, error) {
|
||||
nTags := len(evt.Tags)
|
||||
|
||||
permissions := make([]nip29.Permission, 0, nTags-1)
|
||||
for _, tag := range evt.Tags.GetAll([]string{"permission", ""}) {
|
||||
perm := nip29.Permission(tag[1])
|
||||
if _, ok := nip29.PermissionsMap[perm]; !ok {
|
||||
return nil, fmt.Errorf("unknown permission '%s'", tag[1])
|
||||
}
|
||||
permissions = append(permissions, perm)
|
||||
}
|
||||
|
||||
targets := make([]string, 0, nTags-1)
|
||||
for _, tag := range evt.Tags.GetAll([]string{"p", ""}) {
|
||||
if !nostr.IsValidPublicKey(tag[1]) {
|
||||
return nil, PTagNotValidPublicKey
|
||||
}
|
||||
targets = append(targets, tag[1])
|
||||
}
|
||||
|
||||
if len(permissions) > 0 && len(targets) > 0 {
|
||||
return &AddPermission{Initiator: evt.PubKey, Targets: targets, Permissions: permissions, When: evt.CreatedAt}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("")
|
||||
},
|
||||
nostr.KindSimpleGroupRemovePermission: func(evt *nostr.Event) (Action, error) {
|
||||
nTags := len(evt.Tags)
|
||||
|
||||
permissions := make([]nip29.Permission, 0, nTags-1)
|
||||
for _, tag := range evt.Tags.GetAll([]string{"permission", ""}) {
|
||||
perm := nip29.Permission(tag[1])
|
||||
if _, ok := nip29.PermissionsMap[perm]; !ok {
|
||||
return nil, fmt.Errorf("unknown permission '%s'", tag[1])
|
||||
}
|
||||
permissions = append(permissions, perm)
|
||||
}
|
||||
|
||||
targets := make([]string, 0, nTags-1)
|
||||
for _, tag := range evt.Tags.GetAll([]string{"p", ""}) {
|
||||
if !nostr.IsValidPublicKey(tag[1]) {
|
||||
return nil, PTagNotValidPublicKey
|
||||
}
|
||||
targets = append(targets, tag[1])
|
||||
}
|
||||
|
||||
if len(permissions) > 0 && len(targets) > 0 {
|
||||
return &RemovePermission{Targets: targets, Permissions: permissions, When: evt.CreatedAt}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("")
|
||||
},
|
||||
nostr.KindSimpleGroupDeleteEvent: func(evt *nostr.Event) (Action, error) {
|
||||
tags := evt.Tags.GetAll([]string{"e", ""})
|
||||
if len(tags) == 0 {
|
||||
return nil, fmt.Errorf("missing 'e' tag")
|
||||
}
|
||||
|
||||
targets := make([]string, len(tags))
|
||||
for i, tag := range tags {
|
||||
if nostr.IsValid32ByteHex(tag[1]) {
|
||||
targets[i] = tag[1]
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid event id hex")
|
||||
}
|
||||
}
|
||||
|
||||
return &DeleteEvent{Targets: targets}, nil
|
||||
},
|
||||
nostr.KindSimpleGroupEditGroupStatus: func(evt *nostr.Event) (Action, error) {
|
||||
egs := EditGroupStatus{When: evt.CreatedAt}
|
||||
|
||||
egs.Public = evt.Tags.GetFirst([]string{"public"}) != nil
|
||||
egs.Private = evt.Tags.GetFirst([]string{"private"}) != nil
|
||||
egs.Open = evt.Tags.GetFirst([]string{"open"}) != nil
|
||||
egs.Closed = evt.Tags.GetFirst([]string{"closed"}) != nil
|
||||
|
||||
// disallow contradictions
|
||||
if egs.Public && egs.Private {
|
||||
return nil, fmt.Errorf("contradiction: can't be public and private at the same time")
|
||||
}
|
||||
if egs.Open && egs.Closed {
|
||||
return nil, fmt.Errorf("contradiction: can't be open and closed at the same time")
|
||||
}
|
||||
|
||||
return egs, nil
|
||||
},
|
||||
nostr.KindSimpleGroupCreateGroup: func(evt *nostr.Event) (Action, error) {
|
||||
return &CreateGroup{Creator: evt.PubKey, When: evt.CreatedAt}, nil
|
||||
},
|
||||
nostr.KindSimpleGroupDeleteGroup: func(evt *nostr.Event) (Action, error) {
|
||||
return &CreateGroup{When: evt.CreatedAt}, nil
|
||||
},
|
||||
}
|
||||
|
||||
type DeleteEvent struct {
|
||||
Targets []string
|
||||
}
|
||||
|
||||
func (DeleteEvent) PermissionName() nip29.Permission { return nip29.PermDeleteEvent }
|
||||
func (a DeleteEvent) Apply(group *nip29.Group) {}
|
||||
|
||||
type AddUser struct {
|
||||
Targets []string
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (AddUser) PermissionName() nip29.Permission { return nip29.PermAddUser }
|
||||
func (a AddUser) Apply(group *nip29.Group) {
|
||||
for _, target := range a.Targets {
|
||||
group.Members[target] = nip29.EmptyRole
|
||||
}
|
||||
}
|
||||
|
||||
type RemoveUser struct {
|
||||
Targets []string
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (RemoveUser) PermissionName() nip29.Permission { return nip29.PermRemoveUser }
|
||||
func (a RemoveUser) Apply(group *nip29.Group) {
|
||||
for _, tpk := range a.Targets {
|
||||
if target, ok := group.Members[tpk]; ok {
|
||||
if target != nip29.EmptyRole {
|
||||
_, hasSuperiorOrEqualPermission := target.Permissions[nip29.PermRemoveUser]
|
||||
if hasSuperiorOrEqualPermission {
|
||||
continue
|
||||
}
|
||||
}
|
||||
delete(group.Members, tpk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type EditMetadata struct {
|
||||
NameValue string
|
||||
PictureValue string
|
||||
AboutValue string
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (EditMetadata) PermissionName() nip29.Permission { return nip29.PermEditMetadata }
|
||||
func (a EditMetadata) Apply(group *nip29.Group) {
|
||||
group.Name = a.NameValue
|
||||
group.Picture = a.PictureValue
|
||||
group.About = a.AboutValue
|
||||
group.LastMetadataUpdate = a.When
|
||||
}
|
||||
|
||||
type AddPermission struct {
|
||||
Initiator string // the user who is adding the permissions
|
||||
Targets []string
|
||||
Permissions []nip29.Permission
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (AddPermission) PermissionName() nip29.Permission { return nip29.PermAddPermission }
|
||||
func (a AddPermission) Apply(group *nip29.Group) {
|
||||
for _, tpk := range a.Targets {
|
||||
role, ok := group.Members[tpk]
|
||||
|
||||
// if it's a normal user, create a new role object thing for this user
|
||||
// instead of modifying the global EmptyRole
|
||||
if !ok || role == nip29.EmptyRole {
|
||||
role = &nip29.Role{Permissions: make(map[nip29.Permission]struct{})}
|
||||
group.Members[tpk] = role
|
||||
|
||||
// when the user doesn't exit it will be added, so
|
||||
group.LastMembersUpdate = a.When
|
||||
}
|
||||
|
||||
// only add role that the user performing this already have
|
||||
initiator, ok := group.Members[a.Initiator]
|
||||
if ok {
|
||||
for _, perm := range a.Permissions {
|
||||
if _, has := initiator.Permissions[perm]; has {
|
||||
role.Permissions[perm] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
group.LastAdminsUpdate = a.When
|
||||
}
|
||||
|
||||
type RemovePermission struct {
|
||||
Targets []string
|
||||
Permissions []nip29.Permission
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (RemovePermission) PermissionName() nip29.Permission { return nip29.PermRemovePermission }
|
||||
func (a RemovePermission) Apply(group *nip29.Group) {
|
||||
for _, tpk := range a.Targets {
|
||||
target, ok := group.Members[tpk]
|
||||
if !ok || target == nip29.EmptyRole {
|
||||
continue
|
||||
}
|
||||
|
||||
_, hasSuperiorOrEqualPermission := target.Permissions[nip29.PermRemovePermission]
|
||||
if hasSuperiorOrEqualPermission {
|
||||
continue
|
||||
}
|
||||
|
||||
// remove all permissions listed
|
||||
for _, perm := range a.Permissions {
|
||||
delete(target.Permissions, perm)
|
||||
}
|
||||
|
||||
// if no more permissions are available, change this guy to be a normal user
|
||||
if target.Name == "" && len(target.Permissions) == 0 {
|
||||
group.Members[tpk] = nip29.EmptyRole
|
||||
}
|
||||
}
|
||||
group.LastAdminsUpdate = a.When
|
||||
}
|
||||
|
||||
type EditGroupStatus struct {
|
||||
Public bool
|
||||
Private bool
|
||||
Open bool
|
||||
Closed bool
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (EditGroupStatus) PermissionName() nip29.Permission { return nip29.PermEditGroupStatus }
|
||||
func (a EditGroupStatus) Apply(group *nip29.Group) {
|
||||
if a.Public {
|
||||
group.Private = false
|
||||
} else if a.Private {
|
||||
group.Private = true
|
||||
}
|
||||
|
||||
if a.Open {
|
||||
group.Closed = false
|
||||
} else if a.Closed {
|
||||
group.Closed = true
|
||||
}
|
||||
|
||||
group.LastMetadataUpdate = a.When
|
||||
}
|
||||
|
||||
type CreateGroup struct {
|
||||
Creator string
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (CreateGroup) PermissionName() nip29.Permission { return nip29.PermEditGroupStatus }
|
||||
func (a CreateGroup) Apply(group *nip29.Group) {
|
||||
group.Members[a.Creator] = &nip29.Role{
|
||||
Permissions: map[nip29.Permission]struct{}{
|
||||
nip29.PermAddUser: {},
|
||||
nip29.PermRemoveUser: {},
|
||||
nip29.PermEditMetadata: {},
|
||||
nip29.PermAddPermission: {},
|
||||
nip29.PermRemovePermission: {},
|
||||
nip29.PermDeleteEvent: {},
|
||||
nip29.PermEditGroupStatus: {},
|
||||
nip29.PermDeleteGroupStatus: {},
|
||||
},
|
||||
}
|
||||
group.LastMetadataUpdate = a.When
|
||||
group.LastAdminsUpdate = a.When
|
||||
group.LastMembersUpdate = a.When
|
||||
}
|
||||
|
||||
type DeleteGroup struct {
|
||||
When nostr.Timestamp
|
||||
}
|
||||
|
||||
func (DeleteGroup) PermissionName() nip29.Permission { return nip29.PermDeleteGroupStatus }
|
||||
func (a DeleteGroup) Apply(group *nip29.Group) {
|
||||
group.Members = make(map[string]*nip29.Role)
|
||||
group.Closed = true
|
||||
group.Private = true
|
||||
group.Name = "[deleted]"
|
||||
group.About = ""
|
||||
group.Picture = ""
|
||||
group.LastMetadataUpdate = a.When
|
||||
group.LastAdminsUpdate = a.When
|
||||
group.LastMembersUpdate = a.When
|
||||
}
|
29
queries.go
29
queries.go
@ -3,17 +3,16 @@ package relay29
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/set"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip29"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func (s *State) metadataQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
func (s *State) MetadataQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
ch := make(chan *nostr.Event, 1)
|
||||
|
||||
authed := khatru.GetAuthed(ctx)
|
||||
authed := s.GetAuthed(ctx)
|
||||
go func() {
|
||||
if slices.Contains(filter.Kinds, nostr.KindSimpleGroupMetadata) {
|
||||
if _, ok := filter.Tags["d"]; !ok {
|
||||
@ -32,7 +31,7 @@ func (s *State) metadataQueryHandler(ctx context.Context, filter nostr.Filter) (
|
||||
}
|
||||
|
||||
evt := group.ToMetadataEvent()
|
||||
evt.Sign(s.privateKey)
|
||||
evt.Sign(s.secretKey)
|
||||
ch <- evt
|
||||
return true
|
||||
})
|
||||
@ -40,7 +39,7 @@ func (s *State) metadataQueryHandler(ctx context.Context, filter nostr.Filter) (
|
||||
for _, groupId := range filter.Tags["d"] {
|
||||
if group, _ := s.Groups.Load(groupId); group != nil {
|
||||
evt := group.ToMetadataEvent()
|
||||
evt.Sign(s.privateKey)
|
||||
evt.Sign(s.secretKey)
|
||||
ch <- evt
|
||||
}
|
||||
}
|
||||
@ -53,10 +52,10 @@ func (s *State) metadataQueryHandler(ctx context.Context, filter nostr.Filter) (
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (s *State) adminsQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
func (s *State) AdminsQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
ch := make(chan *nostr.Event, 1)
|
||||
|
||||
authed := khatru.GetAuthed(ctx)
|
||||
authed := s.GetAuthed(ctx)
|
||||
go func() {
|
||||
if slices.Contains(filter.Kinds, nostr.KindSimpleGroupAdmins) {
|
||||
if _, ok := filter.Tags["d"]; !ok {
|
||||
@ -76,7 +75,7 @@ func (s *State) adminsQueryHandler(ctx context.Context, filter nostr.Filter) (ch
|
||||
return true
|
||||
}
|
||||
evt := group.ToAdminsEvent()
|
||||
evt.Sign(s.privateKey)
|
||||
evt.Sign(s.secretKey)
|
||||
ch <- evt
|
||||
return true
|
||||
})
|
||||
@ -97,7 +96,7 @@ func (s *State) adminsQueryHandler(ctx context.Context, filter nostr.Filter) (ch
|
||||
continue
|
||||
}
|
||||
evt := group.ToAdminsEvent()
|
||||
evt.Sign(s.privateKey)
|
||||
evt.Sign(s.secretKey)
|
||||
ch <- evt
|
||||
}
|
||||
}
|
||||
@ -110,10 +109,10 @@ func (s *State) adminsQueryHandler(ctx context.Context, filter nostr.Filter) (ch
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (s *State) membersQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
func (s *State) MembersQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
ch := make(chan *nostr.Event, 1)
|
||||
|
||||
authed := khatru.GetAuthed(ctx)
|
||||
authed := s.GetAuthed(ctx)
|
||||
go func() {
|
||||
if slices.Contains(filter.Kinds, nostr.KindSimpleGroupMembers) {
|
||||
if _, ok := filter.Tags["d"]; !ok {
|
||||
@ -133,7 +132,7 @@ func (s *State) membersQueryHandler(ctx context.Context, filter nostr.Filter) (c
|
||||
return true
|
||||
}
|
||||
evt := group.ToMembersEvent()
|
||||
evt.Sign(s.privateKey)
|
||||
evt.Sign(s.secretKey)
|
||||
ch <- evt
|
||||
return true
|
||||
})
|
||||
@ -154,7 +153,7 @@ func (s *State) membersQueryHandler(ctx context.Context, filter nostr.Filter) (c
|
||||
continue
|
||||
}
|
||||
evt := group.ToMembersEvent()
|
||||
evt.Sign(s.privateKey)
|
||||
evt.Sign(s.secretKey)
|
||||
ch <- evt
|
||||
}
|
||||
}
|
||||
@ -167,14 +166,14 @@ func (s *State) membersQueryHandler(ctx context.Context, filter nostr.Filter) (c
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (s *State) normalEventQuery(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
func (s *State) NormalEventQuery(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
if hTags, hasHTags := filter.Tags["h"]; hasHTags && len(hTags) > 0 {
|
||||
// if these tags are present we already know access is safe because we've verified that in filter_policy.go
|
||||
return s.DB.QueryEvents(ctx, filter)
|
||||
}
|
||||
|
||||
ch := make(chan *nostr.Event)
|
||||
authed := khatru.GetAuthed(ctx)
|
||||
authed := s.GetAuthed(ctx)
|
||||
go func() {
|
||||
// now here in refE/refA/ids we have to check for each result if it is allowed
|
||||
var results chan *nostr.Event
|
||||
|
160
relayer29/relayer.go
Normal file
160
relayer29/relayer.go
Normal file
@ -0,0 +1,160 @@
|
||||
package relayer29
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/fiatjaf/eventstore"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip11"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
NIP11Info func() nip11.RelayInformationDocument
|
||||
RejectFunc func(*nostr.Event) (bool, string)
|
||||
pubkey string
|
||||
opts relay29.Options
|
||||
state *relay29.State
|
||||
}
|
||||
|
||||
func (r *Relay) Name() string {
|
||||
return "nostr-relay29"
|
||||
}
|
||||
|
||||
func (r *Relay) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) Storage(ctx context.Context) eventstore.Store {
|
||||
return &Store{
|
||||
relay: r,
|
||||
state: r.state,
|
||||
store: r.opts.DB,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(ctx context.Context, ev *nostr.Event) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Relay) GetNIP11InformationDocument() nip11.RelayInformationDocument {
|
||||
if r.NIP11Info != nil {
|
||||
return r.NIP11Info()
|
||||
}
|
||||
|
||||
return nip11.RelayInformationDocument{
|
||||
Name: "nostr-relay29",
|
||||
Description: "relay29 rleay powered by the relayer framework",
|
||||
SupportedNIPs: []int{29},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) BroadcastEvent(ev *nostr.Event) {
|
||||
relayer.BroadcastEvent(ev)
|
||||
}
|
||||
|
||||
func (r *Relay) AddEvent(ctx context.Context, ev *nostr.Event) (skipBroadcast bool, writeError error) {
|
||||
err := r.opts.DB.SaveEvent(ctx, ev)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func Init(opts relay29.Options) (relayer.Relay, *relay29.State) {
|
||||
pubkey, _ := nostr.GetPublicKey(opts.SecretKey)
|
||||
|
||||
// create a new relay29.State
|
||||
state := relay29.New(opts)
|
||||
|
||||
// create a new khatru relay
|
||||
relay := &Relay{
|
||||
pubkey: pubkey,
|
||||
state: state,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
// assign khatru relay to relay29.State
|
||||
state.Relay = relay
|
||||
|
||||
// provide GetAuthed function
|
||||
state.GetAuthed = func(context.Context) string {
|
||||
// TODO
|
||||
return ""
|
||||
}
|
||||
|
||||
return relay, state
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
relay *Relay
|
||||
state *relay29.State
|
||||
store eventstore.Store
|
||||
}
|
||||
|
||||
func (s *Store) Init() error {
|
||||
return s.store.Init()
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
s.store.Close()
|
||||
}
|
||||
|
||||
func (s *Store) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||
if rejected, msg := s.state.RequireKindAndSingleGroupIDOrSpecificEventReference(ctx, filter); rejected {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
rfs := []func(context.Context, nostr.Filter) (chan *nostr.Event, error){
|
||||
s.state.NormalEventQuery,
|
||||
s.state.MetadataQueryHandler,
|
||||
s.state.AdminsQueryHandler,
|
||||
s.state.MembersQueryHandler,
|
||||
}
|
||||
for _, rf := range rfs {
|
||||
if evc, err := rf(ctx, filter); err == nil {
|
||||
return evc, nil
|
||||
}
|
||||
}
|
||||
return s.store.QueryEvents(ctx, filter)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteEvent(ctx context.Context, ev *nostr.Event) error {
|
||||
return s.store.DeleteEvent(ctx, ev)
|
||||
}
|
||||
|
||||
func (s *Store) SaveEvent(ctx context.Context, ev *nostr.Event) error {
|
||||
if s.relay.RejectFunc != nil {
|
||||
if rejected, msg := s.relay.RejectFunc(ev); rejected {
|
||||
return errors.New(msg)
|
||||
}
|
||||
}
|
||||
|
||||
bfs := []func(context.Context, *nostr.Event) (bool, string){
|
||||
s.state.RequireHTagForExistingGroup,
|
||||
s.state.RequireModerationEventsToBeRecent,
|
||||
s.state.RestrictWritesBasedOnGroupRules,
|
||||
s.state.RestrictInvalidModerationActions,
|
||||
s.state.PreventWritingOfEventsJustDeleted,
|
||||
}
|
||||
for _, rf := range bfs {
|
||||
if rejected, msg := rf(ctx, ev); rejected {
|
||||
return errors.New(msg)
|
||||
}
|
||||
}
|
||||
|
||||
err := s.store.SaveEvent(ctx, ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
afs := []func(context.Context, *nostr.Event){
|
||||
s.state.ApplyModerationAction,
|
||||
s.state.ReactToJoinRequest,
|
||||
s.state.ReactToLeaveRequest,
|
||||
}
|
||||
for _, rf := range afs {
|
||||
rf(ctx, ev)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
139
state.go
139
state.go
@ -2,26 +2,28 @@ package relay29
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/fiatjaf/eventstore"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/set"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip29"
|
||||
nip29_relay "github.com/nbd-wtf/go-nostr/nip29/relay"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Relay *khatru.Relay
|
||||
Domain string
|
||||
Groups *xsync.MapOf[string, *Group]
|
||||
DB eventstore.Store
|
||||
Relay interface {
|
||||
BroadcastEvent(*nostr.Event)
|
||||
AddEvent(context.Context, *nostr.Event) (skipBroadcast bool, writeError error)
|
||||
}
|
||||
GetAuthed func(context.Context) string
|
||||
|
||||
AllowPrivateGroups bool
|
||||
|
||||
deletedCache set.Set[string]
|
||||
publicKey string
|
||||
privateKey string
|
||||
secretKey string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@ -30,7 +32,7 @@ type Options struct {
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func Init(opts Options) *State {
|
||||
func New(opts Options) *State {
|
||||
pubkey, _ := nostr.GetPublicKey(opts.SecretKey)
|
||||
|
||||
// events that just got deleted will be cached here for `tooOld` seconds such that someone doesn't rebroadcast
|
||||
@ -40,125 +42,20 @@ func Init(opts Options) *State {
|
||||
// we keep basic data about all groups in memory
|
||||
groups := xsync.NewMapOf[string, *Group]()
|
||||
|
||||
// we create a new khatru relay
|
||||
relay := khatru.NewRelay()
|
||||
relay.Info.PubKey = pubkey
|
||||
relay.Info.SupportedNIPs = append(relay.Info.SupportedNIPs, 29)
|
||||
|
||||
state := &State{
|
||||
relay,
|
||||
opts.Domain,
|
||||
groups,
|
||||
opts.DB,
|
||||
deletedCache,
|
||||
pubkey,
|
||||
opts.SecretKey,
|
||||
Domain: opts.Domain,
|
||||
Groups: groups,
|
||||
DB: opts.DB,
|
||||
|
||||
AllowPrivateGroups: true,
|
||||
|
||||
deletedCache: deletedCache,
|
||||
publicKey: pubkey,
|
||||
secretKey: opts.SecretKey,
|
||||
}
|
||||
|
||||
// load all groups
|
||||
state.loadGroups(context.Background())
|
||||
|
||||
// apply basic relay policies
|
||||
relay.StoreEvent = append(relay.StoreEvent, state.DB.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents,
|
||||
state.normalEventQuery,
|
||||
state.metadataQueryHandler,
|
||||
state.adminsQueryHandler,
|
||||
state.membersQueryHandler,
|
||||
)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, state.DB.DeleteEvent)
|
||||
relay.RejectFilter = append(relay.RejectFilter,
|
||||
state.requireKindAndSingleGroupIDOrSpecificEventReference,
|
||||
)
|
||||
relay.RejectEvent = append(relay.RejectEvent,
|
||||
state.requireHTagForExistingGroup,
|
||||
state.requireModerationEventsToBeRecent,
|
||||
state.restrictWritesBasedOnGroupRules,
|
||||
state.restrictInvalidModerationActions,
|
||||
state.preventWritingOfEventsJustDeleted,
|
||||
)
|
||||
relay.OnEventSaved = append(relay.OnEventSaved,
|
||||
state.applyModerationAction,
|
||||
state.reactToJoinRequest,
|
||||
)
|
||||
relay.OnConnect = append(relay.OnConnect, khatru.RequestAuth)
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
nip29.Group
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *State) NewGroup(id string) *Group {
|
||||
return &Group{
|
||||
Group: nip29.Group{
|
||||
Address: nip29.GroupAddress{
|
||||
ID: id,
|
||||
Relay: "wss://" + s.Domain,
|
||||
},
|
||||
Members: map[string]*nip29.Role{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// loadGroups loads all the group metadata from all the past action messages.
|
||||
func (s *State) loadGroups(ctx context.Context) {
|
||||
groupMetadataEvents, _ := s.DB.QueryEvents(ctx, nostr.Filter{Kinds: []int{nostr.KindSimpleGroupCreateGroup}})
|
||||
for evt := range groupMetadataEvents {
|
||||
gtag := evt.Tags.GetFirst([]string{"h", ""})
|
||||
id := (*gtag)[1]
|
||||
|
||||
group := s.NewGroup(id)
|
||||
f := nostr.Filter{
|
||||
Kinds: nip29.ModerationEventKinds,
|
||||
Tags: nostr.TagMap{"h": []string{id}},
|
||||
}
|
||||
ch, _ := s.DB.QueryEvents(ctx, f)
|
||||
|
||||
events := make([]*nostr.Event, 0)
|
||||
for event := range ch {
|
||||
events = append(events, event)
|
||||
}
|
||||
for i := len(events) - 1; i >= 0; i-- {
|
||||
evt := events[i]
|
||||
act, _ := nip29_relay.GetModerationAction(evt)
|
||||
act.Apply(&group.Group)
|
||||
}
|
||||
|
||||
s.Groups.Store(group.Address.ID, group)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) GetGroupFromEvent(event *nostr.Event) *Group {
|
||||
groupID := GetGroupIDFromEvent(event)
|
||||
if groupID == "" {
|
||||
return nil
|
||||
}
|
||||
group, _ := s.Groups.Load(groupID)
|
||||
return group
|
||||
}
|
||||
|
||||
func GetGroupIDFromEvent(event *nostr.Event) string {
|
||||
gtag := event.Tags.GetFirst([]string{"h", ""})
|
||||
if gtag == nil || len(*gtag) < 2 {
|
||||
return ""
|
||||
}
|
||||
groupId := (*gtag)[1]
|
||||
return groupId
|
||||
}
|
||||
|
||||
func GetUsersFromEvent(event *nostr.Event) []string {
|
||||
pTags := event.Tags.GetAll([]string{"p", ""})
|
||||
if len(pTags) == 0 {
|
||||
return nil
|
||||
}
|
||||
var users []string
|
||||
for _, tag := range pTags {
|
||||
if len(tag) > 1 {
|
||||
users = append(users, tag[1])
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
3
strfry29/README.adoc
Normal file
3
strfry29/README.adoc
Normal file
@ -0,0 +1,3 @@
|
||||
= strfry29
|
||||
|
||||
== a plugin for turning strfry into a NIP-29-powered relay
|
28
strfry29/accept.go
Normal file
28
strfry29/accept.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip29"
|
||||
)
|
||||
|
||||
func accept(event *nostr.Event) (reject bool, msg string) {
|
||||
if nip29.MetadataEventKinds.Includes(event.Kind) {
|
||||
return true, "can't write metadata event kinds directly"
|
||||
}
|
||||
|
||||
for _, re := range []func(ctx context.Context, event *nostr.Event) (reject bool, msg string){
|
||||
state.RequireHTagForExistingGroup,
|
||||
state.RequireModerationEventsToBeRecent,
|
||||
state.RestrictWritesBasedOnGroupRules,
|
||||
state.RestrictInvalidModerationActions,
|
||||
state.PreventWritingOfEventsJustDeleted,
|
||||
} {
|
||||
if reject, msg := re(ctx, event); reject {
|
||||
return reject, msg
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
4
strfry29/justfile
Normal file
4
strfry29/justfile
Normal file
@ -0,0 +1,4 @@
|
||||
run:
|
||||
go build .
|
||||
mkdir -p strfry-db
|
||||
strfry --config=./strfry.conf relay
|
130
strfry29/main.go
Normal file
130
strfry29/main.go
Normal file
@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/eventstore/strfry"
|
||||
"github.com/fiatjaf/relay29"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
var (
|
||||
state *relay29.State
|
||||
ctx = context.Background()
|
||||
|
||||
strfrydb strfry.StrfryBackend
|
||||
)
|
||||
|
||||
func main() {
|
||||
incoming := json.NewDecoder(os.Stdin)
|
||||
outgoing := json.NewEncoder(os.Stdout)
|
||||
|
||||
curr, _ := os.Getwd()
|
||||
path := filepath.Join(curr, "strfry29.json")
|
||||
confb, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't open config file at %s: %s", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
var conf struct {
|
||||
Domain string `json:"domain"`
|
||||
RelaySecretKey string `json:"relay_secret_key"`
|
||||
StrfryConfig string `json:"strfry_config_path"`
|
||||
StrfryExecutable string `json:"strfry_executable_path"`
|
||||
}
|
||||
if err := json.Unmarshal(confb, &conf); err != nil {
|
||||
log.Fatalf("invalid json config at %s: %s", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
strfrydb = strfry.StrfryBackend{
|
||||
ConfigPath: conf.StrfryConfig,
|
||||
ExecutablePath: conf.StrfryExecutable,
|
||||
}
|
||||
strfrydb.Init()
|
||||
defer strfrydb.Close()
|
||||
|
||||
state = relay29.New(relay29.Options{
|
||||
Domain: conf.Domain,
|
||||
DB: &strfrydb,
|
||||
SecretKey: conf.RelaySecretKey,
|
||||
})
|
||||
|
||||
state.AllowPrivateGroups = false
|
||||
state.GetAuthed = func(ctx context.Context) string { return "" }
|
||||
state.Relay = protoRelay{}
|
||||
|
||||
// rebuild metadata events (replaceable) for all groups and make them available
|
||||
filter := nostr.Filter{Kinds: []int{nostr.KindSimpleGroupMetadata, nostr.KindSimpleGroupAdmins, nostr.KindSimpleGroupMembers}}
|
||||
if err := republishMetadataEvents(filter); err != nil {
|
||||
log.Fatalf("failed to republish metadata events on startup: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
var msg StrfryMessage
|
||||
|
||||
err := incoming.Decode(&msg)
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Print("[strfry29] failed to decode request. killing: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// message, _ := json.Marshal(msg)
|
||||
// log.Print("[strfry29] got event: ", string(message))
|
||||
|
||||
if reject, rejectMsg := accept(msg.Event); reject {
|
||||
outgoing.Encode(StrfryResponse{
|
||||
ID: msg.Event.ID,
|
||||
Action: "reject",
|
||||
Msg: rejectMsg,
|
||||
})
|
||||
} else {
|
||||
outgoing.Encode(StrfryResponse{
|
||||
ID: msg.Event.ID,
|
||||
Action: "accept",
|
||||
})
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
|
||||
state.ApplyModerationAction(ctx, msg.Event)
|
||||
state.ReactToJoinRequest(ctx, msg.Event)
|
||||
state.ReactToLeaveRequest(ctx, msg.Event)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type StrfryMessage struct {
|
||||
Type string `json:"type"`
|
||||
Event *nostr.Event `json:"event"`
|
||||
SourceType string `json:"sourceType"`
|
||||
}
|
||||
|
||||
type StrfryResponse struct {
|
||||
ID string `json:"id"`
|
||||
Action string `json:"action"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type protoRelay struct{}
|
||||
|
||||
func (p protoRelay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast bool, writeError error) {
|
||||
err := strfrydb.SaveEvent(ctx, evt)
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (p protoRelay) BroadcastEvent(evt *nostr.Event) {
|
||||
strfrydb.SaveEvent(ctx, evt)
|
||||
}
|
44
strfry29/metadata.go
Normal file
44
strfry29/metadata.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func republishMetadataEvents(basefilter nostr.Filter) error {
|
||||
filter := basefilter
|
||||
|
||||
filter.Kinds = []int{nostr.KindSimpleGroupMetadata}
|
||||
if err := republishMetadataEvent(state.MetadataQueryHandler, filter); err != nil {
|
||||
return fmt.Errorf("with filter %s: %w", filter, err)
|
||||
}
|
||||
|
||||
filter.Kinds = []int{nostr.KindSimpleGroupAdmins}
|
||||
if err := republishMetadataEvent(state.AdminsQueryHandler, filter); err != nil {
|
||||
return fmt.Errorf("with filter %s: %w", filter, err)
|
||||
}
|
||||
|
||||
filter.Kinds = []int{nostr.KindSimpleGroupMembers}
|
||||
if err := republishMetadataEvent(state.MembersQueryHandler, filter); err != nil {
|
||||
return fmt.Errorf("with filter %s: %w", filter, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func republishMetadataEvent(querier func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error), filter nostr.Filter) error {
|
||||
ch, err := querier(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build: %s", err)
|
||||
}
|
||||
|
||||
for evt := range ch {
|
||||
if err := strfrydb.SaveEvent(ctx, evt); err != nil {
|
||||
return fmt.Errorf("failed to publish: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
67
strfry29/strfry.conf
Normal file
67
strfry29/strfry.conf
Normal file
@ -0,0 +1,67 @@
|
||||
db = "strfry-db/"
|
||||
|
||||
dbParams {
|
||||
maxreaders = 256
|
||||
mapsize = 10995116277760
|
||||
noReadAhead = false
|
||||
}
|
||||
|
||||
events {
|
||||
maxEventSize = 4096
|
||||
rejectEventsNewerThanSeconds = 30
|
||||
rejectEventsOlderThanSeconds = 30
|
||||
rejectEphemeralEventsOlderThanSeconds = 60
|
||||
ephemeralEventsLifetimeSeconds = 300
|
||||
maxNumTags = 12
|
||||
maxTagValSize = 256
|
||||
}
|
||||
|
||||
relay {
|
||||
bind = "127.0.0.1"
|
||||
port = 52929
|
||||
nofiles = 0
|
||||
realIpHeader = ""
|
||||
|
||||
info {
|
||||
name = "strfry29"
|
||||
description = "this is an strfry instance that only works with nip29 groups"
|
||||
pubkey = ""
|
||||
contact = ""
|
||||
icon = ""
|
||||
}
|
||||
|
||||
maxWebsocketPayloadSize = 5000
|
||||
autoPingSeconds = 29
|
||||
enableTcpKeepalive = false
|
||||
queryTimesliceBudgetMicroseconds = 10000
|
||||
maxFilterLimit = 500
|
||||
maxSubsPerConnection = 20
|
||||
writePolicy {
|
||||
plugin = "./strfry29"
|
||||
}
|
||||
|
||||
compression {
|
||||
enabled = true
|
||||
slidingWindow = true
|
||||
}
|
||||
|
||||
logging {
|
||||
dumpInAll = false
|
||||
dumpInEvents = false
|
||||
dumpInReqs = false
|
||||
dbScanPerf = false
|
||||
invalidEvents = true
|
||||
}
|
||||
|
||||
numThreads {
|
||||
ingester = 3
|
||||
reqWorker = 3
|
||||
reqMonitor = 3
|
||||
negentropy = 2
|
||||
}
|
||||
|
||||
negentropy {
|
||||
enabled = true
|
||||
maxSyncEvents = 1000000
|
||||
}
|
||||
}
|
6
strfry29/strfry29.json
Normal file
6
strfry29/strfry29.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "example.com",
|
||||
"relay_secret_key": "9e36f2b5dcc299a01019471ec13bc911b614245f39464f2496d32bf639c12c3b",
|
||||
"strfry_config_path": "strfry.conf",
|
||||
"strfry_executable_path": "/usr/local/bin/strfry"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user