Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
Merge pull request #26 from devcnairobi/develop
Browse files Browse the repository at this point in the history
Release v1.0.1
  • Loading branch information
profnandaa authored Apr 15, 2017
2 parents 7d53d25 + ee34b1f commit 93d4f93
Show file tree
Hide file tree
Showing 16 changed files with 434 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"presets": [
"es2015"
]
}
8 changes: 8 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
engines:
duplication:
enabled: true
checks:
Similar code:
enabled: false
fixme:
enabled: false
11 changes: 11 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
PORT=5000
PAGE_TOKEN=
VERIFY_TOKEN=
APP_SECRET=
GH_ORG=devcnairobi
FB_API_KEY=
FB_DOMAIN=
FB_URL=
FB_STORAGE_BUCKET=
FB_MESSAGING_SENDER_ID=
GH_OAUTH_TOKEN=
29 changes: 29 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"rules": {
"comma-dangle": "off",
"no-console": "off",
"keyword-spacing": "warn",
"key-spacing": "error",
"prefer-const": "error",
"space-before-function-paren": ["error", "never"],
"eqeqeq": ["error", "smart"],
"no-constant-condition": "off",
"no-var": "warn",
"object-shorthand": "warn",
"semi": "error"
},
"extends": [
"eslint:recommended"
],
"env": {
"es6": true,
"node": true
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
playground
.DS_Store
.env
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,43 @@
# devc-nairobi-bot

[![Code Climate](https://codeclimate.com/github/devcnairobi/devc-nairobi-bot/badges/gpa.svg)](https://codeclimate.com/github/devcnairobi/devc-nairobi-bot) [![Issue Count](https://codeclimate.com/github/devcnairobi/devc-nairobi-bot/badges/issue_count.svg)](https://codeclimate.com/github/devcnairobi/devc-nairobi-bot)

Messenger Bot for DevC Nairobi

### Pre-requisites

- Node 6.9.0+
- Git
- [Ngrok](https://ngrok.com)

### Getting started

- Set environment variables as follows:
* Copy `.env.sample` to `.env` (all env settings will live on this file)
* Register a Firebase test app and use the provided settings to update the `FB_` settings in the just created `.env` file. Ensure database rules are set to public write i.e.:
```
{
"rules": {
".read": false,
".write": true
}
}
```
* On your Github settings page, create a new [_personal access token_](https://github.com/settings/tokens) with at least scope(`admin:org - write`) and update `GH_OAUTH_TOKEN` with the generated token.
* You may need to create a test Github org in order to update `GH_ORG`.
* Run `ngrok http 5000` and note the generated *https* url.
* Set up your bot's [Facebook webhook](https://developers.facebook.com/docs/messenger-platform/guides/setup) using the ngrok url.
* Use the provided Facebook developer app credentials to update `PAGE_TOKEN`, `VERIFY_TOKEN` and `APP_SECRET`.
- Install required node packages with `npm install`.
- Run the bot with `npm start`.

### Linting

- The code is validated using [ESLint](http://eslint.org/).
- If you are using Atom you should install the following plugins:
* [linter](https://atom.io/packages/linter)
* [linter-eslint](https://atom.io/packages/linter-eslint)

### Tests

- TODO
3 changes: 3 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const bot = require('./bot/server');

bot.start(process.env.PORT);
74 changes: 74 additions & 0 deletions bot/gh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const http = require('request');
const createLogger = require('bunyan').createLogger;

const ORG = process.env.GH_ORG;
const headers = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': ORG,
'Authorization': `token ${process.env.GH_OAUTH_TOKEN}`
};

const log = createLogger({
name: 'github',
stream: process.stdout,
level: 'info',
});

function checkUsername(username) {
return new Promise(function(resolve, reject){

const logc = log.child({
username
});

logc.info(`checking username`);

const opts = {
url: `https://api.github.com/users/${username}`,
headers,
};

http.get(opts, (err, res) => {
if (res.statusCode !== 200) {
logc.error('username check failed');
return reject();
}
logc.info('validated username!');
resolve();
});

});
}


function addToOrg(username) {
return new Promise(function(resolve, reject){

const logc = log.child({
username,
org: ORG
});

logc.info(`adding user to org`);

const opts = {
url: `https://api.github.com/orgs/${ORG}/memberships/${username}`,
headers,
};

http.put(opts, (err, res) => {
if (res.statusCode !== 200) {
logc.error('failed to add user to org');
return reject();
}
logc.info('invited/added user to org!');
resolve();
});

});
}

module.exports = {
checkUsername,
addToOrg,
};
54 changes: 54 additions & 0 deletions bot/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const BootBot = require('bootbot');
const replies = require('./replies');
const User = require('./user');
const db = require('../storage/firebase');
const createLogger = require('bunyan').createLogger;

const log = createLogger({
name: 'bot',
stream: process.stdout,
level: 'info',
});

const bot = new BootBot({
accessToken: process.env.PAGE_TOKEN,
verifyToken: process.env.VERIFY_TOKEN,
appSecret: process.env.APP_SECRET
});

bot.on('error', (err) => {
log.error(err.message);
});

bot.on('message', (payload) => {
const ctx = {
sender: payload.sender.id,
message: payload.message.text,
};
log.child(ctx).info('message');
});

bot.hear([/hi/i, /hello/i], (payload, chat) => {
chat.say(replies.default);
});

bot.hear([/register/i, /sign[- ]?up/i], (payload, chat) => {
const psid = payload.sender.id; // Page scoped ID
chat.getUserProfile().then((user) => {
db.saveUser(psid, user, () => {
User.register(chat, (userPatch) => {
db.updateUser(psid, userPatch);
});
});
});
});

bot.hear([/add [a-z ]* github/i], (payload, chat) => {
// phrase like 'add me to Github'
User.addToGithub(chat, (userPatch) => {
// okay to add redudant psid on the user object
db.updateUser(userPatch.psid, userPatch);
});
});

module.exports = bot;
3 changes: 3 additions & 0 deletions bot/load-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const env = require('dotenv'); // https://github.com/motdotla/dotenv/issues/114

env.config({ silent: true });
3 changes: 3 additions & 0 deletions bot/replies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
default: 'Welcome to Developer Circles: Nairobi! This is version 2 of me :-D',
};
4 changes: 4 additions & 0 deletions bot/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require('./load-env');
const bot = require('./');

module.exports = bot;
134 changes: 134 additions & 0 deletions bot/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* User Management
*/
const validator = require('validator');
const gh = require('./gh');

module.exports = {
register(chat, callback) {

const askEmail = (convo) => {
convo.ask(`What's your email?`, (payload, convo) => {
const email = payload.message.text;
if (!validator.isEmail(email)) {
// loop recursively until a valid email is provided
convo.say(`Oops, this doesn't look like a valid email :(`)
.then(() => askEmail(convo));
} else {
convo.set('email', email);
convo.say(`Good!`)
.then(() => askTShirtSize(convo));
}
});
};

const askTShirtSize = (convo) => {
const question = {
text: `What's your t-shirt size?`,
quickReplies: ['S', 'M', 'L', 'XL']
};

convo.ask(question, (payload, convo) => {
convo.set('t_shirt_size', payload.message.text);
askIfDeveloper(convo);
});
};

const askIfDeveloper = (convo) => {
const question = {
text: `Are you a developer?`,
quickReplies: ['Yes', 'No']
};

convo.ask(question, (payload, convo) => {
const re = /yes/i;
if (re.test(payload.message.text)) {
convo.set('occupation', 'developer');
end(convo);
} else {
askOccupation(convo);
}
});
};

const askOccupation = (convo) => {
convo.ask(`What's your occupation?`, (payload, convo) => {
const text = payload.message.text;
if (text && text.length > 3) {
convo.set('occupation', payload.message.text);
end(convo);
} else {
convo.say(`Sorry, I didn't get that.`)
.then(() => askOccupation(convo));
}
});
};

const end = (convo) => {
chat.getUserProfile().then((user) => {
convo.say(`Thanks ${user.first_name}! You are registered :) Welcome to Developer Circle: Nairobi - http://fb.com/groups/DevCNairobi`);
convo.end();
callback({
email: convo.get('email'),
occupation: convo.get('occupation'),
t_shirt_size: convo.get('t_shirt_size'),
});
});
};

chat.conversation((convo) => {
askEmail(convo);
});
},

addToGithub(chat, callback) {
const askUsername = (convo, recursive, count = 0) => {
const question = recursive ? `Please try again` : `What's your Github username?`;

convo.ask(question, (payload, convo) => {
const psid = payload.sender.id;
let username = payload.message.text;
let validUsername = true;
// clean up
if (username[0] === '@') username = username.substring(1);
// validate
validUsername = username.length > 1;
// check if it's a valid GH username
if (validUsername) {

gh.checkUsername(username)
.then(() => {
gh.addToOrg(username)
.then(()=>{
convo.say(`An invite has been sent to @${username}, please accept it to join org - https://github.com/${process.env.GH_ORG}. Thanks!`);
convo.end();
callback({ psid, github_username: username });
}, ()=>{
convo.say(`We couldn't add you to github org automatically so a human will add you manually soon.`);
convo.end();
callback({ psid, github_username: `failed_${username}` });
});
}, () => {
if (count < 2) {
convo
.say(`The username @${username} wasn't found, please provide a valid username`)
.then(() => askUsername(convo, true, ++count));
} else {
convo.say(`There seem to be something wrong, let's start over again. Sorry :(`);
convo.end();
callback({ psid, github_username: `failed_${username}` });
}
});
} else {
convo
.say(`The username you provided looks invalid, please check.`)
.then(askUsername(convo, true));
}
});
};

chat.conversation((convo) => {
askUsername(convo);
});
}
};
Loading

0 comments on commit 93d4f93

Please sign in to comment.