strfrui

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 30, 2024 License: MIT Imports: 6 Imported by: 0

README

strfrui

GitHub Release GoDoc CI

A framework for writing strfry's event-sifter (write policy) plugins in Go.

This project is formerly known as strfry-evsifter.

Installation

go get github.com/jiftechnify/strfrui

Features

  • Offers out-of-the-box event-sifters, including rate limiters.
  • Sifter combinators: you can build own event-sifters by composing small parts together.
  • Provides you foundations for writing a custom event-sifter as a simple function and running it.

Examples

Using Out-of-the-Box Sifters

The code below implements the same logic as this example using built-in event sifters in sifters package:

package main

import (
    "github.com/jiftechnify/strfrui"
    "github.com/jiftechnify/strfrui/sifters"
)

var whiteList = []string{
    "003ba9b2c5bd8afeed41a4ce362a8b7fc3ab59c25b6a1359cae9093f296dac01",
}

func main() {
    // Initializing a strfrui.Runner with an event-sifter
    // that accepts events from pubkeys in the whitelist.
    // Then, start the sifting routine by calling Run().
    strfrui.New(sifters.AuthorList(whiteList, sifters.Allow)).Run()
}

The complete list of available built-in sifters is here.

Using Combinators to Compose Multiple Sifters

strfrui offers ways to compose multiple event-sifters together, called "combinators". They can be used to make a single complex sifter logic from small parts.

The code below shows the usage of these combinators:

package main

import (
    "github.com/jiftechnify/strfrui"
    "github.com/jiftechnify/strfrui/sifters"
)

var (
    adminList = []string{"admin"}
    blacklist = []string{"spammer", "scammer"}
)

func main() {
    acceptAdmin := sifters.AuthorList(adminList, sifters.Allow)
    rejectBlacklist := sifters.AuthorList(blacklist, sifters.Deny)

    // sifters.WithMod() makes sifters modifiable.
    // Sifter modification changes sifter's behavior within combinators.
    // Here is an example of using OnlyIf() modifier.
    // * base sifter says: event’s content must contain the word "nostr".
    // * OnlyIf(...) says: restriction above applies to only kind 1 events.
    nostrPostsOnly := sifters.WithMod(
        sifters.ContentHasAnyWord([]string{"nostr"}, sifters.Allow)
    ).OnlyIf(sifters.KindList([]int{1}, sifters.Allow))

    finalSifter := sifters.      // finalSifter accepts if...
        OneOf(                   // the input satisfies *one of* conditions:
            acceptAdmin,         // 1. author is the admin
            sifters.Pipeline(    // 2. the input satisfies *all* conditions:
                rejectBlacklist, //    a. author is not in the blacklist
                nostrPostsOnly,  //    b. if kind == 1, its content must contain the word "nostr"
            ),
        )
    // run the finalSifter!
    strfrui.New(finalSifter).Run()
}

The complete list of available combinators and modifiers is here.

Bringing Rate Limiter to Strfry

You can easily set up a rate limiter to your Strfry relay by using built-in sifters under ratelimit package!

Below is a brief example of how to apply a rate limiter:

package main

import (
    "github.com/jiftechnify/strfrui"
    "github.com/jiftechnify/strfrui/sifters/ratelimit"
)

func main() {
    limiter := ratelimit.ByUser(
        // every users can write 2 events per second, allowing burst up to 5 events.
        ratelimit.QuotaPerSec(2).WithBurst(5),
        // "users" are identified by pubkey. You can also use ratelimit.IPAddr here.
        ratelimit.Pubkey, 
    ).
    // exclude all ephemeral events from rate limiting
    Exclude(func(input *strfrui.Input) bool { 
        return sifters.KindsAllEphemeral(input.Event.Kind)
    })

    strfrui.New(limiter).Run()
}

You may want to use ratelimit.ByUserAndKind to impose different limits for different event kinds.

limiter := ratelimit.ByUserAndKind([]ratelimit.QuotaForKinds{
    // 2 events/s, burst up to 10 events for kind:1 
    ratelimit.QuotaPerSec(2).WithBurst(10).ForKinds(1),
    // 5 events/s, burst up to 50 events for kind:7
    ratelimit.QuotaPerSec(5).WithBurst(50).ForKinds(7),
}, ratelimit.Pubkey)
Writing Custom Sifter from Scratch

Essentially, event-sifter is just a function that takes an "input" (event + metadata of event source etc.) and returns "result" (action to take on the event: accept or reject).

type Sifter interface {
    Sift (*strfrui.Input) (*strfrui.Result, error)
}

If you feel cumbersome to build sifters you want by combining small blocks, you can still implement overall sifter logic as a Go function. Of course, sifters written in such a way are also composable using the combinators!

The code below is a example of writing event-sifter as a function. The logic is equivalent to the sifter in the first example, but it adds custom logging.

package main

import (
	"log"
	"github.com/jiftechnify/strfrui"
)

var whitelist = map[string]struct{}{
	"003ba9b2c5bd8afeed41a4ce362a8b7fc3ab59c25b6a1359cae9093f296dac01": {},
}

// event-sifting function
func acceptWhitelisted(input *strfrui.Input) (*strfrui.Result, error) {
	if _, ok := whitelist[input.Event.PubKey]; ok {
		return input.Accept()
	}

	// you can emit arbitrary logs by log.Print() family
	log.Println("blocking event!")
	return input.Reject("blocked: not on white-list")
}

func main() {
    // note that we use *NewWithSifterFunc* here to set a sifting function
    // instead of a Sifter interface implementation.
    strfrui.NewWithSifterFunc(acceptWhitelisted).Run()
}

License

MIT

Documentation

Overview

Package strfrui provides foundations for building strfry's event-sifters plugins in Go.

If you want to explore built-in event-sifter implementations and combinators, which can be used to make complex sifter logics from small parts, see the doc of github.com/jiftechnify/strfrui/sifters package.

Index

Examples

Constants

View Source
const (
	RejectReasonPrefixBlocked     = "blocked"
	RejectReasonPrefixRateLimited = "rate-limited"
	RejectReasonPrefixInvalid     = "invalid"
	RejectReasonPrefixPoW         = "pow"
	RejectReasonPrefixError       = "error"
)

"Machine-readable prefixes" for rejection messages. Use them with BuildRejectMessage.

Variables

This section is empty.

Functions

func BuildRejectMessage

func BuildRejectMessage(prefix string, body string) string

BuildRejectMessage builds a rejection message with a machine-readable prefix.

The message format is defined in NIP-01.

Types

type Action

type Action string

Action represents a type of action by event sifter, in other words, how to process an event.

const (
	ActionAccept       Action = "accept"
	ActionReject       Action = "reject"
	ActionShadowReject Action = "shadowReject"
)

type Input

type Input struct {
	// A type of input. As of strfry 0.9.6, it is always "new".
	Type string `json:"type"`

	// An event data sent by a client or imported from file / another relay.
	Event *nostr.Event `json:"event"`

	// Unix timestamp (in second) of when the event was received by the relay.
	ReceivedAt uint64 `json:"receivedAt"`

	// The source type, or where the event came from.
	SourceType SourceType `json:"sourceType"`

	// Information about event source. If SourceType is...
	//
	//   - SourceTypeIP4 or SourceTypeIP6, it's a string representation of client's IP address.
	//   - SourceTypeStream or SourceTypeSync, it's a URL of a source relay.
	//   - SourceTypeImport, it's an empty string.
	SourceInfo string `json:"sourceInfo"`
}

Input is a data structure of event sifter's input.

func (*Input) Accept

func (i *Input) Accept() (*Result, error)

Accept accepts the event in the input.

func (*Input) Reject

func (i *Input) Reject(msg string) (*Result, error)

Reject rejects the event in the input with a rejection message to the client.

As per NIP-01, the message should be prefixed with a machine-readable word followed by ":" (e.g. "blocked: you are not allowed to write events"). You can use BuildRejectMessage to build a rejection message in that format.

func (*Input) ShadowReject

func (i *Input) ShadowReject() (*Result, error)

ShadowReject silently rejects the event in the input, that is, makes it look accepted to the client, but actually reject it.

type Result

type Result struct {
	// The ID of the target event, taken from the ID field of Input.
	ID string `json:"id"`

	// An action to take on the target event.
	Action Action `json:"action"`

	// A message to be sent to a client (included in an OK message) if event is rejected.
	Msg string `json:"msg"`
}

Result is a data structure of event sifter's output. It can be generated from methods of an Input.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner implements the main routine of a event sifter as Run() method. You may want to use strfrui.New to initialize a Runner and set a Sifter at the same time.

The zero value for Runner is a valid Runner that accepts all events.

Example
package main

import (
	"github.com/jiftechnify/strfrui"
	"github.com/jiftechnify/strfrui/sifters"
)

func main() {
	strfrui.New(sifters.KindList([]int{1}, sifters.Allow)).Run()
}
Output:

func New

func New(s Sifter) *Runner

New initializes a new Runner and set the passed Sifter at the same time.

func NewWithSifterFunc

func NewWithSifterFunc(sf func(input *Input) (*Result, error)) *Runner

NewWithSifterFunc initializes a new Runner and set the passed event sifting function as a Sifter at the same time.

func (*Runner) Run

func (r *Runner) Run()

Run executes the main routine of a event sifter.

func (*Runner) SiftWith

func (r *Runner) SiftWith(s Sifter)

SiftWith replaces the Sifter in the Runner with the passed one.

func (*Runner) SiftWithFunc

func (r *Runner) SiftWithFunc(sf func(input *Input) (*Result, error))

SiftWithFunc replaces the Sifter in the Runner with the passed event sifting function.

type Sifter

type Sifter interface {
	Sift(input *Input) (*Result, error)
}

A Sifter decides whether accept or reject an event based on Input, the event data itself with context information.

Sift should return either Result with an action to take on the event, or error if it couldn't process the input. If error is returned from Sift, the event is rejected by default.

type SifterFunc

type SifterFunc func(input *Input) (*Result, error)

SifterFunc is an adapter to allow the use of functions which takes a sifter Input and returns a sifter Result as a Sifter.

func (SifterFunc) Sift

func (s SifterFunc) Sift(input *Input) (*Result, error)

type SourceType

type SourceType string

SourceType represents a source type of a Nostr event, in other words, where an event came from.

const (
	// SourceTypeIP4 shows that an event was sent from a client which has an IPv4 address.
	SourceTypeIP4 SourceType = "IP4"

	// SourceTypeIP6 shows that an event was sent from a client which has an IPv6 address.
	SourceTypeIP6 SourceType = "IP6"

	// SourceTypeImport shows that an event was imported via "strfry import" command.
	SourceTypeImport SourceType = "Import"

	// SourceTypeStream shows that an event was imported from another relay via "strfry stream" or "strfry router" command.
	SourceTypeStream SourceType = "Stream"

	// SourceTypeSync shows that an event was imported from another relay via "strfry sync" command.
	SourceTypeSync SourceType = "Sync"
)

func (SourceType) IsEndUser

func (st SourceType) IsEndUser() bool

IsEndUser checks whether the source is an "end-user" (or, a "user-facing client").

Directories

Path Synopsis
Provides a set of out-of-the-box event-sifters and sifter combinators.
Provides a set of out-of-the-box event-sifters and sifter combinators.
ratelimit
Provides out-of-the-box rate limiting event-sifters.
Provides out-of-the-box rate limiting event-sifters.

Jump to

Keyboard shortcuts

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