From 5a2ebb323d94935ba88a24cffd6ed26a37977b91 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Wed, 18 Apr 2018 18:12:37 +0200 Subject: [PATCH 01/19] Basic MediaWiki changes integration --- mediawiki.js | 79 +++++++++++++++++++++++ package-lock.json | 158 ++++++++++++++++++++++++++++++++++++++++------ package.json | 1 + 3 files changed, 218 insertions(+), 20 deletions(-) create mode 100644 mediawiki.js diff --git a/mediawiki.js b/mediawiki.js new file mode 100644 index 0000000..3ad181e --- /dev/null +++ b/mediawiki.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +const util = require('util'); +const fetch = require('node-fetch'); +const groupArray = require('group-array'); + +if (typeof process.env.KREDITS_MEDIAWIKI_URL === 'undefined') { return false; } +const apiURL = process.env.KREDITS_MEDIAWIKI_URL + 'api.php'; + +const robot = { + data: {}, + brain: { + set(key, value) { + this.data[key] = value; + }, + get(key) { + return this.data[key]; + } + } +}; + +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' + ]; + + const url = `${apiURL}?${params.join('&')}`; + + return fetch(url).then(res => { + if (res.status === 200) { + return res.json(); + } else { + console.log(`Fetching ${url} returned HTTP status ${res.status}:`); + console.log(res.body); + throw Error('Unexpected response from '+url); + } + }).then(res => { + return res.query.recentchanges; + }); +} + +function groupChangesByUser (changes) { + return groupArray(changes, 'user'); +} + +function analyzeUserChanges (user, changes) { + console.log(`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.linesAdded = changes + .map(c => { return (c.oldlen < c.newlen) ? (c.newlen - c.oldlen) : 0; }) + .reduce((a, b) => a + b); + + console.log(`Created ${results.pagesCreated.length} pages`); + console.log(`Edited ${results.pagesChanged.length} pages`); + console.log(`Added ${results.linesAdded} lines of text\n`); + + return results; +} + +function createProposalForUserChanges (user, changes) { + const details = analyzeUserChanges(user, changes); + + // console.log(util.inspect(details)); +} + +fetchChanges() + .then(res => groupChangesByUser(res)) + .then(res => { + Object.keys(res).forEach(user => createProposalForUserChanges(user, res[user])); + }); diff --git a/package-lock.json b/package-lock.json index f3c57c8..b02e2a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,16 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, "babel-code-frame": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", @@ -289,17 +299,6 @@ "repeating": "2.0.1" } }, - "elliptic": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.3.3.tgz", - "integrity": "sha1-VILZZG1UvLif19mU/J4ulWiHbj8=", - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.3", - "inherits": "2.0.3" - } - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -322,10 +321,26 @@ "xmlhttprequest": "1.8.0" }, "dependencies": { + "elliptic": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.3.3.tgz", + "integrity": "sha1-VILZZG1UvLif19mU/J4ulWiHbj8=", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "inherits": "2.0.1" + } + }, "inherits": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=" } } }, @@ -334,16 +349,42 @@ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, "eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -362,6 +403,19 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" }, + "group-array": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/group-array/-/group-array-0.3.3.tgz", + "integrity": "sha1-u9nS9xjfS+M/D7kEMqrxtDYOSY8=", + "requires": { + "arr-flatten": "1.1.0", + "for-own": "0.1.5", + "get-value": "2.0.6", + "kind-of": "3.2.2", + "split-string": "1.0.1", + "union-value": "0.2.4" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -376,7 +430,7 @@ "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", "requires": { "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" + "minimalistic-assert": "1.0.0" } }, "home-or-tmp": { @@ -415,6 +469,16 @@ "loose-envify": "1.3.1" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, "is-finite": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", @@ -423,21 +487,29 @@ "number-is-nan": "1.0.1" } }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "3.0.1" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "js-sha3": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", - "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=" - }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -453,6 +525,14 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + }, "kosmos-schemas": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/kosmos-schemas/-/kosmos-schemas-1.1.2.tgz", @@ -475,9 +555,9 @@ } }, "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" }, "minimatch": { "version": "3.0.4", @@ -636,6 +716,17 @@ "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.3.tgz", "integrity": "sha1-uwBAvgMEPamgEqLOqfyfhSz8h9Q=" }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + }, "setimmediate": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", @@ -661,6 +752,14 @@ } } }, + "split-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-1.0.1.tgz", + "integrity": "sha1-vLqz9BUqzuOg1qskecDSh5w9s84=", + "requires": { + "extend-shallow": "2.0.1" + } + }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -701,11 +800,30 @@ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "3.2.2" + } + }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" }, + "union-value": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-0.2.4.tgz", + "integrity": "sha1-c3UVJ4ZnkFfns3qmdug0aPwCdPA=", + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 01d07cb..99ac407 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "ethers": "^3.0.15", + "group-array": "^0.3.3", "kosmos-schemas": "^1.1.2", "node-fetch": "^2.1.1", "prompt": "^1.0.0", From 33cefac88e03c6db76c75a57dd2f4af6c35c0641 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 11:28:01 +0200 Subject: [PATCH 02/19] Add node-cron --- package-lock.json | 41 ++++++++++++++++++++++++++++++++++++++--- package.json | 3 ++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b02e2a9..1eef311 100644 --- a/package-lock.json +++ b/package-lock.json @@ -299,6 +299,14 @@ "repeating": "2.0.1" } }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "0.4.21" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -447,6 +455,14 @@ "resolved": "https://registry.npmjs.org/i/-/i-0.3.5.tgz", "integrity": "sha1-HSuFQVjsgWkRPGy39raAHpniEdU=" }, + "iconv-lite": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "requires": { + "safer-buffer": "2.1.2" + } + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -495,6 +511,11 @@ "isobject": "3.0.1" } }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -597,10 +618,19 @@ "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=" }, + "node-cron": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-1.2.1.tgz", + "integrity": "sha1-jJC8XccjpWKJsHhmVatKHEy2A2g=" + }, "node-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", - "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } }, "number-is-nan": { "version": "1.0.1", @@ -711,6 +741,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "scrypt-js": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.3.tgz", diff --git a/package.json b/package.json index 99ac407..6c5875b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "ethers": "^3.0.15", "group-array": "^0.3.3", "kosmos-schemas": "^1.1.2", - "node-fetch": "^2.1.1", + "node-cron": "^1.2.1", + "node-fetch": "^1.6.3", "prompt": "^1.0.0", "truffle-kredits": "github:67P/truffle-kredits#library" }, From 5259b56e53b766e886d638752f1df76590f4fbd8 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 11:50:59 +0200 Subject: [PATCH 03/19] Split out integrations, add code section comments --- index.js | 190 ++++++++--------------------------------- integrations/github.js | 153 +++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 155 deletions(-) create mode 100644 integrations/github.js diff --git a/index.js b/index.js index 1f41f72..96072db 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,15 @@ const ipfsConfig = { }; module.exports = async function(robot) { + + function messageRoom(message) { + robot.messageRoom(process.env.KREDITS_ROOM, message); + } + + // + // Ethereum wallet setup + // + let wallet; try { wallet = await ethers.Wallet.fromEncryptedWallet(walletJson, process.env.KREDITS_WALLET_PASSWORD); @@ -39,26 +48,34 @@ module.exports = async function(robot) { process.exit(1); } + // + // Ethereum provider/node setup + // + const ethProvider = new ethers.providers.JsonRpcProvider(providerUrl, {chainId: networkId}); ethProvider.signer = wallet; wallet.provider = ethProvider; + // + // Kredits contracts setup + // + let kredits; try { kredits = await Kredits.setup(ethProvider, wallet, ipfsConfig); } catch(error) { - console.log('[hubot-kredits] Could not setup kredits:', error); + console.log('[hubot-kredits] Could not set up kredits:', error); process.exit(1); } const Contributor = kredits.Contributor; const Operator = kredits.Operator; - function messageRoom(message) { - robot.messageRoom(process.env.KREDITS_ROOM, message); - } - 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'))) { @@ -66,6 +83,10 @@ module.exports = async function(robot) { } }); + // + // 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`); @@ -93,156 +114,9 @@ module.exports = async function(robot) { }); }); - 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}`);A - } else { - return contrib; - } - }); - } - - function createProposal(githubUser, amount, description, url, details) { - return getContributorByGithubUser(githubUser).then((contributor) => { - robot.logger.debug(`[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).then((result) => { - robot.logger.debug('[kredits] proposal created:', util.inspect(result)); - }); - }).catch((error) => { - console.log([hubot-kredits] Error:, error); - messageRoom(`I wanted to propose giving kredits to ${githubUser} for ${url}, but I can't find their contact data. 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 = 50; - break; - case 'kredits-2': - amount = 150; - break; - case 'kredits-3': - amount = 500; - 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); - if (amount === 0) { - console.log('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); - return Promise.resolve(); - } - - if (assignees.length > 0) { - recipients = assignees; - } else { - recipients = [issue.user.login]; - } - - let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1]; - let description = `${repoName}: ${issue.title}`; - - let proposalPromisses = []; - recipients.forEach(recipient => { - proposalPromisses.push( - createProposal(recipient, amount, description, web_url, issue) - .catch(err => robot.logger.error(err)) - ); - }); - - return Promise.all(proposalPromisses); - } - - 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); - if (amount === 0) { - console.log('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); - return; - } - - let repoName = pull_request.base.repo.full_name; - let description = `${repoName}: ${pull_request.title}`; - - let proposalPromisses = []; - recipients.forEach(recipient => { - console.debug(`[hubot-kredits] Creating proposal for ${recipient}...`); - proposalPromisses.push( - createProposal(recipient, amount, description, web_url, pull_request) - .catch(err => robot.logger.error(err)) - ); - }); - return Promise.all(proposalPromisses); - }); - } - - - 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); - } - }); + // + // Smart contract events + // function watchContractEvents() { ethProvider.getBlockNumber().then((blockNumber) => { @@ -267,4 +141,10 @@ module.exports = async function(robot) { watchContractEvents(); + // + // Integrations + // + + require('integrations/github')(robot, kredits); + }; diff --git a/integrations/github.js b/integrations/github.js new file mode 100644 index 0000000..99805d8 --- /dev/null +++ b/integrations/github.js @@ -0,0 +1,153 @@ +module.exports = async function(robot, kredits) { + + 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}`);A + } else { + return contrib; + } + }); + } + + function createProposal(githubUser, amount, description, url, details) { + return getContributorByGithubUser(githubUser).then((contributor) => { + robot.logger.debug(`[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).then((result) => { + robot.logger.debug('[kredits] proposal created:', util.inspect(result)); + }); + }).catch((error) => { + console.log([hubot-kredits] Error:, error); + messageRoom(`I wanted to propose giving kredits to ${githubUser} for ${url}, but I can't find their contact data. 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 = 50; + break; + case 'kredits-2': + amount = 150; + break; + case 'kredits-3': + amount = 500; + 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); + if (amount === 0) { + console.log('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); + return Promise.resolve(); + } + + if (assignees.length > 0) { + recipients = assignees; + } else { + recipients = [issue.user.login]; + } + + let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1]; + let description = `${repoName}: ${issue.title}`; + + let proposalPromisses = []; + recipients.forEach(recipient => { + proposalPromisses.push( + createProposal(recipient, amount, description, web_url, issue) + .catch(err => robot.logger.error(err)) + ); + }); + + return Promise.all(proposalPromisses); + } + + 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); + if (amount === 0) { + console.log('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); + return; + } + + let repoName = pull_request.base.repo.full_name; + let description = `${repoName}: ${pull_request.title}`; + + let proposalPromisses = []; + recipients.forEach(recipient => { + console.debug(`[hubot-kredits] Creating proposal for ${recipient}...`); + proposalPromisses.push( + createProposal(recipient, amount, description, web_url, pull_request) + .catch(err => robot.logger.error(err)) + ); + }); + return Promise.all(proposalPromisses); + }); + } + + 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); + } + }); + +}; From 462efcefbda0f2179e681b4636b15090799f7c51 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 12:10:34 +0200 Subject: [PATCH 04/19] Use robot.logger --- index.js | 4 ++-- integrations/github.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 96072db..f6972c1 100644 --- a/index.js +++ b/index.js @@ -44,7 +44,7 @@ module.exports = async function(robot) { try { wallet = await ethers.Wallet.fromEncryptedWallet(walletJson, process.env.KREDITS_WALLET_PASSWORD); } catch(error) { - console.log('[hubot-kredits] Could not load wallet:', error); + robot.logger.warn('[hubot-kredits] Could not load wallet:', error); process.exit(1); } @@ -64,7 +64,7 @@ module.exports = async function(robot) { try { kredits = await Kredits.setup(ethProvider, wallet, ipfsConfig); } catch(error) { - console.log('[hubot-kredits] Could not set up kredits:', error); + robot.logger.warn('[hubot-kredits] Could not set up kredits:', error); process.exit(1); } const Contributor = kredits.Contributor; diff --git a/integrations/github.js b/integrations/github.js index 99805d8..79c9d15 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -29,7 +29,7 @@ module.exports = async function(robot, kredits) { robot.logger.debug('[kredits] proposal created:', util.inspect(result)); }); }).catch((error) => { - console.log([hubot-kredits] Error:, error); + robot.logger.info(`[hubot-kredits] Error:`, error); messageRoom(`I wanted to propose giving kredits to ${githubUser} for ${url}, but I can't find their contact data. Please add them as a contributor: https://kredits.kosmos.org`); }); } @@ -65,7 +65,7 @@ module.exports = async function(robot, kredits) { let amount = amountFromIssueLabels(issue); if (amount === 0) { - console.log('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); + robot.logger.info('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); return Promise.resolve(); } @@ -112,7 +112,7 @@ module.exports = async function(robot, kredits) { .then(issue => { let amount = amountFromIssueLabels(issue); if (amount === 0) { - console.log('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); + robot.logger.info('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); return; } From e8da47db70aab6ed1754b5965e9238d950fcf29d Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 12:10:44 +0200 Subject: [PATCH 05/19] Use relative path for integration module --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index f6972c1..2773f5c 100644 --- a/index.js +++ b/index.js @@ -145,6 +145,6 @@ module.exports = async function(robot) { // Integrations // - require('integrations/github')(robot, kredits); + require('./integrations/github')(robot, kredits); }; From ed5d127b4c229d05fc6dfa19dfcff09ea6f3090d Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 12:18:12 +0200 Subject: [PATCH 06/19] Add missing variables to GitHub integration --- integrations/github.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integrations/github.js b/integrations/github.js index 79c9d15..102437e 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -1,5 +1,8 @@ module.exports = async function(robot, kredits) { + const Contributor = kredits.Contributor; + const Operator = kredits.Operator; + function getContributorByGithubUser(username) { return Contributor.all().then(contributors => { let contrib = contributors.find(c => { From 74429feb9da8a62cf0fadd056a9c4ebcc04f1719 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 12:31:36 +0200 Subject: [PATCH 07/19] Use contracts from master --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c5875b..43e3c10 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "node-cron": "^1.2.1", "node-fetch": "^1.6.3", "prompt": "^1.0.0", - "truffle-kredits": "github:67P/truffle-kredits#library" + "truffle-kredits": "github:67P/truffle-kredits" }, "repository": { "type": "git", From c0c2f97dae2aaafa3f7f5e47cd2eff0cbd67bb99 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 12:32:03 +0200 Subject: [PATCH 08/19] Add MediaWiki integration basics --- index.js | 4 ++ integrations/github.js | 2 + integrations/mediawiki.js | 85 +++++++++++++++++++++++++++++++++++++++ mediawiki.js | 79 ------------------------------------ 4 files changed, 91 insertions(+), 79 deletions(-) create mode 100644 integrations/mediawiki.js delete mode 100644 mediawiki.js diff --git a/index.js b/index.js index 2773f5c..e8ff887 100644 --- a/index.js +++ b/index.js @@ -147,4 +147,8 @@ module.exports = async function(robot) { require('./integrations/github')(robot, kredits); + if (typeof process.env.KREDITS_MEDIAWIKI_URL !== 'undefined') { + require('./integrations/mediawiki')(robot, kredits); + } + }; diff --git a/integrations/github.js b/integrations/github.js index 102437e..9f276fb 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -1,5 +1,7 @@ module.exports = async function(robot, kredits) { + robot.logger.debug('[hubot-kredits] Loading GitHub integration...') + const Contributor = kredits.Contributor; const Operator = kredits.Operator; diff --git a/integrations/mediawiki.js b/integrations/mediawiki.js new file mode 100644 index 0000000..aa1d56b --- /dev/null +++ b/integrations/mediawiki.js @@ -0,0 +1,85 @@ +const util = require('util'); +const fetch = require('node-fetch'); +const groupArray = require('group-array'); + +module.exports = async function(robot, kredits) { + + robot.logger.debug('[hubot-kredits] Loading MediaWiki integration...') + + const Contributor = kredits.Contributor; + const Operator = kredits.Operator; + + const apiURL = process.env.KREDITS_MEDIAWIKI_URL + 'api.php'; + + const robot = { + data: {}, + brain: { + set(key, value) { + this.data[key] = value; + }, + get(key) { + return this.data[key]; + } + } + }; + + 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' + ]; + + const url = `${apiURL}?${params.join('&')}`; + + return fetch(url).then(res => { + if (res.status === 200) { + return res.json(); + } else { + robot.logger.warn(`Fetching ${url} returned HTTP status ${res.status}:`); + robot.logger.warn(res.body); + throw Error('Unexpected response from '+url); + } + }).then(res => { + return res.query.recentchanges; + }); + } + + function groupChangesByUser (changes) { + return groupArray(changes, 'user'); + } + + function analyzeUserChanges (user, changes) { + robot.logger.info(`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.linesAdded = changes + .map(c => { return (c.oldlen < c.newlen) ? (c.newlen - c.oldlen) : 0; }) + .reduce((a, b) => a + b); + + robot.logger.info(`Created ${results.pagesCreated.length} pages`); + robot.logger.info(`Edited ${results.pagesChanged.length} pages`); + robot.logger.info(`Added ${results.linesAdded} lines of text\n`); + + return results; + } + + function createProposalForUserChanges (user, changes) { + const details = analyzeUserChanges(user, changes); + + // robot.logger.info(util.inspect(details)); + } + + fetchChanges() + .then(res => groupChangesByUser(res)) + .then(res => { + Object.keys(res).forEach(user => createProposalForUserChanges(user, res[user])); + }); + +}; diff --git a/mediawiki.js b/mediawiki.js deleted file mode 100644 index 3ad181e..0000000 --- a/mediawiki.js +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env node - -const util = require('util'); -const fetch = require('node-fetch'); -const groupArray = require('group-array'); - -if (typeof process.env.KREDITS_MEDIAWIKI_URL === 'undefined') { return false; } -const apiURL = process.env.KREDITS_MEDIAWIKI_URL + 'api.php'; - -const robot = { - data: {}, - brain: { - set(key, value) { - this.data[key] = value; - }, - get(key) { - return this.data[key]; - } - } -}; - -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' - ]; - - const url = `${apiURL}?${params.join('&')}`; - - return fetch(url).then(res => { - if (res.status === 200) { - return res.json(); - } else { - console.log(`Fetching ${url} returned HTTP status ${res.status}:`); - console.log(res.body); - throw Error('Unexpected response from '+url); - } - }).then(res => { - return res.query.recentchanges; - }); -} - -function groupChangesByUser (changes) { - return groupArray(changes, 'user'); -} - -function analyzeUserChanges (user, changes) { - console.log(`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.linesAdded = changes - .map(c => { return (c.oldlen < c.newlen) ? (c.newlen - c.oldlen) : 0; }) - .reduce((a, b) => a + b); - - console.log(`Created ${results.pagesCreated.length} pages`); - console.log(`Edited ${results.pagesChanged.length} pages`); - console.log(`Added ${results.linesAdded} lines of text\n`); - - return results; -} - -function createProposalForUserChanges (user, changes) { - const details = analyzeUserChanges(user, changes); - - // console.log(util.inspect(details)); -} - -fetchChanges() - .then(res => groupChangesByUser(res)) - .then(res => { - Object.keys(res).forEach(user => createProposalForUserChanges(user, res[user])); - }); From aab5b58bab0816d1d71148749f7f959219270057 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 14:35:26 +0200 Subject: [PATCH 09/19] Add missing module imports --- integrations/github.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integrations/github.js b/integrations/github.js index 9f276fb..39fd47a 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -1,3 +1,6 @@ +const util = require('util'); +const fetch = require('node-fetch'); + module.exports = async function(robot, kredits) { robot.logger.debug('[hubot-kredits] Loading GitHub integration...') From 48a42d4f2cd077e6ab949897c509ea04bffa0115 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 14:36:47 +0200 Subject: [PATCH 10/19] Improve logging --- index.js | 4 ++-- integrations/github.js | 17 +++++++++-------- integrations/mediawiki.js | 8 ++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index e8ff887..f3de2c8 100644 --- a/index.js +++ b/index.js @@ -123,7 +123,7 @@ module.exports = async function(robot) { // 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`); + robot.logger.debug(`[hubot-kredits] Watching events from block ${nextBlock} onward`); ethProvider.resetEventsBlock(nextBlock); Operator.on('ProposalCreated', handleProposalCreated); @@ -133,7 +133,7 @@ module.exports = async function(robot) { function handleProposalCreated(proposalId, creatorAccount, contributorId, amount) { Contributor.getById(contributorId).then((contributor) => { Operator.getById(proposalId).then((proposal) => { - console.debug('Proposal created:', 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`); }); }); diff --git a/integrations/github.js b/integrations/github.js index 39fd47a..5f56a72 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -3,7 +3,8 @@ const fetch = require('node-fetch'); module.exports = async function(robot, kredits) { - robot.logger.debug('[hubot-kredits] Loading GitHub integration...') + robot.logger.debug('[hubot-kredits] Loading GitHub integration...'); + const Contributor = kredits.Contributor; const Operator = kredits.Operator; @@ -22,8 +23,9 @@ module.exports = async function(robot, kredits) { } function createProposal(githubUser, amount, description, url, details) { - return getContributorByGithubUser(githubUser).then((contributor) => { - robot.logger.debug(`[kredits] Creating proposal to issue ${amount}₭S to ${githubUser} for ${url}...`); + 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, @@ -33,13 +35,12 @@ module.exports = async function(robot, kredits) { details, kind: 'dev' }; - return Operator.addProposal(contributionAttr).then((result) => { - robot.logger.debug('[kredits] proposal created:', util.inspect(result)); - }); - }).catch((error) => { + + return Operator.addProposal(contributionAttr).catch(error => { robot.logger.info(`[hubot-kredits] Error:`, error); - messageRoom(`I wanted to propose giving kredits to ${githubUser} for ${url}, but I can't find their contact data. Please add them as a contributor: https://kredits.kosmos.org`); + messageRoom(`I wanted to propose giving kredits to ${githubUser} for ${url}, but I cannot find their contact data. Please add them as a contributor: https://kredits.kosmos.org`); }); + }); } function amountFromIssueLabels(issue) { diff --git a/integrations/mediawiki.js b/integrations/mediawiki.js index aa1d56b..f4c074b 100644 --- a/integrations/mediawiki.js +++ b/integrations/mediawiki.js @@ -54,7 +54,7 @@ module.exports = async function(robot, kredits) { } function analyzeUserChanges (user, changes) { - robot.logger.info(`Analyzing ${changes.length} edits from ${user} ...`); + robot.logger.debug(`Analyzing ${changes.length} edits from ${user} ...`); const results = {}; results.pagesCreated = changes.filter(c => c.type === 'new'); @@ -63,9 +63,9 @@ module.exports = async function(robot, kredits) { .map(c => { return (c.oldlen < c.newlen) ? (c.newlen - c.oldlen) : 0; }) .reduce((a, b) => a + b); - robot.logger.info(`Created ${results.pagesCreated.length} pages`); - robot.logger.info(`Edited ${results.pagesChanged.length} pages`); - robot.logger.info(`Added ${results.linesAdded} lines of text\n`); + robot.logger.debug(`Created ${results.pagesCreated.length} pages`); + robot.logger.debug(`Edited ${results.pagesChanged.length} pages`); + robot.logger.debug(`Added ${results.linesAdded} lines of text\n`); return results; } From 22b480ae92dcd7f787d9802a2c432808a4a13c80 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 14:37:55 +0200 Subject: [PATCH 11/19] Fix typos --- integrations/github.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/integrations/github.js b/integrations/github.js index 5f56a72..cdd4ed2 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -87,15 +87,15 @@ module.exports = async function(robot, kredits) { let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1]; let description = `${repoName}: ${issue.title}`; - let proposalPromisses = []; + let proposalPromises = []; recipients.forEach(recipient => { - proposalPromisses.push( + proposalPromises.push( createProposal(recipient, amount, description, web_url, issue) .catch(err => robot.logger.error(err)) ); }); - return Promise.all(proposalPromisses); + return Promise.all(proposalPromises); } function handleGitHubPullRequestClosed(data) { @@ -127,16 +127,16 @@ module.exports = async function(robot, kredits) { let repoName = pull_request.base.repo.full_name; let description = `${repoName}: ${pull_request.title}`; - - let proposalPromisses = []; + let proposalPromises = []; recipients.forEach(recipient => { console.debug(`[hubot-kredits] Creating proposal for ${recipient}...`); - proposalPromisses.push( + proposalPromises.push( createProposal(recipient, amount, description, web_url, pull_request) .catch(err => robot.logger.error(err)) ); }); - return Promise.all(proposalPromisses); + + return Promise.all(proposalPromises); }); } From 3d810287c03795f498443c74a489f787bcde670b Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 14:38:09 +0200 Subject: [PATCH 12/19] Implement GitHub repo blacklist closes #17 --- integrations/github.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/integrations/github.js b/integrations/github.js index cdd4ed2..3ec47d4 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -5,6 +5,11 @@ module.exports = async function(robot, kredits) { 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; @@ -15,7 +20,7 @@ module.exports = async function(robot, kredits) { return c.github_username === username; }); if (!contrib) { - throw new Error(`No contributor found for ${username}`);A + throw new Error(`No contributor found for ${username}`); } else { return contrib; } @@ -73,9 +78,15 @@ module.exports = async function(robot, kredits) { 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) { @@ -84,9 +95,6 @@ module.exports = async function(robot, kredits) { recipients = [issue.user.login]; } - let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1]; - let description = `${repoName}: ${issue.title}`; - let proposalPromises = []; recipients.forEach(recipient => { proposalPromises.push( @@ -120,13 +128,17 @@ module.exports = async function(robot, kredits) { }) .then(issue => { let amount = amountFromIssueLabels(issue); - if (amount === 0) { - robot.logger.info('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); - return; - } - 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}...`); From 7904aa8c8407d32dae5e00415133d736cd682cc9 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 14:39:02 +0200 Subject: [PATCH 13/19] Remove robot stub from mediawiki --- integrations/mediawiki.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/integrations/mediawiki.js b/integrations/mediawiki.js index f4c074b..7e15dc3 100644 --- a/integrations/mediawiki.js +++ b/integrations/mediawiki.js @@ -11,18 +11,6 @@ module.exports = async function(robot, kredits) { const apiURL = process.env.KREDITS_MEDIAWIKI_URL + 'api.php'; - const robot = { - data: {}, - brain: { - set(key, value) { - this.data[key] = value; - }, - get(key) { - return this.data[key]; - } - } - }; - function fetchChanges () { const params = [ 'action=query', From 2780c87aaaa9260969c81dea7bd01f6cf33d6b74 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 17:10:40 +0200 Subject: [PATCH 14/19] Last GitHub fix --- integrations/github.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integrations/github.js b/integrations/github.js index 3ec47d4..61f274b 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -3,6 +3,10 @@ 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 = []; @@ -42,8 +46,8 @@ module.exports = async function(robot, kredits) { }; return Operator.addProposal(contributionAttr).catch(error => { - robot.logger.info(`[hubot-kredits] Error:`, error); - messageRoom(`I wanted to propose giving kredits to ${githubUser} for ${url}, but I cannot find their contact data. Please add them as a contributor: https://kredits.kosmos.org`); + 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`); }); }); } From 9d8b2c08dcaa1b5905e161098b944a04a6b36d36 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 17:11:02 +0200 Subject: [PATCH 15/19] [WIP] Add proposals for wiki changes --- integrations/mediawiki.js | 103 ++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/integrations/mediawiki.js b/integrations/mediawiki.js index 7e15dc3..9166321 100644 --- a/integrations/mediawiki.js +++ b/integrations/mediawiki.js @@ -4,6 +4,10 @@ const groupArray = require('group-array'); 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; @@ -11,6 +15,45 @@ module.exports = async function(robot, kredits) { const apiURL = process.env.KREDITS_MEDIAWIKI_URL + 'api.php'; + function getContributorByWikiUser(username) { + return Contributor.all().then(contributors => { + let contrib = contributors.find(c => { + if (typeof c.accounts !== 'object') { return false; } + return c.accounts.find(a => { + a.url === `${process.env.KREDITS_MEDIAWIKI_URL}User:${username}`; + }); + }); + if (!contrib) { + throw new Error(); + } else { + return contrib; + } + }); + } + + 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', @@ -28,12 +71,14 @@ module.exports = async function(robot, kredits) { if (res.status === 200) { return res.json(); } else { - robot.logger.warn(`Fetching ${url} returned HTTP status ${res.status}:`); - robot.logger.warn(res.body); + 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)`); }); } @@ -42,7 +87,7 @@ module.exports = async function(robot, kredits) { } function analyzeUserChanges (user, changes) { - robot.logger.debug(`Analyzing ${changes.length} edits from ${user} ...`); + // robot.logger.debug(`Analyzing ${changes.length} edits from ${user} ...`); const results = {}; results.pagesCreated = changes.filter(c => c.type === 'new'); @@ -51,23 +96,59 @@ module.exports = async function(robot, kredits) { .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.linesAdded} lines of text\n`); - + // robot.logger.debug(`Created ${results.pagesCreated.length} pages`); + // robot.logger.debug(`Edited ${results.pagesChanged.length} pages`); + // robot.logger.debug(`Added ${results.linesAdded} 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 changes.map(c => `"${c.title}"`).join(', '); + } + + function calculateAmountForChanges(details) { + let amount; + + amount = '50'; + + return amount; + } + function createProposalForUserChanges (user, changes) { const details = analyzeUserChanges(user, changes); + const amount = calculateAmountForChanges(changes); - // robot.logger.info(util.inspect(details)); + let desc = `Added ${details.linesAdded} lines of text.`; + if (details.pagesChanged.length > 0) { + desc = `Edited ${pageTitlesFromChanges(details.pagesChanged)}. ${desc}`; + } + if (details.pagesCreated.length > 0) { + desc = `Created ${pageTitlesFromChanges(details.pagesCreated)}. ${desc}`; + } + + let url; + if (changes.length > 1) { + url = `https://wiki.kosmos.org/Special:Contributions/${user}?hideMinor=1`; + } else { + rc = changes[0]; + url = `https://wiki.kosmos.org/index.php?title=${rc.title}&diff=${rc.revid}&oldid=${rc.old_revid}`; + } + + return createProposal(user, amount, desc, url, details); } fetchChanges() .then(res => groupChangesByUser(res)) - .then(res => { - Object.keys(res).forEach(user => createProposalForUserChanges(user, res[user])); - }); + .then(res => createProposals(res)); }; From 6c3070b43bc6acdc9c035d340a66ed2b2fe8a165 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 17:54:59 +0200 Subject: [PATCH 16/19] Increase kredits amounts for GitHub labels --- integrations/github.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integrations/github.js b/integrations/github.js index 61f274b..ddc2da6 100644 --- a/integrations/github.js +++ b/integrations/github.js @@ -62,13 +62,13 @@ module.exports = async function(robot, kredits) { let amount; switch(kreditsLabel) { case 'kredits-1': - amount = 50; + amount = 500; break; case 'kredits-2': - amount = 150; + amount = 1500; break; case 'kredits-3': - amount = 500; + amount = 5000; break; } From d4a3a9c9df935cd8a05722690677c5e8c93b91c4 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 18:00:19 +0200 Subject: [PATCH 17/19] Add hardcoded amounts for wiki edits --- integrations/mediawiki.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/integrations/mediawiki.js b/integrations/mediawiki.js index 9166321..a755b64 100644 --- a/integrations/mediawiki.js +++ b/integrations/mediawiki.js @@ -92,13 +92,13 @@ module.exports = async function(robot, kredits) { results.pagesCreated = changes.filter(c => c.type === 'new'); results.pagesChanged = changes.filter(c => c.type === 'edit'); - results.linesAdded = changes + 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.linesAdded} lines of text\n`); + // robot.logger.debug(`Added ${results.charsAdded} lines of text\n`); return results; } @@ -119,7 +119,14 @@ module.exports = async function(robot, kredits) { function calculateAmountForChanges(details) { let amount; - amount = '50'; + if (details.charsAdded < 280) { + // less than a tweet + amount = 500; + } else if (details.charsAdded < 2000) { + amount = 1500; + } else { + amount = 5000; + } return amount; } @@ -128,7 +135,7 @@ module.exports = async function(robot, kredits) { const details = analyzeUserChanges(user, changes); const amount = calculateAmountForChanges(changes); - let desc = `Added ${details.linesAdded} lines of text.`; + let desc = `Added ${details.charsAdded} characters of text.`; if (details.pagesChanged.length > 0) { desc = `Edited ${pageTitlesFromChanges(details.pagesChanged)}. ${desc}`; } From c97cc82817428c92152c99e9528ebe648fce08e9 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 19:04:20 +0200 Subject: [PATCH 18/19] Only fetch wiki changes since last processing --- integrations/mediawiki.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/integrations/mediawiki.js b/integrations/mediawiki.js index a755b64..77723b4 100644 --- a/integrations/mediawiki.js +++ b/integrations/mediawiki.js @@ -65,6 +65,12 @@ module.exports = async function(robot, kredits) { '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 => { @@ -83,7 +89,7 @@ module.exports = async function(robot, kredits) { } function groupChangesByUser (changes) { - return groupArray(changes, 'user'); + return Promise.resolve(groupArray(changes, 'user')); } function analyzeUserChanges (user, changes) { @@ -154,8 +160,14 @@ module.exports = async function(robot, kredits) { 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()); + } + fetchChanges() .then(res => groupChangesByUser(res)) - .then(res => createProposals(res)); + .then(res => createProposals(res)) + .then(() => updateTimestampForNextFetch()); }; From 581b15da6900a58b1f1f62803c3e69e9c5201418 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 19 Apr 2018 19:21:05 +0200 Subject: [PATCH 19/19] Cron job for checking mediawiki changes --- integrations/mediawiki.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/integrations/mediawiki.js b/integrations/mediawiki.js index 77723b4..e86c5c8 100644 --- a/integrations/mediawiki.js +++ b/integrations/mediawiki.js @@ -1,6 +1,7 @@ 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) { @@ -165,9 +166,13 @@ module.exports = async function(robot, kredits) { robot.brain.set('kredits:mediawiki:last_processed_at', new Date().toISOString()); } - fetchChanges() - .then(res => groupChangesByUser(res)) - .then(res => createProposals(res)) - .then(() => updateTimestampForNextFetch()); + function processWikiChangesSinceLastRun () { + fetchChanges() + .then(res => groupChangesByUser(res)) + .then(res => createProposals(res)) + .then(() => updateTimestampForNextFetch()); + } + + cron.schedule('* 7 * * *', processWikiChangesSinceLastRun); };