Skip to content

Commit

Permalink
some major changes were made for v0.1.0
Browse files Browse the repository at this point in the history
- Compliance inspector codes for rules have transferred to their "rules.js" file
- Switched to "promises" from "callbacks" for async functions
- A detailed GitHub name check algorithm has implemented
- GitHub REST API has implemented instead of using "request" and "cheerio" together to scrap GitHub pages
- All implemented rules were collected under a final js object especially for future use
  • Loading branch information
techrube committed Feb 4, 2018
1 parent 55cb291 commit 4375a81
Show file tree
Hide file tree
Showing 4 changed files with 369 additions and 251 deletions.
31 changes: 8 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
# **SCOUTOPIAN<sub>_DISCORD BOT_</sub>**
Scoutopian is designed to be a utopian.io Discord Bot which will check contributions to give users/mods an opinion whether the contribution meets the [rules](https://utopian.io/rules).
Scoutopian is designed to be a utopian.io Discord Bot which will inspect contributions to give users and mods an opinion whether the contribution meets the [rules](https://utopian.io/rules).

![scoutopian-header-image](https://res.cloudinary.com/hpiynhbhq/image/upload/v1516409451/mj0ar1oom5trozxzqoix.png)

*All features support Utopian.io Rules Update #8

## Features
**1- GitHub repository**
- Looks for README.md and LICENSE files in the repo
- Checks if the last commit is older than 1 year
- Compares Utopian and GitHub names of the contributor. (for development category)
- Scoutopian will also search for the utopian username in the body of pull request if GitHub username is not same as utopian name.
- Scoutopian will also search for the utopian username in the following fields if GitHub username is not same as utopian name.
- Body of the pull request
- The "name", "bio" and "blog fields" of the user's GitHub profile
- Checks if PR merge or Commit date is at most 14 days old. (for development category)
- Gives an estimated score calculated by SOFT and HARD rules of Utopian

**2- Downvotes**
- Look for downvotes of the accounts/bots listed below
- Looks for downvotes of the accounts/bots listed below
- @steemcleaners
- @cheetah (upvotes will also count)
- @mack-bot
- @spaminator

**3- Coming Features & Roadmap**
- All possible rules will be added including category rules
- If you have any suggestion or noticed a bug to report; you can contact me on Discord (@techrube#9826).
___
## How to set up:
**1 - Create a Discord App & a Bot User**
Expand All @@ -34,22 +38,3 @@ ___
**3 - Set Environment Variables**
- Copy the bot user token from the Discord App page
- Set the `DISCORD_BOT_TOKEN` in your `.env` using your bot token that you just copied.

___
## Technology Stack
- **Language:** Javascript
- **Server Framework:** Node.js
- The icon is a modified form of a design which provided by [Freepik](http://www.freepik.com)

**Libraries**
- [Steem.js](https://www.npmjs.com/package/steem) - Interacting with the Steem Blockchain
- [Eris](https://www.npmjs.com/package/eris) - Interacting with the Discord API
- [Cheerio](https://www.npmjs.com/package/cheerio) - Server side jQuery implementation
- [Request](https://www.npmjs.com/package/request) - To make http calls

## Preview
**Sample Case 1:** Utopian username does not match with GitHub username and cannot found attached to pull request or name field on the GitHub profile page of the contributor. (This check is specific to development category)
![Different-GitHub-Name](https://res.cloudinary.com/hpiynhbhq/image/upload/v1516409254/ovpby06zu2pooetqmfnn.gif)

**Sample Case 2:** Contribution is flagged by at least one of the cleaner accounts/bots
![Downvoted-by-Bots](https://res.cloudinary.com/hpiynhbhq/image/upload/v1516409253/od2ljdouiybakvim3of8.gif)
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{
"name": "scoutopian",
"version": "0.0.3",
"version": "0.1.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"eris": "^0.7.2",
"eris": "latest",
"request": "latest",
"cheerio": "latest",
"steem": "latest"
"request-promise": "latest",
"steem": "latest",
"express": "latest"
},
"engines": {
"node": "8.x"
Expand Down
306 changes: 306 additions & 0 deletions rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
"use strict";
const request = require("request-promise")
const steem = require("steem");
const GITHUB_API = "https://api.github.com"
steem.api.setOptions({url: "https://api.steemit.com"});

var rules = {}

rules.githubCheck = (checkList) => {
let utopianName = checkList.contribution.author
let jsonMetaData = JSON.parse(checkList.contribution.json_metadata)
let repoFullName = jsonMetaData.repository.full_name
let repo = {
license : false,
readme : false,
age : false,
name_check : false
}

var githubName = () => {

var githubReqBodyCheck = (reqBody) => {
return new Promise((resolve, reject) => {
if(reqBody.search("@"+utopianName) != -1)
repo.name_check = true
resolve(repo.name_check)
})
}

/** Try to find a pull request link on the utopian.io contribution */
var searchForPRLinks = () => {
let links = jsonMetaData.links
return new Promise((resolve, reject) => {
for(let i=0; i<links.length; i++){
if(links[i].search("https:\/\/github\.com\/"+jsonMetaData.repository.full_name+"\/pull\/") == 0){
let prNo = links[i].split("/")[6]
let prUrl = GITHUB_API + "/repos/"+jsonMetaData.repository.full_name+"/pulls/"+prNo.toString()
getDataByUrl(prUrl)
.then((prJson) => {handlePR(JSON.parse(prJson))})
.then(() => resolve()).catch((err) => reject(err))
return false //stop searching, when the first pull request link is found on the contribution's body
}
}
resolve()
})
}

var handlePR = (prJson) => {
return new Promise((resolve, reject) => {
repo.github_username = prJson.user.login
repo.pr = {
age_check : false,
merged_at : prJson.merged_at,
body : prJson.body
}
if(repo.github_username === utopianName){
repo.name_check = true
}
//creation date&time of the contribution should be at most 14 days after the pr merge date
if(repo.pr.merged_at != null && Date.parse(checkList.contribution.created) - Date.parse(repo.pr.merged_at) <= 14 * 86400000)
repo.pr.age_check = true
resolve()
})
}

/** Tries to find a github commit link on the utopian.io contribution */
var searchForCommitLinks = () => {
return new Promise((resolve, reject) => {
let links = jsonMetaData.links
for(let i=0; i<links.length; i++){
if(links[i].search("https:\/\/github\.com\/" + jsonMetaData.repository.full_name + "\/commit\/") == 0){
let prNo = links[i].split("/")[6]
getDataByUrl(GITHUB_API + "/repos/" + jsonMetaData.repository.full_name + "/commits/" + prNo.toString())
.then((cmJson) => handleCommit(cmJson))
.then(() => resolve())
.catch((err) => reject(err))
return false;
}
}
resolve()
})
}

var handleCommit = (cmJson) => {
return new Promise((resolve, reject) => {
cmJson = JSON.parse(cmJson)
repo.github_username = cmJson.author.login
repo.commit = {
date : cmJson.commit.author.date,
age_check : false
}
if(repo.github_username === utopianName)
repo.name_check = true
if(Date.parse(checkList.contribution.created) - Date.parse(cmJson.commit.author.date) <= 14 * 86400000)
repo.commit.age_check = true
resolve()
})
}

/** Searches the contributor's GitHub profile to find the Utopian.io username specified under the "name", "blog" and "bio" fields */
var checkGithubProfile = () => {
let username = repo.github_username
if(repo.github_username == undefined){ //cannot found any link of a pull request or a commit. project owner maybe?
username = jsonMetaData.repository.owner.login
}
return new Promise((resolve, reject) => {
getDataByUrl(GITHUB_API + "/users/" + username).then((res) => {
let githubProfile = JSON.parse(res)
let rgx = new RegExp(utopianName, 'i')
if(githubProfile.name != null && (githubProfile.name).match(rgx) != null)
repo.name_check = true
if(githubProfile.blog != null && (githubProfile.blog).match(rgx) != null)
repo.name_check = true
if(githubProfile.bio != null && (githubProfile.bio).match(rgx) != null)
repo.name_check = true
resolve()
}).catch((err)=> reject(err))
})
}

return new Promise((resolve, reject)=>{
if(jsonMetaData.pullRequests[0] != null){ //a pull request is attached to the utopian.io contribution
let pr = jsonMetaData.pullRequests[0]
handlePR(pr)
.then(() => {
if(!repo.name_check)
return githubReqBodyCheck(pr.body)
})
.then(() => {
if(!repo.name_check)
return checkGithubProfile()
})
.then(() => resolve())
.catch((err) => reject(err));
}
else{
searchForPRLinks()
.then(() => {
if(repo.github_username == undefined)
return searchForCommitLinks()
})
.then(() => {
if(repo.github_username == undefined || !repo.name_check) //There is not any commit/pr link OR utopian.io name doesn't match with the github name
return checkGithubProfile() // search for the utopian username on the user's profile
})
.then(() => resolve())
.catch((err) => reject(err))
}
})
}

var getDataByUrl = (prUrl) => {
return new Promise((resolve, reject) => {
let options = {
url: prUrl,
headers: {
'User-Agent': 'scoutopian'
}
}
request(options)
.then((result) => resolve(result))
.catch((err) => reject(err))
})
}

var createResponse = () => {
return new Promise((resolve, reject) => {
repo.text = ((repo.readme) ? ":white_check_mark:" : ":x:") + " README.md [SOFT] \n" +
((repo.license) ? ":white_check_mark:" : ":x:") + " LICENSE [SOFT] \n" +
((repo.age) ? ":white_check_mark:" : ":x:") + " Age [HARD] \n";
if(jsonMetaData.type === "development"){
githubName().then(() => {
repo.text += ((repo.name_check) ? ":white_check_mark:" : ":x:") + " GitHub Name \n" +
((repo.commit != undefined && repo.commit.age_check) || (repo.pr != undefined &&repo.pr.age_check) ? ":white_check_mark:" : ":x:") + " PR/Commit Age \n"
resolve()
}).catch((err) => reject(err))
}
else resolve()
})
}

var fetchRepoData = () => {
return new Promise((resolve, reject) => {
Promise.all([getDataByUrl(GITHUB_API + "/repos/" + repoFullName + "/readme")
, getDataByUrl(GITHUB_API + "/repos/" + repoFullName)])
.then((results) => {
let readme = results[0]
if(readme.message == undefined)
repo.readme = true
let repoJson = JSON.parse(results[1])
repo.pushed_at = repoJson.pushed_at
if((parseInt(Date.parse(checkList.contribution.created))) - parseInt(Date.parse(repo.pushed_at)) <= 31556952000) //1 year
repo.age = true
if(repoJson.license != null || (repoJson.parent != undefined && repoJson.parent.license != null))
repo.license = true
resolve()
}).catch((err) => reject(err))
})
}

return new Promise((resolve, reject) => {
fetchRepoData()
.then(() => createResponse())
.then(() => {
rules.score.decSoft(repo.readme)
rules.score.decSoft(repo.license)
rules.score.decHard(repo.age)
if(jsonMetaData.type === "development"){
rules.score.decHard(repo.name_check)
rules.score.decHard((repo.commit != undefined && repo.commit.age_check) || (repo.pr != undefined &&repo.pr.age_check))
}
})
.then(() => {
resolve(repo)
}).catch((err) => reject(err))
})

}


rules.downvoteCheck = (votes) => {
let downvotes = {
steemcleaners : true,
cheetah : true,
mack_bot : true,
spaminator : true
};
for(let i=0; i<votes.length;i++){
//cheetah *upvotes* when it detects plagiarism
if(votes[i].voter === "cheetah"){
downvotes.cheetah = false;
}
else{
if(votes[i].rshares < 0){
switch (votes[i].voter){
case "steemcleaners": downvotes.steemcleaners = false; break;
//case "cheetah": downvotes.cheetah = false; break;
case "mack-bot": downvotes.mack_bot = false; break;
case "spaminator": downvotes.spaminator = false; break;
default : break;
}
}
}
}
downvotes.text = (downvotes.steemcleaners ? ":white_check_mark:" : ":x:") + " steemcleaners\n" +
(downvotes.cheetah ? ":white_check_mark:" : ":x:") + " cheetah\n" +
(downvotes.mack_bot ? ":white_check_mark:" : ":x:") + " mack-bot\n" +
(downvotes.spaminator ? ":white_check_mark:" : ":x:") + " spaminator\n";
if(!downvotes.steemcleaners || !downvotes.cheetah || !downvotes.mack_bot || !downvotes.spaminator)
rules.score.decHard(false);
return new Promise((resolve, reject) => {
resolve(downvotes);
})
}


rules.score = {
decSoft: function(checkedRule){
if(!checkedRule){
this.value -= 10; // score decrement can be adjusted by total number of the soft rules
this.value = (this.value < 0) ? 0 : this.value;
}
},
decHard: function(checkedRule){
if(!checkedRule){
this.value = Math.floor(this.value/1.5);
}
},
value: 100
}


rules.getContribution = (url) => {
url = url.split("/")
let author = url[4].split("@")[1]
let permlink = url[5]
return new Promise((resolve, reject) => {
steem.api.getContent(author, permlink, (err, res) => {
if(err) return reject(err);
resolve(res)
})
})
}


rules.completeRuleCheck = (url) => {
let checkList = {}
return new Promise((resolve, reject) => {
rules.getContribution(url)
.then((contribution) => {
checkList.contribution = contribution
Promise.all([rules.githubCheck(checkList), rules.downvoteCheck(checkList.contribution.active_votes)])
.then((results) => {
checkList.github = results[0]
checkList.downvotes = results[1]
checkList.score = rules.score.value
resolve(checkList)
})
}).catch((err) => reject(err))
})


}


module.exports = rules;

0 comments on commit 4375a81

Please sign in to comment.