From d3bc08dd24292333e752961d8233d3655cf0954a Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Wed, 24 Apr 2024 13:55:10 -0500 Subject: [PATCH] improvements to proposal event forwarding to matrix, and other bot features --- backend/.env.example | 3 +- backend/src/db.js | 3 ++ backend/src/matrix.js | 77 ++++++++++++++++++++++++++++++++-------- backend/src/proposals.js | 4 +-- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index bb06656..2267539 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,4 +8,5 @@ MATRIX_HOMESERVER_URL="https://matrix.dgov.io" MATRIX_USER="forum-api" MATRIX_PASSWORD= BOT_STORAGE_PATH="./data/bot-storage.json" -BOT_CRYPTO_STORAGE_PATH="./data/bot-crypto" \ No newline at end of file +BOT_CRYPTO_STORAGE_PATH="./data/bot-crypto" +BOT_INSTANCE_ID= \ No newline at end of file diff --git a/backend/src/db.js b/backend/src/db.js index c0877c5..8d0cad1 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -6,4 +6,7 @@ module.exports = { forum: new Level(`${dataDir}/forum`, { valueEncoding: 'json' }), authorAddresses: new Level(`${dataDir}/authorAddresses`, { valueEncoding: 'utf8' }), authorPrivKeys: new Level(`${dataDir}/authorPrivKeys`, { valueEncoding: 'utf8' }), + appState: new Level(`${dataDir}/appState`, { valueEncoding: 'json' }), + proposalEventIds: new Level(`${dataDir}/proposalEventIds`, { keyEncoding: 'json', valueEncoding: 'utf8' }), + referendumEventIds: new Level(`${dataDir}/referendumEventIds`, { keyEncoding: 'json', valueEncoding: 'utf8' }), }; diff --git a/backend/src/matrix.js b/backend/src/matrix.js index 53ccda2..f5c225a 100644 --- a/backend/src/matrix.js +++ b/backend/src/matrix.js @@ -6,7 +6,8 @@ const { SimpleFsStorageProvider, } = require('matrix-bot-sdk'); const fastq = require('fastq'); -const Promise = require('bluebird'); + +const { appState, proposalEventIds } = require('./db'); const { MATRIX_HOMESERVER_URL, @@ -14,18 +15,36 @@ const { MATRIX_PASSWORD, BOT_STORAGE_PATH, BOT_CRYPTO_STORAGE_PATH, + BOT_INSTANCE_ID, + ETH_NETWORK, } = process.env; const storageProvider = new SimpleFsStorageProvider(BOT_STORAGE_PATH); const cryptoProvider = new RustSdkCryptoStorageProvider(BOT_CRYPTO_STORAGE_PATH); let client; let joinedRooms; -const processOutboundQueue = async ({ text }) => { - console.log('broadcasting', { text }); - await Promise.each(joinedRooms, async (roomId) => { - await client.sendText(roomId, text); - }); +let targetRoomId; + +const processOutboundQueue = async ({ type, ...args }) => { + if (!targetRoomId) return; + switch (type) { + case 'NewProposal': { + const { proposalIndex, text } = args; + try { + await proposalEventIds.get(Number(proposalIndex)); + } catch (e) { + if (e.status === 404) { + console.log('sending to room', targetRoomId, { text }); + const eventId = await client.sendText(targetRoomId, text); + await proposalEventIds.put(Number(proposalIndex), eventId); + } + } + break; + } + default: + } }; + const outboundQueue = fastq(processOutboundQueue, 1); outboundQueue.pause(); @@ -46,18 +65,46 @@ const start = async () => { joinedRooms = await client.getJoinedRooms(); console.log('joined rooms:', joinedRooms); + try { + targetRoomId = await appState.get('targetRoomId'); + } catch (e) { + // Leave targetRoomId uninitialized for now + } + async function handleCommand(roomId, event) { // Don't handle unhelpful events (ones that aren't text messages, are redacted, or sent by us) if (event.content?.msgtype !== 'm.text') return; if (event.sender === await client.getUserId()) return; - // Check to ensure that the `!hello` command is being run - const { body } = event.content; - if (!body?.startsWith('!hello')) return; + const helloRegex = /^!hello\b/i; + const targetRegex = /^!target (.*)\b/i; + const proposalRegex = /\bprop(|osal) ([0-9]+)\b/i; - // Now that we've passed all the checks, we can actually act upon the command - console.log(`!hello roomId ${roomId}`); - await client.replyNotice(roomId, event, 'Hello world!'); + const { body } = event.content; + + if (helloRegex.test(body)) { + console.log(`!hello roomId ${roomId}`); + await client.replyNotice(roomId, event, 'Hello world!'); + } else if (targetRegex.test(body)) { + const [, instanceId] = targetRegex.exec(body); + console.log(`!target roomId ${roomId} instanceId ${instanceId}`); + if (instanceId === BOT_INSTANCE_ID) { + targetRoomId = roomId; + await appState.put('targetRoomId', targetRoomId); + await client.replyNotice(roomId, event, `Proposal events will be sent to this room for network ${ETH_NETWORK}`); + } + } else if (proposalRegex.test(body)) { + const [, , proposalIndexStr] = proposalRegex.exec(body); + const proposalIndex = parseInt(proposalIndexStr, 10); + console.log(`mention of proposal ${proposalIndex} in roomId ${roomId}`); + try { + const proposalEventId = await proposalEventIds.get(proposalIndex); + const proposalEventUri = `https://matrix.to/#/${roomId}/${proposalEventId}`; + await client.sendText(roomId, `Proposal ${proposalIndex}: ${proposalEventUri}`); + } catch (e) { + // Not found + } + } } // Before we start the bot, register our command handler @@ -69,11 +116,11 @@ const start = async () => { }); }; -const broadcastMessage = (text) => { - outboundQueue.push({ text }); +const sendNewProposalEvent = (proposalIndex, text) => { + outboundQueue.push({ type: 'NewProposal', proposalIndex, text }); }; module.exports = { start, - broadcastMessage, + sendNewProposalEvent, }; diff --git a/backend/src/proposals.js b/backend/src/proposals.js index 70866b4..89d4ff4 100644 --- a/backend/src/proposals.js +++ b/backend/src/proposals.js @@ -1,6 +1,6 @@ const { proposals } = require('./contracts'); const read = require('./read'); -const { broadcastMessage } = require('./matrix'); +const { sendNewProposalEvent } = require('./matrix'); // Subscribe to proposal events const start = () => { @@ -20,7 +20,7 @@ const start = () => { if (post.embeddedData && Object.entries(post.embeddedData).length) { message += `\n\n${JSON.stringify(post.embeddedData, null, 2)}`; } - broadcastMessage(message); + sendNewProposalEvent(proposalIndex, message); }); };