underbbs/adapter/honk.go

326 lines
6.3 KiB
Go
Raw Permalink Normal View History

package adapter
import (
"bytes"
2025-01-05 03:45:22 +00:00
"crypto"
2024-12-05 03:22:38 +00:00
"encoding/json"
"errors"
"fmt"
2024-12-07 02:30:41 +00:00
. "forge.lightcrystal.systems/nilix/underbbs/models"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
2024-12-05 03:22:38 +00:00
"strconv"
"strings"
2024-12-05 03:22:38 +00:00
"sync"
"time"
)
2024-12-05 03:22:38 +00:00
type donk struct {
Desc string
URL string
}
type honk struct {
ID int
Honker string
Handles string
Oonker string
2024-12-05 03:22:38 +00:00
XID string
RID *string
Noise string
Donks []donk
Convoy string
Public bool
Date string
}
type honkResponse struct {
Honks []honk `json:"honks"`
ChatCount int `json:"chatcount"`
MeCount int `json:"mecount"`
}
type HonkAdapter struct {
data *chan SocketData
nickname string
server string
username string
password string
token string
2025-01-05 03:45:22 +00:00
apSigner *crypto.PrivateKey
apDomain *string
2024-12-05 03:22:38 +00:00
cache map[string]time.Time
maxId int
mtx sync.RWMutex
stop chan bool
}
func (self *HonkAdapter) isAnonymous() bool {
return self.token == ""
}
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 {
self.data = data
// separate name and server in handle
parts := strings.Split(*settings.Handle, "@")
self.username = parts[1]
self.server = "https://" + parts[2]
self.nickname = settings.Nickname
2025-01-05 03:45:22 +00:00
self.apSigner = settings.ApSigner
if settings.Password == nil || *settings.Password == "" {
// we're anonymous!
return nil
}
self.password = *settings.Password
2024-12-05 03:22:38 +00:00
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)
2024-12-07 02:25:58 +00:00
self.stop = make(chan bool)
return nil
}
2024-12-07 02:25:58 +00:00
func (self *HonkAdapter) Stop() {
close(self.stop)
}
func (self *HonkAdapter) Subscribe(filter string, target *string) []error {
if self.isAnonymous() {
return nil
}
2024-12-05 03:22:38 +00:00
if self.stop != nil {
close(self.stop)
}
self.stop = make(chan bool)
go self.gethonks(filter, target)
2024-12-05 03:22:38 +00:00
return nil
}
func (self *HonkAdapter) gethonks(filter string, target *string) {
2024-12-07 02:25:58 +00:00
x := self.stop
self.maxId = 0
2024-12-05 03:22:38 +00:00
for {
select {
2024-12-07 02:25:58 +00:00
case _, ok := <-x:
2024-12-05 03:22:38 +00:00
if !ok {
2024-12-07 02:25:58 +00:00
fmt.Println("stopping! filter was " + filter)
2024-12-05 03:22:38 +00:00
return
}
default:
2024-12-07 02:25:58 +00:00
fmt.Println("getting honks, filter is " + filter)
2024-12-05 03:22:38 +00:00
honkForm := url.Values{
"action": []string{"gethonks"},
"token": []string{self.token},
"page": []string{filter},
}
if self.maxId != 0 {
honkForm["after"] = []string{strconv.FormatInt(int64(self.maxId), 10)}
}
res, err := http.PostForm(self.server+"/api", honkForm)
if err != nil {
fmt.Println("fucked up: " + err.Error())
self.stop <- true
return
2024-12-05 03:22:38 +00:00
}
hr := honkResponse{}
err = json.NewDecoder(res.Body).Decode(&hr)
if err != nil {
fmt.Println("malformed honks: " + err.Error())
self.stop <- true
return
}
for _, h := range hr.Honks {
2024-12-05 03:22:38 +00:00
if h.ID > self.maxId {
self.maxId = h.ID
}
msg := self.toMsg(h, target)
self.send(msg)
2024-12-05 03:22:38 +00:00
}
time.Sleep(5 * time.Second)
}
}
}
func (self *HonkAdapter) toMsg(h honk, target *string) Message {
2024-12-05 03:22:38 +00:00
t, err := time.Parse(time.RFC3339, h.Date)
if err != nil {
t = time.Now()
}
tt := t.UnixMilli()
a := h.Honker
if h.Oonker != "" {
a = h.Oonker
2024-12-05 03:22:38 +00:00
}
msg := Message{
Datagram: Datagram{
Id: h.XID,
Uri: h.XID,
Protocol: "honk",
Adapter: self.nickname,
Type: "message",
Created: tt,
},
Author: a,
Content: h.Noise,
ReplyTo: h.RID,
Visibility: "Private",
}
if h.Public {
msg.Visibility = "Public"
}
if h.Oonker != "" {
2024-12-05 03:22:38 +00:00
r := fmt.Sprintf("%s/bonk/%d", h.Honker, h.ID)
fmt.Println(r)
msg.Renoter = &h.Honker
2024-12-05 03:22:38 +00:00
msg.RenoteId = &r
msg.RenoteTime = &tt
}
for _, d := range h.Donks {
a := Attachment{
Src: d.URL,
Desc: d.Desc,
}
msg.Attachments = append(msg.Attachments, a)
}
if target != nil {
msg.Target = target
}
2024-12-05 03:22:38 +00:00
return msg
}
func (self *HonkAdapter) Fetch(etype string, ids []string) error {
// honk API is limited, we fall back to the anonymous adapter for fetch ops
aaa := anonAPAdapter{}
2025-01-05 03:45:22 +00:00
aaa.Init(self.data, self.server, "honk", self.nickname, self.apSigner, self.apDomain)
return aaa.Fetch(etype, ids)
}
func (self *HonkAdapter) donk(data map[string]string) error {
var b bytes.Buffer
w := multipart.NewWriter(&b)
fw, err := w.CreateFormField("action")
io.Copy(fw, strings.NewReader("honk"))
fw, err = w.CreateFormField("token")
io.Copy(fw, strings.NewReader(self.token))
for k, v := range data {
if k == "file" {
if fw, err = w.CreateFormFile("donk", "donk"); err != nil {
return err
}
} else {
fieldName := "noise"
switch k {
case "desc":
fieldName = "donkdesc"
case "content":
fieldName = "noise"
case "replyto":
fieldName = "rid"
}
if fw, err = w.CreateFormField(fieldName); err != nil {
return err
}
}
if _, err := io.Copy(fw, strings.NewReader(v)); err != nil {
return err
}
}
w.Close()
req, err := http.NewRequest("POST", self.server+"/api", &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", w.FormDataContentType())
c := http.Client{}
res, err := c.Do(req)
if err != nil {
return err
}
if res.StatusCode < 400 {
return nil
} else {
return errors.New(fmt.Sprintf("status: %d", res.StatusCode))
}
}
func (self *HonkAdapter) Do(action string, data map[string]string) error {
if self.isAnonymous() {
return nil
}
switch action {
case "post":
honkForm := url.Values{
"action": []string{"honk"},
"token": []string{self.token},
"noise": []string{data["content"]},
}
_, exists := data["file"]
if exists {
return self.donk(data)
}
replyto, exists := data["replyto"]
if exists {
honkForm["rid"] = []string{replyto}
}
res, err := http.PostForm(self.server+"/api", honkForm)
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 "home"
}