Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce87aaa3fd | |||
| fa86d0a3c0 | |||
| 94baf62c51 | |||
| 0e6898d93b | |||
| a5886bc122 | |||
| 582f6b7f5a | |||
| 9f078a0d84 | |||
| b1ea520fdf | |||
| 6d98c2cf19 | |||
| 7d8c6032c9 | |||
| 8596483721 | |||
| f54e124dba |
225
index.js
225
index.js
@@ -6,8 +6,14 @@
|
|||||||
// KREDITS_ROOM: Kredit proposals are posted to this chatroom
|
// KREDITS_ROOM: Kredit proposals are posted to this chatroom
|
||||||
// KREDITS_WALLET_PATH: Path to a etherum wallet JSON file
|
// KREDITS_WALLET_PATH: Path to a etherum wallet JSON file
|
||||||
// KREDITS_WALLET_PASSWORD: Wallet password
|
// KREDITS_WALLET_PASSWORD: Wallet password
|
||||||
|
// KREDITS_CONTRACT_ADDRESS: Address of Kredits contract
|
||||||
|
// KREDITS_PROVIDER_URL: Ethereum JSON-RPC URL (default 'http://localhost:8545')
|
||||||
|
// IPFS_API_HOST: Host/domain (default 'localhost')
|
||||||
|
// IPFS_API_PORT: Port number (default '5001')
|
||||||
|
// IPFS_API_PROTOCOL: Protocol, e.g. 'http' or 'https' (default 'http')
|
||||||
//
|
//
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const util = require('util');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const kreditsContracts = require('kredits-contracts');
|
const kreditsContracts = require('kredits-contracts');
|
||||||
const ProviderEngine = require('web3-provider-engine');
|
const ProviderEngine = require('web3-provider-engine');
|
||||||
@@ -15,23 +21,24 @@ const Wallet = require('ethereumjs-wallet');
|
|||||||
const WalletSubprovider = require('ethereumjs-wallet/provider-engine');
|
const WalletSubprovider = require('ethereumjs-wallet/provider-engine');
|
||||||
const Web3Subprovider = require('web3-provider-engine/subproviders/web3.js');
|
const Web3Subprovider = require('web3-provider-engine/subproviders/web3.js');
|
||||||
const Web3 = require('web3');
|
const Web3 = require('web3');
|
||||||
|
const ipfsAPI = require('ipfs-api');
|
||||||
|
const schemas = require('kosmos-schemas');
|
||||||
|
const tv4 = require('tv4');
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Instantiate ethereum client and wallet
|
||||||
|
//
|
||||||
let engine = new ProviderEngine();
|
let engine = new ProviderEngine();
|
||||||
|
|
||||||
let walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json';
|
let walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json';
|
||||||
let walletJson = fs.readFileSync(walletPath);
|
let walletJson = fs.readFileSync(walletPath);
|
||||||
let wallet = Wallet.fromV3(JSON.parse(walletJson), process.env.KREDITS_WALLET_PASSWORD);
|
let wallet = Wallet.fromV3(JSON.parse(walletJson), process.env.KREDITS_WALLET_PASSWORD);
|
||||||
let providerUrl = process.env.KREDITS_PROVIDER_URL || 'http://localhost:8545';
|
let providerUrl = process.env.KREDITS_PROVIDER_URL || 'http://localhost:8545';
|
||||||
let hubotWalletAddress = '0x' + wallet.getAddress().toString('hex');
|
let hubotWalletAddress = '0x' + wallet.getAddress().toString('hex');
|
||||||
|
|
||||||
let config = {};
|
|
||||||
if (process.env.KREDITS_CONTRACT_ADDRESS) {
|
|
||||||
config = { Kredits: { address: process.env.KREDITS_CONTRACT_ADDRESS }};
|
|
||||||
}
|
|
||||||
|
|
||||||
engine.addProvider(new WalletSubprovider(wallet, {}));
|
engine.addProvider(new WalletSubprovider(wallet, {}));
|
||||||
engine.addProvider(new Web3Subprovider(new Web3.providers.HttpProvider(providerUrl)));
|
engine.addProvider(new Web3Subprovider(new Web3.providers.HttpProvider(providerUrl)));
|
||||||
// TODO only start engine if providerURL is accessible
|
// TODO only start engine if providerURL is accessible
|
||||||
@@ -40,9 +47,29 @@ const Web3 = require('web3');
|
|||||||
let web3 = new Web3(engine);
|
let web3 = new Web3(engine);
|
||||||
web3.eth.defaultAccount = hubotWalletAddress;
|
web3.eth.defaultAccount = hubotWalletAddress;
|
||||||
|
|
||||||
let contracts = kreditsContracts(web3, config);
|
//
|
||||||
|
// Instantiate contracts
|
||||||
|
//
|
||||||
|
let contractConfig = {};
|
||||||
|
if (process.env.KREDITS_CONTRACT_ADDRESS) {
|
||||||
|
contractConfig = { Kredits: { address: process.env.KREDITS_CONTRACT_ADDRESS }};
|
||||||
|
}
|
||||||
|
let contracts = kreditsContracts(web3, contractConfig);
|
||||||
let kredits = contracts['Kredits'];
|
let kredits = contracts['Kredits'];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Instantiate IPFS API client
|
||||||
|
//
|
||||||
|
let ipfsConfig = {};
|
||||||
|
if (process.env.IPFS_API_HOST) {
|
||||||
|
ipfsConfig = {
|
||||||
|
host: process.env.IPFS_API_HOST,
|
||||||
|
port: process.env.IPFS_API_PORT,
|
||||||
|
protocol: process.env.IPFS_API_PROTOCOL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let ipfs = ipfsAPI(ipfsConfig);
|
||||||
|
|
||||||
module.exports = function(robot) {
|
module.exports = function(robot) {
|
||||||
|
|
||||||
robot.logger.info('[hubot-kredits] Wallet address: ' + hubotWalletAddress);
|
robot.logger.info('[hubot-kredits] Wallet address: ' + hubotWalletAddress);
|
||||||
@@ -75,20 +102,58 @@ const Web3 = require('web3');
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadProfileFromIPFS(contributor) {
|
||||||
|
let promise = new Promise((resolve, reject) => {
|
||||||
|
return ipfs.cat(contributor.ipfsHash, { buffer: true }).then(res => {
|
||||||
|
let content = res.toString();
|
||||||
|
let profile = JSON.parse(content);
|
||||||
|
|
||||||
|
contributor.name = profile.name;
|
||||||
|
contributor.kind = profile.kind;
|
||||||
|
|
||||||
|
let accounts = profile.accounts;
|
||||||
|
let github = accounts.find(a => a.site === 'github.com');
|
||||||
|
let wiki = accounts.find(a => a.site === 'wiki.kosmos.org');
|
||||||
|
|
||||||
|
if (github) {
|
||||||
|
contributor.github_username = github.username;
|
||||||
|
contributor.github_uid = github.uid;
|
||||||
|
}
|
||||||
|
if (wiki) {
|
||||||
|
contributor.wiki_username = wiki.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(contributor);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
function getContributorData(i) {
|
function getContributorData(i) {
|
||||||
let promise = new Promise((resolve, reject) => {
|
let promise = new Promise((resolve, reject) => {
|
||||||
getValueFromContract('contributorAddresses', i).then(address => {
|
getValueFromContract('contributorAddresses', i).then(address => {
|
||||||
robot.logger.debug('address', address);
|
// robot.logger.debug('address', address);
|
||||||
getValueFromContract('contributors', address).then(person => {
|
getValueFromContract('contributors', address).then(person => {
|
||||||
robot.logger.debug('person', person);
|
// robot.logger.debug('person', person);
|
||||||
let contributor = {
|
let c = {
|
||||||
address: address,
|
address: address,
|
||||||
github_username: person[1],
|
name: person[1],
|
||||||
github_uid: person[0],
|
id: person[0],
|
||||||
ipfsHash: person[2]
|
ipfsHash: person[2]
|
||||||
};
|
};
|
||||||
robot.logger.debug('[kredits] contributor', contributor);
|
if (c.ipfsHash) {
|
||||||
resolve(contributor);
|
// robot.logger.debug('[kredits] loading contributor profile loaded for', c.name, c.ipfsHash, '...');
|
||||||
|
loadProfileFromIPFS(c).then(contributor => {
|
||||||
|
// robot.logger.debug('[kredits] contributor profile loaded for', c.name);
|
||||||
|
resolve(contributor);
|
||||||
|
}).catch(() => console.log('[kredits] error fetching contributor info from IPFS for '+c.name));
|
||||||
|
} else {
|
||||||
|
resolve(c);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).catch(err => reject(err));
|
}).catch(err => reject(err));
|
||||||
});
|
});
|
||||||
@@ -123,6 +188,22 @@ const Web3 = require('web3');
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getContributorByAddress(address) {
|
||||||
|
let promise = new Promise((resolve, reject) => {
|
||||||
|
getContributors().then(contributors => {
|
||||||
|
let contrib = contributors.find(c => {
|
||||||
|
return c.address === address;
|
||||||
|
});
|
||||||
|
if (contrib) {
|
||||||
|
resolve(contrib);
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
function messageRoom(message) {
|
function messageRoom(message) {
|
||||||
robot.messageRoom(process.env.KREDITS_ROOM, message);
|
robot.messageRoom(process.env.KREDITS_ROOM, message);
|
||||||
}
|
}
|
||||||
@@ -150,21 +231,52 @@ const Web3 = require('web3');
|
|||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProposal(recipient, amount, url/*, metaData*/) {
|
function createContributionDocument(contributor, url, description, details) {
|
||||||
return new Promise((resolve, reject) => {
|
let contribution = {
|
||||||
// TODO write metaData to IPFS
|
"@context": "https://schema.kosmos.org",
|
||||||
robot.logger.debug(`Creating proposal to issue ${amount}₭S to ${recipient} for ${url}...`);
|
"@type": "Contribution",
|
||||||
|
contributor: {
|
||||||
|
ipfs: contributor.ipfsHash
|
||||||
|
},
|
||||||
|
kind: 'dev',
|
||||||
|
url: url,
|
||||||
|
description: description,
|
||||||
|
details: details
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! tv4.validate(contribution, schemas["contribution"])) {
|
||||||
|
robot.logger.error('[kredits] invalid contribution data: ', util.inspect(contribution));
|
||||||
|
return Promise.reject('invalid contribution data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// robot.logger.debug('[kredits] creating IPFS document for contribution:', contribution.description);
|
||||||
|
|
||||||
|
return ipfs.add(new ipfs.Buffer(JSON.stringify(contribution)))
|
||||||
|
.then(res => {
|
||||||
|
// robot.logger.debug('[kredits] created IPFS document', res[0].hash);
|
||||||
|
return res[0].hash;
|
||||||
|
}).catch(err => robot.logger.error('[kredits] couldn\'t create IPFS document', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProposal(recipient, amount, url, description, details) {
|
||||||
|
robot.logger.debug(`[kredits] Creating proposal to issue ${amount}₭S to ${recipient} for ${url}...`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Get contributor details for GitHub user
|
||||||
getContributorByGithubUser(recipient).then(c => {
|
getContributorByGithubUser(recipient).then(c => {
|
||||||
kredits.addProposal(c.address, amount, url, '', (e/* , d */) => {
|
// Create document containing contribution data on IPFS
|
||||||
if (e) { reject(); return; }
|
createContributionDocument(c, url, description, details).then(ipfsHash => {
|
||||||
messageRoom(`New proposal created: ${amount} for ${recipient}`);
|
// Create proposal on ethereum blockchain
|
||||||
|
kredits.addProposal(c.address, amount, url, ipfsHash, (e, d) => {
|
||||||
|
if (e) { reject(e); return; }
|
||||||
|
robot.logger.debug('[kredits] proposal created:', util.inspect(d));
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, () => {
|
}, () => {
|
||||||
messageRoom(`Couldn\'t find contributor data for ${recipient}. Please add them first!`);
|
messageRoom(`I wanted to propose giving kredits to ${recipient} for ${url}, but I can't find their contact data. Please add them as a contributor: https://kredits.kosmos.org`);
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +297,12 @@ const Web3 = require('web3');
|
|||||||
recipients = [issue.user.login];
|
recipients = [issue.user.login];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1];
|
||||||
|
let description = `${repoName}: ${issue.title}`;
|
||||||
|
|
||||||
recipients.forEach(recipient => {
|
recipients.forEach(recipient => {
|
||||||
createProposal(recipient, amount, web_url, issue);
|
createProposal(recipient, amount, web_url, description, issue)
|
||||||
|
.catch(err => robot.logger.error(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
@@ -220,8 +336,12 @@ const Web3 = require('web3');
|
|||||||
let amount = amountFromIssueLabels(issue);
|
let amount = amountFromIssueLabels(issue);
|
||||||
if (amount === 0) { resolve(); return; }
|
if (amount === 0) { resolve(); return; }
|
||||||
|
|
||||||
|
let repoName = pull_request.base.repo.full_name;
|
||||||
|
let description = `${repoName}: ${pull_request.title}`;
|
||||||
|
|
||||||
recipients.forEach(recipient => {
|
recipients.forEach(recipient => {
|
||||||
createProposal(recipient, amount, web_url, pull_request);
|
createProposal(recipient, amount, web_url, description, pull_request)
|
||||||
|
.catch(err => robot.logger.error(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
@@ -246,6 +366,10 @@ const Web3 = require('web3');
|
|||||||
robot.router.post('/incoming/kredits/github/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => {
|
robot.router.post('/incoming/kredits/github/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => {
|
||||||
let evt = req.header('X-GitHub-Event');
|
let evt = req.header('X-GitHub-Event');
|
||||||
let data = req.body;
|
let data = req.body;
|
||||||
|
// For some reason data is contained in a payload property on one
|
||||||
|
// machine, but directly in the root of the object on others
|
||||||
|
if (data.payload) { data = JSON.parse(data.payload); }
|
||||||
|
|
||||||
robot.logger.info(`Received GitHub hook. Event: ${evt}, action: ${data.action}`);
|
robot.logger.info(`Received GitHub hook. Event: ${evt}, action: ${data.action}`);
|
||||||
|
|
||||||
if (evt === 'pull_request' && data.action === 'closed') {
|
if (evt === 'pull_request' && data.action === 'closed') {
|
||||||
@@ -258,5 +382,50 @@ const Web3 = require('web3');
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function watchContractEvents() {
|
||||||
|
web3.eth.getBlockNumber((err, blockNumber) => {
|
||||||
|
if (err) {
|
||||||
|
robot.logger.error('[kredits] couldn\t get current block number');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// current block is the last mined one, thus we check from the next
|
||||||
|
// mined one onwards to prevent getting previous events
|
||||||
|
let nextBlock = blockNumber + 1;
|
||||||
|
robot.logger.debug(`[kredits] watching events from block ${nextBlock} onward`);
|
||||||
|
|
||||||
|
kredits.allEvents({fromBlock: nextBlock, toBlock: 'latest'}, (error, data) => {
|
||||||
|
robot.logger.debug('[kredits] received contract event', data.event);
|
||||||
|
if (data.blockNumber < nextBlock) {
|
||||||
|
// I don't know why, but the filter doesn't work as intended
|
||||||
|
robot.logger.debug('[kredits] dismissing old event from block', data.blockNumber);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (data.event) {
|
||||||
|
case 'ProposalCreated':
|
||||||
|
handleProposalCreated(data);
|
||||||
|
break;
|
||||||
|
// case 'ProposalExecuted':
|
||||||
|
// handleProposalExecuted(data);
|
||||||
|
// break;
|
||||||
|
// case 'ProposalVoted':
|
||||||
|
// handleProposalVoted(data);
|
||||||
|
// break;
|
||||||
|
// case 'Transfer':
|
||||||
|
// handleTransfer(data);
|
||||||
|
// break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProposalCreated(data) {
|
||||||
|
getContributorByAddress(data.args.recipient).then((contributor) => {
|
||||||
|
messageRoom(`Let's give ${contributor.name} some kredits for ${data.args.url}: https://kredits.kosmos.org`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watchContractEvents();
|
||||||
|
|
||||||
};
|
};
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hubot-kredits",
|
"name": "hubot-kredits",
|
||||||
"version": "1.4.4",
|
"version": "1.7.0",
|
||||||
"description": "Kosmos Kredits functionality for chat bots",
|
"description": "Kosmos Kredits functionality for chat bots",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,9 +11,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ethereumjs-wallet": "mvayngrib/ethereumjs-wallet",
|
"ethereumjs-wallet": "mvayngrib/ethereumjs-wallet",
|
||||||
|
"ipfs-api": "^14.0.3",
|
||||||
|
"kosmos-schemas": "^1.1.2",
|
||||||
"kredits-contracts": "2.0.0",
|
"kredits-contracts": "2.0.0",
|
||||||
"node-fetch": "^1.6.3",
|
"node-fetch": "^1.6.3",
|
||||||
"prompt": "^1.0.0",
|
"prompt": "^1.0.0",
|
||||||
|
"tv4": "^1.3.0",
|
||||||
"web3": "^0.18.4",
|
"web3": "^0.18.4",
|
||||||
"web3-provider-engine": "^12.0.3"
|
"web3-provider-engine": "^12.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user