Compare commits

..

7 commits

19 changed files with 221 additions and 360 deletions

1
.gitignore vendored
View file

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

View file

@ -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

View file

@ -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
} }

View file

@ -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 ""
}

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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
}

View file

@ -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) {

View file

@ -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) {
}
}

View file

@ -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 {

View 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
View 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">&times;</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)

View file

@ -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));

View file

@ -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 {

View file

@ -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`
} }

View file

@ -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)

View file

@ -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

View file

@ -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