From f7bd1fc67bbaa2b4929115a6f8f4ff914a492892 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Sat, 27 Apr 2024 13:37:16 -0500 Subject: [PATCH] refactor matrix.js into separate files --- backend/src/matrix-bot/commands.js | 62 ++++++++++ backend/src/matrix-bot/index.js | 128 ++------------------ backend/src/matrix-bot/outbound-queue.js | 49 ++++++++ backend/src/matrix-bot/register-identity.js | 0 backend/src/matrix-bot/room-events.js | 50 ++++++++ 5 files changed, 168 insertions(+), 121 deletions(-) create mode 100644 backend/src/matrix-bot/commands.js create mode 100644 backend/src/matrix-bot/outbound-queue.js create mode 100644 backend/src/matrix-bot/register-identity.js create mode 100644 backend/src/matrix-bot/room-events.js diff --git a/backend/src/matrix-bot/commands.js b/backend/src/matrix-bot/commands.js new file mode 100644 index 0000000..8ea5a7d --- /dev/null +++ b/backend/src/matrix-bot/commands.js @@ -0,0 +1,62 @@ +const { setTargetRoomId } = require('./outbound-queue'); + +const { + appState, + proposalEventIds, +} = require('../util/db'); + +const { + BOT_INSTANCE_ID, + ETH_NETWORK, +} = process.env; + +const handleCommand = async (client, 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; + + const helloRegex = /^!hello\b/i; + const targetRegex = /^!target (.*)\b/i; + const proposalRegex = /\bprop(|osal) ([0-9]+)\b/i; + + 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) { + setTargetRoomId(roomId); + await appState.put('targetRoomId', roomId); + 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}`; + const content = { + body: `Proposal ${proposalIndex}: ${proposalEventUri}`, + msgtype: 'm.text', + }; + if (event.content['m.relates_to']?.rel_type === 'm.thread') { + content['m.relates_to'] = event.content['m.relates_to']; + } + await client.sendEvent(roomId, 'm.room.message', content); + } catch (e) { + // Not found + } + } +}; + +const registerCommands = (client) => { + client.on('room.message', (roomId, event) => handleCommand(client, roomId, event)); +}; + +module.exports = { + registerCommands, +}; diff --git a/backend/src/matrix-bot/index.js b/backend/src/matrix-bot/index.js index 42a7367..3765ef5 100644 --- a/backend/src/matrix-bot/index.js +++ b/backend/src/matrix-bot/index.js @@ -4,53 +4,23 @@ const { RustSdkCryptoStorageProvider, SimpleFsStorageProvider, } = require('matrix-bot-sdk'); -const fastq = require('fastq'); -const { recoverPersonalSignature } = require('@metamask/eth-sig-util'); -const { - appState, - proposalEventIds, - matrixUserToAuthorAddress, - authorAddressToMatrixUser, -} = require('../util/db'); +const { registerCommands } = require('./commands'); +const { registerRoomEventHandler } = require('./room-events'); const { MATRIX_HOMESERVER_URL, MATRIX_ACCESS_TOKEN, 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; -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(); +const { outboundQueue, startOutboundQueue } = require('./outbound-queue'); const start = async () => { console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL); @@ -67,100 +37,16 @@ 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 - } - - const handleCommand = async (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; - - const helloRegex = /^!hello\b/i; - const targetRegex = /^!target (.*)\b/i; - const proposalRegex = /\bprop(|osal) ([0-9]+)\b/i; - - 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}`; - const content = { - body: `Proposal ${proposalIndex}: ${proposalEventUri}`, - msgtype: 'm.text', - }; - if (event.content['m.relates_to']?.rel_type === 'm.thread') { - content['m.relates_to'] = event.content['m.relates_to']; - } - await client.sendEvent(roomId, 'm.room.message', content); - } catch (e) { - // Not found - } - } - }; - - const handleRegisterIdentity = async (roomId, event) => { - const { message, signature } = event.content; - console.log('Received request to register identity'); - let account; - try { - account = recoverPersonalSignature({ data: message, signature }); - } catch (e) { - console.log('error: failed to recover signature:', e.message); - } - if (account) { - try { - const authorAddress = await matrixUserToAuthorAddress.get(event.sender); - if (account === authorAddress) { - await client.sendNotice(roomId, `Matrix user ${event.sender} already linked to author address ${account}`); - } else { - await client.sendNotice(roomId, `Matrix user ${event.sender} was linked to author address ${authorAddress}, now linked to ${account}`); - } - } catch (e) { - // Not found - await client.sendNotice(roomId, `Registered matrix user ${event.sender} to author address ${account}`); - } - await matrixUserToAuthorAddress.put(event.sender, account); - await authorAddressToMatrixUser.put(account, event.sender); - } - }; - // Before we start the bot, register our command handler - client.on('room.message', handleCommand); + registerCommands(client); // Handler for custom events - client.on('room.event', (roomId, event) => { - // Note that state events can also be sent down this listener too - if (event.state_key !== undefined) return; // state event - - switch (event.type) { - case 'io.dgov.identity.register': - handleRegisterIdentity(roomId, event); - break; - default: - } - }); + registerRoomEventHandler(client); client.start().then(() => { console.log('Bot started!'); - outboundQueue.resume(); + // Start the outbound queue + startOutboundQueue(client); }); }; diff --git a/backend/src/matrix-bot/outbound-queue.js b/backend/src/matrix-bot/outbound-queue.js new file mode 100644 index 0000000..16646e8 --- /dev/null +++ b/backend/src/matrix-bot/outbound-queue.js @@ -0,0 +1,49 @@ +const fastq = require('fastq'); + +const { + proposalEventIds, +} = require('../util/db'); + +let client; +let targetRoomId; + +const setTargetRoomId = (roomId) => { + targetRoomId = roomId; +}; + +const processOutboundQueue = async ({ type, ...args }) => { + if (!targetRoomId) return; + switch (type) { + case 'NewProposal': { + const { proposalIndex, text } = args; + try { + await proposalEventIds.get(Number(proposalIndex)); + // If this doesn't throw, it means we already sent a message for this proposal + } 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); +// Pause until client is set +outboundQueue.pause(); + +const startOutboundQueue = (c) => { + client = c; + // Resume now that client is set + outboundQueue.resume(); +}; + +module.exports = { + setTargetRoomId, + outboundQueue, + startOutboundQueue, +}; diff --git a/backend/src/matrix-bot/register-identity.js b/backend/src/matrix-bot/register-identity.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/matrix-bot/room-events.js b/backend/src/matrix-bot/room-events.js new file mode 100644 index 0000000..27f7f08 --- /dev/null +++ b/backend/src/matrix-bot/room-events.js @@ -0,0 +1,50 @@ +const { recoverPersonalSignature } = require('@metamask/eth-sig-util'); + +const { + matrixUserToAuthorAddress, + authorAddressToMatrixUser, +} = require('../util/db'); + +const handleRegisterIdentity = async (client, roomId, event) => { + const { message, signature } = event.content; + console.log('Received request to register identity'); + let account; + try { + account = recoverPersonalSignature({ data: message, signature }); + } catch (e) { + console.log('error: failed to recover signature:', e.message); + } + if (account) { + try { + const authorAddress = await matrixUserToAuthorAddress.get(event.sender); + if (account === authorAddress) { + await client.sendNotice(roomId, `Matrix user ${event.sender} author address ${account} already registered`); + } else { + await client.sendNotice(roomId, `Matrix user ${event.sender} updated author address from ${authorAddress} to ${account}`); + } + } catch (e) { + // Not found + await client.sendNotice(roomId, `Matrix user ${event.sender} registered author address ${account}`); + } + await matrixUserToAuthorAddress.put(event.sender, account); + await authorAddressToMatrixUser.put(account, event.sender); + } +}; + +const registerRoomEventHandler = (client) => { + client.on('room.event', (roomId, event) => { + // Note that state events can also be sent down this listener too + if (event.state_key !== undefined) return; // state event + + switch (event.type) { + case 'io.dgov.identity.register': + handleRegisterIdentity(client, roomId, event); + break; + default: + } + }); +}; + +module.exports = { + registerRoomEventHandler, +};