2024-05-26 18:50:19 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2025-01-05 03:45:22 +00:00
|
|
|
"bytes"
|
|
|
|
"crypto"
|
|
|
|
"crypto/x509"
|
2024-12-09 17:35:44 +00:00
|
|
|
"encoding/base64"
|
2024-05-26 23:24:49 +00:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2024-06-02 16:17:12 +00:00
|
|
|
"fmt"
|
2024-11-28 23:51:12 +00:00
|
|
|
"forge.lightcrystal.systems/nilix/quartzgun/renderer"
|
|
|
|
"forge.lightcrystal.systems/nilix/quartzgun/router"
|
|
|
|
"forge.lightcrystal.systems/nilix/quartzgun/util"
|
2024-12-07 02:30:41 +00:00
|
|
|
"forge.lightcrystal.systems/nilix/underbbs/adapter"
|
|
|
|
"forge.lightcrystal.systems/nilix/underbbs/models"
|
2024-05-26 18:50:19 +00:00
|
|
|
"html/template"
|
2024-09-28 17:39:03 +00:00
|
|
|
"log"
|
2024-05-26 18:50:19 +00:00
|
|
|
"net/http"
|
2024-05-26 23:24:49 +00:00
|
|
|
"strings"
|
2024-05-26 18:50:19 +00:00
|
|
|
)
|
|
|
|
|
2025-01-05 03:45:22 +00:00
|
|
|
type PrivKeyAux interface {
|
|
|
|
Public() crypto.PublicKey
|
|
|
|
Equal(x crypto.PrivateKey) bool
|
|
|
|
}
|
|
|
|
|
2024-12-09 17:35:44 +00:00
|
|
|
type doRequest struct {
|
|
|
|
Action string `json:"action"`
|
|
|
|
Content *string `json:"content,omitempty"`
|
|
|
|
File *string `json:"file,omitempty"`
|
|
|
|
Desc *string `json:"desc,omitempty"`
|
|
|
|
}
|
|
|
|
|
2025-01-05 03:45:22 +00:00
|
|
|
func getSubscriberKey(req *http.Request) string {
|
|
|
|
return req.Header.Get("X-Underbbs-Subscriber")
|
2024-05-26 23:24:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func getSubscriberByKey(key string, subscribers map[*Subscriber][]adapter.Adapter) *Subscriber {
|
|
|
|
for s, _ := range subscribers {
|
|
|
|
if s.key == key {
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func setAdaptersForSubscriber(key string, adapters []adapter.Adapter, subscribers map[*Subscriber][]adapter.Adapter) error {
|
|
|
|
var ptr *Subscriber = nil
|
|
|
|
for s, _ := range subscribers {
|
|
|
|
if s.key == key {
|
|
|
|
ptr = s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ptr != nil {
|
|
|
|
subscribers[ptr] = adapters
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return errors.New("subscriber not present in map")
|
|
|
|
}
|
|
|
|
|
2025-01-05 03:45:22 +00:00
|
|
|
func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter, apKey *crypto.PrivateKey) http.Handler {
|
2024-05-26 18:50:19 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
2025-01-05 03:45:22 +00:00
|
|
|
skey := getSubscriberKey(req)
|
2024-05-26 23:24:49 +00:00
|
|
|
subscriber := getSubscriberByKey(skey, subscribers)
|
|
|
|
if subscriber == nil {
|
|
|
|
w.WriteHeader(404)
|
|
|
|
return
|
|
|
|
}
|
2025-01-05 03:45:22 +00:00
|
|
|
|
2024-05-26 23:24:49 +00:00
|
|
|
// decode adapter config from request body
|
|
|
|
settings := make([]models.Settings, 0)
|
2025-01-05 03:45:22 +00:00
|
|
|
err := json.NewDecoder(req.Body).Decode(&settings)
|
2024-05-26 23:24:49 +00:00
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(400)
|
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// iterate through settings and create adapters
|
|
|
|
adapters := make([]adapter.Adapter, 0)
|
|
|
|
for _, s := range settings {
|
|
|
|
var a adapter.Adapter
|
|
|
|
switch s.Protocol {
|
|
|
|
case "nostr":
|
|
|
|
a = &adapter.NostrAdapter{}
|
2024-06-08 20:40:14 +00:00
|
|
|
case "mastodon":
|
|
|
|
a = &adapter.MastoAdapter{}
|
2025-01-05 03:45:22 +00:00
|
|
|
s.ApSigner = apKey
|
2024-06-23 04:02:15 +00:00
|
|
|
case "misskey":
|
|
|
|
a = &adapter.MisskeyAdapter{}
|
2025-01-05 03:45:22 +00:00
|
|
|
s.ApSigner = apKey
|
2024-12-01 21:08:02 +00:00
|
|
|
case "honk":
|
|
|
|
a = &adapter.HonkAdapter{}
|
2025-01-05 03:45:22 +00:00
|
|
|
s.ApSigner = apKey
|
2024-05-26 23:24:49 +00:00
|
|
|
default:
|
|
|
|
break
|
|
|
|
|
|
|
|
}
|
2024-09-28 17:39:03 +00:00
|
|
|
err := a.Init(s, &subscriber.data)
|
2024-05-26 23:24:49 +00:00
|
|
|
if err != nil {
|
|
|
|
util.AddContextValue(req, "data", err.Error())
|
|
|
|
w.WriteHeader(500)
|
|
|
|
next.ServeHTTP(w, req)
|
2024-06-02 16:17:12 +00:00
|
|
|
return
|
2024-05-26 23:24:49 +00:00
|
|
|
}
|
|
|
|
|
2024-12-06 04:05:01 +00:00
|
|
|
log.Print("adapter initialized; adding to array")
|
2024-06-02 16:17:12 +00:00
|
|
|
|
2024-05-26 23:24:49 +00:00
|
|
|
adapters = append(adapters, a)
|
2024-09-28 17:39:03 +00:00
|
|
|
log.Print("adapter added to array")
|
2024-05-26 23:24:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: cancel subscriptions on any existing adapters
|
|
|
|
// store the adapters in the subscriber map
|
|
|
|
err = setAdaptersForSubscriber(skey, adapters, subscribers)
|
|
|
|
if err != nil {
|
|
|
|
util.AddContextValue(req, "data", err.Error())
|
|
|
|
w.WriteHeader(500)
|
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
}
|
|
|
|
|
2024-05-26 18:50:19 +00:00
|
|
|
w.WriteHeader(201)
|
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-12-06 04:05:01 +00:00
|
|
|
type subscribeParams struct {
|
|
|
|
Filter string `json:"filter"`
|
|
|
|
Target *string `json:"target,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler {
|
2024-05-26 18:50:19 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
2025-01-05 03:45:22 +00:00
|
|
|
skey := getSubscriberKey(req)
|
2024-12-06 04:05:01 +00:00
|
|
|
subscriber := getSubscriberByKey(skey, subscribers)
|
|
|
|
if subscriber == nil {
|
|
|
|
w.WriteHeader(404)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
adapters := subscribers[subscriber]
|
|
|
|
|
|
|
|
urlParams := req.Context().Value("params").(map[string]string)
|
|
|
|
adapter := urlParams["adapter_id"]
|
|
|
|
sp := subscribeParams{}
|
2025-01-05 03:45:22 +00:00
|
|
|
err := json.NewDecoder(req.Body).Decode(&sp)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(500)
|
|
|
|
fmt.Println(err.Error())
|
|
|
|
}
|
2024-12-06 04:05:01 +00:00
|
|
|
for _, a := range adapters {
|
|
|
|
if a.Name() == adapter {
|
|
|
|
fmt.Printf("adapter.subscribe call: %s {%s, %s}\n", adapter, sp.Filter, *sp.Target)
|
|
|
|
a.Subscribe(sp.Filter, sp.Target)
|
|
|
|
|
|
|
|
w.WriteHeader(201)
|
|
|
|
|
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w.WriteHeader(404)
|
2024-05-26 18:50:19 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-05-26 23:24:49 +00:00
|
|
|
func ProtectWithSubscriberKey(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler {
|
2024-05-26 18:50:19 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
2025-01-05 03:45:22 +00:00
|
|
|
skey := getSubscriberKey(req)
|
|
|
|
if getSubscriberByKey(skey, subscribers) != nil {
|
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
return
|
2024-05-26 23:24:49 +00:00
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
2024-05-26 18:50:19 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-07-07 03:13:18 +00:00
|
|
|
func apiAdapterFetch(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler {
|
2024-07-07 20:54:33 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
2025-01-05 03:45:22 +00:00
|
|
|
skey := getSubscriberKey(req)
|
|
|
|
s := getSubscriberByKey(skey, subscribers)
|
|
|
|
if s != nil {
|
|
|
|
apiParams := req.Context().Value("params").(map[string]string)
|
|
|
|
queryParams := req.URL.Query()
|
|
|
|
for _, a := range subscribers[s] {
|
|
|
|
if a.Name() == apiParams["adapter_id"] {
|
|
|
|
err := a.Fetch(queryParams["entity_type"][0], queryParams["entity_id"])
|
|
|
|
if err != nil {
|
|
|
|
log.Print(err.Error())
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
} else {
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
2024-07-07 20:54:33 +00:00
|
|
|
}
|
2025-01-05 03:45:22 +00:00
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
return
|
2024-07-07 20:54:33 +00:00
|
|
|
}
|
2024-07-07 03:13:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-12-09 17:35:44 +00:00
|
|
|
func apiAdapterDo(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
2025-01-05 03:45:22 +00:00
|
|
|
skey := getSubscriberKey(req)
|
|
|
|
s := getSubscriberByKey(skey, subscribers)
|
|
|
|
if s != nil {
|
|
|
|
apiParams := req.Context().Value("params").(map[string]string)
|
|
|
|
for _, a := range subscribers[s] {
|
|
|
|
if a.Name() == apiParams["adapter_id"] {
|
|
|
|
// request body is json
|
|
|
|
// if we have a `file`, it needs to be transformed from base64 to standard bytes/string
|
|
|
|
doReq := map[string]string{}
|
|
|
|
err := json.NewDecoder(req.Body).Decode(&doReq)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(422)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if f, exists := doReq["file"]; exists {
|
|
|
|
firstCommaIdx := strings.Index(f, ",")
|
|
|
|
rawFile, err := base64.StdEncoding.DecodeString(f[firstCommaIdx+1:])
|
2024-12-09 17:35:44 +00:00
|
|
|
if err != nil {
|
2025-01-05 03:45:22 +00:00
|
|
|
w.WriteHeader(500)
|
2024-12-10 04:18:08 +00:00
|
|
|
return
|
|
|
|
}
|
2025-01-05 03:45:22 +00:00
|
|
|
doReq["file"] = string(rawFile)
|
|
|
|
}
|
|
|
|
action, ok := doReq["action"]
|
|
|
|
if ok {
|
|
|
|
delete(doReq, "action")
|
|
|
|
} else {
|
|
|
|
w.WriteHeader(422)
|
2024-12-09 17:35:44 +00:00
|
|
|
return
|
|
|
|
}
|
2025-01-05 03:45:22 +00:00
|
|
|
a.Do(action, doReq)
|
|
|
|
next.ServeHTTP(w, req)
|
|
|
|
return
|
2024-12-09 17:35:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-01-05 03:45:22 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiGetActor(next http.Handler, apKey crypto.PrivateKey, apDomain string) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
|
|
pubkey := apKey.(PrivKeyAux).Public()
|
|
|
|
pubkeypem, err := x509.MarshalPKIXPublicKey(pubkey)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
encodedkey := make([]byte, len(pubkeypem)*2)
|
|
|
|
base64.StdEncoding.Encode(encodedkey, pubkeypem)
|
|
|
|
lastpos := bytes.Index(encodedkey, []byte{byte(0)})
|
|
|
|
|
|
|
|
keystring := "-----BEGIN PUBLIC KEY-----\n"
|
|
|
|
keystring += string(encodedkey[:lastpos])
|
|
|
|
keystring += "\n-----END PUBLIC KEY-----\n"
|
|
|
|
|
|
|
|
actor := adapter.ApActor{
|
|
|
|
Id: apDomain + "/api/actor",
|
|
|
|
Name: "underbbs_actor",
|
|
|
|
PreferredUsername: "underbbs_actor",
|
|
|
|
Summary: "gateway for building modular social integrations",
|
|
|
|
Url: apDomain + "/api/actor",
|
|
|
|
PublicKey: adapter.ApKey{
|
|
|
|
Id: apDomain + "/api/actor#key",
|
|
|
|
Owner: apDomain + "/api/actor",
|
|
|
|
PublicKeyPem: keystring,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
util.AddContextValue(req, "actor", actor)
|
|
|
|
next.ServeHTTP(w, req)
|
2024-12-09 17:35:44 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-05-26 23:24:49 +00:00
|
|
|
func (self *BBSServer) apiMux() http.Handler {
|
2024-05-26 18:50:19 +00:00
|
|
|
errTemplate, err := template.New("err").Parse("{{ $params := (.Context).Value \"params\" }}<html><body><h1>ERROR {{ $params.ErrorCode }}</h1><p class='error'>{{ $params.ErrorMessage }}</p></body></html>")
|
|
|
|
if err != nil {
|
|
|
|
panic("error template was malformed")
|
|
|
|
}
|
|
|
|
rtr := &router.Router{
|
|
|
|
Fallback: *errTemplate,
|
|
|
|
}
|
|
|
|
|
2024-05-26 23:24:49 +00:00
|
|
|
rtr.Post("/adapters", ProtectWithSubscriberKey(
|
2025-01-05 03:45:22 +00:00
|
|
|
apiConfigureAdapters(renderer.JSON("data"), self.subscribers, self.apKey),
|
2024-05-26 23:24:49 +00:00
|
|
|
self.subscribers,
|
|
|
|
))
|
|
|
|
|
2024-12-06 04:05:01 +00:00
|
|
|
rtr.Post(`/adapters/(?P<adapter_id>\S+)/subscribe`, ProtectWithSubscriberKey(
|
|
|
|
apiAdapterSubscribe(renderer.JSON("data"), self.subscribers),
|
2024-05-26 23:24:49 +00:00
|
|
|
self.subscribers,
|
|
|
|
))
|
2024-07-07 20:54:33 +00:00
|
|
|
|
2024-07-07 03:13:18 +00:00
|
|
|
rtr.Get(`/adapters/(?P<adapter_id>\S+)/fetch`, ProtectWithSubscriberKey(
|
2024-07-07 20:54:33 +00:00
|
|
|
apiAdapterFetch(renderer.JSON("data"), self.subscribers),
|
|
|
|
self.subscribers,
|
2024-07-07 03:13:18 +00:00
|
|
|
))
|
2024-05-26 18:50:19 +00:00
|
|
|
|
2024-12-10 04:18:08 +00:00
|
|
|
rtr.Post(`/adapters/(?P<adapter_id>\S+)/do`, ProtectWithSubscriberKey(
|
2024-12-09 17:35:44 +00:00
|
|
|
apiAdapterDo(renderer.JSON("data"), self.subscribers),
|
|
|
|
self.subscribers,
|
|
|
|
))
|
|
|
|
|
2025-01-05 03:45:22 +00:00
|
|
|
if self.apKey != nil && self.apDomain != nil {
|
|
|
|
rtr.Get("/actor", apiGetActor(renderer.JSON("actor"), *self.apKey, *self.apDomain))
|
|
|
|
}
|
|
|
|
|
2024-05-26 18:50:19 +00:00
|
|
|
return http.HandlerFunc(rtr.ServeHTTP)
|
|
|
|
}
|