Skip to content

Commit

Permalink
Merge pull request #7 from TheRebelOfBabylon/1-refator
Browse files Browse the repository at this point in the history
Refactor
  • Loading branch information
TheRebelOfBabylon authored Dec 8, 2024
2 parents c9e9df1 + 80d33ef commit d12c876
Show file tree
Hide file tree
Showing 39 changed files with 4,617 additions and 1,094 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@

# Dependency directories (remove the comment below to include it)
# vendor/

# test configs
test.toml
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 TheRebelOfBabylon
Copyright (c) 2024 TheRebelOfBabylon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
80 changes: 79 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,80 @@
# tandem
Nostr relay, written in Go
Nostr relay, written in Go.

# NIP Compliance
- [x] NIP-01
- [x] NIP-02: Follow List
- [ ] NIP-05: Mapping Nostr keys to DNS-based Internet Identifiers
- [ ] NIP-09: Event Deletion Request
- [ ] NIP-11: Relay Information Document
- [ ] NIP-13: Proof of Work
- [ ] NIP-17: Private Direct Messages
- [ ] NIP-29: Relay-based Groups
- [ ] NIP-40: Expiration Timestamp
- [ ] NIP-42: Authentication of clients to relays
- [ ] NIP-45: Event Counts
- [x] NIP-50: Search Capability*
- [ ] NIP-56: Reporting
- [ ] NIP-64: Chess (Portable Game Notation)
- [x] NIP-65: Relay List Metadata**
- [ ] NIP-70: Protected Events
- [ ] NIP-86: Relay Management API
- [ ] NIP-96: HTTP File Storage Integration

\* = Search is not ordered by quality or treated differently for each kind. Applies only to content field and no special syntax is used (not even * for wildcard)

** = tandem does not currently disallow any users from submitting lists

# Goals

- Easy to deploy: anyone's Uncle Jim with any sliver of IT knowledge should be able to deploy a relay.
- Easy to moderate: blocking IPs, whitelisting pubkeys, setting up specific moderation rules, all should be achievable without code knowledge.
- Community driven: nostr's main usecase is as a global townsquare but it can also be used to create small communities. tandem's main goal is to serve the latter usecase.

# Roadmap

- [ ] Define a roadmap

# Usage

## Prerequisites
- [edgedb](https://www.edgedb.com/)

## Installation

Define a configuration file:
```toml
[http]
host=localhost # env var: HTTP_HOST
port=5150 # env var: HTTP_PORT

[log]
level=info # env var: LOG_LEVEL, one of debug|info|error, default: info
log_file_path=/path/to/file.log # env var: LOG_FILE_PATH, optional

[storage]
uri="edgedb://edgedb:<password>@localhost:10701/main" # env var: STORAGE_URI, replace with your edgedb credentials, one of edgedb|memory
skip_tls_verify=true # env var: STORAGE_SKIP_TLS_VERIFY, default: false
```

Run:
```shell
$ tandem -config <path_to_toml_file>
```

# Tests

with edgedb
```shell
$ STORAGE_URI=<your_uri> go test -v -tags=storage,edgedb ./...
```

with memorydb
```shell
$ STORAGE_URI="memory://" go test -v -tags=storage,memory ./...
```

without storage
```shell
$ go test -v ./...
```
116 changes: 89 additions & 27 deletions cmd/tandem/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,106 @@ package main

import (
"flag"
"fmt"
"os"
"slices"
"strings"

"github.com/SSSOC-CAN/laniakea/intercept"
"github.com/SSSOC-CAN/laniakea/utils"
"github.com/TheRebelOfBabylon/tandem"
"github.com/TheRebelOfBabylon/tandem/config"
"github.com/TheRebelOfBabylon/tandem/logger"
u "github.com/TheRebelOfBabylon/tandem/utils"
"github.com/TheRebelOfBabylon/tandem/filter"
"github.com/TheRebelOfBabylon/tandem/ingester"
"github.com/TheRebelOfBabylon/tandem/logging"
"github.com/TheRebelOfBabylon/tandem/signal"
"github.com/TheRebelOfBabylon/tandem/storage"
"github.com/TheRebelOfBabylon/tandem/websocket"
)

type Module interface {
Start() error
Stop() error
}

var (
cfgFilePath = flag.String("config", "tandem.toml", "path to the TOML config file")
modules = []Module{}
stopModules = func(logger logging.Logger) {
slices.Reverse(modules) // we shut down in reverse order
for _, m := range modules {
if err := m.Stop(); err != nil {
logger.Fatal().Err(err).Msg("failed to safely shutdown")
}
}
}
startModules = func(logger logging.Logger) {
for _, m := range modules {
if err := m.Start(); err != nil {
logger.Fatal().Err(err).Msg("failed to start modules")
}
}
}
)

func main() {
configDir := flag.String("config", utils.AppDataDir("tandem", false), "Directory of the toml config file")
verFlag := flag.Bool("version", false, "Display current tandem version")
// parse command line flags
flag.Parse()
if *verFlag {
fmt.Fprintf(os.Stdout, "tandem version %s\n", u.AppVersion)
os.Exit(0)
}
interceptor, err := intercept.InitInterceptor()

// initialize logging
logger := logging.NewLogger()

// read and validate config
logger.Info().Msgf("reading configuration file %s...", *cfgFilePath)
cfg, err := config.ReadConfig(*cfgFilePath)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not initialize signal interceptor: %v\n", err)
os.Exit(1)
logger.Fatal().Err(err).Msg("failed to read config file")
}
cfg, err := config.InitConfig(*configDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not initialize configuration: %v\n", err)
os.Exit(1)
logger.Info().Msg("validating configuration file...")
if err := cfg.Validate(); err != nil {
logger.Fatal().Err(err).Msg("failed to validate config")
}
log, err := logger.InitLogger(&cfg.Logging)

// configure Logging
logger, err = logger.Configure(cfg.Log)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not initialize logger: %v\n", err)
os.Exit(1)
logger.Warn().Err(err).Msg("failed to set log level")
}
interceptor.Logger = &log
err = tandem.Main(cfg, log, interceptor)
logger.Info().Msgf("using log level %s", strings.ToUpper(logger.GetLevel().String()))

// initialize signal handler
interruptHandler := signal.NewInterruptHandler(logger.With().Str("module", "interruptHandler").Logger())

// initialize ingester
logger.Info().Msg("initializing ingester...")
ingest := ingester.NewIngester(logger.With().Str("module", "ingester").Logger())
modules = append(modules, ingest)

// initialize connection to storage backend
logger.Info().Msg("initializing connection to storage backend...")
storageBackend, err := storage.Connect(cfg.Storage, logger.With().Str("module", "storageBackend").Logger(), ingest.SendToDBChannel())
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
logger.Fatal().Err(err).Msg("failed to connect to storage backend")
}
modules = append(modules, storageBackend)
ingest.SetQueryFunc(storageBackend.Store.QueryEvents)

// initialize filter manager
logger.Info().Msg("initializing filter manager...")
filterManager := filter.NewFilterManager(ingest.SendToFilterManager(), storageBackend, logger.With().Str("module", "filterManager").Logger())
modules = append(modules, filterManager)

// initialize websocket handler
logger.Info().Msg("initializing websocket server...")
wsHandler := websocket.NewWebsocketServer(cfg.HTTP, logger.With().Str("module", "websocketServer").Logger(), ingest.SendToWSHandlerChannel(), filterManager.SendChannel())
modules = append(modules, wsHandler)

// ingester and websocket handler now communicating bi-directionally
ingest.SetRecvChannel(wsHandler.SendChannel())

// start modules
logger.Info().Msg("starting modules...")
startModules(logger)

// hang until we shutdown
<-interruptHandler.ShutdownDoneChannel()

// shutdown
stopModules(logger)
logger.Info().Msg("shutdown complete")
}
Empty file removed cmd/tandemctl/main.go
Empty file.
134 changes: 56 additions & 78 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,101 +1,79 @@
package config

import (
"context"
"errors"
"fmt"
"math"
"path"
"os"
"slices"

"github.com/BurntSushi/toml"
"github.com/SSSOC-CAN/laniakea/utils"
bg "github.com/SSSOCPaulCote/blunderguard"
"github.com/sethvargo/go-envconfig"
)

const (
ErrFileNotFound = bg.Error("file not found")
var (
validLogLvls = []string{
"debug",
"info",
"error",
}
defaultLogLvl = "info"
ErrInvalidLogLevel = errors.New("invalid log level")
defaultHost = "localhost"
defaultPort = 5000
)

type Info struct {
URL string `toml:"url"` // the advertised URL for websocket connection Ex: wss://nostr.example.com
Name string `toml:"name"` // name for your relay
Description string `toml:"description"` // description for clients
Pubkey string `toml:"pubkey"` // Relay admin contact pubkey
Contact string `toml:"contact"` // Relay admin contact URI
}

type Logging struct {
LogLevel string `toml:"log_level"` // (TRACE|DEBUG|INFO|ERROR)
ConsoleOutput bool `toml:"console_output"` // Print logs to console
LogFileDir string `toml:"log_file_location"` // location of the log files. Default is AppData/Tandem | Application Support/Tandem or ~/.tandem
LogFileSize uint32 `toml:"log_file_size"` // Maximum size of a log file in MB
MaxLogFiles uint16 `toml:"max_log_files"` // Maximum number of log files in a rotation. 0 for no rotation: Default 0
}

type Data struct {
DataDir string `toml:"data_directory"` // Directory for the database. Defaults to AppData | Application Support | ~/.tandem
}

type Network struct {
BindAddress string `toml:"bind_address"` // Bind to this network address
Port uint16 `toml:"port"` // Port to listen on
PingInterval uint16 `toml:"ping_interval"` // WebSockets ping interval in seconds, default: 300
type HTTP struct {
Host string `toml:"host" env:"HOST, overwrite"`
Port int `toml:"port" env:"PORT, overwrite"`
}

type Limits struct {
MsgPerSec uint32 `toml:"message_per_second"` // Limit of events that can be created per second
MaxEventSize uint32 `toml:"maximum_event_size"` // Maximum size in bytes, an event can be. Max=2^32 - 1
MaxWSMsgSize uint32 `toml:"maximum_ws_message_size"` // Maximum size in bytes a websocket message can be. Max=2^32 - 1
RejectFutureSecs uint64 `toml:"reject_future_seconds"` // Reject events that have timestamps greater than this many seconds in the future. Recommended to reject anything greater than 30 minutes from the current time, but the default is to allow any date.
type Log struct {
Level string `toml:"level" env:"LEVEL, overwrite"`
LogFilePath string `toml:"log_file_path" env:"FILE_PATH, overwrite"`
}

type Authorization struct {
PubkeyWhitelist []string `toml:"whitelist"` // List of authorized pubkeys for event publishing. If not set, all pubkeys are accepted
type Storage struct {
Uri string `toml:"uri" env:"URI, overwrite"`
SkipTlsVerify bool `toml:"skip_tls_verify" env:"SKIP_TLS_VERIFY, overwrite"`
}

type Config struct {
Relay Info // relevant relay information
Logging Logging // relevant logging settings
Database Data // relevant data settings
Network Network // relevant network settings
Limits Limits // relevant limit settings
Auth Authorization // relevant auth settings
HTTP HTTP `toml:"http" env:", prefix=HTTP_"`
Log Log `toml:"log" env:", prefix=LOG_"`
Storage Storage `toml:"storage" env:", prefix=STORAGE_"`
}

var (
config_file_name = "tandem.toml"
default_bind_address = "0.0.0.0"
default_port uint16 = 5150
default_ping_interval uint16 = 300
default_data_dir = utils.AppDataDir("tandem", false)
default_log_level = "ERROR"
default_log_dir = default_data_dir
default_console_out = true
default_msg_per_sec uint32 = 50000
default_max_event_size uint32 = math.MaxUint32
default_max_ws_msg_size uint32 = math.MaxUint32
default_reject_future_secs uint64 = 1800
default_log_size uint32 = 10
default_max_log_files uint16 = 0
default_config = func() Config {
return Config{
Logging: Logging{LogLevel: default_log_level, LogFileDir: default_log_dir, ConsoleOutput: default_console_out, LogFileSize: default_log_size, MaxLogFiles: default_max_log_files},
Database: Data{DataDir: default_data_dir},
Network: Network{BindAddress: default_bind_address, Port: default_port, PingInterval: default_ping_interval},
Limits: Limits{MsgPerSec: default_msg_per_sec, MaxEventSize: default_max_event_size, MaxWSMsgSize: default_max_ws_msg_size, RejectFutureSecs: default_reject_future_secs},
}
// ReadConfig reads the given config file
func ReadConfig(pathToConfig string) (*Config, error) {
var cfg Config
cfgFileBytes, err := os.ReadFile(pathToConfig)
if err != nil {
return nil, err
}
)
if err := toml.Unmarshal(cfgFileBytes, &cfg); err != nil {
return nil, err
}
// env vars override anything in the config file
if err := envconfig.Process(context.Background(), &cfg); err != nil {
return nil, err
}
return &cfg, nil
}

// Initialize the config either from a configuration file or using default values
func InitConfig(cfgDir string) (*Config, error) {
cfgFile := path.Join(cfgDir, config_file_name)
config := default_config()
if utils.FileExists(cfgFile) {
_, err := toml.DecodeFile(cfgFile, &config)
if err != nil {
return nil, err
}
} else {
fmt.Println("Using default configuration...")
// Validate will perform validation on the given configuration
func (c *Config) Validate() error {
if c.Log.Level == "" {
c.Log.Level = defaultLogLvl
}
if !slices.Contains(validLogLvls, c.Log.Level) {
return fmt.Errorf("%w: %s", ErrInvalidLogLevel, c.Log.Level)
}
if c.HTTP.Host == "" {
c.HTTP.Host = defaultHost
}
if c.HTTP.Port == 0 {
c.HTTP.Port = defaultPort
}
return &config, nil
return nil
}
Loading

0 comments on commit d12c876

Please sign in to comment.