diff --git a/README.md b/README.md index c6e9d23..340ebe5 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,25 @@ Point a GitHub organization webhook to the following URL: | --- | --- | | `KREDITS_GITHUB_REPO_BLACKLIST` | Repos which you do not want to issue kredits for. Format: `orgname/reponame`, e.g. `67P/test-one-two` | +### Gitea + +The Gitea 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 contribution tokens for all of them. + +#### Setup + +Point a Gitea organization webhook to the following URL: + + https://your-hubot.example.com/incoming/kredits/gitea/{webhook_token} + +#### Config + +| Key | Description | +| --- | --- | +| `KREDITS_GITEA_REPO_BLACKLIST` | Repos which you do not want to issue kredits for. Format: `orgname/reponame`, e.g. `kosmos/test-one-two` | + ### MediaWiki The MediaWiki integration will periodically check for wiki page creations and diff --git a/index.js b/index.js index 10f35e2..9e71618 100644 --- a/index.js +++ b/index.js @@ -149,6 +149,7 @@ module.exports = async function(robot) { // require('./integrations/github')(robot, kredits); + require('./integrations/gitea')(robot, kredits); if (typeof process.env.KREDITS_MEDIAWIKI_URL !== 'undefined') { require('./integrations/mediawiki')(robot, kredits); diff --git a/integrations/gitea.js b/integrations/gitea.js new file mode 100644 index 0000000..19d3163 --- /dev/null +++ b/integrations/gitea.js @@ -0,0 +1,179 @@ +const util = require('util'); +const fetch = require('node-fetch'); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +module.exports = async function(robot, kredits) { + + function messageRoom(message) { + robot.messageRoom(process.env.KREDITS_ROOM, message); + } + + robot.logger.debug('[hubot-kredits] Loading Gitea integration...'); + + let repoBlackList = []; + if (process.env.KREDITS_GITEA_REPO_BLACKLIST) { + repoBlackList = process.env.KREDITS_GITEA_REPO_BLACKLIST.split(','); + robot.logger.debug('[hubot-kredits] Ignoring Gitea actions from ', util.inspect(repoBlackList)); + } + + const Contributor = kredits.Contributor; + const Contribution = kredits.Contribution; + + function getContributorByGiteaUser(username) { + return Contributor.all().then(contributors => { + const contrib = contributors.find(c => { + return c.gitea_username === username; + }); + if (!contrib) { + throw new Error(`No contributor found for ${username}`); + } else { + return contrib; + } + }); + } + + function createContribution(giteaUser, date, time, amount, description, url, details) { + return getContributorByGiteaUser(giteaUser).then(contributor => { + robot.logger.debug(`[hubot-kredits] Creating contribution token for ${amount}₭S to ${giteaUser} for ${url}...`); + + const contributionAttr = { + contributorId: contributor.id, + contributorIpfsHash: contributor.ipfsHash, + date, + time, + amount, + url, + description, + details, + kind: 'dev' + }; + + return Contribution.addContribution(contributionAttr).catch(error => { + robot.logger.error(`[hubot-kredits] Error:`, error); + messageRoom(`I tried to add a contribution for ${giteaUser} for ${url}, but I encountered an error when submitting the tx:`); + messageRoom(error.message); + }); + }); + } + + function amountFromLabels(labels) { + const kreditsLabel = 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; + } + + async function handleGiteaIssueClosed(data) { + const issue = data.issue; + const repoName = data.repository.full_name; + const web_url = `${data.repository.html_url}/issues/${issue.id}`; + const description = `${repoName}: ${issue.title}`; + const amount = amountFromLabels(issue.labels); + const assignees = issue.assignees ? issue.assignees.map(a => a.login) : []; + [ date, time ] = issue.closed_at.split('T'); + + if (amount === 0) { + robot.logger.info('[hubot-kredits] Kredits 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 recipients; + if (assignees.length > 0) { + recipients = assignees; + } else { + recipients = [issue.user.login]; + } + + for (const recipient of recipients) { + try { + await createContribution(recipient, date, time, amount, description, web_url, + { issue, repository: data.repository }); + await sleep(60000); + } + catch (err) { robot.logger.error(err); } + } + + return Promise.resolve(); + } + + async function handleGiteaPullRequestClosed(data) { + const pull_request = data.pull_request; + const repoName = data.repository.full_name; + const web_url = pull_request.html_url; + const description = `${repoName}: ${pull_request.title}`; + const amount = amountFromLabels(pull_request.labels); + const assignees = pull_request.assignees ? pull_request.assignees.map(a => a.login) : []; + [ date, time ] = pull_request.merged_at.split('T'); + + if (amount === 0) { + robot.logger.info('[hubot-kredits] Kredits 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 recipients; + if (assignees.length > 0) { + recipients = assignees; + } else { + recipients = [pull_request.user.login]; + } + + + for (const recipient of recipients) { + try { + await createContribution(recipient, date, time, amount, description, web_url, + { pull_request, repository: data.repository }); + await sleep(60000); + } + catch (err) { robot.logger.error(err); } + } + + return Promise.resolve(); + } + + robot.router.post('/incoming/kredits/gitea/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => { + const evt = req.header('X-Gitea-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 Gitea hook. Event: ${evt}, action: ${data.action}`); + + if (evt === 'pull_request' && data.action === 'closed' && data.pull_request.merged) { + handleGiteaPullRequestClosed(data); + res.sendStatus(200); + } + else if (evt === 'issues' && data.action === 'closed') { + handleGiteaIssueClosed(data); + res.sendStatus(200); + } else { + res.sendStatus(200); + } + }); + +}; diff --git a/package-lock.json b/package-lock.json index 3b2f07e..334d2b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1175,14 +1175,13 @@ } }, "kredits-contracts": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/kredits-contracts/-/kredits-contracts-5.1.1.tgz", - "integrity": "sha512-hEM/ZOcoYejOix8LMG/mMVQVoD/9q18yjQTXeQwuX2x/APP95CH8HK/UsPIqxXtMP1L3XklWnMuqTVbYUc5UYg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/kredits-contracts/-/kredits-contracts-5.3.0.tgz", + "integrity": "sha512-Wz4zuA6yo0Q4WbVEO61fvFin+6VTNjkBqHPhHCqq6dIoGdFSjUZ3BCKan1ei0axIAda7ZDP+eebe2vCr+eqcHg==", "requires": { "ethers": "^4.0.27", "ipfs-http-client": "^30.1.1", "kosmos-schemas": "^2.0.0", - "rsvp": "^4.8.2", "tv4": "^1.3.0" }, "dependencies": { @@ -1364,9 +1363,9 @@ } }, "multicodec": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-0.5.0.tgz", - "integrity": "sha512-lKsJeT4cKeSq0rVEWhO3oSBgDN4sMY1sNZKlvl68g/ZAahjPS1KIVyF4IqhuYmCdtOyKs4Q4hQ6M0C3iqRnuqQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-0.5.1.tgz", + "integrity": "sha512-Q5glyZLdXVbbBxvRYHLQHpu8ydVf1422Z+v9fU47v2JCkiue7n+JcFS7uRv0cQW8hbVtgdtIDgYWPWaIKEXuXA==", "requires": { "varint": "^5.0.0" } @@ -1732,11 +1731,6 @@ "optimist": "~0.3.5" } }, - "rsvp": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.4.tgz", - "integrity": "sha512-6FomvYPfs+Jy9TfXmBpBuMWNH94SgCsZmJKcanySzgNNP6LjWxBvyLTa9KaMfDDM5oxRfrKDB0r/qeRsLwnBfA==" - }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", diff --git a/package.json b/package.json index e01a21f..4fdf7e3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "ethers": "^4.0.27", "group-array": "^0.3.3", "kosmos-schemas": "^1.1.2", - "kredits-contracts": "^5.1.1", + "kredits-contracts": "^5.3.0", "node-cron": "^2.0.3", "node-fetch": "^2.3.0", "prompt": "^1.0.0"