16 Commits

Author SHA1 Message Date
Râu Cao
70a74ba5fb 4.0.0 2022-11-02 18:32:19 +01:00
Râu Cao
1ab5ae55c6 Use new @kredits/contracts release 2022-11-02 18:30:57 +01:00
Râu Cao
710bd90172 Update and adapt for new kredits contracts release 2022-10-31 13:01:29 +01:00
c27fefcfdc Merge pull request #64 from 67P/feature/49-code_review_kredits
Create contributions for pull request reviews
2021-01-29 11:41:00 +01:00
cbfd25e880 Exit with error message when API token is missing 2021-01-29 11:06:31 +01:00
8b82b8b159 Add timeframe to contribution description 2021-01-29 11:05:45 +01:00
084dec58a9 Remove unused variable 2021-01-29 11:01:46 +01:00
9a4e465608 Remove unused variable 2021-01-28 13:05:36 +01:00
93d7c8944b Collect contribution data for pull request reviews 2021-01-12 15:32:06 +01:00
ad0e34b990 3.8.0 2020-10-29 15:15:10 +01:00
063a9c6b37 Merge pull request #63 from 67P/bugfix/mediawiki_accident
Fix mediawiki integration
2020-10-29 15:13:12 +01:00
f05436e9b9 Fix mediawiki integration
Accidentally deleted a line in a recent PR, and it slipped through the
review.
2020-10-29 15:12:50 +01:00
e305643f69 Merge pull request #61 from 67P/chore/replace_deprecated_contract_calls
Replace deprecated contract API calls
2020-10-29 14:13:36 +01:00
82a003ffeb Merge pull request #62 from 67P/feature/wiki_changes
Only create small automatic contributions for wiki edits
2020-10-29 14:10:33 +01:00
fc0c113997 Only create small automatic contributions for wiki edits
We decided that it's too difficult for a machine to gauge the meaning
and value of wiki edits by line numbers, so automatic kredits are now
always a small contributions. Until we have new tools for larger wiki
contributions (e.g. mediawiki tags), we can create manual contributions
for those.
2020-10-29 12:56:49 +01:00
5c6540580b Replace deprecated contract API calls
Use the new method.
2020-10-29 12:07:42 +01:00
13 changed files with 5516 additions and 1887 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
GITEA_TOKEN=your-token-here
GITHUB_TOKEN=your-token-here

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules node_modules
wallet.json wallet.json
.env

View File

@@ -10,15 +10,20 @@ contributions.
## Setup ## Setup
### Ethereum wallet ### Wallet
You will need an Ethereum wallet for your bot, so it can interact with the You will need a keypair/wallet for your bot, so it can interact with the smart
Ethereum smart contracts. `npm run create-wallet` will do the job for you. contracts. `npm run create-wallet` will do the job for you.
The wallet must be funded with enough ETH to interact with the contracts. The wallet must be funded with enough native chain tokens to interact with the
contracts (i.e. it must be able to pay gas/tx fees)
### Contract permissions ### Contract permissions
**Warning: outdated instructions!**
*TODO adapt instructions for new permission model*
The bot wallet needs the following Aragon contract permissions to interact The bot wallet needs the following Aragon contract permissions to interact
with [kredits-contracts]: with [kredits-contracts]:
@@ -41,11 +46,10 @@ As usual in Hubot, you can add all config as environment variables.
| --- | --- | | --- | --- |
| `KREDITS_WEBHOOK_TOKEN` | A string for building your secret webhook URLs | | `KREDITS_WEBHOOK_TOKEN` | A string for building your secret webhook URLs |
| `KREDITS_ROOM` | The bot will talk to you in this room | | `KREDITS_ROOM` | The bot will talk to you in this room |
| `KREDITS_WALLET_PATH` | Path to an Etherum wallet JSON file (default: `./wallet.json`) | | `KREDITS_WALLET_PATH` | Path to an 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` | JSON-RPC URL of a blockchain node (default: `http://localhost:7545`) |
| `KREDITS_WEB_URL` | URL of the Kredits Web app (default: `https://kredits.kosmos.org`) | | `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_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_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") | | `KREDITS_GRANT_PROTOCOL` | Protocol (http or https) used by [grant](https://www.npmjs.com/package/grant") to generate the OAuth redirect URLs (default: "http") |

View File

@@ -1,14 +1,11 @@
const fs = require('fs'); const fs = require('fs');
const util = require('util');
const fetch = require('node-fetch');
const ethers = require('ethers'); const ethers = require('ethers');
const NonceManager = require('@ethersproject/experimental').NonceManager; const NonceManager = require('@ethersproject/experimental').NonceManager;
const Kredits = require('kredits-contracts'); 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 || 'http://localhost:7545';
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',
@@ -38,12 +35,9 @@ module.exports = async function(robot) {
// Ethereum provider/node setup // Ethereum provider/node setup
// //
let ethProvider; robot.logger.info('[hubot-kredits] Using blockchain node/API at', providerUrl);
if (providerUrl) {
ethProvider = new ethers.providers.JsonRpcProvider(providerUrl); const ethProvider = new ethers.providers.JsonRpcProvider(providerUrl);
} else {
ethProvider = new ethers.getDefaultProvider('rinkeby');
}
const signer = new NonceManager(wallet.connect(ethProvider)); const signer = new NonceManager(wallet.connect(ethProvider));
// //
@@ -51,9 +45,6 @@ module.exports = async function(robot) {
// //
const opts = { ipfsConfig }; const opts = { ipfsConfig };
if (daoAddress) {
opts.addresses = { Kernel: daoAddress };
}
let kredits; let kredits;
try { try {
@@ -63,8 +54,8 @@ module.exports = async function(robot) {
process.exit(1); process.exit(1);
} }
const Contributor = kredits.Contributor; const Contributor = kredits.Contributor;
const Proposal = kredits.Proposal;
const Contribution = kredits.Contribution; const Contribution = kredits.Contribution;
// TODO const Reimbursement = kredits.Reimbursement;
robot.logger.info('[hubot-kredits] Wallet address: ' + wallet.address); robot.logger.info('[hubot-kredits] Wallet address: ' + wallet.address);
@@ -73,9 +64,9 @@ module.exports = async function(robot) {
// //
ethProvider.getBalance(wallet.address).then(balance => { ethProvider.getBalance(wallet.address).then(balance => {
robot.logger.info('[hubot-kredits] Wallet balance: ' + ethers.utils.formatEther(balance) + 'ETH'); robot.logger.info('[hubot-kredits] Wallet balance: ' + ethers.utils.formatEther(balance) + ' RBTC');
if (balance.lt(ethers.utils.parseEther('0.0001'))) { if (balance.lt(ethers.utils.parseEther('0.0001'))) {
messageRoom(`Yo gang, I\'m broke! Please drop me some ETH to ${wallet.address}. kthxbai.`); messageRoom(`Yo gang, I\'m broke! Please send some RBTC to ${wallet.address}. kthxbai.`);
} }
}); });
@@ -83,30 +74,9 @@ module.exports = async function(robot) {
// Robot chat commands/interaction // Robot chat commands/interaction
// //
robot.respond(/got ETH\??/i, res => { robot.respond(/got RBTC\??/i, res => {
ethProvider.getBalance(wallet.address).then((balance) => { ethProvider.getBalance(wallet.address).then((balance) => {
res.send(`My wallet contains ${ethers.utils.formatEther(balance)} ETH`); res.send(`My wallet contains ${ethers.utils.formatEther(balance)} RBTC`);
});
});
robot.respond(/propose (\d*)\s?\S*\s?to (\S+)(?:\sfor (.*))?$/i, res => {
let [_, amount, githubUser, description] = res.match;
let url = null;
createProposal(githubUser, amount, description, url).then((result) => {
messageRoom('Sounds good! Will be listed on https://kredits.kosmos.org in a bit...');
});
});
robot.respond(/list open proposals/i, res => {
Proposal.all().then((proposals) => {
proposals.forEach((proposal) => {
if (!proposal.executed) {
Contributor.getById(proposal.contributorId).then((contributor) => {
messageRoom(`* ${proposal.amount} kredits to ${contributor.name} for ${proposal.description}`);
});
}
});
messageRoom('https://kredits.kosmos.org');
}); });
}); });
@@ -115,31 +85,23 @@ module.exports = async function(robot) {
// //
function watchContractEvents() { function watchContractEvents() {
ethProvider.getBlockNumber().then((blockNumber) => { ethProvider.getBlockNumber().then(blockNumber => {
// current block is the last mined one, thus we check from the next // current block is the last mined one, thus we check from the next
// mined one onwards to prevent getting previous events // mined one onwards to prevent getting previous events
let nextBlock = blockNumber + 1; let nextBlock = blockNumber + 1;
robot.logger.debug(`[hubot-kredits] Watching events from block ${nextBlock} onward`); robot.logger.debug(`[hubot-kredits] Watching events from block ${nextBlock} onward`);
ethProvider.resetEventsBlock(nextBlock); ethProvider.resetEventsBlock(nextBlock);
Proposal.on('ProposalCreated', handleProposalCreated); // TODO handle all known events (that make sense here)
// Contribution.on('ContributorAdded', handleContributorAdded);
Contribution.on('ContributionAdded', handleContributionAdded); Contribution.on('ContributionAdded', handleContributionAdded);
}); });
} }
function handleProposalCreated(proposalId, creatorAccount, contributorId, amount) {
Contributor.getById(contributorId).then((contributor) => {
Proposal.getById(proposalId).then((proposal) => {
robot.logger.debug(`[hubot-kredits] Proposal created (${proposal.description})`);
// messageRoom(`Let's give ${contributor.name} some kredits for ${proposal.url} (${proposal.description}): https://kredits.kosmos.org`);
});
});
}
function handleContributionAdded(contributionId, contributorId, amount) { function handleContributionAdded(contributionId, contributorId, amount) {
Contributor.getById(contributorId).then((contributor) => { Contributor.getById(contributorId).then(_ => {
Contribution.getById(contributionId).then((contribution) => { Contribution.getById(contributionId).then(contribution => {
robot.logger.debug(`[hubot-kredits] Contribution #${contribution.id} added (${contribution.description})`); robot.logger.debug(`[hubot-kredits] Contribution #${contribution.id} added (${amount} kredits for "${contribution.description}")`);
}); });
}); });
} }

View File

@@ -1,5 +1,4 @@
const util = require('util'); const util = require('util');
const fetch = require('node-fetch');
const amountFromLabels = require('./utils/amount-from-labels'); const amountFromLabels = require('./utils/amount-from-labels');
const kindFromLabels = require('./utils/kind-from-labels'); const kindFromLabels = require('./utils/kind-from-labels');
@@ -56,7 +55,7 @@ module.exports = async function(robot, kredits) {
robot.logger.debug(`[hubot-kredits] contribution attributes:`); robot.logger.debug(`[hubot-kredits] contribution attributes:`);
robot.logger.debug(util.inspect(contributionAttr, { depth: 1, colors: true })); robot.logger.debug(util.inspect(contributionAttr, { depth: 1, colors: true }));
return Contribution.addContribution(contributionAttr).catch(error => { return Contribution.add(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:`);
messageRoom(error.message); messageRoom(error.message);

View File

@@ -61,7 +61,7 @@ module.exports = async function(robot, kredits) {
robot.logger.debug(`[hubot-kredits] contribution attributes:`); robot.logger.debug(`[hubot-kredits] contribution attributes:`);
robot.logger.debug(util.inspect(contributionAttr, { depth: 1, colors: true })); robot.logger.debug(util.inspect(contributionAttr, { depth: 1, colors: true }));
return Contribution.addContribution(contributionAttr).catch(error => { return Contribution.add(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:`);
messageRoom(error.message); messageRoom(error.message);

View File

@@ -48,7 +48,7 @@ module.exports = async function(robot, kredits) {
kind: 'docs' kind: 'docs'
}; };
return Contribution.addContribution(contribution).catch(error => { return Contribution.add(contribution).catch(error => {
robot.logger.error(`[hubot-kredits] Adding contribution failed:`, error); robot.logger.error(`[hubot-kredits] Adding contribution failed:`, error);
}); });
}).catch(() => { }).catch(() => {
@@ -126,6 +126,7 @@ module.exports = async function(robot, kredits) {
return [...new Set(changes.map(c => `"${c.title}"`))].join(', '); return [...new Set(changes.map(c => `"${c.title}"`))].join(', ');
} }
// Currently not used
function calculateAmountForChanges(details) { function calculateAmountForChanges(details) {
let amount; let amount;
@@ -144,9 +145,8 @@ module.exports = async function(robot, kredits) {
const dateNow = new Date(); const dateNow = new Date();
const dateYesterday = dateNow.setDate(dateNow.getDate() - 1); const dateYesterday = dateNow.setDate(dateNow.getDate() - 1);
const date = (new Date(dateYesterday)).toISOString().split('T')[0]; const date = (new Date(dateYesterday)).toISOString().split('T')[0];
const details = analyzeUserChanges(user, changes); const details = analyzeUserChanges(user, changes);
const amount = calculateAmountForChanges(details); const amount = 500;
let desc = `Added ${details.charsAdded} characters of text.`; let desc = `Added ${details.charsAdded} characters of text.`;
if (details.pagesChanged.length > 0) { if (details.pagesChanged.length > 0) {

6876
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,31 @@
{ {
"name": "hubot-kredits", "name": "hubot-kredits",
"version": "3.7.0", "version": "4.0.0",
"description": "Kosmos Kredits functionality for chat bots", "description": "Kosmos Kredits functionality for chat bots",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"bin": { "bin": {
"create-wallet": "scripts/create-wallet.js" "create-wallet": "scripts/create-wallet.js",
"review-kredits": "scripts/review-kredits.js"
}, },
"dependencies": { "dependencies": {
"@ethersproject/experimental": "5.0.0",
"@kredits/contracts": "7.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^8.2.0",
"eth-provider": "^0.2.2", "eth-provider": "^0.2.2",
"ethers": "^5.0.5", "ethers": "^5.0.5",
"@ethersproject/experimental": "5.0.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.16.2", "express-session": "^1.16.2",
"grant-express": "^4.6.1", "grant-express": "^4.6.1",
"group-array": "^1.0.0", "group-array": "^1.0.0",
"kosmos-schemas": "^1.1.2", "kosmos-schemas": "^1.1.2",
"kredits-contracts": "^6.0.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",
"yargs": "^16.2.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -36,7 +39,8 @@
"author": "Kosmos Developers <mail@kosmos.org>", "author": "Kosmos Developers <mail@kosmos.org>",
"contributors": [ "contributors": [
"Sebastian Kippe <sebastian@kip.pe>", "Sebastian Kippe <sebastian@kip.pe>",
"Michael Bumann <hello@michaelbumann.com>" "Michael Bumann <hello@michaelbumann.com>",
"Garret Alfert <alfert@wevelop.de>"
], ],
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {

25
repos.json Normal file
View File

@@ -0,0 +1,25 @@
{
"github": [
"67P/botka",
"67P/hal8000",
"67P/hubot-kredits",
"67P/hubot-remotestorage-logger",
"67P/hyperchannel",
"67P/kosmos-schemas",
"67P/kredits-contracts",
"67P/kredits-signup",
"67P/kredits-web",
"67P/remotestorage-module-kosmos",
"67P/waves",
"sockethub/sockethub"
],
"gitea": [
"kosmos/akkounts",
"kosmos/chef",
"kosmos/gitea.kosmos.org",
"kosmos/ipfs-cookbook",
"kosmos/kredits-ipfs-pinner",
"kosmos/website",
"kosmos/wormhole"
]
}

View File

@@ -0,0 +1,97 @@
const fetch = require('node-fetch');
module.exports = class GiteaReviews {
token = null;
kreditsAmounts = null;
pageLimit = 100;
constructor (token, kreditsAmounts) {
this.token = token;
this.kreditsAmounts = kreditsAmounts;
}
async request (path) {
return fetch(
`https://gitea.kosmos.org/api/v1${path}`,
{
headers: {
'accepts': 'application/json',
'Authorization': `token ${this.token}`
}
}
).then(response => response.json());
}
async getReviewContributions (repos, startDate, endDate) {
let reviewContributions = {}
await Promise.all(repos.map(async (repo) => {
let page = 1;
let result;
do {
try {
result = await this.request(`/repos/${repo}/pulls?state=closed&limit=${this.pageLimit}&page=${page}`);
} catch(error) {
console.log(`failed to fetch PRs for repo ${repo}:`, error.message);
continue;
}
if (!result || result.length === 0) {
continue;
}
let pullRequests = result.filter(pr => {
if (!pr.merged) return false; // only interested in merged PRs
// check if the PR has been merged in the given timeframe
const mergeDate = new Date(pr.merged_at);
if (mergeDate < startDate || mergeDate > endDate) return false;
// check if the PR has a kredits label
return pr.labels.some(label => label.name.match(/kredits-[123]/));
});
await Promise.all(pullRequests.map(async (pr) => {
let reviews;
try {
reviews = await this.request(`/repos/${repo}/pulls/${pr.number}/reviews`);
} catch(error) {
console.log(`failed to fetch reviews for repo ${repo}, PR ${pr.number}:`, error.message);
return;
}
if (!reviews || reviews.length === 0) {
return;
}
reviews = reviews.filter(review => {
return ['APPROVED', 'REJECTED'].includes(review.state);
});
reviews.forEach(review => {
if (typeof reviewContributions[review.user.login] === 'undefined') {
reviewContributions[review.user.login] = [];
}
let kreditsLabel = pr.labels.find(label => label.name.match(/kredits-[123]/));
reviewContributions[review.user.login].push({
pr,
prNumber: pr.number,
review,
reviewState: review.state,
kredits: this.kreditsAmounts[kreditsLabel.name]
});
});
}));
page++;
} while (result && result.length > 0);
}));
return reviewContributions;
}
}

View File

@@ -0,0 +1,98 @@
const fetch = require('node-fetch');
module.exports = class GithubReviews {
token = null;
kreditsAmounts = null;
pageLimit = 100;
constructor (token, kreditsAmounts) {
this.token = token;
this.kreditsAmounts = kreditsAmounts;
}
async request (path) {
return fetch(
`https://api.github.com${path}`,
{
headers: {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'Kosmos Kredits for reviews',
'Authorization': `token ${this.token}`
}
}
).then(response => response.json());
}
async getReviewContributions (repos, startDate, endDate) {
let reviewContributions = {}
await Promise.all(repos.map(async (repo) => {
let page = 1;
let result;
do {
try {
result = await this.request(`/repos/${repo}/pulls?state=closed&perPage=${this.pageLimit}&page=${page}`);
} catch(error) {
console.log(`failed to fetch PRs for repo ${repo}:`, error.message);
continue;
}
if (!result || result.length === 0) {
continue;
}
let pullRequests = result.filter(pr => {
if (!pr.merged_at) return false; // only interested in merged PRs
// check if the PR has been merged in the given timeframe
const mergeDate = new Date(pr.merged_at);
if (mergeDate < startDate || mergeDate > endDate) return false;
// check if the PR has a kredits label
return pr.labels.some(label => label.name.match(/kredits-[123]/));
});
await Promise.all(pullRequests.map(async (pr) => {
let reviews;
try {
reviews = await this.request(`/repos/${repo}/pulls/${pr.number}/reviews`);
} catch(error) {
console.log(`failed to fetch reviews for repo ${repo}, PR ${pr.number}:`, error.message);
return;
}
if (!reviews || reviews.length === 0) {
return;
}
reviews = reviews.filter(review => {
return ['APPROVED', 'REJECTED'].includes(review.state);
});
reviews.forEach(review => {
if (typeof reviewContributions[review.user.login] === 'undefined') {
reviewContributions[review.user.login] = [];
}
let kreditsLabel = pr.labels.find(label => label.name.match(/kredits-[123]/));
reviewContributions[review.user.login].push({
pr,
prNumber: pr.number,
review,
reviewState: review.state,
kredits: this.kreditsAmounts[kreditsLabel.name]
});
});
}));
page++;
} while (result && result.length > 0);
}));
return reviewContributions;
}
}

189
scripts/review-kredits.js Executable file
View File

@@ -0,0 +1,189 @@
#!/usr/bin/env node
require('dotenv').config({ path: '../.env' });
const GiteaReviews = require('./lib/gitea-reviews');
const GithubReviews = require('./lib/github-reviews');
const ethers = require('ethers');
const Kredits = require('kredits-contracts');
const util = require('util');
const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
const providerUrl = process.env.KREDITS_PROVIDER_URL;
const daoAddress = process.env.KREDITS_DAO_ADDRESS;
const ipfsConfig = {
host: process.env.IPFS_API_HOST || 'localhost',
port: process.env.IPFS_API_PORT || '5001',
protocol: process.env.IPFS_API_PROTOCOL || 'http'
};
const kreditsAmounts = {
'kredits-1': 100,
'kredits-2': 300,
'kredits-3': 1000
};
const repos = require('../repos.json');
const argv = yargs(hideBin(process.argv))
.option('start', {
alias: 's',
description: 'Include reviews for PRs merged after this date'
})
.option('end', {
alias: 'e',
description: 'Include reviews for PRs merged before this date'
})
.option('dry', {
alias: 'd',
type: 'boolean',
description: 'Only list contribution details without creating them'
})
.help()
.version()
.demandOption('start', 'Please provide a start date')
.default('end', function now () {
return (new Date()).toISOString().split('.')[0]+"Z";
})
.example([
['$0 --start 2020-11-01 --end 2020-11-30T23:59:59Z', 'Create contributions for reviews of pull requests merged in November 2020'],
['$0 --start 2021-01-01', 'Create contributions for reviews of pull requests merged from Januar 2021 until now'],
])
.argv
const startTimestamp = Date.parse(argv.start);
const endTimestamp = Date.parse(argv.end);
if (isNaN(startTimestamp)) {
console.log('The provided start date is invalid');
process.exit(1);
}
if (isNaN(endTimestamp)) {
console.log('The provided end date is invalid');
process.exit(1);
}
// check for existence of GITHUB_TOKEN and GITEA_TOKEN
if (!process.env.GITHUB_TOKEN || !process.env.GITEA_TOKEN) {
console.log('Please set both GITHUB_TOKEN and GITEA_TOKEN');
process.exit(1);
}
const startDate = new Date(startTimestamp);
const endDate = new Date(endTimestamp);
async function getAllReviews(repos, startDate, endDate) {
const githubReviews = new GithubReviews(process.env.GITHUB_TOKEN, kreditsAmounts);
const giteaReviews = new GiteaReviews(process.env.GITEA_TOKEN, kreditsAmounts);
return Promise.all([
githubReviews.getReviewContributions(repos.github, startDate, endDate),
giteaReviews.getReviewContributions(repos.gitea, startDate, endDate)
]).then(reviews => {
// console.log(util.inspect(reviews[0], { depth: 3, colors: true }));
return { github: reviews[0], gitea: reviews[1] }
});
}
async function initializeKredits () {
//
// Ethereum provider/node setup
//
let ethProvider;
if (providerUrl) {
ethProvider = new ethers.providers.JsonRpcProvider(providerUrl);
} else {
ethProvider = new ethers.getDefaultProvider('rinkeby');
}
//
// Kredits contracts setup
//
const opts = { ipfsConfig };
if (daoAddress) {
opts.addresses = { Kernel: daoAddress };
}
let kredits;
try {
kredits = await new Kredits(ethProvider, null, opts).init();
} catch(error) {
console.log('Could not set up kredits:', error);
process.exit(1);
}
return kredits;
}
async function generateContributionData(reviews, Contributor, startDate, endDate) {
const dateFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' };
const contributors = await Contributor.all();
const contributionData = {};
const now = (new Date()).toISOString().split('.')[0]+"Z";
[date, time] = now.split('T');
function addContributionDataForPlatform(platform) {
for (const [username, platformReviews] of Object.entries(reviews[platform])) {
const contributor = contributors.find(c => {
return c[`${platform}_username`] === username;
});
if (!contributor) {
console.log(`Could not find contributor for ${platform} user "${username}"`);
continue;
}
const urls = platformReviews.map(review => review.pr.html_url);
const kreditsAmount = platformReviews.reduce((amount, review) => {
return review.kredits + amount;
}, 0);
if (typeof contributionData[contributor.name] !== 'undefined') {
contributionData[contributor.name].amount += kreditsAmount;
contributionData[contributor.name].details.pullRequests.push(...urls);
} else {
const formattedStartDate = startDate.toLocaleString('en-us', dateFormatOptions);
const formattedEndDate = endDate.toLocaleString('en-us', dateFormatOptions);
contributionData[contributor.name] = {
contributorId: contributor.id,
contributorIpfsHash: contributor.ipfsHash,
date,
time,
amount: kreditsAmount,
kind: 'dev',
description: `PR reviews from ${formattedStartDate} to ${formattedEndDate}`,
details: {
'pullRequests': urls
}
}
}
}
}
addContributionDataForPlatform('gitea');
addContributionDataForPlatform('github');
return contributionData;
}
Promise.all([initializeKredits(), getAllReviews(repos, startDate, endDate)]).then((values) => {
const kredits = values[0];
const reviews = values[1];
generateContributionData(reviews, kredits.Contributor, startDate, endDate).then(contributionData => {
if (argv.dry) {
console.log('contributions:');
console.log(util.inspect(contributionData, { depth: 3, colors: true }));
}
// TODO create contributions
});
});