Skip to content

fiatjaf/relay29

Repository files navigation

relay29 relay29

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 khatru using the khatru29 wrapper, strfry with strfry29 and relayer with relayer29.

Caution
This is probably broken so please don’t trust it for anything serious and be prepared to delete your database.
package main

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"
)

var (
	adminRole     = &nip29.Role{Name: "admin", Description: "the group's max top admin"}
	moderatorRole = &nip29.Role{Name: "moderator", Description: "the person who cleans up unwanted stuff"}
)

func main() {
	relayPrivateKey := nostr.GeneratePrivateKey()

	db := &slicestore.SliceStore{} // this only keeps things in memory, use a different eventstore in production
	db.Init()

	relay, state := khatru29.Init(relay29.Options{
		Domain:    "localhost:2929",
		DB:        db,
		SecretKey: relayPrivateKey,
		DefaultRoles:            []*nip29.Role{adminRole, moderatorRole},
		GroupCreatorDefaultRole: adminRole,
	})

	// setup group-related restrictions
	state.AllowAction = func(ctx context.Context, group nip29.Group, role *nip29.Role, action relay29.Action) bool {
		// this is simple:
		if _, ok := action.(relay29.PutUser); ok {
			// anyone can invite new users
			return true
		}
		if role == adminRole {
			// owners can do everything
			return true
		}
		if role == moderatorRole {
			// admins can delete people and messages
			switch action.(type) {
			case relay29.RemoveUser:
				return true
			case relay29.DeleteEvent:
				return true
			}
		}
		// no one else can do anything else
		return false
	}

	// init relay
	relay.Info.Name = "very ephemeral chat relay"
	relay.Info.Description = "everything will be deleted as soon as I turn off my computer"

	// 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 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, 39003) 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);