Merge pull request #16 from 67P/feature/6-mediawiki_integration
MediaWiki integration & integration architecture improvements
This commit is contained in:
		
						commit
						b852bd24af
					
				
							
								
								
									
										230
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										230
									
								
								index.js
									
									
									
									
									
								
							| @ -31,34 +31,51 @@ const ipfsConfig = { | ||||
| }; | ||||
| 
 | ||||
| module.exports = async function(robot) { | ||||
|   let wallet; | ||||
|   try { | ||||
|     wallet = await ethers.Wallet.fromEncryptedWallet(walletJson, process.env.KREDITS_WALLET_PASSWORD); | ||||
|   } catch(error) { | ||||
|     console.log('[hubot-kredits] Could not load wallet:', error); | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   const ethProvider = new ethers.providers.JsonRpcProvider(providerUrl, {chainId: networkId}); | ||||
|   ethProvider.signer = wallet; | ||||
|   wallet.provider = ethProvider; | ||||
| 
 | ||||
|   let kredits; | ||||
|   try { | ||||
|     kredits = await Kredits.setup(ethProvider, wallet, ipfsConfig); | ||||
|   } catch(error) { | ||||
|     console.log('[hubot-kredits] Could not setup kredits:', error); | ||||
|     process.exit(1); | ||||
|   } | ||||
|   const Contributor = kredits.Contributor; | ||||
|   const Operator = kredits.Operator; | ||||
| 
 | ||||
|   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); | ||||
|   } catch(error) { | ||||
|     robot.logger.warn('[hubot-kredits] Could not load wallet:', error); | ||||
|     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) { | ||||
|     robot.logger.warn('[hubot-kredits] Could not set up kredits:', error); | ||||
|     process.exit(1); | ||||
|   } | ||||
|   const Contributor = kredits.Contributor; | ||||
|   const Operator = kredits.Operator; | ||||
| 
 | ||||
|   robot.logger.info('[hubot-kredits] Wallet address: ' + wallet.address); | ||||
| 
 | ||||
|   //
 | ||||
|   // Check robot's wallet balance and alert when it's broke
 | ||||
|   //
 | ||||
| 
 | ||||
|   ethProvider.getBalance(wallet.address).then(balance => { | ||||
|     robot.logger.info('[hubot-kredits] Wallet balance: ' + ethers.utils.formatEther(balance) + 'ETH'); | ||||
|     if (balance.lt(ethers.utils.parseEther('0.0001'))) { | ||||
| @ -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,163 +114,16 @@ 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) => { | ||||
|       // 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); | ||||
| @ -259,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`);
 | ||||
|       }); | ||||
|     }); | ||||
| @ -267,4 +141,14 @@ module.exports = async function(robot) { | ||||
| 
 | ||||
|   watchContractEvents(); | ||||
| 
 | ||||
|   //
 | ||||
|   // Integrations
 | ||||
|   //
 | ||||
| 
 | ||||
|   require('./integrations/github')(robot, kredits); | ||||
| 
 | ||||
|   if (typeof process.env.KREDITS_MEDIAWIKI_URL !== 'undefined') { | ||||
|     require('./integrations/mediawiki')(robot, kredits); | ||||
|   } | ||||
| 
 | ||||
| }; | ||||
|  | ||||
							
								
								
									
										178
									
								
								integrations/github.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								integrations/github.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,178 @@ | ||||
| const util = require('util'); | ||||
| const fetch = require('node-fetch'); | ||||
| 
 | ||||
| module.exports = async function(robot, kredits) { | ||||
| 
 | ||||
|   function messageRoom(message) { | ||||
|     robot.messageRoom(process.env.KREDITS_ROOM, message); | ||||
|   } | ||||
| 
 | ||||
|   robot.logger.debug('[hubot-kredits] Loading GitHub integration...'); | ||||
| 
 | ||||
|   let repoBlackList = []; | ||||
|   if (process.env.KREDITS_GITHUB_REPO_BLACKLIST) { | ||||
|     repoBlackList = process.env.KREDITS_GITHUB_REPO_BLACKLIST.split(','); | ||||
|     robot.logger.debug('[hubot-kredits] Ignoring GitHub actions from ', util.inspect(repoBlackList)); | ||||
|   } | ||||
| 
 | ||||
|   const Contributor = kredits.Contributor; | ||||
|   const Operator = kredits.Operator; | ||||
| 
 | ||||
|   function getContributorByGithubUser(username) { | ||||
|     return Contributor.all().then(contributors => { | ||||
|       let contrib = contributors.find(c => { | ||||
|         return c.github_username === username; | ||||
|       }); | ||||
|       if (!contrib) { | ||||
|         throw new Error(`No contributor found for ${username}`); | ||||
|       } else { | ||||
|         return contrib; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function createProposal(githubUser, amount, description, url, details) { | ||||
|     return getContributorByGithubUser(githubUser).then(contributor => { | ||||
|       robot.logger.debug(`[hubot-kredits] Creating proposal to issue ${amount}₭S to ${githubUser} for ${url}...`); | ||||
| 
 | ||||
|       let contributionAttr = { | ||||
|         contributorId: contributor.id, | ||||
|         amount: amount, | ||||
|         contributorIpfsHash: contributor.ipfsHash, | ||||
|         url, | ||||
|         description, | ||||
|         details, | ||||
|         kind: 'dev' | ||||
|       }; | ||||
| 
 | ||||
|       return Operator.addProposal(contributionAttr).catch(error => { | ||||
|         robot.logger.error(`[hubot-kredits] Error:`, error); | ||||
|         messageRoom(`I wanted to propose giving kredits to GitHub user ${githubUser} for ${url}, but I cannot find their info. Please add them as a contributor: https://kredits.kosmos.org`); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function amountFromIssueLabels(issue) { | ||||
|     let kreditsLabel = issue.labels.map(l => l.name) | ||||
|                             .filter(n => n.match(/^kredits/))[0]; | ||||
|     // No label, no kredits
 | ||||
|     if (typeof kreditsLabel === 'undefined') { return 0; } | ||||
| 
 | ||||
|     // TODO move to config maybe?
 | ||||
|     let amount; | ||||
|     switch(kreditsLabel) { | ||||
|       case 'kredits-1': | ||||
|         amount = 500; | ||||
|         break; | ||||
|       case 'kredits-2': | ||||
|         amount = 1500; | ||||
|         break; | ||||
|       case 'kredits-3': | ||||
|         amount = 5000; | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return amount; | ||||
|   } | ||||
| 
 | ||||
|   function handleGitHubIssueClosed(data) { | ||||
|     let recipients; | ||||
|     let issue        = data.issue; | ||||
|     let assignees    = issue.assignees.map(a => a.login); | ||||
|     let web_url      = issue.html_url; | ||||
| 
 | ||||
|     let amount = amountFromIssueLabels(issue); | ||||
|     let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1]; | ||||
|     let description = `${repoName}: ${issue.title}`; | ||||
| 
 | ||||
|     if (amount === 0) { | ||||
|       robot.logger.info('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); | ||||
|       return Promise.resolve(); | ||||
|     } else if (repoBlackList.includes(repoName)) { | ||||
|       robot.logger.debug(`[hubot-kredits] ${repoName} is on black list; ignoring`); | ||||
|       return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     if (assignees.length > 0) { | ||||
|       recipients = assignees; | ||||
|     } else { | ||||
|       recipients = [issue.user.login]; | ||||
|     } | ||||
| 
 | ||||
|     let proposalPromises = []; | ||||
|     recipients.forEach(recipient => { | ||||
|       proposalPromises.push( | ||||
|         createProposal(recipient, amount, description, web_url, issue) | ||||
|           .catch(err => robot.logger.error(err)) | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     return Promise.all(proposalPromises); | ||||
|   } | ||||
| 
 | ||||
|   function handleGitHubPullRequestClosed(data) { | ||||
|     let recipients; | ||||
|     let pull_request = data.pull_request; | ||||
|     let assignees    = pull_request.assignees.map(a => a.login); | ||||
|     let web_url      = pull_request._links.html.href; | ||||
|     let pr_issue_url = pull_request.issue_url; | ||||
| 
 | ||||
|     if (assignees.length > 0) { | ||||
|       recipients = assignees; | ||||
|     } else { | ||||
|       recipients = [pull_request.user.login]; | ||||
|     } | ||||
| 
 | ||||
|     return fetch(pr_issue_url) | ||||
|       .then(response => { | ||||
|         if (response.status >= 400) { | ||||
|           throw new Error('Bad response from fetching PR issue'); | ||||
|         } | ||||
|         return response.json(); | ||||
|       }) | ||||
|       .then(issue => { | ||||
|         let amount = amountFromIssueLabels(issue); | ||||
|         let repoName = pull_request.base.repo.full_name; | ||||
|         let description = `${repoName}: ${pull_request.title}`; | ||||
| 
 | ||||
|         if (amount === 0) { | ||||
|           robot.logger.info('[hubot-kredits] Proposal amount from issue label is zero; ignoring'); | ||||
|           return Promise.resolve(); | ||||
|         } else if (repoBlackList.includes(repoName)) { | ||||
|           robot.logger.debug(`[hubot-kredits] ${repoName} is on black list; ignoring`); | ||||
|           return Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         let proposalPromises = []; | ||||
|         recipients.forEach(recipient => { | ||||
|           console.debug(`[hubot-kredits] Creating proposal for ${recipient}...`); | ||||
|           proposalPromises.push( | ||||
|             createProposal(recipient, amount, description, web_url, pull_request) | ||||
|               .catch(err => robot.logger.error(err)) | ||||
|           ); | ||||
|         }); | ||||
| 
 | ||||
|         return Promise.all(proposalPromises); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   robot.router.post('/incoming/kredits/github/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => { | ||||
|     let evt = req.header('X-GitHub-Event'); | ||||
|     let data = req.body; | ||||
|     // For some reason data is contained in a payload property on one
 | ||||
|     // machine, but directly in the root of the object on others
 | ||||
|     if (data.payload) { data = JSON.parse(data.payload); } | ||||
| 
 | ||||
|     robot.logger.info(`Received GitHub hook. Event: ${evt}, action: ${data.action}`); | ||||
| 
 | ||||
|     if (evt === 'pull_request' && data.action === 'closed') { | ||||
|       handleGitHubPullRequestClosed(data).then(() => res.send(200)); | ||||
|     } | ||||
|     else if (evt === 'issues' && data.action === 'closed') { | ||||
|       handleGitHubIssueClosed(data).then(() => res.send(200)); | ||||
|     } else { | ||||
|       res.send(200); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
| }; | ||||
							
								
								
									
										178
									
								
								integrations/mediawiki.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								integrations/mediawiki.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,178 @@ | ||||
| const util = require('util'); | ||||
| const fetch = require('node-fetch'); | ||||
| const groupArray = require('group-array'); | ||||
| const cron = require('node-cron'); | ||||
| 
 | ||||
| module.exports = async function(robot, kredits) { | ||||
| 
 | ||||
|   function messageRoom(message) { | ||||
|     robot.messageRoom(process.env.KREDITS_ROOM, message); | ||||
|   } | ||||
| 
 | ||||
|   robot.logger.debug('[hubot-kredits] Loading MediaWiki integration...') | ||||
| 
 | ||||
|   const Contributor = kredits.Contributor; | ||||
|   const Operator = kredits.Operator; | ||||
| 
 | ||||
|   const 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', | ||||
|       'format=json', | ||||
|       'list=recentchanges', | ||||
|       'rctype=edit|new', | ||||
|       'rcshow=!minor|!bot|!anon|!redirect', | ||||
|       'rclimit=max', | ||||
|       'rcprop=ids|title|timestamp|user|sizes|comment|flags' | ||||
|     ]; | ||||
| 
 | ||||
|     let endTime = robot.brain.get('kredits:mediawiki:last_processed_at'); | ||||
|     if (endTime) { | ||||
|       robot.logger.debug(`[hubot-kredits] Fetching wiki edits since ${endTime}`); | ||||
|       params.push(`rcend=${endTime}`); | ||||
|     } | ||||
| 
 | ||||
|     const url = `${apiURL}?${params.join('&')}`; | ||||
| 
 | ||||
|     return fetch(url).then(res => { | ||||
|       if (res.status === 200) { | ||||
|         return res.json(); | ||||
|       } else { | ||||
|         robot.logger.info(`Fetching ${url} returned HTTP status ${res.status}:`); | ||||
|         robot.logger.info(res.body); | ||||
|         throw Error('Unexpected response from '+url); | ||||
|       } | ||||
|     }).then(res => { | ||||
|       return res.query.recentchanges; | ||||
|     }).catch(res => { | ||||
|       robot.logger.error(`[hubot-kredits] Failed to fetch ${url} (likely due to a network issue)`); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function groupChangesByUser (changes) { | ||||
|     return Promise.resolve(groupArray(changes, 'user')); | ||||
|   } | ||||
| 
 | ||||
|   function analyzeUserChanges (user, changes) { | ||||
|     // robot.logger.debug(`Analyzing ${changes.length} edits from ${user} ...`);
 | ||||
|     const results = {}; | ||||
| 
 | ||||
|     results.pagesCreated = changes.filter(c => c.type === 'new'); | ||||
|     results.pagesChanged = changes.filter(c => c.type === 'edit'); | ||||
|     results.charsAdded = changes | ||||
|       .map(c => { return (c.oldlen < c.newlen) ? (c.newlen - c.oldlen) : 0; }) | ||||
|       .reduce((a, b) => a + b); | ||||
| 
 | ||||
|     // robot.logger.debug(`Created ${results.pagesCreated.length} pages`);
 | ||||
|     // robot.logger.debug(`Edited ${results.pagesChanged.length} pages`);
 | ||||
|     // robot.logger.debug(`Added ${results.charsAdded} lines of text\n`);
 | ||||
|     return results; | ||||
|   } | ||||
| 
 | ||||
|   function createProposals (changes) { | ||||
|     let promises = []; | ||||
| 
 | ||||
|     Object.keys(changes).forEach(user => { | ||||
|       promises.push(createProposalForUserChanges(user, changes[user])); | ||||
|     }); | ||||
| 
 | ||||
|     return Promise.all(promises); | ||||
|   } | ||||
| 
 | ||||
|   function pageTitlesFromChanges(changes) { | ||||
|     return changes.map(c => `"${c.title}"`).join(', '); | ||||
|   } | ||||
| 
 | ||||
|   function calculateAmountForChanges(details) { | ||||
|     let amount; | ||||
| 
 | ||||
|     if (details.charsAdded < 280) { | ||||
|       // less than a tweet
 | ||||
|       amount = 500; | ||||
|     } else if (details.charsAdded < 2000) { | ||||
|       amount = 1500; | ||||
|     } else { | ||||
|       amount = 5000; | ||||
|     } | ||||
| 
 | ||||
|     return amount; | ||||
|   } | ||||
| 
 | ||||
|   function createProposalForUserChanges (user, changes) { | ||||
|     const details = analyzeUserChanges(user, changes); | ||||
|     const amount = calculateAmountForChanges(changes); | ||||
| 
 | ||||
|     let desc = `Added ${details.charsAdded} characters of text.`; | ||||
|     if (details.pagesChanged.length > 0) { | ||||
|       desc = `Edited ${pageTitlesFromChanges(details.pagesChanged)}. ${desc}`; | ||||
|     } | ||||
|     if (details.pagesCreated.length > 0) { | ||||
|       desc = `Created ${pageTitlesFromChanges(details.pagesCreated)}. ${desc}`; | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
|   } | ||||
| 
 | ||||
|   function updateTimestampForNextFetch () { | ||||
|     robot.logger.debug(`[hubot-kredits] Set timestamp for wiki changes fetch`); | ||||
|     robot.brain.set('kredits:mediawiki:last_processed_at', new Date().toISOString()); | ||||
|   } | ||||
| 
 | ||||
|   function processWikiChangesSinceLastRun () { | ||||
|     fetchChanges() | ||||
|       .then(res => groupChangesByUser(res)) | ||||
|       .then(res => createProposals(res)) | ||||
|       .then(() => updateTimestampForNextFetch()); | ||||
|   } | ||||
| 
 | ||||
|   cron.schedule('* 7 * * *', processWikiChangesSinceLastRun); | ||||
| 
 | ||||
| }; | ||||
							
								
								
									
										193
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										193
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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,15 +299,12 @@ | ||||
|         "repeating": "2.0.1" | ||||
|       } | ||||
|     }, | ||||
|     "elliptic": { | ||||
|       "version": "6.3.3", | ||||
|       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.3.3.tgz", | ||||
|       "integrity": "sha1-VILZZG1UvLif19mU/J4ulWiHbj8=", | ||||
|     "encoding": { | ||||
|       "version": "0.1.12", | ||||
|       "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", | ||||
|       "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", | ||||
|       "requires": { | ||||
|         "bn.js": "4.11.8", | ||||
|         "brorand": "1.1.0", | ||||
|         "hash.js": "1.1.3", | ||||
|         "inherits": "2.0.3" | ||||
|         "iconv-lite": "0.4.21" | ||||
|       } | ||||
|     }, | ||||
|     "escape-string-regexp": { | ||||
| @ -322,10 +329,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 +357,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 +411,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 +438,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": { | ||||
| @ -393,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", | ||||
| @ -415,6 +485,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 +503,34 @@ | ||||
|         "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" | ||||
|       } | ||||
|     }, | ||||
|     "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", | ||||
|       "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 +546,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 +576,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", | ||||
| @ -517,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", | ||||
| @ -631,11 +741,27 @@ | ||||
|       "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", | ||||
|       "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 +787,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 +835,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", | ||||
|  | ||||
| @ -11,10 +11,12 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "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" | ||||
|     "truffle-kredits": "github:67P/truffle-kredits" | ||||
|   }, | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user