This repository has been archived by the owner on Sep 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from devcnairobi/develop
Release v1.0.1
- Loading branch information
Showing
16 changed files
with
434 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"presets": [ | ||
"es2015" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
playground | ||
.DS_Store | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const bot = require('./bot/server'); | ||
|
||
bot.start(process.env.PORT); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
require('./load-env'); | ||
const bot = require('./'); | ||
|
||
module.exports = bot; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} | ||
}; |
Oops, something went wrong.