67 Commits

Author SHA1 Message Date
35f6acc150 3.6.0 2020-05-22 12:10:24 +02:00
095a1e0004 Merge pull request #56 from 67P/feature/zoom-meeting-whitelist
Add zoom meeting whitelist
2020-05-18 10:43:25 +02:00
95290a7715 Apply suggestions from code review
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2020-05-14 12:13:38 +02:00
fca017c61a Add readme for zoom integration 2020-05-14 10:38:51 +02:00
fb1a471303 Make kredits amount for zoom calls configurable
defaults to 500 - a general small contribution
2020-05-14 10:33:13 +02:00
d82e2e9256 Revert "Update integrations/zoom.js"
This reverts commit 634dc207e6.
2020-05-14 10:00:00 +02:00
634dc207e6 Update integrations/zoom.js
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2020-05-14 09:45:42 +02:00
6fd3989118 Add zoom meeting whitelist
This allows to only record meetings for certain whitelisted meeting ids
2020-04-30 16:19:49 +02:00
41f5aef460 3.5.1 2020-04-16 21:43:48 +02:00
c121713a13 Merge pull request #54 from 67P/bugfix/ignore-small-meetings
Ignore meetings that have less than 3 unique participants
2020-04-16 21:17:16 +02:00
e10dd4abc3 Ignore meetings that have less than 3 unique participants
zoom's participants_count is not unique and the same person can be counted
multiple times.
We need to check for unique names.
2020-04-16 18:07:17 +02:00
e823797ee2 3.5.0 2020-04-16 17:40:46 +02:00
7d3c2cae19 Merge branch 'feature/zoom' 2020-04-16 17:34:31 +02:00
960dcb55de Moar await 2020-04-16 17:33:53 +02:00
60ed697460 Add comment when tx is undefined
We createContributionFor() simply returns if no contributor is found.
2020-04-16 17:17:40 +02:00
7f653f23ce typo 2020-04-16 17:08:26 +02:00
98ff61ab0a Add handling of missing zoom profiles 2020-04-16 17:03:03 +02:00
e7f8723f6e Make sure zoom participants are unique
to make sure we only create one contribution per participants
2020-04-16 16:36:32 +02:00
164782bd25 Only load Zoom integration when JWT configured 2020-04-16 12:18:52 +02:00
8f961bb102 Apply suggestions from code review
Co-Authored-By: Sebastian Kippe <sebastian@kip.pe>
2020-04-16 12:08:50 +02:00
c4ef8de018 Nicer log messages
Co-Authored-By: Sebastian Kippe <sebastian@kip.pe>
2020-04-16 12:08:06 +02:00
110c4384e0 Autoload zoom integration 2020-04-15 21:51:02 +02:00
70ea031b31 Zoom integration using the JWT API 2020-04-15 21:29:21 +02:00
ab8d043593 Merge pull request #52 from 67P/dependabot/npm_and_yarn/kind-of-6.0.3
Bump kind-of from 6.0.2 to 6.0.3
2020-03-31 19:56:23 -05:00
dependabot[bot]
8b5c2a3274 Bump kind-of from 6.0.2 to 6.0.3
Bumps [kind-of](https://github.com/jonschlinkert/kind-of) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/jonschlinkert/kind-of/releases)
- [Changelog](https://github.com/jonschlinkert/kind-of/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jonschlinkert/kind-of/compare/6.0.2...6.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-04-01 00:54:28 +00:00
5cc0116163 Skeleton of the zoom integration
using the new zoom API
2020-02-27 15:48:55 +01:00
708f0b6622 Add release-drafter config 2019-09-01 17:06:38 +02:00
f20a8327af 3.4.1 2019-09-01 17:05:42 +02:00
571de43aa8 Merge pull request #47 from 67P/chore/fix_warnings
Fix warnings from express session
2019-09-01 17:05:22 +02:00
50002194c5 Fix warnings from express session 2019-09-01 17:04:19 +02:00
cecc632620 Merge pull request #46 from 67P/bugfix/signup_issue
Fix critical bug in signup code
2019-09-01 17:03:06 +02:00
6972d6c88e Fix critical bug in signup code
Variable scope/assignment issue
2019-09-01 17:00:56 +02:00
157d2dee49 3.4.0 2019-08-31 13:42:50 +02:00
590ca961a6 Merge pull request #45 from 67P/docs/github-signup
Add GitHub signup oracle documentation
2019-08-31 13:26:23 +02:00
6476fe5c88 Improve signup docs 2019-08-31 13:23:11 +02:00
101e35622a Add GitHub signup oracle documentation 2019-08-31 12:52:36 +02:00
afa67e5ac7 Prefix all configuration environment variables
The convention here is to prefix all Kredit related configuration
variables with `KREDITS_`
2019-08-31 12:51:44 +02:00
cbbc6c359e Merge pull request #41 from 67P/feature/github-signup
Add GitHub signup oracle
2019-08-28 15:56:54 +02:00
c16ea51769 Error handling for fetching Github access token 2019-08-27 18:35:40 +02:00
fc49213b01 Update contracts min version 2019-08-27 18:23:22 +02:00
32dc6a7358 GitHub signup fixes
* Don't fail when profile has no name set
* Use scope that can only read public profile information (no scope)
* Better error handling
2019-08-27 18:22:36 +02:00
53dee63a76 Add env variable for session secret 2019-08-27 15:30:30 +02:00
97ec6ce9bc Add KREDITS_DAO_ADDRESS to readme 2019-08-27 15:22:38 +02:00
342a5cd829 Add gasLimit option when adding contributor 2019-08-27 13:16:43 +02:00
45064df737 Fix wrong logger function call 2019-08-27 12:57:36 +02:00
d9a9ad7793 Update deps
Fixes security vulns
2019-08-27 12:51:31 +02:00
d65e92e1fe Merge branch 'master' into feature/github-signup 2019-08-27 12:39:15 +02:00
0fff3b1db0 Merge pull request #43 from 67P/feature/custom_dao_address
Add ENV var for custom DAO address
2019-08-27 12:38:45 +02:00
1f1053e289 Remove apm address from options
It's already the default in the contracts module.
2019-08-27 12:37:50 +02:00
84f20efff0 Add ENV var for custom DAO address
This makes it much easier to test hubot-kredits with local development
chains.
2019-08-27 12:19:01 +02:00
cd8343516f 3.3.1 2019-08-14 15:42:23 +02:00
Greg Karékinian
41c5903cf5 Merge pull request #42 from 67P/chore/mediawiki_small_threshold
Change threshold for small wiki contributions
2019-08-14 12:51:48 +02:00
9a4dc43aa5 Change threshold for small wiki contributions
Turns out 280 is a bit low. Let's try 500 instead.
2019-08-14 12:46:19 +02:00
082bf4848d Minor changes 2019-07-26 20:50:36 +02:00
41dd813eaf Replace custom CORS header solution with middleware 2019-07-26 20:48:20 +02:00
b35cbcc63a Finish implementation of GitHub signup oracle 2019-07-26 04:15:11 +02:00
137e9eb4ed Add initial implementation of the GitHub signup oracle 2019-07-11 15:44:58 +02:00
67fd9ff031 3.3.0 2019-05-08 16:45:55 +02:00
800950ebc5 Merge pull request #39 from 67P/feature/38-label_mappings
Map issue/PR labels to contribution kind
2019-05-08 16:42:05 +02:00
98ccae96f0 [gitea] Use labels for contribution kind 2019-05-08 16:16:40 +02:00
e6730802f6 Use proper syntax for sending HTTP status
Fixes deprecation warnings for the old syntax.
2019-05-08 16:14:41 +02:00
d2976b312e Create utility modules for label processing 2019-05-08 16:14:41 +02:00
d2feee8b14 [github] Use labels for contribution kind 2019-05-08 16:14:24 +02:00
a5acf466a4 3.2.1 2019-04-30 10:41:50 +01:00
Greg Karékinian
f1bb5b391d Merge pull request #35 from 67P/bugfix/33-gitea_issue_id
Fix wrong Gitea issue IDs/URLs
2019-04-30 10:26:21 +02:00
840fcf9d25 Add commented setting for custom local DAO address 2019-04-28 13:01:33 +01:00
c390fc9ce7 Fix Gitea issue ID/URL being wrong
Internal issue IDs are different from the public ones in Gitea. The one
used in URLs etc. is called "number" instead of "id" in hook payloads.

fixes #33
2019-04-28 13:01:09 +01:00
11 changed files with 1224 additions and 328 deletions

4
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
template: |
## Changes
$CHANGES

View File

@@ -10,9 +10,29 @@ contributions.
## Setup ## Setup
### Ethereum wallet
You will need an Ethereum wallet for your bot, so it can interact with the 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. Ethereum smart contracts. `npm run create-wallet` will do the job for you.
The wallet must be funded with enough ETH to interact with the contracts.
### Contract permissions
The bot wallet needs the following Aragon contract permissions to interact
with [kredits-contracts]:
1. `ADD_CONTRIBUTION_ROLE` on the `Contribution` contract
2. `MANAGE_CONTRIBUTORS_ROLE` on the `Contributor` contract
These permissions can be configured using the [Aragon
CLI](https://hack.aragon.org/docs/cli-intro.html) (see [kredits-contracts].
aragon dao acl grant [DAO address] [contribution app address] ADD_CONTRIBUTION_ROLE [bot wallet address]
aragon dao acl grant [DAO address] [contributor app address] MANAGE_CONTRIBUTORS_ROLE [bot wallet address]
To get the `Contribution` and `Contributor` app addresses use `aragon dao apps`.
## Configuration ## Configuration
As usual in Hubot, you can add all config as environment variables. As usual in Hubot, you can add all config as environment variables.
@@ -24,6 +44,11 @@ As usual in Hubot, you can add all config as environment variables.
| `KREDITS_WALLET_PATH` | Path to an Etherum wallet JSON file (default: `./wallet.json`) | | `KREDITS_WALLET_PATH` | Path to an Etherum wallet JSON file (default: `./wallet.json`) |
| `KREDITS_WALLET_PASSWORD` | Wallet password | | `KREDITS_WALLET_PASSWORD` | Wallet password |
| `KREDITS_PROVIDER_URL` | Ethereum JSON-RPC URL (default: `http://localhost:7545`) | | `KREDITS_PROVIDER_URL` | Ethereum JSON-RPC URL (default: `http://localhost:7545`) |
| `KREDITS_WEB_URL` | URL of the Kredits Web app (default: `https://kredits.kosmos.org`) |
| `KREDITS_DAO_ADDRESS` | DAO Kernel address |
| `KREDITS_SESSION_SECRET` | Secret used by [grant](https://www.npmjs.com/package/grant) to sign the session ID |
| `KREDITS_GRANT_HOST` | Host used by [grant](https://www.npmjs.com/package/grant) to generate OAuth redirect URLs (default: `localhost:8888`) |
| `KREDITS_GRANT_PROTOCOL` | Protocol (http or https) used by [grant](https://www.npmjs.com/package/grant") to generate the OAuth redirect URLs (default: "http") |
## Integrations ## Integrations
@@ -34,6 +59,12 @@ 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 medium and large contributions. If there are multiple people assigned, it will
issue contribution tokens for all of them. issue contribution tokens for all of them.
If `KREDITS_GITHUB_KEY` and `KREDITS_GITHUB_SECRET` are set, the bot will also
expose OAuth endpoints to authenticate new contributors and register new
contributor profiles on the smart contract. For this feature, a [GitHub OAuth
app] is required and the [OAuth grant config variables](#Configuration) must be
set.
#### Setup #### Setup
Point a GitHub organization webhook to the following URL: Point a GitHub organization webhook to the following URL:
@@ -45,6 +76,8 @@ Point a GitHub organization webhook to the following URL:
| Key | Description | | 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` | | `KREDITS_GITHUB_REPO_BLACKLIST` | Repos which you do not want to issue kredits for. Format: `orgname/reponame`, e.g. `67P/test-one-two` |
| `KREDITS_GITHUB_KEY` | Key of the [GitHub OAuth app] used to authenticate contributors |
| `KREDITS_GITHUB_SECRET` | Secret of the [GitHub OAuth app] used to authenticate contributors |
### Gitea ### Gitea
@@ -80,3 +113,32 @@ wiki's API on its own.
| Key | Description | | Key | Description |
| --- | --- | | --- | --- |
| `KREDITS_MEDIAWIKI_URL` | Your wiki URL, e.g. `https://wiki.kosmos.org/` | | `KREDITS_MEDIAWIKI_URL` | Your wiki URL, e.g. `https://wiki.kosmos.org/` |
[kredits-contracts]: https://github.com/67P/kredits-contracts
[GitHub OAuth app]: https://developer.github.com/apps/about-apps/#about-oauth-apps
### Zoom
The Zoom integration creates contributions for meeting participations.
Every meeting that is longer than 15 minutes and with more than 2 participants will be registered.
An optional meeting whitelist can be configured to create contributions only for specific meetings.
#### Setup
A Zoom JWT app has to be set up and an [event webhook subscription](https://marketplace.zoom.us/docs/api-reference/webhook-reference/meeting-events/meeting-ending")
on `meeting.ended` has to be configured to the following URL:
https://your-hubot.example.com/incoming/kredits/zoom/{webhook_token}
#### Config
| Key | Description |
| --- | --- |
| `KREDITS_ZOOM_JWT` | The JWT for the Zoom application (required)
| `KREDITS_ZOOM_MEETING_WHITELIST` | Comma separated list of meeting names for which kredits should be tracked (optional)
| `KREDITS_ZOOM_CONTRIBUTION_AMOUNT` | The amount of kredits issued for each meeting. (default: 500)
[Zoom apps](https://marketplace.zoom.us/user/build)

View File

@@ -7,6 +7,7 @@ const Kredits = require('kredits-contracts');
const walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json'; const walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json';
const walletJson = fs.readFileSync(walletPath); const walletJson = fs.readFileSync(walletPath);
const providerUrl = process.env.KREDITS_PROVIDER_URL; const providerUrl = process.env.KREDITS_PROVIDER_URL;
const daoAddress = process.env.KREDITS_DAO_ADDRESS;
const ipfsConfig = { const ipfsConfig = {
host: process.env.IPFS_API_HOST || 'localhost', host: process.env.IPFS_API_HOST || 'localhost',
@@ -48,13 +49,14 @@ module.exports = async function(robot) {
// Kredits contracts setup // Kredits contracts setup
// //
const opts = { ipfsConfig };
if (daoAddress) {
opts.addresses = { Kernel: daoAddress };
}
let kredits; let kredits;
try { try {
kredits = await new Kredits(signer.provider, signer, { kredits = await new Kredits(signer.provider, signer, opts).init();
// TODO support local devchain custom address
apm: 'open.aragonpm.eth',
ipfsConfig
}).init();
} catch(error) { } catch(error) {
robot.logger.warning('[hubot-kredits] Could not set up kredits:', error); robot.logger.warning('[hubot-kredits] Could not set up kredits:', error);
process.exit(1); process.exit(1);
@@ -150,6 +152,10 @@ module.exports = async function(robot) {
require('./integrations/github')(robot, kredits); require('./integrations/github')(robot, kredits);
require('./integrations/gitea')(robot, kredits); require('./integrations/gitea')(robot, kredits);
if (typeof process.env.KREDITS_ZOOM_JWT !== 'undefined') {
require('./integrations/zoom')(robot, kredits);
}
if (typeof process.env.KREDITS_MEDIAWIKI_URL !== 'undefined') { if (typeof process.env.KREDITS_MEDIAWIKI_URL !== 'undefined') {
require('./integrations/mediawiki')(robot, kredits); require('./integrations/mediawiki')(robot, kredits);
} }

View File

@@ -1,5 +1,7 @@
const util = require('util'); const util = require('util');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const amountFromLabels = require('./utils/amount-from-labels');
const kindFromLabels = require('./utils/kind-from-labels');
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
@@ -35,7 +37,7 @@ module.exports = async function(robot, kredits) {
}); });
} }
function createContribution(giteaUser, date, time, amount, description, url, details) { function createContribution(giteaUser, date, time, amount, kind, description, url, details) {
return getContributorByGiteaUser(giteaUser).then(contributor => { return getContributorByGiteaUser(giteaUser).then(contributor => {
robot.logger.debug(`[hubot-kredits] Creating contribution token for ${amount}₭S to ${giteaUser} for ${url}...`); robot.logger.debug(`[hubot-kredits] Creating contribution token for ${amount}₭S to ${giteaUser} for ${url}...`);
@@ -45,12 +47,15 @@ module.exports = async function(robot, kredits) {
date, date,
time, time,
amount, amount,
url, kind,
description, description,
details, url,
kind: 'dev' details
}; };
robot.logger.debug(`[hubot-kredits] contribution attributes:`);
robot.logger.debug(util.inspect(contributionAttr, { depth: 1, colors: true }));
return Contribution.addContribution(contributionAttr).catch(error => { return Contribution.addContribution(contributionAttr).catch(error => {
robot.logger.error(`[hubot-kredits] Error:`, 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(`I tried to add a contribution for ${giteaUser} for ${url}, but I encountered an error when submitting the tx:`);
@@ -59,35 +64,14 @@ module.exports = async function(robot, kredits) {
}); });
} }
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) { async function handleGiteaIssueClosed(data) {
const issue = data.issue; const issue = data.issue;
const repoName = data.repository.full_name; const repoName = data.repository.full_name;
const web_url = `${data.repository.html_url}/issues/${issue.id}`; const web_url = `${data.repository.html_url}/issues/${issue.number}`;
const description = `${repoName}: ${issue.title}`; const description = `${repoName}: ${issue.title}`;
const amount = amountFromLabels(issue.labels); const labels = issue.labels.map(l => l.name);
const amount = amountFromLabels(labels);
const kind = kindFromLabels(labels);
const assignees = issue.assignees ? issue.assignees.map(a => a.login) : []; const assignees = issue.assignees ? issue.assignees.map(a => a.login) : [];
[ date, time ] = issue.closed_at.split('T'); [ date, time ] = issue.closed_at.split('T');
@@ -108,7 +92,8 @@ module.exports = async function(robot, kredits) {
for (const recipient of recipients) { for (const recipient of recipients) {
try { try {
await createContribution(recipient, date, time, amount, description, web_url, await createContribution(recipient, date, time, amount,
kind, description, web_url,
{ issue, repository: data.repository }); { issue, repository: data.repository });
await sleep(60000); await sleep(60000);
} }
@@ -123,7 +108,9 @@ module.exports = async function(robot, kredits) {
const repoName = data.repository.full_name; const repoName = data.repository.full_name;
const web_url = pull_request.html_url; const web_url = pull_request.html_url;
const description = `${repoName}: ${pull_request.title}`; const description = `${repoName}: ${pull_request.title}`;
const amount = amountFromLabels(pull_request.labels); const labels = pull_request.labels.map(l => l.name);
const amount = amountFromLabels(labels);
const kind = kindFromLabels(labels);
const assignees = pull_request.assignees ? pull_request.assignees.map(a => a.login) : []; const assignees = pull_request.assignees ? pull_request.assignees.map(a => a.login) : [];
[ date, time ] = pull_request.merged_at.split('T'); [ date, time ] = pull_request.merged_at.split('T');
@@ -142,10 +129,10 @@ module.exports = async function(robot, kredits) {
recipients = [pull_request.user.login]; recipients = [pull_request.user.login];
} }
for (const recipient of recipients) { for (const recipient of recipients) {
try { try {
await createContribution(recipient, date, time, amount, description, web_url, await createContribution(recipient, date, time, amount,
kind, description, web_url,
{ pull_request, repository: data.repository }); { pull_request, repository: data.repository });
await sleep(60000); await sleep(60000);
} }

View File

@@ -1,5 +1,10 @@
const util = require('util'); const util = require('util');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const session = require('express-session');
const grant = require('grant-express');
const cors = require('cors');
const amountFromLabels = require('./utils/amount-from-labels');
const kindFromLabels = require('./utils/kind-from-labels');
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
@@ -19,6 +24,8 @@ module.exports = async function(robot, kredits) {
robot.logger.debug('[hubot-kredits] Ignoring GitHub actions from ', util.inspect(repoBlackList)); robot.logger.debug('[hubot-kredits] Ignoring GitHub actions from ', util.inspect(repoBlackList));
} }
const kreditsWebUrl = process.env.KREDITS_WEB_URL || 'https://kredits.kosmos.org';
const Contributor = kredits.Contributor; const Contributor = kredits.Contributor;
const Contribution = kredits.Contribution; const Contribution = kredits.Contribution;
@@ -35,9 +42,9 @@ module.exports = async function(robot, kredits) {
}); });
} }
function createContribution(githubUser, date, time, amount, description, url, details) { function createContribution(githubUser, date, time, amount, kind, description, url, details) {
return getContributorByGithubUser(githubUser).then(contributor => { return getContributorByGithubUser(githubUser).then(contributor => {
robot.logger.debug(`[hubot-kredits] Creating contribution token for ${amount}₭S to ${githubUser} for ${url}...`); robot.logger.info(`[hubot-kredits] Creating contribution token for ${amount}₭S to ${githubUser} for ${url}...`);
const contributionAttr = { const contributionAttr = {
contributorId: contributor.id, contributorId: contributor.id,
@@ -45,12 +52,15 @@ module.exports = async function(robot, kredits) {
date, date,
time, time,
amount, amount,
url, kind,
description, description,
details, url,
kind: 'dev' details
}; };
robot.logger.debug(`[hubot-kredits] contribution attributes:`);
robot.logger.debug(util.inspect(contributionAttr, { depth: 1, colors: true }));
return Contribution.addContribution(contributionAttr).catch(error => { return Contribution.addContribution(contributionAttr).catch(error => {
robot.logger.error(`[hubot-kredits] Error:`, error); robot.logger.error(`[hubot-kredits] Error:`, error);
messageRoom(`I tried to add a contribution for ${githubUser} for ${url}, but I encountered an error when submitting the tx:`); messageRoom(`I tried to add a contribution for ${githubUser} for ${url}, but I encountered an error when submitting the tx:`);
@@ -59,38 +69,17 @@ module.exports = async function(robot, kredits) {
}); });
} }
function amountFromIssueLabels(issue) {
const 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;
}
async function handleGitHubIssueClosed(data) { async function handleGitHubIssueClosed(data) {
let recipients; let recipients;
const issue = data.issue; const issue = data.issue;
const assignees = issue.assignees.map(a => a.login); const assignees = issue.assignees.map(a => a.login);
const web_url = issue.html_url; const web_url = issue.html_url;
[date, time] = issue.closed_at.split('T'); [date, time] = issue.closed_at.split('T');
const amount = amountFromIssueLabels(issue); const labels = issue.labels.map(l => l.name);
const repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1]; const amount = amountFromLabels(labels);
const kind = kindFromLabels(labels);
const repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1];
const description = `${repoName}: ${issue.title}`; const description = `${repoName}: ${issue.title}`;
if (amount === 0) { if (amount === 0) {
@@ -109,7 +98,7 @@ module.exports = async function(robot, kredits) {
for (const recipient of recipients) { for (const recipient of recipients) {
try { try {
await createContribution(recipient, date, time, amount, description, web_url, issue); await createContribution(recipient, date, time, amount, kind, description, web_url, issue);
await sleep(60000); await sleep(60000);
} }
catch (err) { robot.logger.error(err); } catch (err) { robot.logger.error(err); }
@@ -141,8 +130,10 @@ module.exports = async function(robot, kredits) {
return response.json(); return response.json();
}) })
.then(async (issue) => { .then(async (issue) => {
const amount = amountFromIssueLabels(issue); const labels = issue.labels.map(l => l.name);
const repoName = pull_request.base.repo.full_name; const amount = amountFromLabels(labels);
const kind = kindFromLabels(labels);
const repoName = pull_request.base.repo.full_name;
const description = `${repoName}: ${pull_request.title}`; const description = `${repoName}: ${pull_request.title}`;
if (amount === 0) { if (amount === 0) {
@@ -155,7 +146,7 @@ module.exports = async function(robot, kredits) {
for (const recipient of recipients) { for (const recipient of recipients) {
try { try {
await createContribution(recipient, date, time, amount, description, web_url, pull_request); await createContribution(recipient, date, time, amount, kind, description, web_url, pull_request);
await sleep(60000); await sleep(60000);
} }
catch (err) { robot.logger.error(err); } catch (err) { robot.logger.error(err); }
@@ -176,14 +167,111 @@ module.exports = async function(robot, kredits) {
if (evt === 'pull_request' && data.action === 'closed' && data.pull_request.merged) { if (evt === 'pull_request' && data.action === 'closed' && data.pull_request.merged) {
handleGitHubPullRequestClosed(data); handleGitHubPullRequestClosed(data);
res.send(200); res.sendStatus(200);
} }
else if (evt === 'issues' && data.action === 'closed') { else if (evt === 'issues' && data.action === 'closed') {
handleGitHubIssueClosed(data); handleGitHubIssueClosed(data);
res.send(200); res.sendStatus(200);
} else { } else {
res.send(200); res.sendStatus(200);
} }
}); });
//
// GitHub signup
//
if (process.env.KREDITS_GITHUB_KEY && process.env.KREDITS_GITHUB_SECRET) {
const grantConfig = {
defaults: {
protocol: (process.env.KREDITS_GRANT_PROTOCOL || "http"),
host: (process.env.KREDITS_GRANT_HOST || 'localhost:8888'),
transport: 'session',
response: 'tokens',
path: '/kredits/signup'
},
github: {
key: process.env.KREDITS_GITHUB_KEY,
secret: process.env.KREDITS_GITHUB_SECRET,
callback: '/kredits/signup/github'
}
};
robot.router.use(session({
secret: process.env.KREDITS_SESSION_SECRET || 'grant',
resave: false,
saveUninitialized: false
}));
robot.router.use('/kredits/signup', grant(grantConfig));
robot.router.get('/kredits/signup/github', async (req, res) => {
const access_token = req.session.grant.response.access_token;
res.redirect(`${kreditsWebUrl}/signup/github#access_token=${access_token}`);
});
robot.router.options('/kredits/signup/github', cors());
robot.router.post('/kredits/signup/github', cors(), async (req, res) => {
const accessToken = req.body.accessToken;
if (!accessToken) {
res.status(400).json({});
return;
}
let githubResponse;
try {
githubResponse = await fetch('https://api.github.com/user', {
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `token ${accessToken}`
}
});
} catch (error) {
robot.logger.error('[hubot-kredits] Fetching user data from GitHub failed:', error);
res.status(500).json({ error });
};
if (githubResponse.status >= 300) {
res.status(githubResponse.status).json({});
return;
}
const user = await githubResponse.json();
const contributor = await kredits.Contributor.findByAccount({
site: 'github.com',
username: user.login
});
if (!contributor) {
let contributorAttr = {};
contributorAttr.account = req.body.account;
contributorAttr.name = user.name || user.login;
contributorAttr.kind = "person";
contributorAttr.url = user.blog;
contributorAttr.github_username = user.login;
contributorAttr.github_uid = user.id;
kredits.Contributor.add(contributorAttr, { gasLimit: 350000 })
.then(transaction => {
robot.logger.info('[hubot-kredits] Contributor added from GitHub signup', transaction.hash);
res.status(201);
res.json({
transactionHash: transaction.hash,
github_username: user.login
});
}, error => {
robot.logger.error(`[hubot-kredits] Adding contributor failed: ${error}`);
res.status(422);
res.json({ error })
});
} else {
res.json({
github_username: user.login
});
}
});
} else {
robot.logger.warning('[hubot-kredits] No KREDITS_GITHUB_KEY and KREDITS_GITHUB_SECRET configured for OAuth signup');
}
}; };

View File

@@ -129,8 +129,7 @@ module.exports = async function(robot, kredits) {
function calculateAmountForChanges(details) { function calculateAmountForChanges(details) {
let amount; let amount;
if (details.charsAdded < 280) { if (details.charsAdded < 500) {
// less than a tweet
amount = 500; amount = 500;
} else if (details.charsAdded < 2000) { } else if (details.charsAdded < 2000) {
amount = 1500; amount = 1500;

View File

@@ -0,0 +1,21 @@
module.exports = function (labels) {
const kreditsLabel = labels.filter(n => n.match(/^kredits/))[0];
// No label, no kredits
if (typeof kreditsLabel === 'undefined') { return 0; }
// TODO move amounts to config?
let amount;
switch(kreditsLabel) {
case 'kredits-1':
amount = 500;
break;
case 'kredits-2':
amount = 1500;
break;
case 'kredits-3':
amount = 5000;
break;
}
return amount;
};

View File

@@ -0,0 +1,18 @@
module.exports = function (labels) {
let kind = 'dev';
if (labels.find(l => l.match(/ops|operations/))) {
kind = 'ops';
}
else if (labels.find(l => l.match(/docs|documentation/))) {
kind = 'docs';
}
else if (labels.find(l => l.match(/design/))) {
kind = 'design';
}
else if (labels.find(l => l.match(/community/))) {
kind = 'community';
}
return kind;
};

100
integrations/zoom.js Normal file
View File

@@ -0,0 +1,100 @@
const fetch = require('node-fetch');
module.exports = async function(robot, kredits) {
function messageRoom(message) {
robot.messageRoom(process.env.KREDITS_ROOM, message);
}
const { Contributor, Contribution } = kredits;
const kreditsContributionAmount = process.env.KREDITS_ZOOM_CONTRIBUTION_AMOUNT || 500;
const kreditsContributionKind = 'community';
const zoomAccessToken = process.env.KREDITS_ZOOM_JWT;
const walletTransactionCount = await kredits.provider.getTransactionCount(kredits.signer.address);
let nonce = walletTransactionCount;
async function createContributionFor (displayName, meeting) {
const contributor = await getContributorByZoomDisplayName(displayName);
if (!contributor) {
robot.logger.info(`[hubot-kredits] Contributor not found: Zoom display name: ${displayName}`);
messageRoom(`I tried to add a contribution for zoom user ${displayName}, but did not find a matching contributor profile.`);
return Promise.resolve();
}
const contribution = {
contributorId: contributor.id,
contributorIpfsHash: contributor.ipfsHash,
amount: kreditsContributionAmount,
kind: kreditsContributionKind,
description: 'Team/Community Call',
date: meeting.end_time.split('T')[0],
time: meeting.end_time.split('T')[1]
}
return Contribution.add(contribution, { nonce: nonce++ })
.then(tx => {
robot.logger.info(`[hubot-kredits] Contribution created: ${tx.hash}`);
})
.catch(error => {
robot.logger.error(`[hubot-kredits] Adding contribution for Zoom call failed:`, error);
});
}
function getContributorByZoomDisplayName(displayName) {
return Contributor.findByAccount({ site: 'zoom.us', username: displayName });
}
function request(path) {
return fetch(
`https://api.zoom.us/v2${path}`,
{headers: {authorization: `Bearer ${zoomAccessToken}`}}
);
}
function getMeetingParticipants(meetingUUID) {
return request(`/past_meetings/${meetingUUID}/participants`)
.then(response => response.json())
.then(json => json.participants)
}
function getMeetingDetails(meetingUUID) {
return request(`/past_meetings/${meetingUUID}`)
.then(r => r.json());
}
async function handleZoomMeetingEnded(data) {
const meetingDetails = await getMeetingDetails(data.uuid);
const participants = await getMeetingParticipants(data.uuid);
const names = Array.from(new Set(participants.map(p => p.name)));
if (meetingDetails.duration < 15 || names.length < 3) {
robot.logger.info(`[hubot-kredits] Ignoring zoom call ${data.uuid} (duration: ${meetingDetails.duration}, participants_count: ${meetingDetails.participants_count})`);
return;
}
for (const displayName of names) {
await createContributionFor(displayName, meetingDetails);
};
}
robot.router.post('/incoming/kredits/zoom/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => {
let data = req.body;
const eventName = data.event;
const payload = data.payload;
const object = payload.object;
if (eventName === 'meeting.ended' && (
!process.env.KREDITS_ZOOM_MEETING_WHITELIST ||
process.env.KREDITS_ZOOM_MEETING_WHITELIST.split(',').includes(object.id)
)) {
handleZoomMeetingEnded(object);
}
res.sendStatus(200);
})
}

1095
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "hubot-kredits", "name": "hubot-kredits",
"version": "3.2.0", "version": "3.6.0",
"description": "Kosmos Kredits functionality for chat bots", "description": "Kosmos Kredits functionality for chat bots",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -10,11 +10,15 @@
"create-wallet": "scripts/create-wallet.js" "create-wallet": "scripts/create-wallet.js"
}, },
"dependencies": { "dependencies": {
"cors": "^2.8.5",
"eth-provider": "^0.2.2", "eth-provider": "^0.2.2",
"ethers": "^4.0.27", "ethers": "^4.0.27",
"group-array": "^0.3.3", "express": "^4.17.1",
"express-session": "^1.16.2",
"grant-express": "^4.6.1",
"group-array": "^1.0.0",
"kosmos-schemas": "^1.1.2", "kosmos-schemas": "^1.1.2",
"kredits-contracts": "^5.3.0", "kredits-contracts": "^5.4.0",
"node-cron": "^2.0.3", "node-cron": "^2.0.3",
"node-fetch": "^2.3.0", "node-fetch": "^2.3.0",
"prompt": "^1.0.0" "prompt": "^1.0.0"