Compare commits
2 Commits
44821a2556
...
f7bd1fc67b
Author | SHA1 | Date |
---|---|---|
Ladd Hoffman | f7bd1fc67b | |
Ladd Hoffman | 17e8a559cf |
|
@ -4,9 +4,9 @@ const crypto = require('crypto');
|
|||
const objectHash = require('object-hash');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const verifySignature = require('./verify-signature');
|
||||
const { authorAddresses, authorPrivKeys, forum } = require('./db');
|
||||
const { dao } = require('./contracts');
|
||||
const verifySignature = require('../util/verify-signature');
|
||||
const { authorAddresses, authorPrivKeys, forum } = require('../util/db');
|
||||
const { dao } = require('../util/contracts');
|
||||
|
||||
// Each post allocates 30% of its reputation to citations
|
||||
const PPM_TO_CITATIONS = 300000;
|
|
@ -1,7 +1,7 @@
|
|||
const objectHash = require('object-hash');
|
||||
|
||||
const verifySignature = require('./verify-signature');
|
||||
const { forum } = require('./db');
|
||||
const verifySignature = require('../util/verify-signature');
|
||||
const { forum } = require('../util/db');
|
||||
|
||||
const read = async (hash) => {
|
||||
// Fetch content
|
|
@ -1,7 +1,7 @@
|
|||
const objectHash = require('object-hash');
|
||||
|
||||
const verifySignature = require('./verify-signature');
|
||||
const { forum } = require('./db');
|
||||
const verifySignature = require('../util/verify-signature');
|
||||
const { forum } = require('../util/db');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
|
@ -0,0 +1,9 @@
|
|||
const proposalsListener = require('./proposals');
|
||||
|
||||
const start = () => {
|
||||
proposalsListener.start();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
const { proposals } = require('./contracts');
|
||||
const read = require('./read');
|
||||
const { sendNewProposalEvent } = require('./matrix');
|
||||
const { proposals } = require('../util/contracts');
|
||||
const read = require('../api/read');
|
||||
const { sendNewProposalEvent } = require('../matrix-bot');
|
||||
|
||||
// Subscribe to proposal events
|
||||
const start = () => {
|
|
@ -1,5 +1,5 @@
|
|||
require('dotenv').config();
|
||||
|
||||
require('./api').start();
|
||||
require('./matrix').start();
|
||||
require('./proposals').start();
|
||||
require('./matrix-bot').start();
|
||||
require('./contract-listeners').start();
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
const {
|
||||
AutojoinRoomsMixin,
|
||||
MatrixClient,
|
||||
RustSdkCryptoStorageProvider,
|
||||
SimpleFsStorageProvider,
|
||||
} = require('matrix-bot-sdk');
|
||||
|
||||
const { registerCommands } = require('./commands');
|
||||
const { registerRoomEventHandler } = require('./room-events');
|
||||
|
||||
const {
|
||||
MATRIX_HOMESERVER_URL,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
BOT_STORAGE_PATH,
|
||||
BOT_CRYPTO_STORAGE_PATH,
|
||||
} = process.env;
|
||||
|
||||
const storageProvider = new SimpleFsStorageProvider(BOT_STORAGE_PATH);
|
||||
const cryptoProvider = new RustSdkCryptoStorageProvider(BOT_CRYPTO_STORAGE_PATH);
|
||||
let client;
|
||||
let joinedRooms;
|
||||
|
||||
const { outboundQueue, startOutboundQueue } = require('./outbound-queue');
|
||||
|
||||
const start = async () => {
|
||||
console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL);
|
||||
client = new MatrixClient(
|
||||
MATRIX_HOMESERVER_URL,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
storageProvider,
|
||||
cryptoProvider,
|
||||
);
|
||||
|
||||
// Automatically join a room to which we are invited
|
||||
AutojoinRoomsMixin.setupOnClient(client);
|
||||
|
||||
joinedRooms = await client.getJoinedRooms();
|
||||
console.log('joined rooms:', joinedRooms);
|
||||
|
||||
// Before we start the bot, register our command handler
|
||||
registerCommands(client);
|
||||
|
||||
// Handler for custom events
|
||||
registerRoomEventHandler(client);
|
||||
|
||||
client.start().then(() => {
|
||||
console.log('Bot started!');
|
||||
// Start the outbound queue
|
||||
startOutboundQueue(client);
|
||||
});
|
||||
};
|
||||
|
||||
const sendNewProposalEvent = (proposalIndex, text) => {
|
||||
outboundQueue.push({ type: 'NewProposal', proposalIndex, text });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
sendNewProposalEvent,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -1,174 +0,0 @@
|
|||
const {
|
||||
AutojoinRoomsMixin,
|
||||
MatrixClient,
|
||||
RustSdkCryptoStorageProvider,
|
||||
SimpleFsStorageProvider,
|
||||
} = require('matrix-bot-sdk');
|
||||
const fastq = require('fastq');
|
||||
const { recoverPersonalSignature } = require('@metamask/eth-sig-util');
|
||||
|
||||
const {
|
||||
appState,
|
||||
proposalEventIds,
|
||||
matrixUserToAuthorAddress,
|
||||
authorAddressToMatrixUser,
|
||||
} = require('./db');
|
||||
|
||||
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 start = async () => {
|
||||
console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL);
|
||||
client = new MatrixClient(
|
||||
MATRIX_HOMESERVER_URL,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
storageProvider,
|
||||
cryptoProvider,
|
||||
);
|
||||
|
||||
// Automatically join a room to which we are invited
|
||||
AutojoinRoomsMixin.setupOnClient(client);
|
||||
|
||||
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);
|
||||
|
||||
// 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:
|
||||
}
|
||||
});
|
||||
|
||||
client.start().then(() => {
|
||||
console.log('Bot started!');
|
||||
outboundQueue.resume();
|
||||
});
|
||||
};
|
||||
|
||||
const sendNewProposalEvent = (proposalIndex, text) => {
|
||||
outboundQueue.push({ type: 'NewProposal', proposalIndex, text });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
sendNewProposalEvent,
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
const contractAddresses = require('../contract-addresses.json');
|
||||
const contractAddresses = require('../../contract-addresses.json');
|
||||
|
||||
const networks = {
|
||||
localhost: '0x539',
|
|
@ -1,8 +1,8 @@
|
|||
const ethers = require('ethers');
|
||||
|
||||
const { getContractAddressByNetworkName } = require('./contract-config');
|
||||
const DAOArtifact = require('../contractArtifacts/DAO.json');
|
||||
const ProposalsArtifact = require('../contractArtifacts/Proposals.json');
|
||||
const DAOArtifact = require('../../contractArtifacts/DAO.json');
|
||||
const ProposalsArtifact = require('../../contractArtifacts/Proposals.json');
|
||||
|
||||
const network = process.env.ETH_NETWORK;
|
||||
|
Loading…
Reference in New Issue