successfully forwarding blockchain events to matrix
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 26s Details

This commit is contained in:
Ladd Hoffman 2024-04-23 20:01:49 -05:00
parent 2e53252fc7
commit fe2cbe0e27
14 changed files with 1142 additions and 91 deletions

View File

@ -1,6 +1,11 @@
PORT=3000
DATA_DIR="./data"
API_LISTEN_PORT=3000
LEVEL_DATA_DIR="./data"
SEMANTIC_SCHOLAR_API_KEY=
ETH_NETWORK="localhost"
ETH_PRIVATE_KEY=
INFURA_API_KEY=
INFURA_API_KEY=
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"

3
backend/.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/
.env
data/
data/
registration.yaml

17
backend/README.md Normal file
View File

@ -0,0 +1,17 @@
# Setup
1.
cp .env.example .env
1.
npm install
1.
npm run registration
1.
docker compose up -d --build

View File

@ -7,4 +7,6 @@ services:
volumes:
- ./data:/data
environment:
- DATA_DIR=/data
- LEVEL_DATA_DIR=/data
- BOT_STORAGE_PATH="./data/bot-storage.json"
- BOT_CRYPTO_STORAGE_PATH="./data/bot-crypto"

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,9 @@
"ethers": "^6.12.0",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"fastq": "^1.17.1",
"level": "^8.0.1",
"matrix-bot-sdk": "^0.7.1",
"object-hash": "^3.0.0"
},
"devDependencies": {

46
backend/src/api.js Normal file
View File

@ -0,0 +1,46 @@
const express = require('express');
require('express-async-errors');
const read = require('./read');
const write = require('./write');
const importFromSS = require('./import-from-ss');
const app = express();
const port = process.env.API_LISTEN_PORT || 3000;
app.use(express.json());
app.post('/write', write);
app.get('/read/:hash', async (req, res) => {
const { hash } = req.params;
console.log('read', hash);
const data = await read(hash);
res.json(data);
});
app.post('/importFromSemanticScholar', importFromSS);
app.get('*', (req, res) => {
console.log(`404 req.path: ${req.path}`);
res.status(404).json({ errorCode: 404 });
});
app.use((err, req, res, next) => {
const status = err.response?.status ?? err.status ?? 500;
const message = err.response?.data?.error ?? err.message;
console.error(`error: ${message}`, err);
res.status(status).send(message);
next();
});
const start = () => {
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});
};
module.exports = {
start,
};

View File

@ -2,6 +2,7 @@ const ethers = require('ethers');
const { getContractAddressByNetworkName } = require('./contract-config');
const DAOArtifact = require('../contractArtifacts/DAO.json');
const ProposalsArtifact = require('../contractArtifacts/Proposals.json');
const network = process.env.ETH_NETWORK;
@ -23,12 +24,15 @@ const getProvider = () => {
const wallet = new ethers.Wallet(process.env.ETH_PRIVATE_KEY, getProvider());
const getContract = (name) => new ethers.Contract(
getContractAddressByNetworkName(process.env.ETH_NETWORK, name),
DAOArtifact.abi,
wallet,
);
module.exports = {
dao: getContract('DAO'),
dao: new ethers.Contract(
getContractAddressByNetworkName(process.env.ETH_NETWORK, 'DAO'),
DAOArtifact.abi,
wallet,
),
proposals: new ethers.Contract(
getContractAddressByNetworkName(process.env.ETH_NETWORK, 'Proposals'),
ProposalsArtifact.abi,
wallet,
),
};

View File

@ -1,6 +1,6 @@
const { Level } = require('level');
const dataDir = process.env.DATA_DIR || 'data';
const dataDir = process.env.LEVEL_DATA_DIR || 'data';
module.exports = {
forum: new Level(`${dataDir}/forum`, { valueEncoding: 'json' }),

View File

@ -212,4 +212,6 @@ module.exports = async (req, res) => {
} else {
res.status(400).end();
}
// TODO: Send matrix room event on SS import
};

View File

@ -1,40 +1,5 @@
const express = require('express');
const read = require('./read');
const write = require('./write');
const importFromSS = require('./import-from-ss');
require('dotenv').config();
require('express-async-errors');
const app = express();
const port = process.env.PORT || 3000;
app.use(express.json());
app.post('/write', write);
app.get('/read/:hash', read);
app.post('/importFromSemanticScholar', importFromSS);
app.get('*', (req, res) => {
console.log(`404 req.path: ${req.path}`);
res.status(404).json({ errorCode: 404 });
});
app.use((err, req, res, next) => {
const status = err.response?.status ?? 500;
const message = err.response?.data?.error ?? err.message;
console.error(`error: ${message}`, err);
res.status(status).send(message);
next();
});
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});
// TODO: Subscribe to contract events
// TODO: Send matrix room events for proposal events
// TODO: Send matrix room event on SS import
require('./api').start();
require('./matrix').start();
require('./proposals').start();

76
backend/src/matrix.js Normal file
View File

@ -0,0 +1,76 @@
const {
AutojoinRoomsMixin,
MatrixAuth,
MatrixClient,
RustSdkCryptoStorageProvider,
SimpleFsStorageProvider,
} = require('matrix-bot-sdk');
const fastq = require('fastq');
const {
MATRIX_HOMESERVER_URL,
MATRIX_USER,
MATRIX_PASSWORD,
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 processOutboundQueue = async ({ text }) => {
joinedRooms.forEach(async (roomId) => {
await client.sendText(roomId, text);
});
};
const outboundQueue = fastq(processOutboundQueue, 1);
outboundQueue.pause();
const start = async () => {
console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL);
const auth = new MatrixAuth(MATRIX_HOMESERVER_URL);
const authClient = await auth.passwordLogin(MATRIX_USER, MATRIX_PASSWORD);
client = new MatrixClient(
MATRIX_HOMESERVER_URL,
authClient.accessToken,
storageProvider,
cryptoProvider,
);
// Automatically join a room to which we are invited
AutojoinRoomsMixin.setupOnClient(client);
joinedRooms = await client.getJoinedRooms();
console.log('joined rooms:', joinedRooms);
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;
// Now that we've passed all the checks, we can actually act upon the command
await client.replyNotice(roomId, event, 'Hello world!');
}
// Before we start the bot, register our command handler
client.on('room.message', handleCommand);
client.start().then(() => {
console.log('Bot started!');
outboundQueue.resume();
});
};
const broadcastMessage = (text) => {
outboundQueue.push({ text });
};
module.exports = {
start,
broadcastMessage,
};

29
backend/src/proposals.js Normal file
View File

@ -0,0 +1,29 @@
const { proposals } = require('./contracts');
const read = require('./read');
const { broadcastMessage } = require('./matrix');
// Subscribe to proposal events
const start = () => {
proposals.on('NewProposal', async (proposalIndex) => {
// TODO: Cache these in leveldb so we know when we've already seen one and sent to matrix
console.log('New Proposal, index', proposalIndex);
const proposal = await proposals.proposals(proposalIndex);
console.log('postId:', proposal.postId);
// Read post from database
const post = await read(proposal.postId);
console.log('post.content:', post.content);
// Send matrix room event
let message = `Proposal ${proposalIndex}\n\n${post.content}`;
if (post.embeddedData && Object.entries(post.embeddedData).length) {
message += `\n\n${JSON.stringify(post.embeddedData, null, 2)}`;
}
broadcastMessage(message);
});
};
module.exports = {
start,
};

View File

@ -3,24 +3,14 @@ const objectHash = require('object-hash');
const verifySignature = require('./verify-signature');
const { forum } = require('./db');
module.exports = async (req, res) => {
const { hash } = req.params;
console.log('read', hash);
const read = async (hash) => {
// Fetch content
let data;
try {
data = await forum.get(req.params.hash);
} catch (e) {
console.log('read error:', e.message, hash);
res.status(e.status).end();
return;
}
const data = await forum.get(hash);
data.embeddedData = data.embeddedData || undefined;
const {
authors, content, signature, embeddedData,
authors, content, signature, embeddedData, citations,
} = data;
// Verify hash
@ -28,18 +18,17 @@ module.exports = async (req, res) => {
authors, content, signature, embeddedData,
});
if (derivedHash !== hash) {
console.log('error: hash mismatch');
res.status(500).end();
return;
throw new Error('hash mismatch');
}
// Verify signature
if (!verifySignature(data)) {
console.log('error: signature verificaition failed');
res.status(500).end();
return;
throw new Error('signature verificaition failed');
}
// Return content
res.json(data);
return {
authors, content, signature, embeddedData, citations,
};
};
module.exports = read;