Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
20da6097ca | |||
36aa29dd95 | |||
1f37fda94f | |||
396b47a6f7 | |||
82e462a7f7 | |||
69594d485f | |||
1660da7f73 |
19 changed files with 221 additions and 360 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,5 +2,4 @@ node_modules/
|
||||||
frontend/dist/*.js
|
frontend/dist/*.js
|
||||||
frontend/.js
|
frontend/.js
|
||||||
underbbs
|
underbbs
|
||||||
underbbs-cli
|
|
||||||
__debug_*
|
__debug_*
|
11
README.md
11
README.md
|
@ -1,23 +1,16 @@
|
||||||
# underBBS
|
# underBBS
|
||||||
|
|
||||||
underBBS is a protocol-agnostic decentralized social media client and toolkit
|
underBBS is a platform-agnostic messaging and social media client (feat consultation and motivational support from
|
||||||
|
miggymofongo!!!)
|
||||||
|
|
||||||
## 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
|
||||||
|
|
|
@ -5,10 +5,10 @@ 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
|
||||||
Do(string, map[string]string) error
|
Do(string) error
|
||||||
DefaultSubscriptionFilter() string
|
DefaultSubscriptionFilter() string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
. "forge.lightcrystal.systems/lightcrystal/underbbs/models"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HonkAdapter struct {
|
|
||||||
data *chan SocketData
|
|
||||||
nickname string
|
|
||||||
server string
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
token string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *HonkAdapter) send(data SocketData) {
|
|
||||||
if self.data != nil {
|
|
||||||
*self.data <- data
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(os.Stdout, string(data.ToDatagram()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *HonkAdapter) Name() string {
|
|
||||||
return self.nickname
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *HonkAdapter) Init(settings Settings, data *chan SocketData) error {
|
|
||||||
// separate name and server in handle
|
|
||||||
parts := strings.Split(*settings.Handle, "@")
|
|
||||||
self.username = parts[1]
|
|
||||||
self.server = "https://" + parts[2]
|
|
||||||
self.password = *settings.Password
|
|
||||||
self.nickname = settings.Nickname
|
|
||||||
// store all the settings
|
|
||||||
// make a request to get the token
|
|
||||||
r, err := http.PostForm(self.server+"/dologin", url.Values{
|
|
||||||
"username": []string{self.username},
|
|
||||||
"password": []string{self.password},
|
|
||||||
"gettoken": []string{"1"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf [32]byte
|
|
||||||
_, err = r.Body.Read(buf[:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
self.token = string(buf[:])
|
|
||||||
fmt.Println(self.token)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *HonkAdapter) Subscribe(string) []error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *HonkAdapter) Fetch(etype string, ids []string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *HonkAdapter) Do(action string, data map[string]string) error {
|
|
||||||
switch action {
|
|
||||||
case "post":
|
|
||||||
res, err := http.PostForm(self.server+"/api", url.Values{
|
|
||||||
"action": []string{"honk"},
|
|
||||||
"token": []string{self.token},
|
|
||||||
"noise": []string{data["content"]},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var buf [256]byte
|
|
||||||
_, err = res.Body.Read(buf[:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(string(buf[:]))
|
|
||||||
default:
|
|
||||||
return errors.New("Do: unknown action")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *HonkAdapter) DefaultSubscriptionFilter() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -4,12 +4,10 @@ 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
|
||||||
|
@ -23,19 +21,11 @@ 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
|
||||||
|
@ -72,7 +62,7 @@ func (self *MastoAdapter) Subscribe(filter string) []error {
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
for e := range self.events {
|
for e := range self.events {
|
||||||
log.Printf("event: %s !!!", e.Event)
|
fmt.Println("event: %s !!!", e.Event)
|
||||||
switch e.Event {
|
switch e.Event {
|
||||||
case "error":
|
case "error":
|
||||||
case "update":
|
case "update":
|
||||||
|
@ -87,7 +77,7 @@ func (self *MastoAdapter) Subscribe(filter string) []error {
|
||||||
msg = self.mastoUpdateToMessage(v)
|
msg = self.mastoUpdateToMessage(v)
|
||||||
}
|
}
|
||||||
if msg != nil {
|
if msg != nil {
|
||||||
self.send(msg)
|
self.data <- msg
|
||||||
}
|
}
|
||||||
case "notification":
|
case "notification":
|
||||||
case "delete":
|
case "delete":
|
||||||
|
@ -104,7 +94,7 @@ func (self *MastoAdapter) Fetch(etype string, ids []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *MastoAdapter) Do(action string, data map[string]string) error {
|
func (self *MastoAdapter) Do(action string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,13 @@ 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
|
||||||
|
@ -33,27 +31,19 @@ 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 {
|
||||||
log.Print("initializing misskey adapter")
|
fmt.Println("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
|
||||||
|
|
||||||
log.Print("getting ready to initialize internal client")
|
fmt.Println("getting ready to initialize internal client")
|
||||||
|
|
||||||
client, err := misskey.NewClientWithOptions(
|
client, err := misskey.NewClientWithOptions(
|
||||||
misskey.WithAPIToken(self.apiKey),
|
misskey.WithAPIToken(self.apiKey),
|
||||||
|
@ -61,10 +51,10 @@ func (self *MisskeyAdapter) Init(settings Settings, data *chan SocketData) error
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err.Error())
|
fmt.Println(err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Print("misskey client initialized")
|
fmt.Println("misskey client initialized")
|
||||||
self.mk = client
|
self.mk = client
|
||||||
|
|
||||||
self.cache = make(map[string]time.Time)
|
self.cache = make(map[string]time.Time)
|
||||||
|
@ -129,10 +119,10 @@ func (self *MisskeyAdapter) poll() {
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err.Error())
|
fmt.Println(err.Error())
|
||||||
}
|
}
|
||||||
if merr != nil {
|
if merr != nil {
|
||||||
log.Print(merr.Error())
|
fmt.Println(merr.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the cache for everything we just collected
|
// check the cache for everything we just collected
|
||||||
|
@ -141,13 +131,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.send(msg)
|
self.data <- msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, n := range mentions {
|
for _, n := range mentions {
|
||||||
msg := self.toMessageIfNew(n)
|
msg := self.toMessageIfNew(n)
|
||||||
if msg != nil {
|
if msg != nil {
|
||||||
self.send(msg)
|
self.data <- msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +150,7 @@ func (self *MisskeyAdapter) poll() {
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err.Error())
|
fmt.Println(err.Error())
|
||||||
}
|
}
|
||||||
for _, n := range notes {
|
for _, n := range notes {
|
||||||
msg := self.toMessageIfNew(n)
|
msg := self.toMessageIfNew(n)
|
||||||
|
@ -168,7 +158,7 @@ func (self *MisskeyAdapter) poll() {
|
||||||
latest = &probenote[0].CreatedAt
|
latest = &probenote[0].CreatedAt
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
self.send(msg)
|
self.data <- msg
|
||||||
}
|
}
|
||||||
if *latest == probenote[0].CreatedAt {
|
if *latest == probenote[0].CreatedAt {
|
||||||
break
|
break
|
||||||
|
@ -226,7 +216,6 @@ func (self *MisskeyAdapter) toMessage(n mkm.Note, bustCache bool) *Message {
|
||||||
ReplyTo: n.ReplyID,
|
ReplyTo: n.ReplyID,
|
||||||
ReplyCount: int(n.RepliesCount),
|
ReplyCount: int(n.RepliesCount),
|
||||||
Replies: []string{},
|
Replies: []string{},
|
||||||
RenoteId: (*string)(n.RenoteID),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range n.Files {
|
for _, f := range n.Files {
|
||||||
|
@ -264,7 +253,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) {
|
||||||
log.Print("converting author: " + usr.ID)
|
fmt.Println("converting author: " + usr.ID)
|
||||||
if usr.UpdatedAt != nil {
|
if usr.UpdatedAt != nil {
|
||||||
self.cache[authorId] = *usr.UpdatedAt
|
self.cache[authorId] = *usr.UpdatedAt
|
||||||
} else {
|
} else {
|
||||||
|
@ -294,8 +283,6 @@ func (self *MisskeyAdapter) toAuthor(usr mkm.User, bustCache bool) *Author {
|
||||||
func (self *MisskeyAdapter) Fetch(etype string, ids []string) error {
|
func (self *MisskeyAdapter) Fetch(etype string, ids []string) error {
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
switch etype {
|
switch etype {
|
||||||
case "byAuthor":
|
|
||||||
// fetch notes by this author
|
|
||||||
case "message":
|
case "message":
|
||||||
data, err := self.mk.Notes().Show(id)
|
data, err := self.mk.Notes().Show(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -303,7 +290,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.send(msg)
|
self.data <- msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "children":
|
case "children":
|
||||||
|
@ -317,7 +304,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.send(msg)
|
self.data <- msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,7 +319,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.send(msg)
|
self.data <- msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -350,6 +337,7 @@ 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,
|
||||||
|
@ -359,16 +347,16 @@ 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.send(a)
|
self.data <- a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *MisskeyAdapter) Do(action string, data map[string]string) error {
|
func (self *MisskeyAdapter) Do(action string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,31 +7,21 @@ 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
|
||||||
|
@ -57,32 +47,35 @@ func (self *NostrAdapter) Subscribe(filter string) []error {
|
||||||
|
|
||||||
errs := make([]error, 0)
|
errs := make([]error, 0)
|
||||||
|
|
||||||
log.Print("unmarshalled filter from json; iterating through relays to subscribe...")
|
fmt.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.send(m)
|
self.data <- m
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
log.Print("subscription operation completed with errors")
|
fmt.Println("subscription operation completed with errors")
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
log.Print("subscription operation completed without errors")
|
fmt.Println("subscription operation completed without errors")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +83,7 @@ func (self *NostrAdapter) Fetch(etype string, ids []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *NostrAdapter) Do(action string, data map[string]string) error {
|
func (self *NostrAdapter) Do(action string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
116
cli/cli.go
116
cli/cli.go
|
@ -1,116 +0,0 @@
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"forge.lightcrystal.systems/lightcrystal/underbbs/adapter"
|
|
||||||
"forge.lightcrystal.systems/lightcrystal/underbbs/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetConfigLocation() string {
|
|
||||||
home := os.Getenv("HOME")
|
|
||||||
appdata := os.Getenv("APPDATA")
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
return filepath.Join(appdata, "underbbs")
|
|
||||||
case "darwin":
|
|
||||||
return filepath.Join(home, "Library", "Application Support", "underbbs")
|
|
||||||
case "plan9":
|
|
||||||
return filepath.Join(home, "lib", "underbbs")
|
|
||||||
default:
|
|
||||||
return filepath.Join(home, ".config", "underbbs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnsureConfigLocationExists() {
|
|
||||||
fileInfo, err := os.Stat(GetConfigLocation())
|
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
os.MkdirAll(GetConfigLocation(), os.ModePerm)
|
|
||||||
} else if !fileInfo.IsDir() {
|
|
||||||
panic("Config location is not a directory!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Process(args ...string) error {
|
|
||||||
// allocate storage for the settings array
|
|
||||||
var settings []models.Settings
|
|
||||||
var s *models.Settings
|
|
||||||
|
|
||||||
if len(args) < 3 {
|
|
||||||
return errors.New("CLI requires at least 3 args: ADAPTER ACTION DATA...")
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureConfigLocationExists()
|
|
||||||
cfgdir := GetConfigLocation()
|
|
||||||
|
|
||||||
// get adapter from first arg
|
|
||||||
adapterName := args[0]
|
|
||||||
args = args[1:]
|
|
||||||
|
|
||||||
// get config from config fle based on adapter
|
|
||||||
content, err := ioutil.ReadFile(filepath.Join(cfgdir, "config.json"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(content, &settings)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, x := range settings {
|
|
||||||
if x.Nickname == adapterName {
|
|
||||||
s = &x
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s == nil {
|
|
||||||
return errors.New("given adapter " + adapterName + " is not in the config file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// instantiate adapter with config
|
|
||||||
var a adapter.Adapter
|
|
||||||
switch s.Protocol {
|
|
||||||
case "nostr":
|
|
||||||
a = &adapter.NostrAdapter{}
|
|
||||||
case "mastodon":
|
|
||||||
a = &adapter.MastoAdapter{}
|
|
||||||
case "misskey":
|
|
||||||
a = &adapter.MisskeyAdapter{}
|
|
||||||
case "honk":
|
|
||||||
a = &adapter.HonkAdapter{}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
a.Init(*s, nil)
|
|
||||||
// process remaining args and execute
|
|
||||||
|
|
||||||
switch args[0] {
|
|
||||||
case "fetch":
|
|
||||||
a.Fetch(args[1], args[2:])
|
|
||||||
case "do":
|
|
||||||
data := map[string]string{}
|
|
||||||
for _, a := range args[2:] {
|
|
||||||
if !strings.Contains(a, "=") {
|
|
||||||
return errors.New("args are in the form KEY=VALUE")
|
|
||||||
} else {
|
|
||||||
aa := strings.Split(a, "=")
|
|
||||||
k := aa[0]
|
|
||||||
v := strings.Join(aa[1:], "=")
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a.Do(args[1], data)
|
|
||||||
default:
|
|
||||||
log.Print(args)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -24,7 +24,6 @@ export class AdapterElement extends HTMLElement {
|
||||||
// TODO: use visibility of the thread to organize into DMs and public threads
|
// TODO: use visibility of the thread to organize into DMs and public threads
|
||||||
private _threads: MessageThread[] = [];
|
private _threads: MessageThread[] = [];
|
||||||
private _orphans: Message[] = [];
|
private _orphans: Message[] = [];
|
||||||
private _boosts: Message[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -103,7 +102,6 @@ export class AdapterElement extends HTMLElement {
|
||||||
tse.setAttribute("data-author", this._latest);
|
tse.setAttribute("data-author", this._latest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// also update any boosts by this author
|
|
||||||
case "thread":
|
case "thread":
|
||||||
case "profile":
|
case "profile":
|
||||||
break;
|
break;
|
||||||
|
@ -115,7 +113,7 @@ export class AdapterElement extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
setIdxView() {
|
setIdxView() {
|
||||||
this.innerHTML = "<ul id='boost_carousel'></ul><ul id='dm_list'></ul><ul id='public_list'></ul>"
|
this.innerHTML = "<ul id='dm_list'></ul><ul id='public_list'></ul>"
|
||||||
}
|
}
|
||||||
|
|
||||||
setThreadView() {
|
setThreadView() {
|
||||||
|
@ -134,8 +132,6 @@ export class AdapterElement extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
populateIdxView() {
|
populateIdxView() {
|
||||||
// populate boost carousel
|
|
||||||
|
|
||||||
// skip dm list for now
|
// skip dm list for now
|
||||||
// public/unified list
|
// public/unified list
|
||||||
const pl = util.$("public_list");
|
const pl = util.$("public_list");
|
||||||
|
@ -153,12 +149,11 @@ export class AdapterElement extends HTMLElement {
|
||||||
const existingThread = document.querySelector(threadSelector);
|
const existingThread = document.querySelector(threadSelector);
|
||||||
const thread = this._threads.find(t=>t.root.data.id == rootId);
|
const thread = this._threads.find(t=>t.root.data.id == rootId);
|
||||||
if (existingThread && thread) {
|
if (existingThread && thread) {
|
||||||
|
debugger;
|
||||||
existingThread.setAttribute("data-latest", `${thread.latest}`);
|
existingThread.setAttribute("data-latest", `${thread.latest}`);
|
||||||
existingThread.setAttribute("data-len", `${thread.messageCount}`);
|
existingThread.setAttribute("data-len", `${thread.messageCount}`);
|
||||||
existingThread.setAttribute("data-new", "true");
|
existingThread.setAttribute("data-new", "true");
|
||||||
} else {
|
} else {
|
||||||
// if latest is a boost, put it in the carousel
|
|
||||||
|
|
||||||
// unified/public list for now
|
// unified/public list for now
|
||||||
const pl = util.$("public_list");
|
const pl = util.$("public_list");
|
||||||
if (pl && thread) {
|
if (pl && thread) {
|
||||||
|
@ -195,14 +190,14 @@ export class AdapterElement extends HTMLElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// make multiple passes over the store until every message is either
|
// make multiple passes over the store until every message is either
|
||||||
// placed in a thread, the boost carousel, or orphaned and waiting for its parent to be returned
|
// placed in a thread, or orphaned and waiting for its parent to be returned
|
||||||
do{
|
do{
|
||||||
for (let k of datastore.messages.keys()) {
|
for (let k of datastore.messages.keys()) {
|
||||||
this.placeMsg(k);
|
this.placeMsg(k);
|
||||||
}
|
}
|
||||||
} while (this._threads.reduce((sum: number, thread: MessageThread)=>{
|
} while (this._threads.reduce((sum: number, thread: MessageThread)=>{
|
||||||
return sum + thread.messageCount;
|
return sum + thread.messageCount;
|
||||||
}, 0) + this._boosts.length + this._orphans.length < datastore.messages.size);
|
}, 0) + this._orphans.length < datastore.messages.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
placeMsg(k: string): string | null {
|
placeMsg(k: string): string | null {
|
||||||
|
@ -216,20 +211,11 @@ export class AdapterElement extends HTMLElement {
|
||||||
util.errMsg(`message [${this._name}:${k}] doesn't exist`);
|
util.errMsg(`message [${this._name}:${k}] doesn't exist`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (msg.renoteId) {
|
|
||||||
// fetch the referent thread and put the boost in the carousel
|
|
||||||
this._convoyBatchTimer.queue(msg.renoteId, 2000);
|
|
||||||
if (!this._boosts.some(m=>m.id == msg.id)) {
|
|
||||||
this._boosts.push(msg);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (let t of this._threads) {
|
for (let t of this._threads) {
|
||||||
// avoid processing nodes again on subsequent passes
|
// avoid processing nodes again on subsequent passes
|
||||||
if (!msg || t.findNode(t.root, msg.id)) {
|
if (!msg || t.findNode(t.root, msg.id)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.replyTo) {
|
if (msg.replyTo) {
|
||||||
let x = t.addReply(msg.replyTo, msg);
|
let x = t.addReply(msg.replyTo, msg);
|
||||||
if (x) {
|
if (x) {
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
export class BoostTileElement extends HTMLElement {
|
|
||||||
|
|
||||||
static observedAttributes = [ "data-boostid", "data-msgid", "data-author", "data-booster" ];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.innerHTML = "<div class='boost_booster'></div><div class='boost_author'></div><div class='boost_content'></div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,6 @@ export class Message {
|
||||||
public created: number = 0;
|
public created: number = 0;
|
||||||
public edited: number | null = null;
|
public edited: number | null = null;
|
||||||
public visibility: string = "public";
|
public visibility: string = "public";
|
||||||
public renoteId: string | null = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Author {
|
export class Author {
|
||||||
|
|
14
frontend/ts/profile-element.ts
Normal file
14
frontend/ts/profile-element.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export class ProfileView extends HTMLElement {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
const prof_view = document.createElement("span");
|
||||||
|
|
||||||
|
prof_view.setAttribute("class", "wrapper")
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.
|
||||||
|
}
|
||||||
|
}
|
144
frontend/ts/profile.js
Normal file
144
frontend/ts/profile.js
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
/* WEPA this is miggymofongo's contribution to underBBS! I'm practicing web components
|
||||||
|
in this bishhh
|
||||||
|
|
||||||
|
this template contains the html code of the side bar
|
||||||
|
*/
|
||||||
|
const template = document.createElement("template")
|
||||||
|
|
||||||
|
template.innerHTML = `
|
||||||
|
<style>
|
||||||
|
|
||||||
|
/* side profile view menu */
|
||||||
|
.sidenav {
|
||||||
|
height: 50%;
|
||||||
|
width: 0;
|
||||||
|
position: fixed; /* Stay in place */
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #888;
|
||||||
|
overflow-x: hidden; /* Disable horizontal scroll */
|
||||||
|
padding-top: 60px; /* Place content 60px from the top */
|
||||||
|
transition: width 0.5s ease; /* smooth transition */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*nav menu links */
|
||||||
|
.sidenav a {
|
||||||
|
padding: 8px 8px 8px 32px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 25px;
|
||||||
|
color: #818181;
|
||||||
|
display: block;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* change their color when you hover the mouse */
|
||||||
|
.sidenav a:hover {
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* style the close button (puts in the top right corner) */
|
||||||
|
.sidenav .closeBtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 25px;
|
||||||
|
font-size: 36px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style page content - use this if you want to push the page content to the right when you open the side navigation */
|
||||||
|
#main {
|
||||||
|
transition: margin-left .5s ease;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* change the style of the sidenav on smaller screens where height is less than 450px,
|
||||||
|
(less padding and a smaller font size) */
|
||||||
|
|
||||||
|
@media screen and (max-height: 450px) {
|
||||||
|
.sidenav {padding-top: 15px;}
|
||||||
|
.sidenav a {font-size: 18px;}
|
||||||
|
}
|
||||||
|
/* the div block has an id and class so that its targeted by the styles
|
||||||
|
and the component underneath */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="mySidenav" class="sidenav">
|
||||||
|
<div id="profile">
|
||||||
|
|
||||||
|
<img src="Kaido.png" alt="profile pic" style="width:100px;height100px"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<button href="" class="closeBtn">×</button>
|
||||||
|
<a href=""><button>Message</button></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="openBtn">open profile view</button>
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
/* setting up a web component will extend the set of available HTML elements for you to use in your
|
||||||
|
project and keep your codebase organized and modular. Set up a web component in a separate file
|
||||||
|
and link it directly in your html document head.
|
||||||
|
|
||||||
|
creating a custom web component is just like creating a class. after setting up
|
||||||
|
the constructor function, you need to create some methods that tell the browser more about it.
|
||||||
|
you can add whatever methods you want, but they at least need some of the following:
|
||||||
|
|
||||||
|
1) connectedCallback() => call this method when the element is added to the document,
|
||||||
|
2) disconnectedCallback() => call this method when the element is removed from the document
|
||||||
|
3) static get observedAttributes() => this returns an array of attribute names to track for changes
|
||||||
|
4) attributeChangedCallback(name, oldValue, newValue) => this one is called when one of the attributes
|
||||||
|
5) adoptedCallback() => called when the element is moved to a new document
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileSideBar extends HTMLElement {
|
||||||
|
//
|
||||||
|
constructor() { // the constructor function initiates the new side bar
|
||||||
|
super()
|
||||||
|
this.appendChild(template.content.cloneNode(true))
|
||||||
|
this.sidenav = this.querySelector("#mySidenav");
|
||||||
|
this.openBtn = this.querySelector("#openBtn");
|
||||||
|
this.closeBtn = this.querySelector(".closeBtn");
|
||||||
|
this.mainContent = document.getElementById("main");
|
||||||
|
|
||||||
|
}
|
||||||
|
/* this method adds event listeners to the openBtn and closeBtn classes that
|
||||||
|
call the openNav closeNav function to open and close the sidebar*/
|
||||||
|
connectedCallback() {
|
||||||
|
|
||||||
|
this.openBtn.addEventListener("click", this.openNav.bind(this));
|
||||||
|
this.closeBtn.addEventListener("click", this.closeNav.bind(this));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.openBtn.removeEventListener("click", this.openNav.bind(this));
|
||||||
|
this.closeBtn.removeEventListener("click", this.closeNav.bind(this));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* these two methods will open and close the profile side menu*/
|
||||||
|
openNav() {
|
||||||
|
this.sidenav.style.width = "450px"; // changes sidenav width from 0px to 450px
|
||||||
|
if (this.mainContent) {
|
||||||
|
this.mainContent.style.marginLeft = "250px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeNav() {
|
||||||
|
this.sidenav.style.width = "0"; //changes sidenav's width back to 0px
|
||||||
|
if (this.mainContent) {
|
||||||
|
this.mainContent.style.marginLeft = "0px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*lastly, we need to define the element tag itself and attach it to the webcomponent */
|
||||||
|
customElements.define("profile-side-bar", ProfileSideBar)
|
|
@ -58,12 +58,13 @@ export class DatagramSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
static connect(): void {
|
static connect(): void {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const wsProto = location.protocol == "https:" ? "wss" : "ws";
|
const wsProto = location.protocol == "https:" ? "wss" : "ws";
|
||||||
const _conn = new WebSocket(`${wsProto}://${location.host}/subscribe`, "underbbs");
|
const _conn = new WebSocket(`${wsProto}://${location.host}/subscribe`, "underbbs");
|
||||||
|
|
||||||
_conn.addEventListener("open", DatagramSocket.onOpen);
|
_conn.addEventListener("open", DatagramSocket.onOpen);
|
||||||
_conn.addEventListener("message", DatagramSocket.onMsg);
|
_conn.addEventListener("message", DatagramSocket.onMsg);
|
||||||
|
|
||||||
_conn.addEventListener("error", (e: any) => {
|
_conn.addEventListener("error", (e: any) => {
|
||||||
console.log("websocket connection error");
|
console.log("websocket connection error");
|
||||||
console.log(JSON.stringify(e));
|
console.log(JSON.stringify(e));
|
||||||
|
|
|
@ -25,7 +25,6 @@ type Message struct {
|
||||||
ReplyCount int `json:"replyCount"`
|
ReplyCount int `json:"replyCount"`
|
||||||
Mentions []string `json:"mentions"`
|
Mentions []string `json:"mentions"`
|
||||||
Visibility string `json:"visibility"`
|
Visibility string `json:"visibility"`
|
||||||
RenoteId *string `json:"renoteId,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Author struct {
|
type Author struct {
|
||||||
|
|
|
@ -7,6 +7,4 @@ type Settings struct {
|
||||||
Relays []string `json:"relays",omitempty`
|
Relays []string `json:"relays",omitempty`
|
||||||
Server *string `json:"server",omitempty`
|
Server *string `json:"server",omitempty`
|
||||||
ApiKey *string `json:"apiKey",omitempty`
|
ApiKey *string `json:"apiKey",omitempty`
|
||||||
Handle *string `json:"handle",omitempty`
|
|
||||||
Password *string `json:"password",omitempty`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
@ -34,14 +33,16 @@ 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
|
||||||
log.Print("looking for subscriber in map...")
|
fmt.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 {
|
||||||
log.Print("setting adaters for the found subscriber: " + ptr.key)
|
fmt.Println("setting adaters for the found subscriber: " + ptr.key)
|
||||||
subscribers[ptr] = adapters
|
subscribers[ptr] = adapters
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -84,7 +85,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)
|
||||||
|
@ -92,13 +93,13 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("adapter initialized - subscribing with default filter")
|
fmt.Println("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 {
|
||||||
log.Print("processing an error")
|
fmt.Println("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)
|
||||||
|
@ -107,10 +108,10 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("adapter ready for use; adding to array")
|
fmt.Println("adapter ready for use; adding to array")
|
||||||
|
|
||||||
adapters = append(adapters, a)
|
adapters = append(adapters, a)
|
||||||
log.Print("adapter added to array")
|
fmt.Println("adapter added to array")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: cancel subscriptions on any existing adapters
|
// TODO: cancel subscriptions on any existing adapters
|
||||||
|
@ -168,7 +169,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 {
|
||||||
log.Print(err.Error())
|
fmt.Println(err.Error())
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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"
|
||||||
|
@ -89,14 +90,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() {
|
||||||
self.logf("waiting for data on the subscriber's channel")
|
fmt.Println("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():
|
||||||
self.logf("subscriber has disconnected")
|
fmt.Println("subscriber has disconnected")
|
||||||
close(s.data)
|
close(s.data)
|
||||||
return //ctx.Err()
|
return //ctx.Err()
|
||||||
}
|
}
|
||||||
|
@ -109,7 +110,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)
|
||||||
|
|
||||||
self.logf("data listener is done!")
|
fmt.Println("data listener is done!")
|
||||||
|
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return
|
return
|
||||||
|
|
25
underbbs.go
25
underbbs.go
|
@ -2,44 +2,25 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.lightcrystal.systems/lightcrystal/underbbs/cli"
|
|
||||||
"forge.lightcrystal.systems/lightcrystal/underbbs/server"
|
"forge.lightcrystal.systems/lightcrystal/underbbs/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
err := run()
|
||||||
args := os.Args
|
|
||||||
|
|
||||||
var err error = nil
|
|
||||||
|
|
||||||
progname := filepath.Base(args[0])
|
|
||||||
switch progname {
|
|
||||||
case "underbbs-cli":
|
|
||||||
err = run_cli(args[1:]...)
|
|
||||||
default:
|
|
||||||
err = run_srvr()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run_cli(args ...string) error {
|
func run() error {
|
||||||
return cli.Process(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
|
Loading…
Reference in a new issue