refactor things to enable CLI mode

This commit is contained in:
Iris Lightshard 2024-09-28 11:39:03 -06:00
parent 09c7eb8318
commit c6cfdf9e9f
Signed by: nilix
GPG key ID: 688407174966CAF3
9 changed files with 101 additions and 49 deletions

1
.gitignore vendored
View file

@ -2,4 +2,5 @@ node_modules/
frontend/dist/*.js frontend/dist/*.js
frontend/.js frontend/.js
underbbs underbbs
underbbs-cli
__debug_* __debug_*

View file

@ -1,15 +1,23 @@
# underBBS # underBBS
underBBS is a platform-agnostic messaging and social media client underBBS is a protocol-agnostic decentralized social media client and toolkit
## design ## design
`underbbs` can run in two modes depending on its executable name:
### web client
`underbbs` supports multiple simultaneous account logins, mediating them for each user through a gateway server that handles all protocol-specific logic via `adapter`s and streaming content to the user through a single websocket connection with a singular data interface. `underbbs` supports multiple simultaneous account logins, mediating them for each user through a gateway server that handles all protocol-specific logic via `adapter`s and streaming content to the user through a single websocket connection with a singular data interface.
each distinct `adapter` connection/configuration is represented in the frontend as a tab, and using the websocket's event-driven javascript interface with web components we can simply either store the data or tell the currently visible adapter that it might need to respond to the new data each distinct `adapter` connection/configuration is represented in the frontend as a tab, and using the websocket's event-driven javascript interface with web components we can simply either store the data or tell the currently visible adapter that it might need to respond to the new data
adapters receive commands via a quartzgun web API and send data back on their shared websocket connection adapters receive commands via a quartzgun web API and send data back on their shared websocket connection
### CLI
`underbbs-cli` pulls adapter credentials from `~/.config/underbbs/cli.conf` and accepts commands on individual adapters, printing data to standard output.
## building and running ## building and running
requirements are requirements are

View file

@ -5,7 +5,7 @@ import (
) )
type Adapter interface { type Adapter interface {
Init(Settings, chan SocketData) error Init(Settings, *chan SocketData) error
Name() string Name() string
Subscribe(string) []error Subscribe(string) []error
Fetch(string, []string) error Fetch(string, []string) error

View file

@ -4,10 +4,12 @@ import (
"fmt" "fmt"
. "forge.lightcrystal.systems/lightcrystal/underbbs/models" . "forge.lightcrystal.systems/lightcrystal/underbbs/models"
madon "github.com/McKael/madon" madon "github.com/McKael/madon"
"log"
"os"
) )
type MastoAdapter struct { type MastoAdapter struct {
data chan SocketData data *chan SocketData
nickname string nickname string
server string server string
apiKey string apiKey string
@ -21,11 +23,19 @@ type MastoAdapter struct {
var scopes = []string{"read", "write", "follow"} var scopes = []string{"read", "write", "follow"}
func (self *MastoAdapter) send(data SocketData) {
if self.data != nil {
*self.data <- data
} else {
fmt.Println(os.Stdout, string(data.ToDatagram()))
}
}
func (self *MastoAdapter) Name() string { func (self *MastoAdapter) Name() string {
return self.nickname return self.nickname
} }
func (self *MastoAdapter) Init(settings Settings, data chan SocketData) error { func (self *MastoAdapter) Init(settings Settings, data *chan SocketData) error {
self.nickname = settings.Nickname self.nickname = settings.Nickname
self.server = *settings.Server self.server = *settings.Server
self.apiKey = *settings.ApiKey self.apiKey = *settings.ApiKey
@ -62,7 +72,7 @@ func (self *MastoAdapter) Subscribe(filter string) []error {
} }
go func() { go func() {
for e := range self.events { for e := range self.events {
fmt.Println("event: %s !!!", e.Event) log.Printf("event: %s !!!", e.Event)
switch e.Event { switch e.Event {
case "error": case "error":
case "update": case "update":
@ -77,7 +87,7 @@ func (self *MastoAdapter) Subscribe(filter string) []error {
msg = self.mastoUpdateToMessage(v) msg = self.mastoUpdateToMessage(v)
} }
if msg != nil { if msg != nil {
self.data <- msg self.send(msg)
} }
case "notification": case "notification":
case "delete": case "delete":

View file

@ -9,13 +9,15 @@ import (
n "github.com/yitsushi/go-misskey/services/notes" n "github.com/yitsushi/go-misskey/services/notes"
tl "github.com/yitsushi/go-misskey/services/notes/timeline" tl "github.com/yitsushi/go-misskey/services/notes/timeline"
users "github.com/yitsushi/go-misskey/services/users" users "github.com/yitsushi/go-misskey/services/users"
"log"
"os"
"strings" "strings"
"sync" "sync"
"time" "time"
) )
type MisskeyAdapter struct { type MisskeyAdapter struct {
data chan SocketData data *chan SocketData
nickname string nickname string
server string server string
apiKey string apiKey string
@ -31,19 +33,27 @@ type MisskeyAdapter struct {
stop chan bool stop chan bool
} }
func (self *MisskeyAdapter) send(data SocketData) {
if self.data != nil {
*self.data <- data
} else {
fmt.Fprintln(os.Stderr, string(data.ToDatagram()))
}
}
func (self *MisskeyAdapter) Name() string { func (self *MisskeyAdapter) Name() string {
return self.nickname return self.nickname
} }
func (self *MisskeyAdapter) Init(settings Settings, data chan SocketData) error { func (self *MisskeyAdapter) Init(settings Settings, data *chan SocketData) error {
fmt.Println("initializing misskey adapter") log.Print("initializing misskey adapter")
self.nickname = settings.Nickname self.nickname = settings.Nickname
self.server = *settings.Server self.server = *settings.Server
self.apiKey = *settings.ApiKey self.apiKey = *settings.ApiKey
self.data = data self.data = data
fmt.Println("getting ready to initialize internal client") log.Print("getting ready to initialize internal client")
client, err := misskey.NewClientWithOptions( client, err := misskey.NewClientWithOptions(
misskey.WithAPIToken(self.apiKey), misskey.WithAPIToken(self.apiKey),
@ -51,10 +61,10 @@ func (self *MisskeyAdapter) Init(settings Settings, data chan SocketData) error
) )
if err != nil { if err != nil {
fmt.Println(err.Error()) log.Print(err.Error())
return err return err
} }
fmt.Println("misskey client initialized") log.Print("misskey client initialized")
self.mk = client self.mk = client
self.cache = make(map[string]time.Time) self.cache = make(map[string]time.Time)
@ -119,10 +129,10 @@ func (self *MisskeyAdapter) poll() {
Limit: 100, Limit: 100,
}) })
if err != nil { if err != nil {
fmt.Println(err.Error()) log.Print(err.Error())
} }
if merr != nil { if merr != nil {
fmt.Println(merr.Error()) log.Print(merr.Error())
} }
// check the cache for everything we just collected // check the cache for everything we just collected
@ -131,13 +141,13 @@ func (self *MisskeyAdapter) poll() {
for _, n := range notes { for _, n := range notes {
msg := self.toMessageIfNew(n) msg := self.toMessageIfNew(n)
if msg != nil { if msg != nil {
self.data <- msg self.send(msg)
} }
} }
for _, n := range mentions { for _, n := range mentions {
msg := self.toMessageIfNew(n) msg := self.toMessageIfNew(n)
if msg != nil { if msg != nil {
self.data <- msg self.send(msg)
} }
} }
@ -150,7 +160,7 @@ func (self *MisskeyAdapter) poll() {
Limit: 100, Limit: 100,
}) })
if err != nil { if err != nil {
fmt.Println(err.Error()) log.Print(err.Error())
} }
for _, n := range notes { for _, n := range notes {
msg := self.toMessageIfNew(n) msg := self.toMessageIfNew(n)
@ -158,7 +168,7 @@ func (self *MisskeyAdapter) poll() {
latest = &probenote[0].CreatedAt latest = &probenote[0].CreatedAt
break break
} }
self.data <- msg self.send(msg)
} }
if *latest == probenote[0].CreatedAt { if *latest == probenote[0].CreatedAt {
break break
@ -254,7 +264,7 @@ func (self *MisskeyAdapter) toAuthor(usr mkm.User, bustCache bool) *Author {
} }
if bustCache || !exists || (updated != nil && timestamp.Before(time.UnixMilli(*updated))) || timestamp.Before(*usr.CreatedAt) { if bustCache || !exists || (updated != nil && timestamp.Before(time.UnixMilli(*updated))) || timestamp.Before(*usr.CreatedAt) {
fmt.Println("converting author: " + usr.ID) log.Print("converting author: " + usr.ID)
if usr.UpdatedAt != nil { if usr.UpdatedAt != nil {
self.cache[authorId] = *usr.UpdatedAt self.cache[authorId] = *usr.UpdatedAt
} else { } else {
@ -291,7 +301,7 @@ func (self *MisskeyAdapter) Fetch(etype string, ids []string) error {
} else { } else {
msg := self.toMessage(data, true) msg := self.toMessage(data, true)
if msg != nil { if msg != nil {
self.data <- msg self.send(msg)
} }
} }
case "children": case "children":
@ -305,7 +315,7 @@ func (self *MisskeyAdapter) Fetch(etype string, ids []string) error {
for _, n := range data { for _, n := range data {
msg := self.toMessage(n, true) msg := self.toMessage(n, true)
if msg != nil { if msg != nil {
self.data <- msg self.send(msg)
} }
} }
} }
@ -320,7 +330,7 @@ func (self *MisskeyAdapter) Fetch(etype string, ids []string) error {
for _, n := range data { for _, n := range data {
msg := self.toMessage(n, true) msg := self.toMessage(n, true)
if msg != nil { if msg != nil {
self.data <- msg self.send(msg)
} }
} }
} }
@ -338,7 +348,6 @@ func (self *MisskeyAdapter) Fetch(etype string, ids []string) error {
hostPtr = &host hostPtr = &host
} }
// fmt.Printf("attempting user resolution: @%s@%s\n", user, host)
data, err := self.mk.Users().Show(users.ShowRequest{ data, err := self.mk.Users().Show(users.ShowRequest{
Username: &user, Username: &user,
Host: hostPtr, Host: hostPtr,
@ -348,7 +357,7 @@ func (self *MisskeyAdapter) Fetch(etype string, ids []string) error {
} else { } else {
a := self.toAuthor(data, false) a := self.toAuthor(data, false)
if a != nil { if a != nil {
self.data <- a self.send(a)
} }
} }

View file

@ -7,21 +7,31 @@ import (
"fmt" "fmt"
. "forge.lightcrystal.systems/lightcrystal/underbbs/models" . "forge.lightcrystal.systems/lightcrystal/underbbs/models"
nostr "github.com/nbd-wtf/go-nostr" nostr "github.com/nbd-wtf/go-nostr"
"log"
"os"
"strings" "strings"
) )
type NostrAdapter struct { type NostrAdapter struct {
data chan SocketData data *chan SocketData
nickname string nickname string
privkey string privkey string
relays []*nostr.Relay relays []*nostr.Relay
} }
func (self *NostrAdapter) send(data SocketData) {
if self.data != nil {
*self.data <- data
} else {
fmt.Fprintln(os.Stdout, string(data.ToDatagram()))
}
}
func (self *NostrAdapter) Name() string { func (self *NostrAdapter) Name() string {
return self.nickname return self.nickname
} }
func (self *NostrAdapter) Init(settings Settings, data chan SocketData) error { func (self *NostrAdapter) Init(settings Settings, data *chan SocketData) error {
self.nickname = settings.Nickname self.nickname = settings.Nickname
self.privkey = *settings.PrivKey self.privkey = *settings.PrivKey
self.data = data self.data = data
@ -47,35 +57,32 @@ func (self *NostrAdapter) Subscribe(filter string) []error {
errs := make([]error, 0) errs := make([]error, 0)
fmt.Print("unmarshalled filter from json; iterating through relays to subscribe..") log.Print("unmarshalled filter from json; iterating through relays to subscribe...")
for _, r := range self.relays { for _, r := range self.relays {
fmt.Print(".")
sub, err := r.Subscribe(context.Background(), filters) sub, err := r.Subscribe(context.Background(), filters)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} else { } else {
go func() { go func() {
for ev := range sub.Events { for ev := range sub.Events {
fmt.Print("!")
// try sequentially to encode into an underbbs object // try sequentially to encode into an underbbs object
// and send it to the appropriate channel // and send it to the appropriate channel
m, err := self.nostrEventToMsg(ev) m, err := self.nostrEventToMsg(ev)
if err == nil { if err == nil {
self.data <- m self.send(m)
} }
} }
}() }()
} }
fmt.Println()
} }
if len(errs) > 0 { if len(errs) > 0 {
fmt.Println("subscription operation completed with errors") log.Print("subscription operation completed with errors")
return errs return errs
} }
fmt.Println("subscription operation completed without errors") log.Print("subscription operation completed without errors")
return nil return nil
} }

View file

@ -10,6 +10,7 @@ import (
"hacklab.nilfm.cc/quartzgun/router" "hacklab.nilfm.cc/quartzgun/router"
"hacklab.nilfm.cc/quartzgun/util" "hacklab.nilfm.cc/quartzgun/util"
"html/template" "html/template"
"log"
"net/http" "net/http"
"strings" "strings"
) )
@ -33,16 +34,14 @@ func getSubscriberByKey(key string, subscribers map[*Subscriber][]adapter.Adapte
func setAdaptersForSubscriber(key string, adapters []adapter.Adapter, subscribers map[*Subscriber][]adapter.Adapter) error { func setAdaptersForSubscriber(key string, adapters []adapter.Adapter, subscribers map[*Subscriber][]adapter.Adapter) error {
var ptr *Subscriber = nil var ptr *Subscriber = nil
fmt.Print("looking for subscriber in map..") log.Print("looking for subscriber in map...")
for s, _ := range subscribers { for s, _ := range subscribers {
fmt.Print(".")
if s.key == key { if s.key == key {
ptr = s ptr = s
} }
} }
fmt.Println()
if ptr != nil { if ptr != nil {
fmt.Println("setting adaters for the found subscriber: " + ptr.key) log.Print("setting adaters for the found subscriber: " + ptr.key)
subscribers[ptr] = adapters subscribers[ptr] = adapters
return nil return nil
} }
@ -85,7 +84,7 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
break break
} }
err := a.Init(s, subscriber.data) err := a.Init(s, &subscriber.data)
if err != nil { if err != nil {
util.AddContextValue(req, "data", err.Error()) util.AddContextValue(req, "data", err.Error())
w.WriteHeader(500) w.WriteHeader(500)
@ -93,13 +92,13 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
return return
} }
fmt.Println("adapter initialized - subscribing with default filter") log.Print("adapter initialized - subscribing with default filter")
errs := a.Subscribe(a.DefaultSubscriptionFilter()) errs := a.Subscribe(a.DefaultSubscriptionFilter())
if errs != nil { if errs != nil {
errMsg := "" errMsg := ""
for _, e := range errs { for _, e := range errs {
fmt.Println("processing an error") log.Print("processing an error")
errMsg += fmt.Sprintf("- %s\n", e.Error()) errMsg += fmt.Sprintf("- %s\n", e.Error())
} }
util.AddContextValue(req, "data", errMsg) util.AddContextValue(req, "data", errMsg)
@ -108,10 +107,10 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
return return
} }
fmt.Println("adapter ready for use; adding to array") log.Print("adapter ready for use; adding to array")
adapters = append(adapters, a) adapters = append(adapters, a)
fmt.Println("adapter added to array") log.Print("adapter added to array")
} }
// TODO: cancel subscriptions on any existing adapters // TODO: cancel subscriptions on any existing adapters
@ -169,7 +168,7 @@ func apiAdapterFetch(next http.Handler, subscribers map[*Subscriber][]adapter.Ad
if a.Name() == apiParams["adapter_id"] { if a.Name() == apiParams["adapter_id"] {
err := a.Fetch(queryParams["entity_type"][0], queryParams["entity_id"]) err := a.Fetch(queryParams["entity_type"][0], queryParams["entity_id"])
if err != nil { if err != nil {
fmt.Println(err.Error()) log.Print(err.Error())
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} else { } else {
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)

View file

@ -3,7 +3,6 @@ package server
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"forge.lightcrystal.systems/lightcrystal/underbbs/adapter" "forge.lightcrystal.systems/lightcrystal/underbbs/adapter"
"forge.lightcrystal.systems/lightcrystal/underbbs/models" "forge.lightcrystal.systems/lightcrystal/underbbs/models"
"golang.org/x/time/rate" "golang.org/x/time/rate"
@ -90,14 +89,14 @@ func (self *BBSServer) subscribeHandler(w http.ResponseWriter, r *http.Request)
defer c.Close(websocket.StatusInternalError, "") defer c.Close(websocket.StatusInternalError, "")
go func() { go func() {
fmt.Println("waiting for data on the subscriber's channel") self.logf("waiting for data on the subscriber's channel")
for { for {
select { select {
case msg := <-s.msgs: case msg := <-s.msgs:
writeTimeout(ctx, time.Second*5, c, msg) writeTimeout(ctx, time.Second*5, c, msg)
case <-ctx.Done(): case <-ctx.Done():
fmt.Println("subscriber has disconnected") self.logf("subscriber has disconnected")
close(s.data) close(s.data)
return //ctx.Err() return //ctx.Err()
} }
@ -110,7 +109,7 @@ func (self *BBSServer) subscribeHandler(w http.ResponseWriter, r *http.Request)
// block on the data channel, serializing and passing the data to the subscriber // block on the data channel, serializing and passing the data to the subscriber
listen([]chan models.SocketData{s.data}, s.msgs) listen([]chan models.SocketData{s.data}, s.msgs)
fmt.Println("data listener is done!") self.logf("data listener is done!")
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return return

View file

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strconv" "strconv"
"time" "time"
@ -14,13 +15,31 @@ import (
) )
func main() { func main() {
err := run()
args := os.Args
var err error = nil
switch filepath.Base(args[0]) {
case "underbbs-cli":
err = run_cli(args[1:]...)
break
default:
err = run_srvr()
break
}
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func run() error { func run_cli(args ...string) error {
log.Print("test!!")
return nil
}
func run_srvr() error {
l, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(9090), 10)) l, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(9090), 10))
if err != nil { if err != nil {
return err return err