51 Commits

Author SHA1 Message Date
98f4516217 2.1.0 2018-05-06 21:15:04 +02:00
149bc0c052 Use new wrapper function for finding contributor
Fixes a bug with the find algo as well.
2018-05-06 21:14:04 +02:00
c190490d9b Add context to contribution description
It didn't actually say where the pages were edited, i.e. the wiki.
2018-05-06 21:12:57 +02:00
eb767a90f2 Deduplicate title of pages edited/created 2018-05-06 21:12:37 +02:00
156ea44f0d Use configured wiki URL for contribution details 2018-05-06 21:12:06 +02:00
ec7f4a4d33 2.0.1 2018-05-03 13:32:15 +02:00
5f9e237f36 Add missing keywords to package config 2018-05-03 13:31:13 +02:00
38db976c29 2.0.0 2018-05-03 13:27:11 +02:00
36e73beb3d Update README 2018-05-03 13:24:53 +02:00
8d067d221f Update kredits-contracts to latest release 2018-05-03 13:21:00 +02:00
35222456a4 Merge pull request #19 from 67P/docs/9-readme
Add README/documentation
2018-04-22 14:36:54 +00:00
7636e08e8f Merge pull request #20 from 67P/dev/cleanup
Remove obsolete lib files
2018-04-22 14:36:05 +00:00
b4229c7879 Document wallet creation, rename npm script 2018-04-22 13:47:32 +02:00
201c982466 Remove obsolete lib files
We're using the npm module now
2018-04-22 13:42:59 +02:00
bba5407c7d Fix typo, link kredits wiki page 2018-04-22 13:39:29 +02:00
417eb42905 Add README/documentation 2018-04-22 13:20:08 +02:00
b852bd24af Merge pull request #16 from 67P/feature/6-mediawiki_integration
MediaWiki integration & integration architecture improvements
2018-04-21 09:31:29 +00:00
581b15da69 Cron job for checking mediawiki changes 2018-04-19 19:21:05 +02:00
c97cc82817 Only fetch wiki changes since last processing 2018-04-19 19:04:20 +02:00
d4a3a9c9df Add hardcoded amounts for wiki edits 2018-04-19 18:00:19 +02:00
6c3070b43b Increase kredits amounts for GitHub labels 2018-04-19 17:54:59 +02:00
9d8b2c08dc [WIP] Add proposals for wiki changes 2018-04-19 17:11:02 +02:00
2780c87aaa Last GitHub fix 2018-04-19 17:10:51 +02:00
7904aa8c84 Remove robot stub from mediawiki 2018-04-19 14:39:02 +02:00
3d810287c0 Implement GitHub repo blacklist
closes #17
2018-04-19 14:38:09 +02:00
22b480ae92 Fix typos 2018-04-19 14:37:55 +02:00
48a42d4f2c Improve logging 2018-04-19 14:37:21 +02:00
aab5b58bab Add missing module imports 2018-04-19 14:35:26 +02:00
c0c2f97dae Add MediaWiki integration basics 2018-04-19 12:32:03 +02:00
74429feb9d Use contracts from master 2018-04-19 12:31:36 +02:00
ed5d127b4c Add missing variables to GitHub integration 2018-04-19 12:18:12 +02:00
e8da47db70 Use relative path for integration module 2018-04-19 12:10:44 +02:00
462efcefbd Use robot.logger 2018-04-19 12:10:34 +02:00
5259b56e53 Split out integrations, add code section comments 2018-04-19 11:50:59 +02:00
33cefac88e Add node-cron 2018-04-19 11:39:33 +02:00
5a2ebb323d Basic MediaWiki changes integration 2018-04-19 11:38:01 +02:00
eecfa7e8f2 Improve variable assignment 2018-04-19 11:35:09 +02:00
5d76b2dead Revert "minor refactorings"
This reverts commit 8c481179b4.
2018-04-19 11:34:47 +02:00
22cb49df2d Add missing network id documentation 2018-04-19 01:08:03 +02:00
8c481179b4 minor refactorings 2018-04-19 01:01:33 +02:00
2be3d4e8e3 Improve room messages a bit
And don't notify everyone for every contribution.
2018-04-19 00:44:12 +02:00
f4b484ed5a Improve logging
Add prefix, some formatting
2018-04-19 00:42:50 +02:00
e4fb97c0c9 Better message for new proposal notifications 2018-04-19 00:21:48 +02:00
3fe6dfb2ce properly handle github webhooks 2018-04-19 00:13:42 +02:00
9dc1ece94f Working kredits-contract integration 2018-04-18 22:54:41 +02:00
e99addf37e Use kredits-contracts from truffle-kredits 2018-04-18 20:50:43 +02:00
050e8ce79f WIP update to use new kredits library
Using the new kredits library to load contract data and create
proposals.
currently the kredits lib is only copied - which needs to be removed
asap.
2018-04-18 16:27:22 +02:00
821543056b Use ethers.js to create a new wallet
ethers.js is smaller and nicer than web3
2018-04-18 16:25:46 +02:00
ce87aaa3fd 1.7.0 2017-07-12 14:48:20 +02:00
fa86d0a3c0 Merge pull request #12 from 67P/feature/events
Notify channel from ProposalCreated event
2017-07-12 14:47:17 +02:00
94baf62c51 Notify channel from ProposalCreated event
This handles incoming events (for now only ProposalCreated, but easy to
extend), and moves the channel message for new proposals to the event
handler, so that they're being announced for all proposals, including
those created manually from kredits-web for example.
2017-06-20 18:59:58 -07:00
7 changed files with 2357 additions and 347 deletions

64
README.md Normal file
View File

@@ -0,0 +1,64 @@
[![npm](https://img.shields.io/npm/v/hubot-kredits.svg)](https://www.npmjs.com/package/hubot-kredits)
# Hubot Kredits
This repository provides scripts for integrating [Kosmos
Kredits](https://wiki.kosmos.org/Kredits) in [Hubot](http://hubot.github.com/)
chatbots. The bot will watch for project-related things happening on the
Internet and automatically create proposals for issuing kredits for project
contributions.
## Setup
You will need an Ethereum wallet for your bot, so it can interact with the
Ethereum smart contracts. `npm run create-wallet` will do the job for you.
## Configuration
As usual in Hubot, you can add all config as environment variables.
| Key | Description |
| --- | --- |
| `KREDITS_WEBHOOK_TOKEN` | A string for building your secret webhook URLs |
| `KREDITS_ROOM` | The bot will talk to you in this room |
| `KREDITS_WALLET_PATH` | Path to an Etherum wallet JSON file (default: `./wallet.json`) |
| `KREDITS_WALLET_PASSWORD` | Wallet password |
| `KREDITS_PROVIDER_URL` | Ethereum JSON-RPC URL (default: `http://localhost:7545`) |
| `KREDITS_NETWORK_ID` | The ethereum network ID to use (default: 100 = local) |
## Integrations
### GitHub
The GitHub integration will watch for closed issues and merged pull requests,
which carry a kredits label: `kredits-1`, `kredits-2`, `kredits-3` for small,
medium and large contributions. If there are multiple people assigned, it will
issue proposals for all of them.
#### Setup
Point a GitHub organization webhook to the following URL:
https://your-hubot.example.com/incoming/kredits/github/{webhook_token}
#### Config
| Key | Description |
| --- | --- |
| `KREDITS_GITHUB_REPO_BLACKLIST` | Repos which you do not want to issue kredits for. Format: `orgname/reponame`, e.g. `67P/test-one-two` |
### MediaWiki
The MediaWiki integration will periodically check for wiki page creations and
edits. It will create kredits proposals based on amount of text added.
#### Setup
No setup needed, except for configuring the wiki URL. The bot will poll your
wiki's API on its own.
#### Config
| Key | Description |
| --- | --- |
| `KREDITS_MEDIAWIKI_URL` | Your wiki URL, e.g. `https://wiki.kosmos.org/` |

440
index.js
View File

@@ -1,363 +1,139 @@
// Description:
// Kosmos Kredits chat integration
//
// Configuration:
// KREDITS_WEBHOOK_TOKEN: A string for building your secret webhook URL
// KREDITS_ROOM: Kredit proposals are posted to this chatroom
// KREDITS_WALLET_PATH: Path to a etherum wallet JSON file
// 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 util = require('util');
const fetch = require('node-fetch');
const kreditsContracts = require('kredits-contracts');
const ProviderEngine = require('web3-provider-engine');
const Wallet = require('ethereumjs-wallet');
const WalletSubprovider = require('ethereumjs-wallet/provider-engine');
const Web3Subprovider = require('web3-provider-engine/subproviders/web3.js');
const Web3 = require('web3');
const ipfsAPI = require('ipfs-api');
const schemas = require('kosmos-schemas');
const tv4 = require('tv4');
const ethers = require('ethers');
const Kredits = require('kredits-contracts');
(function() {
"use strict";
const walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json';
const walletJson = fs.readFileSync(walletPath);
const providerUrl = process.env.KREDITS_PROVIDER_URL || 'http://localhost:7545';
const networkId = parseInt(process.env.KREDITS_NETWORK_ID || 100);
//
// Instantiate ethereum client and wallet
//
let engine = new ProviderEngine();
const ipfsConfig = {
host: process.env.IPFS_API_HOST || 'localhost',
port: process.env.IPFS_API_PORT || '5001',
protocol: process.env.IPFS_API_PROTOCOL || 'http'
};
let walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json';
let walletJson = fs.readFileSync(walletPath);
let wallet = Wallet.fromV3(JSON.parse(walletJson), process.env.KREDITS_WALLET_PASSWORD);
let providerUrl = process.env.KREDITS_PROVIDER_URL || 'http://localhost:8545';
let hubotWalletAddress = '0x' + wallet.getAddress().toString('hex');
module.exports = async function(robot) {
engine.addProvider(new WalletSubprovider(wallet, {}));
engine.addProvider(new Web3Subprovider(new Web3.providers.HttpProvider(providerUrl)));
// TODO only start engine if providerURL is accessible
engine.start();
let web3 = new Web3(engine);
web3.eth.defaultAccount = hubotWalletAddress;
//
// Instantiate contracts
//
let contractConfig = {};
if (process.env.KREDITS_CONTRACT_ADDRESS) {
contractConfig = { Kredits: { address: process.env.KREDITS_CONTRACT_ADDRESS }};
function messageRoom(message) {
robot.messageRoom(process.env.KREDITS_ROOM, message);
}
let contracts = kreditsContracts(web3, contractConfig);
let kredits = contracts['Kredits'];
//
// Instantiate IPFS API client
// Ethereum wallet setup
//
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 wallet;
try {
wallet = await ethers.Wallet.fromEncryptedWallet(walletJson, process.env.KREDITS_WALLET_PASSWORD);
} catch(error) {
robot.logger.warn('[hubot-kredits] Could not load wallet:', error);
process.exit(1);
}
let ipfs = ipfsAPI(ipfsConfig);
module.exports = function(robot) {
//
// Ethereum provider/node setup
//
robot.logger.info('[hubot-kredits] Wallet address: ' + hubotWalletAddress);
const ethProvider = new ethers.providers.JsonRpcProvider(providerUrl, {chainId: networkId});
ethProvider.signer = wallet;
wallet.provider = ethProvider;
getBalance().then(balance => {
if (balance <= 0) {
messageRoom(`Yo gang, I\'m broke! Please drop me some ETH to ${hubotWalletAddress}. kthxbai.`);
}
//
// Kredits contracts setup
//
let kredits;
try {
kredits = await Kredits.setup(ethProvider, wallet, ipfsConfig);
} catch(error) {
robot.logger.warn('[hubot-kredits] Could not set up kredits:', error);
process.exit(1);
}
const Contributor = kredits.Contributor;
const Operator = kredits.Operator;
robot.logger.info('[hubot-kredits] Wallet address: ' + wallet.address);
//
// Check robot's wallet balance and alert when it's broke
//
ethProvider.getBalance(wallet.address).then(balance => {
robot.logger.info('[hubot-kredits] Wallet balance: ' + ethers.utils.formatEther(balance) + 'ETH');
if (balance.lt(ethers.utils.parseEther('0.0001'))) {
messageRoom(`Yo gang, I\'m broke! Please drop me some ETH to ${wallet.address}. kthxbai.`);
}
});
//
// Robot chat commands/interaction
//
robot.respond(/got ETH\??/i, res => {
ethProvider.getBalance(wallet.address).then((balance) => {
res.send(`my wallet contains ${ethers.utils.formatEther(balance)} ETH`);
});
});
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);
});
});
}
robot.respond(/propose (\d*)\s?\S*\s?to (\S+)(?:\sfor (.*))?$/i, res => {
let [_, amount, githubUser, description] = res.match;
let url = null;
createProposal(githubUser, amount, description, url).then((result) => {
messageRoom('Sounds good! Will be listed on https://kredits.kosmos.org in a bit...');
});
});
function getValueFromContract(contractMethod, ...args) {
return new Promise((resolve, reject) => {
kredits[contractMethod](...args, (err, data) => {
if (err) { reject(err); }
resolve(data);
});
});
}
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) => {
getValueFromContract('contributorAddresses', i).then(address => {
robot.logger.debug('address', address);
getValueFromContract('contributors', address).then(person => {
robot.logger.debug('person', person);
let c = {
address: address,
name: person[1],
id: person[0],
ipfsHash: person[2]
};
if (c.ipfsHash) {
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);
}
robot.respond(/list open proposals/i, res => {
Operator.all().then((proposals) => {
proposals.forEach((proposal) => {
if (!proposal.executed) {
Contributor.getById(proposal.contributorId).then((contributor) => {
messageRoom(`* ${proposal.amount} kredits to ${contributor.name} for ${proposal.description}`);
});
}).catch(err => reject(err));
});
return promise;
}
function getContributors() {
return getValueFromContract('contributorsCount').then(contributorsCount => {
let contributors = [];
for(var i = 0; i < contributorsCount.toNumber(); i++) {
contributors.push(getContributorData(i));
}
return Promise.all(contributors);
});
}
messageRoom('https://kredits.kosmos.org');
});
});
function getContributorByGithubUser(username) {
let promise = new Promise((resolve, reject) => {
getContributors().then(contributors => {
let contrib = contributors.find(c => {
return c.github_username === username;
});
if (contrib) {
resolve(contrib);
} else {
reject();
}
});
});
return promise;
}
//
// Smart contract events
//
function messageRoom(message) {
robot.messageRoom(process.env.KREDITS_ROOM, message);
}
function watchContractEvents() {
ethProvider.getBlockNumber().then((blockNumber) => {
// 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(`[hubot-kredits] Watching events from block ${nextBlock} onward`);
ethProvider.resetEventsBlock(nextBlock);
function amountFromIssueLabels(issue) {
let kreditsLabel = issue.labels.map(l => l.name)
.filter(n => n.match(/^kredits/))[0];
// No label, no kredits
if (typeof kreditsLabel === 'undefined') { return 0; }
Operator.on('ProposalCreated', handleProposalCreated);
});
}
// TODO move to config maybe?
let amount;
switch(kreditsLabel) {
case 'kredits-1':
amount = 50;
break;
case 'kredits-2':
amount = 150;
break;
case 'kredits-3':
amount = 500;
break;
}
return amount;
}
function createContributionDocument(contributor, url, description, details) {
let contribution = {
"@context": "https://schema.kosmos.org",
"@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 => {
// Create document containing contribution data on IPFS
createContributionDocument(c, url, description, details).then(ipfsHash => {
// 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(`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();
});
}
function handleGitHubIssueClosed(data) {
return new Promise((resolve/*, reject*/) => {
// fs.writeFileSync('tmp/github-issue.json', JSON.stringify(data, null, 4));
let recipients;
let issue = data.issue;
let assignees = issue.assignees.map(a => a.login);
let web_url = issue.html_url;
let amount = amountFromIssueLabels(issue);
if (amount === 0) { resolve(); return; }
if (assignees.length > 0) {
recipients = assignees;
} else {
recipients = [issue.user.login];
}
let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1];
let description = `${repoName}: ${issue.title}`;
recipients.forEach(recipient => {
createProposal(recipient, amount, web_url, description, issue);
});
resolve();
});
}
function handleGitHubPullRequestClosed(data) {
return new Promise((resolve, reject) => {
// fs.writeFileSync('tmp/github-pr.json', JSON.stringify(data, null, 4));
let recipients;
let pull_request = data.pull_request;
let assignees = pull_request.assignees.map(a => a.login);
let web_url = pull_request._links.html.href;
let pr_issue_url = pull_request.issue_url;
if (assignees.length > 0) {
recipients = assignees;
} else {
recipients = [pull_request.user.login];
}
fetch(pr_issue_url)
.then(response => {
if (response.status >= 400) {
reject('Bad response from fetching PR issue');
}
return response.json();
})
.then(issue => {
// fs.writeFileSync('tmp/github-pr-issue.json', JSON.stringify(data, null, 4));
let amount = amountFromIssueLabels(issue);
if (amount === 0) { resolve(); return; }
let repoName = pull_request.base.repo.full_name;
let description = `${repoName}: ${pull_request.title}`;
recipients.forEach(recipient => {
createProposal(recipient, amount, web_url, description, pull_request);
});
resolve();
});
});
}
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. :(`);
}
function handleProposalCreated(proposalId, creatorAccount, contributorId, amount) {
Contributor.getById(contributorId).then((contributor) => {
Operator.getById(proposalId).then((proposal) => {
robot.logger.debug(`[hubot-kredits] Proposal created (${proposal.description})`);
// messageRoom(`Let's give ${contributor.name} some kredits for ${proposal.url} (${proposal.description}): https://kredits.kosmos.org`);
});
});
}
robot.router.post('/incoming/kredits/github/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => {
let evt = req.header('X-GitHub-Event');
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); }
watchContractEvents();
robot.logger.info(`Received GitHub hook. Event: ${evt}, action: ${data.action}`);
//
// Integrations
//
if (evt === 'pull_request' && data.action === 'closed') {
handleGitHubPullRequestClosed(data).then(() => res.send(200));
}
else if (evt === 'issues' && data.action === 'closed') {
handleGitHubIssueClosed(data).then(() => res.send(200));
} else {
res.send(200);
}
});
require('./integrations/github')(robot, kredits);
};
}());
if (typeof process.env.KREDITS_MEDIAWIKI_URL !== 'undefined') {
require('./integrations/mediawiki')(robot, kredits);
}
};

178
integrations/github.js Normal file
View File

@@ -0,0 +1,178 @@
const util = require('util');
const fetch = require('node-fetch');
module.exports = async function(robot, kredits) {
function messageRoom(message) {
robot.messageRoom(process.env.KREDITS_ROOM, message);
}
robot.logger.debug('[hubot-kredits] Loading GitHub integration...');
let repoBlackList = [];
if (process.env.KREDITS_GITHUB_REPO_BLACKLIST) {
repoBlackList = process.env.KREDITS_GITHUB_REPO_BLACKLIST.split(',');
robot.logger.debug('[hubot-kredits] Ignoring GitHub actions from ', util.inspect(repoBlackList));
}
const Contributor = kredits.Contributor;
const Operator = kredits.Operator;
function getContributorByGithubUser(username) {
return Contributor.all().then(contributors => {
let contrib = contributors.find(c => {
return c.github_username === username;
});
if (!contrib) {
throw new Error(`No contributor found for ${username}`);
} else {
return contrib;
}
});
}
function createProposal(githubUser, amount, description, url, details) {
return getContributorByGithubUser(githubUser).then(contributor => {
robot.logger.debug(`[hubot-kredits] Creating proposal to issue ${amount}₭S to ${githubUser} for ${url}...`);
let contributionAttr = {
contributorId: contributor.id,
amount: amount,
contributorIpfsHash: contributor.ipfsHash,
url,
description,
details,
kind: 'dev'
};
return Operator.addProposal(contributionAttr).catch(error => {
robot.logger.error(`[hubot-kredits] Error:`, error);
messageRoom(`I wanted to propose giving kredits to GitHub user ${githubUser} for ${url}, but I cannot find their info. Please add them as a contributor: https://kredits.kosmos.org`);
});
});
}
function amountFromIssueLabels(issue) {
let kreditsLabel = issue.labels.map(l => l.name)
.filter(n => n.match(/^kredits/))[0];
// No label, no kredits
if (typeof kreditsLabel === 'undefined') { return 0; }
// TODO move to config maybe?
let amount;
switch(kreditsLabel) {
case 'kredits-1':
amount = 500;
break;
case 'kredits-2':
amount = 1500;
break;
case 'kredits-3':
amount = 5000;
break;
}
return amount;
}
function handleGitHubIssueClosed(data) {
let recipients;
let issue = data.issue;
let assignees = issue.assignees.map(a => a.login);
let web_url = issue.html_url;
let amount = amountFromIssueLabels(issue);
let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1];
let description = `${repoName}: ${issue.title}`;
if (amount === 0) {
robot.logger.info('[hubot-kredits] Proposal amount from issue label is zero; ignoring');
return Promise.resolve();
} else if (repoBlackList.includes(repoName)) {
robot.logger.debug(`[hubot-kredits] ${repoName} is on black list; ignoring`);
return Promise.resolve();
}
if (assignees.length > 0) {
recipients = assignees;
} else {
recipients = [issue.user.login];
}
let proposalPromises = [];
recipients.forEach(recipient => {
proposalPromises.push(
createProposal(recipient, amount, description, web_url, issue)
.catch(err => robot.logger.error(err))
);
});
return Promise.all(proposalPromises);
}
function handleGitHubPullRequestClosed(data) {
let recipients;
let pull_request = data.pull_request;
let assignees = pull_request.assignees.map(a => a.login);
let web_url = pull_request._links.html.href;
let pr_issue_url = pull_request.issue_url;
if (assignees.length > 0) {
recipients = assignees;
} else {
recipients = [pull_request.user.login];
}
return fetch(pr_issue_url)
.then(response => {
if (response.status >= 400) {
throw new Error('Bad response from fetching PR issue');
}
return response.json();
})
.then(issue => {
let amount = amountFromIssueLabels(issue);
let repoName = pull_request.base.repo.full_name;
let description = `${repoName}: ${pull_request.title}`;
if (amount === 0) {
robot.logger.info('[hubot-kredits] Proposal amount from issue label is zero; ignoring');
return Promise.resolve();
} else if (repoBlackList.includes(repoName)) {
robot.logger.debug(`[hubot-kredits] ${repoName} is on black list; ignoring`);
return Promise.resolve();
}
let proposalPromises = [];
recipients.forEach(recipient => {
console.debug(`[hubot-kredits] Creating proposal for ${recipient}...`);
proposalPromises.push(
createProposal(recipient, amount, description, web_url, pull_request)
.catch(err => robot.logger.error(err))
);
});
return Promise.all(proposalPromises);
});
}
robot.router.post('/incoming/kredits/github/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => {
let evt = req.header('X-GitHub-Event');
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}`);
if (evt === 'pull_request' && data.action === 'closed') {
handleGitHubPullRequestClosed(data).then(() => res.send(200));
}
else if (evt === 'issues' && data.action === 'closed') {
handleGitHubIssueClosed(data).then(() => res.send(200));
} else {
res.send(200);
}
});
};

175
integrations/mediawiki.js Normal file
View File

@@ -0,0 +1,175 @@
const util = require('util');
const fetch = require('node-fetch');
const groupArray = require('group-array');
const cron = require('node-cron');
module.exports = async function(robot, kredits) {
function messageRoom(message) {
robot.messageRoom(process.env.KREDITS_ROOM, message);
}
robot.logger.debug('[hubot-kredits] Loading MediaWiki integration...')
const Contributor = kredits.Contributor;
const Operator = kredits.Operator;
const wikiURL = process.env.KREDITS_MEDIAWIKI_URL;
const apiURL = wikiURL + 'api.php';
function getContributorByWikiUser(username) {
let account = {
site: url.parse(process.env.KREDITS_MEDIAWIKI_URL).hostname,
username: username
}
return Contributor.findByAccount(account).then(contributor => {
robot.logger.debug('CONTRIBUTOR: ', contributor)
if (contributor) { return contributor; } else { throw new Error(); }
});
}
function createProposal(username, amount, description, url, details={}) {
return getContributorByWikiUser(username).then(contributor => {
robot.logger.debug(`[hubot-kredits] Creating proposal to issue ${amount}₭S to ${contributor.name} for ${url}...`);
let contribution = {
contributorId: contributor.id,
amount: amount,
contributorIpfsHash: contributor.ipfsHash,
url,
description,
details,
kind: 'docs'
};
return Operator.addProposal(contribution).catch(error => {
robot.logger.error(`[hubot-kredits] Adding proposal failed:`, error);
});
}).catch(() => {
robot.logger.info(`[hubot-kredits] No contributor found for ${username}`);
messageRoom(`I wanted to propose giving kredits to wiki user ${username}, but I cannot find their info. Please add them as a contributor: https://kredits.kosmos.org`);
});
}
function fetchChanges () {
const params = [
'action=query',
'format=json',
'list=recentchanges',
'rctype=edit|new',
'rcshow=!minor|!bot|!anon|!redirect',
'rclimit=max',
'rcprop=ids|title|timestamp|user|sizes|comment|flags'
];
let endTime = robot.brain.get('kredits:mediawiki:last_processed_at');
if (endTime) {
robot.logger.debug(`[hubot-kredits] Fetching wiki edits since ${endTime}`);
params.push(`rcend=${endTime}`);
}
const url = `${apiURL}?${params.join('&')}`;
return fetch(url).then(res => {
if (res.status === 200) {
return res.json();
} else {
robot.logger.info(`Fetching ${url} returned HTTP status ${res.status}:`);
robot.logger.info(res.body);
throw Error('Unexpected response from '+url);
}
}).then(res => {
return res.query.recentchanges;
}).catch(res => {
robot.logger.error(`[hubot-kredits] Failed to fetch ${url} (likely due to a network issue)`);
});
}
function groupChangesByUser (changes) {
return Promise.resolve(groupArray(changes, 'user'));
}
function analyzeUserChanges (user, changes) {
// robot.logger.debug(`Analyzing ${changes.length} edits from ${user} ...`);
const results = {};
results.pagesCreated = changes.filter(c => c.type === 'new');
results.pagesChanged = changes.filter(c => c.type === 'edit');
results.charsAdded = changes
.map(c => { return (c.oldlen < c.newlen) ? (c.newlen - c.oldlen) : 0; })
.reduce((a, b) => a + b);
// robot.logger.debug(`Created ${results.pagesCreated.length} pages`);
// robot.logger.debug(`Edited ${results.pagesChanged.length} pages`);
// robot.logger.debug(`Added ${results.charsAdded} lines of text\n`);
return results;
}
function createProposals (changes) {
let promises = [];
Object.keys(changes).forEach(user => {
promises.push(createProposalForUserChanges(user, changes[user]));
});
return Promise.all(promises);
}
function pageTitlesFromChanges(changes) {
return [...new Set(changes.map(c => `"${c.title}"`))].join(', ');
}
function calculateAmountForChanges(details) {
let amount;
if (details.charsAdded < 280) {
// less than a tweet
amount = 500;
} else if (details.charsAdded < 2000) {
amount = 1500;
} else {
amount = 5000;
}
return amount;
}
function createProposalForUserChanges (user, changes) {
const details = analyzeUserChanges(user, changes);
const amount = calculateAmountForChanges(changes);
let desc = `Added ${details.charsAdded} characters of text.`;
if (details.pagesChanged.length > 0) {
desc = `Edited ${pageTitlesFromChanges(details.pagesChanged)}. ${desc}`;
}
if (details.pagesCreated.length > 0) {
desc = `Created ${pageTitlesFromChanges(details.pagesCreated)}. ${desc}`;
}
desc = `Wiki contributions: ${desc}`;
let url;
if (changes.length > 1) {
url = `${wikiURL}Special:Contributions/${user}?hideMinor=1`;
} else {
rc = changes[0];
url = `${wikiURL}index.php?title=${rc.title}&diff=${rc.revid}&oldid=${rc.old_revid}`;
}
return createProposal(user, amount, desc, url, details);
}
function updateTimestampForNextFetch () {
robot.logger.debug(`[hubot-kredits] Set timestamp for wiki changes fetch`);
robot.brain.set('kredits:mediawiki:last_processed_at', new Date().toISOString());
}
function processWikiChangesSinceLastRun () {
fetchChanges()
.then(res => groupChangesByUser(res))
.then(res => createProposals(res))
.then(() => updateTimestampForNextFetch());
}
cron.schedule('* 7 * * *', processWikiChangesSinceLastRun);
};

1817
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,31 @@
{
"name": "hubot-kredits",
"version": "1.6.0",
"version": "2.1.0",
"description": "Kosmos Kredits functionality for chat bots",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"kredits-wallet-create": "scripts/create-wallet.js"
"create-wallet": "scripts/create-wallet.js"
},
"dependencies": {
"ethereumjs-wallet": "mvayngrib/ethereumjs-wallet",
"ipfs-api": "^14.0.3",
"ethers": "^3.0.15",
"group-array": "^0.3.3",
"kosmos-schemas": "^1.1.2",
"kredits-contracts": "2.0.0",
"node-cron": "^1.2.1",
"node-fetch": "^1.6.3",
"prompt": "^1.0.0",
"tv4": "^1.3.0",
"web3": "^0.18.4",
"web3-provider-engine": "^12.0.3"
"kredits-contracts": "3.x"
},
"repository": {
"type": "git",
"url": "git+https://github.com/67P/hubot-kredits.git"
},
"keywords": [
"kosmos"
"kosmos",
"kredits",
"hubot-scripts"
],
"author": "Kosmos Developers <mail@kosmos.org>",
"contributors": [

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
const fs = require('fs');
const Wallet = require('ethereumjs-wallet');
const ethers = require('ethers');
const userPrompt = require('prompt');
let schema = {
@@ -24,10 +24,10 @@ userPrompt.start();
userPrompt.get(schema, (err, result) => {
if (err) { throw(err); }
let wallet = Wallet.generate();
let content = JSON.stringify(wallet.toV3(result.password));
let wallet = ethers.Wallet.createRandom();
wallet.encrypt(result.password).then((walletJSON) => {
fs.writeFileSync(result.path, walletJSON);
console.log(`\nWrote encrypted wallet config to ${result.path}`);
});
fs.writeFileSync(result.path, content);
console.log(`\nWrote encrypted wallet config to ${result.path}`);
});