19 Commits

Author SHA1 Message Date
0e6898d93b 1.6.0 2017-06-08 23:45:24 +02:00
a5886bc122 Merge pull request #5 from 67P/feature/ipfs
Use/store IPFS metadata for people and proposals
2017-06-08 23:44:09 +02:00
582f6b7f5a Use/store IPFS metadata for people and proposals
closes #3
closes #4
2017-06-08 21:10:22 +02:00
9f078a0d84 1.5.0 2017-05-13 17:05:23 +02:00
b1ea520fdf Improve wording, add URLs 2017-05-13 17:05:08 +02:00
6d98c2cf19 1.4.6 2017-05-13 16:37:05 +02:00
7d8c6032c9 Fix payload parsing 2017-05-13 16:36:48 +02:00
8596483721 1.4.5 2017-05-13 15:49:38 +02:00
f54e124dba Fix diverging webhook object structure
No idea why, but on the server, all data is contained within a single
'payload' property, while I've never seen any such property on my
machine. Could be different node or express versions, but in any case,
this should fix it so that it just works no matter what.
2017-05-13 15:47:35 +02:00
fb8fc88101 1.4.4 2017-05-13 15:15:30 +02:00
6ca1254c9a Fix GitHub POST handler
1. robot.logger.debug didn't show any output
2. Send response for all requests
2017-05-13 15:14:22 +02:00
38483cbbeb 1.4.3 2017-05-12 14:02:54 +02:00
0203cb1a62 Fix typo 2017-05-12 14:02:46 +02:00
797fddfafb 1.4.2 2017-05-12 13:58:16 +02:00
82fe54b90a Fix string interpolation 2017-05-12 13:58:05 +02:00
4a8ea160a8 1.4.1 2017-05-12 01:39:55 +02:00
17543cb67b Return ether instead of wei 2017-05-12 01:39:27 +02:00
9bd875acba 1.4.0 2017-05-12 01:28:35 +02:00
cae2bd84b2 Ask for balance 2017-05-12 01:28:14 +02:00
2 changed files with 169 additions and 37 deletions

201
index.js
View File

@@ -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,50 +47,119 @@ 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);
web3.eth.getBalance(hubotWalletAddress, function (err, balance) { getBalance().then(balance => {
if (err) { robot.logger.error('[hubot-kredits] Error checking balance'); return; }
if (balance <= 0) { if (balance <= 0) {
robot.logger.info('[hubot-kredits] Hubot is broke. Please send some ETH to ' + hubotWalletAddress); messageRoom(`Yo gang, I\'m broke! Please drop me some ETH to ${hubotWalletAddress}. kthxbai.`);
} }
}); });
let getValueFromContract = function(contractMethod, ...args) { function getBalance() {
return new Promise((resolve, reject) => {
web3.eth.getBalance(hubotWalletAddress, function (err, balance) {
if (err) {
robot.logger.error('[hubot-kredits] Error checking balance');
reject(err);
return;
}
resolve(balance);
});
});
}
function getValueFromContract(contractMethod, ...args) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
kredits[contractMethod](...args, (err, data) => { kredits[contractMethod](...args, (err, data) => {
if (err) { reject(err); } if (err) { reject(err); }
resolve(data); resolve(data);
}); });
}); });
}; }
let getContributorData = function(i) { 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) {
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); loadProfileFromIPFS(c).then(contributor => {
robot.logger.debug('[kredits] contributor', contributor);
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));
}); });
return promise; return promise;
}; }
let getContributors = function() { function getContributors() {
return getValueFromContract('contributorsCount').then(contributorsCount => { return getValueFromContract('contributorsCount').then(contributorsCount => {
let contributors = []; let contributors = [];
@@ -93,9 +169,9 @@ const Web3 = require('web3');
return Promise.all(contributors); return Promise.all(contributors);
}); });
}; }
let getContributorByGithubUser = function(username) { function getContributorByGithubUser(username) {
let promise = new Promise((resolve, reject) => { let promise = new Promise((resolve, reject) => {
getContributors().then(contributors => { getContributors().then(contributors => {
let contrib = contributors.find(c => { let contrib = contributors.find(c => {
@@ -109,7 +185,7 @@ const Web3 = require('web3');
}); });
}); });
return promise; return promise;
}; }
function messageRoom(message) { function messageRoom(message) {
robot.messageRoom(process.env.KREDITS_ROOM, message); robot.messageRoom(process.env.KREDITS_ROOM, message);
@@ -138,18 +214,45 @@ 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"])) {
console.log('[kredits] invalid contribution data: ', util.inspect(contribution));
return Promise.reject('invalid contribution data');
}
return ipfs.add(new ipfs.Buffer(JSON.stringify(contribution)))
.then(res => { return res[0].hash; })
.catch(err => console.log(err));
}
function createProposal(recipient, amount, url, description, details) {
robot.logger.debug(`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; }
messageRoom(`Let's give ${recipient} some kredits for ${url}: https://kredits.kosmos.org`);
});
}); });
}, () => { }, () => {
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();
@@ -173,8 +276,11 @@ 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);
}); });
resolve(); resolve();
@@ -208,8 +314,11 @@ 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);
}); });
resolve(); resolve();
@@ -217,16 +326,36 @@ const Web3 = require('web3');
}); });
} }
robot.respond(/(got ETH)|(got gas)\?/i, res => {
getBalance().then(balance => {
if (balance <= 0) {
res.send(`HALP, I\'m totally broke! Not a single wei in my pocket.`);
}
else if (balance >= 1e+17) {
res.send(`my wallet contains ${web3.fromWei(balance, 'ether')} ETH`);
}
else {
res.send(`I\'m almost broke! Only have ${web3.fromWei(balance, 'ether')} ETH left in my pocket. :(`);
}
});
});
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;
robot.logger.debug(`Received GitHub hook. Event: ${evt}, action: ${data.action}`); // 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}`);
if (evt === 'pull_request' && data.action === 'closed') { if (evt === 'pull_request' && data.action === 'closed') {
handleGitHubPullRequestClosed(data).then(() => res.send(200)); handleGitHubPullRequestClosed(data).then(() => res.send(200));
} }
else if (evt === 'issues' && data.action === 'closed') { else if (evt === 'issues' && data.action === 'closed') {
handleGitHubIssueClosed(data).then(() => res.send(200)); handleGitHubIssueClosed(data).then(() => res.send(200));
} else {
res.send(200);
} }
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "hubot-kredits", "name": "hubot-kredits",
"version": "1.3.0", "version": "1.6.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"
}, },