From 30a53868140b8702a164a587baeaf276895298bf Mon Sep 17 00:00:00 2001 From: olive Date: Tue, 15 Sep 2026 13:31:29 +0100 Subject: Quaddle support --- main.lua | 1 + quaddle/pylon.lua | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ todo.txt | 3 ++ wilson.ini.example | 8 +++++ 4 files changed, 113 insertions(+) create mode 100644 quaddle/pylon.lua diff --git a/main.lua b/main.lua index aebc001..e928fdb 100644 --- a/main.lua +++ b/main.lua @@ -11,6 +11,7 @@ local pylon_classes = { xmpp = require'xmpp.pylon', discord = require'discord.pylon', nanochat = require'nanochat.pylon', + quaddle = require'quaddle.pylon', } local Wilson = class() diff --git a/quaddle/pylon.lua b/quaddle/pylon.lua new file mode 100644 index 0000000..d176260 --- /dev/null +++ b/quaddle/pylon.lua @@ -0,0 +1,101 @@ +local websocket = require'http.websocket' +local request = require'http.request' +local json = require 'dkjson' +local class = require 'r.class' + +local BasePylon = require 'pylon' +local Channel = require 'channel' + +local Quaddle = class.extend(BasePylon) +function Quaddle.init(self) + self:_check_fields "base_url bot_name bot_password" +end + +-- https://codeberg.org/QWD/Quaddle/src/branch/main/doc/api.md + +function Quaddle._req(self, url, payload) + local req = request.new_from_uri(self.base_url..url) + req.headers:upsert(':method',payload and 'POST' or 'GET') + if self.token then req.headers:upsert('authorization',self.token) end + req.headers:upsert('user-agent','wilson (https://g.gh0.pw/wilson/, v0.0)') + if payload then + req.headers:upsert('content-type','application/json') + req:set_body(assert(json.encode(payload))) end + + local head,body = assert(req:go()) + local status = head:get':status' + local bod = body:get_body_as_string() + + assert(status:match"^2", 'status was '..status..' body '..bod) + + if #bod == 0 then return nil end + local val, _, err = json.decode(bod) + if err then error(err) else return val end +end + +function Quaddle._connect(self) + local r = assert(self:_req("auth/login", { name=self.bot_name, password=self.bot_password })) + self.token = r.token + + self.ws = websocket.new_from_uri(self.base_url:gsub("^http","ws").."app") + assert(self.ws:connect()) + self.ws:send(json.encode{ op="identify", token=self.token }, 'text') + + for busname, bus in pairs(self.wilson.busses) do + for _, channel in ipairs(bus) do + if channel.pylon == self then + self:add_channel(channel) + end + end + end +end + +function Quaddle.add_channel(self, channel) + self.log("Subscribing to channel",channel.descriptor) + self.ws:send(json.encode{ op="subscribe", channel_id=channel.descriptor }) +end + +function Quaddle.recving(self) + for packet in self.ws:each() do + local event = json.decode(packet) + self.log:debug('event', event.event) + if event.event == "ready" then + -- self.session_id = event.session_id + self.bot_user = event.user + self.log("Logged in as id",self.bot_user.id,"name",self.bot_user.name) + elseif event.event == "error" then + self.log:error("Error from server:",event.reasons) + elseif event.event == "message_create" then + self:handle_message(event.message) + elseif event.event == "message_edit" then + -- TODO: wilson doesn't have edits yet + self:handle_message(event.message) + -- elseif event.event == "message_delete" then + -- TODO + else + self.log:debug("Unhandled Quaddle event:",json.encode(event)) + end + end +end + +function Quaddle.handle_message(self, msg) + self.log:proto('<',msg.channel_id,msg.author.username,msg.content) + if msg.author.id ~= self.bot_user.id then + self.wilson:deliver(Channel(self, msg.channel), { + body = msg.content, + sender = '[q]' .. msg.author.name, + }) + end +end + +function Quaddle.sending(self) + for dest_channel, message in self.inbox:iter() do + self.log:proto('>', dest_channel, message.sender, message.body) + + self:_req('channels/'..dest_channel.descriptor..'/messages', { + content = message.sender..": "..message.body, + }) + end +end + +return Quaddle diff --git a/todo.txt b/todo.txt index 0ee8f75..d79aa3c 100644 --- a/todo.txt +++ b/todo.txt @@ -2,3 +2,6 @@ pylon.subclass should not have a pylon_type arg. is only used for logging. a pylon's _name_ should be used in log messages. pylons harcode prepend names with [x] [d] [n] etc. this is not robust and is weird it should be in some way dynamic? maybe pylons can have configured short names? but then what in the case of two channels with different users of the same nick? they should not be conflated.. + +discord and quaddle hard-depend on the configured url ending with a / +note that http does not conflate // into / in the path. diff --git a/wilson.ini.example b/wilson.ini.example index 303d8a2..2eb7708 100644 --- a/wilson.ini.example +++ b/wilson.ini.example @@ -26,9 +26,17 @@ server=gloop.woop.example port=44322 poll=5 +[pylon quaddle-boingy] +type=quaddle +base_url=https://boingy.example/ +bot_username=wilson +bot_password=seeeeeecret_password_here + + [bus test] xmpp-barbaz test@conference.example.org irc-boopbanana #test discord-foobar channel_id_here nanochat-gloop #test +quaddle-boingy channel_id_here -- cgit v1.2.3