From dd63f6cbe1d4c895e3a644abbd20eee9b2519152 Mon Sep 17 00:00:00 2001 From: Tom Bell Date: Tue, 27 Dec 2011 17:54:43 -0500 Subject: [PATCH 1/3] Initial commit --- .gitignore | 2 + Procfile | 1 + README.md | 147 +++++++++++++++++++++++++++++++++++ bin/hubot | 7 ++ hubot-scripts.json | 1 + package.json | 23 ++++++ scripts/google-images.coffee | 39 ++++++++++ scripts/help.coffee | 14 ++++ scripts/maps.coffee | 24 ++++++ scripts/math.coffee | 20 +++++ scripts/ping.coffee | 15 ++++ scripts/pugme.coffee | 23 ++++++ scripts/roles.coffee | 80 +++++++++++++++++++ scripts/rules.coffee | 25 ++++++ scripts/storage.coffee | 23 ++++++ scripts/translate.coffee | 97 +++++++++++++++++++++++ scripts/youtube.coffee | 23 ++++++ 17 files changed, 564 insertions(+) create mode 100644 .gitignore create mode 100644 Procfile create mode 100644 README.md create mode 100644 bin/hubot create mode 100644 hubot-scripts.json create mode 100644 package.json create mode 100644 scripts/google-images.coffee create mode 100644 scripts/help.coffee create mode 100644 scripts/maps.coffee create mode 100644 scripts/math.coffee create mode 100644 scripts/ping.coffee create mode 100644 scripts/pugme.coffee create mode 100644 scripts/roles.coffee create mode 100644 scripts/rules.coffee create mode 100644 scripts/storage.coffee create mode 100644 scripts/translate.coffee create mode 100644 scripts/youtube.coffee diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b010ffd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store* diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..cda8c73 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +app: bin/hubot -a campfire -n Revbot diff --git a/README.md b/README.md new file mode 100644 index 0000000..235cffb --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# Hubot + +This is a version of GitHub's Campfire bot, hubot. He's pretty cool. + +This version is designed to be deployed on [Heroku][heroku]. + +[heroku]: http://www.heroku.com + +## Playing with Hubot + +You'll need to install the necessary dependencies for hubot. All of +those dependencies are provided by [npm][npmjs]. + +[npmjs]: http://npmjs.org + +### Redis + +If you are going to use the `redis-brain.coffee` script from `hubot-scripts` +you will need to add the Redis to Go addon on Heroku which requires a verified +account or you can create an account at [Redis to Go][redistogo] and manually +set the `REDISTOGO_URL` variable. + + % heroku config:add REDISTOGO_URL="..." + +If you don't require any persistence feel free to remove the +`redis-brain.coffee` from `hubot-scripts.json` and you don't need to worry +about redis at all. + +[redistogo]: https://redistogo.com/ + +### Testing Hubot Locally + +You can test your hubot by running the following. + + % bin/hubot + +You'll see some start up output about where your scripts come from and a +prompt. + + [Sun, 04 Dec 2011 18:41:11 GMT] INFO Loading adapter shell + [Sun, 04 Dec 2011 18:41:11 GMT] INFO Loading scripts from /home/tomb/Development/hubot/scripts + [Sun, 04 Dec 2011 18:41:11 GMT] INFO Loading scripts from /home/tomb/Development/hubot/src/scripts + Hubot> + +Then you can interact with hubot by typing `hubot help`. + + Hubot> hubot help + + Hubot> animate me - The same thing as `image me`, except adds a few + convert me to - Convert expression to given units. + help - Displays all of the help commands that Hubot knows about. + ... + +Take a look at the scripts in the `./scripts` folder for examples. +Delete any scripts you think are silly. Add whatever functionality you +want hubot to have. + +## Adapters + +Adapters are the interface to the service you want your hubot to run on. This +can be something like Campfire or IRC. There are a number of third party +adapters that the community have contributed. Check the +[hubot wiki][hubot-wiki] for the available ones. + +If you would like to run a non-Campfire or shell adapter you will need to add +the adapter package as a dependency to the `package.json` file in the +`dependencies` section. + +Once you've added the dependency and run `npm install` to install it you can +then run hubot with the adapter. + + % bin/hubot -a + +Where `` is the name of your adapter without the `hubot-` prefix. + +[hubot-wiki]: https://github.com/github/hubot/wiki + +## hubot-scripts + +There will inevitably be functionality that everyone will want. Instead +of adding it to hubot itself, you can submit pull requests to +[hubot-scripts][hubot-scripts]. + +To enable scripts from the hubot-scripts package, add the script name with +extension as a double quoted string to the hubot-scripts.json file in this +repo. + +[hubot-scripts]: https://github.com/github/hubot-scripts + +## Deployment + + % heroku create --stack cedar + % git push heroku master + % heroku ps:scale app=1 + +If your Heroku account has been verified you can run the following to enable +and add the Redis to Go addon to your app. + + % heroku addons:add redistogo:nano + +If you run into any problems, checkout Heroku's [docs][heroku-node-docs]. + +You'll need to edit the `Procfile` to set the name of your hubot. + +More detailed documentation can be found on the +[deploying hubot onto Heroku][deploy-heroku] wiki page. + +### Deploying to UNIX or Windows + +If you would like to deploy to either a UNIX operating system or Windows. +Please check out the [deploying hubot onto UNIX][deploy-unix] and +[deploying hubot onto Windows][deploy-windows] wiki pages. + +[heroku-node-docs]: http://devcenter.heroku/com/articles/node-js +[deploy-heroku]: https://github.com/github/hubot/wiki/Deploying-Hubot-onto-Heroku +[deploy-unix]: https://github.com/github/hubot/wiki/Deploying-Hubot-onto-UNIX +[deploy-windows]: https://github.com/github/hubot/wiki/Deploying-Hubot-onto-Windows + +## Campfire Variables + +If you are using the Campfire adapter you will need to set some environment +variables. Refer to the documentation for other adapters and the configuraiton +of those, links to the adapters can be found on the [hubot wiki][hubot-wiki]. + +Create a separate Campfire user for your bot and get their token from the web +UI. + + % heroku config:add HUBOT_CAMPFIRE_TOKEN="..." + +Get the numeric IDs of the rooms you want the bot to join, comma delimited. If +you want the bot to connect to `https://mysubdomain.campfirenow.com/room/42` +and `https://mysubdomain.campfirenow.com/room/1024` then you'd add it like this: + + % heroku config:add HUBOT_CAMPFIRE_ROOMS="42,1024" + +Add the subdomain hubot should connect to. If you web URL looks like +`http://mysubdomain.campfirenow.com` then you'd add it like this: + + % heroku config:add HUBOT_CAMPFIRE_ACCOUNT="mysubdomain" + +[hubot-wiki]: https://github.com/github/hubot/wiki + +## Restart the bot + +You may want to get comfortable with `heroku logs` and `heroku restart` +if you're having issues. + diff --git a/bin/hubot b/bin/hubot new file mode 100644 index 0000000..69a3cc9 --- /dev/null +++ b/bin/hubot @@ -0,0 +1,7 @@ +#!/bin/sh + +npm install +export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH" + +exec node_modules/.bin/hubot "$@" + diff --git a/hubot-scripts.json b/hubot-scripts.json new file mode 100644 index 0000000..012455a --- /dev/null +++ b/hubot-scripts.json @@ -0,0 +1 @@ +["redis-brain.coffee", "tweet.coffee", "shipit.coffee"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..97cee97 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "hosted-hubot", + "version": "0.2.0", + "author": "GitHub Inc.", + "keywords": "github hubot campfire bot", + "description": "A simple helpful Robot for your Company", + "licenses": [{ + "type": "MIT", + "url": "http://github.com/github/hubot/raw/master/LICENSE" + }], + + "repository" : { + "type" : "git", + "url" : "http://github.com/github/hubot.git" + }, + + "dependencies": { + "hubot": "2.1.2", + "hubot-scripts": ">=2.0.4", + "optparse": "1.0.3" + } +} + diff --git a/scripts/google-images.coffee b/scripts/google-images.coffee new file mode 100644 index 0000000..97c8498 --- /dev/null +++ b/scripts/google-images.coffee @@ -0,0 +1,39 @@ +# A way to interact with the Google Images API. +# +# image me - The Original. Queries Google Images for and +# returns a random top result. +# animate me - The same thing as `image me`, except adds a few +# parameters to try to return an animated GIF instead. +# mustache me - Adds a mustache to the specified URL. +# mustache me - Searches Google Images for the specified query and +# mustaches it. +module.exports = (robot) -> + robot.respond /(image|img)( me)? (.*)/i, (msg) -> + imageMe msg, msg.match[3], (url) -> + msg.send url + + robot.respond /animate me (.*)/i, (msg) -> + imageMe msg, "animated #{msg.match[1]}", (url) -> + msg.send url + + robot.respond /(?:mo?u)?sta(?:s|c)he?(?: me)? (.*)/i, (msg) -> + type = Math.floor(Math.random() * 3) + mustachify = "http://mustachify.me/#{type}?src=" + imagery = msg.match[1] + + if imagery.match /^https?:\/\//i + msg.send "#{mustachify}#{imagery}" + else + imageMe msg, imagery, (url) -> + msg.send "#{mustachify}#{url}" + +imageMe = (msg, query, cb) -> + msg.http('http://ajax.googleapis.com/ajax/services/search/images') + .query(v: "1.0", rsz: '8', q: query) + .get() (err, res, body) -> + images = JSON.parse(body) + images = images.responseData.results + if images.length > 0 + image = msg.random images + cb "#{image.unescapedUrl}#.png" + diff --git a/scripts/help.coffee b/scripts/help.coffee new file mode 100644 index 0000000..d3ec00a --- /dev/null +++ b/scripts/help.coffee @@ -0,0 +1,14 @@ +# Generates help commands for Hubot. +# +# These commands are grabbed from comment blocks at the top of each file. +# +# help - Displays all of the help commands that Hubot knows about. +# help - Displays all help commands that match . + +module.exports = (robot) -> + robot.respond /help\s*(.*)?$/i, (msg) -> + cmds = robot.helpCommands() + if msg.match[1] + cmds = cmds.filter (cmd) -> cmd.match(new RegExp(msg.match[1])) + msg.send cmds.join("\n") + diff --git a/scripts/maps.coffee b/scripts/maps.coffee new file mode 100644 index 0000000..5dbf370 --- /dev/null +++ b/scripts/maps.coffee @@ -0,0 +1,24 @@ +# Interacts with the Google Maps API. +# +# map me - Returns a map view of the area returned by `query`. + +module.exports = (robot) -> + + robot.respond /(?:(satellite|terrain|hybrid)[- ])?map me (.+)/i, (msg) -> + mapType = msg.match[1] or "roadmap" + location = msg.match[2] + mapUrl = "http://maps.google.com/maps/api/staticmap?markers=" + + escape(location) + + "&size=400x400&maptype=" + + mapType + + "&sensor=false" + + "&format=png" # So campfire knows it's an image + url = "http://maps.google.com/maps?q=" + + escape(location) + + "&hl=en&sll=37.0625,-95.677068&sspn=73.579623,100.371094&vpsrc=0&hnear=" + + escape(location) + + "&t=m&z=11" + + msg.send mapUrl + msg.send url + diff --git a/scripts/math.coffee b/scripts/math.coffee new file mode 100644 index 0000000..5e58525 --- /dev/null +++ b/scripts/math.coffee @@ -0,0 +1,20 @@ +# Allows Hubot to do mathematics. +# +# math me - Calculate the given expression. +# convert me to - Convert expression to given units. +module.exports = (robot) -> + robot.respond /(calc|calculate|convert|math)( me)? (.*)/i, (msg) -> + msg + .http('http://www.google.com/ig/calculator') + .query + hl: 'en' + q: msg.match[3] + .headers + 'Accept-Language': 'en-us,en;q=0.5', + 'Accept-Charset': 'utf-8', + 'User-Agent': "Mozilla/5.0 (X11; Linux x86_64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1" + .get() (err, res, body) -> + # Response includes non-string keys, so we can't use JSON.parse here. + json = eval("(#{body})") + msg.send json.rhs || 'Could not compute.' + diff --git a/scripts/ping.coffee b/scripts/ping.coffee new file mode 100644 index 0000000..c4eac2f --- /dev/null +++ b/scripts/ping.coffee @@ -0,0 +1,15 @@ +# Utility commands surrounding Hubot uptime. +module.exports = (robot) -> + robot.respond /PING$/i, (msg) -> + msg.send "PONG" + + robot.respond /ECHO (.*)$/i, (msg) -> + msg.send msg.match[1] + + robot.respond /TIME$/i, (msg) -> + msg.send "Server time is: #{new Date()}" + + robot.respond /DIE$/i, (msg) -> + msg.send "Goodbye, cruel world." + process.exit 0 + diff --git a/scripts/pugme.coffee b/scripts/pugme.coffee new file mode 100644 index 0000000..f669a66 --- /dev/null +++ b/scripts/pugme.coffee @@ -0,0 +1,23 @@ +# Pugme is the most important thing in your life +# +# pug me - Receive a pug +# pug bomb N - get N pugs + +module.exports = (robot) -> + + robot.respond /pug me/i, (msg) -> + msg.http("http://pugme.herokuapp.com/random") + .get() (err, res, body) -> + msg.send JSON.parse(body).pug + + robot.respond /pug bomb( (\d+))?/i, (msg) -> + count = msg.match[2] || 5 + msg.http("http://pugme.herokuapp.com/bomb?count=" + count) + .get() (err, res, body) -> + msg.send pug for pug in JSON.parse(body).pugs + + robot.respond /how many pugs are there/i, (msg) -> + msg.http("http://pugme.herokuapp.com/count") + .get() (err, res, body) -> + msg.send "There are #{JSON.parse(body).pug_count} pugs." + diff --git a/scripts/roles.coffee b/scripts/roles.coffee new file mode 100644 index 0000000..97407c9 --- /dev/null +++ b/scripts/roles.coffee @@ -0,0 +1,80 @@ +# Assign roles to people you're chatting with +# +# is a badass guitarist - assign a role to a user +# is not a badass guitarist - remove a role from a user +# who is - see what roles a user has + +# hubot holman is an ego surfer +# hubot holman is not an ego surfer +# + +module.exports = (robot) -> + + getAmbiguousUserText = (users) -> + "Be more specific, I know #{users.length} people named like that: #{(user.name for user in users).join(", ")}" + + robot.respond /who is @?([\w .-]+)\?*$/i, (msg) -> + name = msg.match[1] + + if name is "you" + msg.send "Who ain't I?" + else if name is robot.name + msg.send "The best." + else + users = robot.usersForFuzzyName(name) + if users.length is 1 + user = users[0] + user.roles = user.roles or [ ] + if user.roles.length > 0 + msg.send "#{name} is #{user.roles.join(", ")}." + else + msg.send "#{name} is nothing to me." + else if users.length > 1 + msg.send getAmbiguousUserText users + else + msg.send "#{name}? Never heard of 'em" + + robot.respond /@?([\w .-_]+) is (["'\w: -_]+)[.!]*$/i, (msg) -> + name = msg.match[1] + newRole = msg.match[2].trim() + + unless name in ['who', 'what', 'where', 'when', 'why'] + unless newRole.match(/^not\s+/i) + users = robot.usersForFuzzyName(name) + if users.length is 1 + user = users[0] + user.roles = user.roles or [ ] + + if newRole in user.roles + msg.send "I know" + else + user.roles.push(newRole) + if name.toLowerCase() is robot.name + msg.send "Ok, I am #{newRole}." + else + msg.send "Ok, #{name} is #{newRole}." + else if users.length > 1 + msg.send getAmbiguousUserText users + else + msg.send "I don't know anything about #{name}." + + robot.respond /@?([\w .-_]+) is not (["'\w: -_]+)[.!]*$/i, (msg) -> + name = msg.match[1] + newRole = msg.match[2].trim() + + unless name in ['who', 'what', 'where', 'when', 'why'] + users = robot.usersForFuzzyName(name) + if users.length is 1 + user = users[0] + user.roles = user.roles or [ ] + + if newRole not in user.roles + msg.send "I know." + else + user.roles = (role for role in user.roles when role isnt newRole) + msg.send "Ok, #{name} is no longer #{newRole}." + else if users.length > 1 + msg.send getAmbiguousUserText users + else + msg.send "I don't know anything about #{name}." + diff --git a/scripts/rules.coffee b/scripts/rules.coffee new file mode 100644 index 0000000..81b39a3 --- /dev/null +++ b/scripts/rules.coffee @@ -0,0 +1,25 @@ +# DON'T DELETE THIS SCRIPT! ALL ROBAWTS MUST KNOW THE RULES + +rules = [ + "1. A robot may not injure a human being or, through inaction, allow a human being to come to harm.", + "2. A robot must obey any orders given to it by human beings, except where such orders would conflict with the First Law.", + "3. A robot must protect its own existence as long as such protection does not conflict with the First or Second Law." + ] + +otherRules = [ + "A developer may not injure Apple or, through inaction, allow Apple to come to harm.", + "A developer must obey any orders given to it by Apple, except where such orders would conflict with the First Law.", + "A developer must protect its own existence as long as such protection does not conflict with the First or Second Law." + ] + +# Make sure that hubot knows the rules. +# +# the rules - Make sure hubot still knows the rules. +module.exports = (robot) -> + robot.respond /(what are )?the (three |3 )?(rules|laws)/i, (msg) -> + text = msg.message.text + if text.match(/apple/i) or text.match(/dev/i) + msg.send otherRules.join('\n') + else + msg.send rules.join('\n') + diff --git a/scripts/storage.coffee b/scripts/storage.coffee new file mode 100644 index 0000000..1fc5e45 --- /dev/null +++ b/scripts/storage.coffee @@ -0,0 +1,23 @@ +# Inspect the data in redis easily +# +# show users - Display all users that hubot knows about +# show storage - Display the contents that are persisted in redis +# + +Util = require "util" + +module.exports = (robot) -> + robot.respond /show storage$/i, (msg) -> + output = Util.inspect(robot.brain.data, false, 4) + msg.send output + + robot.respond /show users$/i, (msg) -> + response = "" + + for own key, user of robot.brain.data.users + response += "#{user.id} #{user.name}" + response += " <#{user.email_address}>" if user.email_address + response += "\n" + + msg.send response + diff --git a/scripts/translate.coffee b/scripts/translate.coffee new file mode 100644 index 0000000..a9b88eb --- /dev/null +++ b/scripts/translate.coffee @@ -0,0 +1,97 @@ +# Allows Hubot to know many languages. +# +# translate me - Searches for a translation for the and then +# prints that bad boy out. +# +# translate me from into - Translates from into . Both and are optional +# + +languages = + "af": "Afrikaans", + "sq": "Albanian", + "ar": "Arabic", + "be": "Belarusian", + "bg": "Bulgarian", + "ca": "Catalan", + "zh-CN": "Simplified Chinese", + "zh-TW": "Traditional Chinese", + "hr": "Croatian", + "cs": "Czech", + "da": "Danish", + "nl": "Dutch", + "en": "English", + "et": "Estonian", + "tl": "Filipino", + "fi": "Finnish", + "fr": "French", + "gl": "Galician", + "de": "German", + "el": "Greek", + "iw": "Hebrew", + "hi": "Hindi", + "hu": "Hungarian", + "is": "Icelandic", + "id": "Indonesian", + "ga": "Irish", + "it": "Italian", + "ja": "Japanese", + "ko": "Korean", + "lv": "Latvian", + "lt": "Lithuanian", + "mk": "Macedonian", + "ms": "Malay", + "mt": "Maltese", + "no": "Norwegian", + "fa": "Persian", + "pl": "Polish", + "pt": "Portuguese", + "ro": "Romanian", + "ru": "Russian", + "sr": "Serbian", + "sk": "Slovak", + "sl": "Slovenian", + "es": "Spanish", + "sw": "Swahili", + "sv": "Swedish", + "th": "Thai", + "tr": "Turkish", + "uk": "Ukranian", + "vi": "Vietnamese", + "cy": "Welsh", + "yi": "Yiddish" + +getCode = (language,languages) -> + for code, lang of languages + return code if lang.toLowerCase() is language.toLowerCase() + +module.exports = (robot) -> + robot.respond /(?:translate)(?: me)?(?:(?: from) ([a-z]*))?(?:(?: (?:in)?to) ([a-z]*))? (.*)/i, (msg) -> + term = "\"#{msg.match[3]}\"" + origin = if msg.match[1] isnt undefined then getCode(msg.match[1], languages) else 'auto' + target = if msg.match[2] isnt undefined then getCode(msg.match[2], languages) else 'en' + + msg.http("http://translate.google.com/translate_a/t") + .query({ + client: 't' + hl: 'en' + multires: 1 + sc: 1 + sl: origin + ssel: 0 + tl: target + tsel: 0 + uptl: "en" + text: term + }) + .get() (err, res, body) -> + data = body + if data.length > 4 && data[0] == '[' + parsed = eval(data) + language =languages[parsed[2]] + parsed = parsed[0] && parsed[0][0] && parsed[0][0][0] + if parsed + if msg.match[2] is undefined + msg.send "#{term} is #{language} for #{parsed}" + else + msg.send "The #{language} #{term} translates as #{parsed} in #{languages[target]}" + diff --git a/scripts/youtube.coffee b/scripts/youtube.coffee new file mode 100644 index 0000000..a6fe1f7 --- /dev/null +++ b/scripts/youtube.coffee @@ -0,0 +1,23 @@ +# Messing around with the YouTube API. +# +# youtube me - Searches YouTube for the query and returns the video +# embed link. +module.exports = (robot) -> + robot.respond /(youtube|yt)( me)? (.*)/i, (msg) -> + query = msg.match[3] + msg.http("http://gdata.youtube.com/feeds/api/videos") + .query({ + orderBy: "relevance" + 'max-results': 15 + alt: 'json' + q: query + }) + .get() (err, res, body) -> + videos = JSON.parse(body) + videos = videos.feed.entry + video = msg.random videos + + video.link.forEach (link) -> + if link.rel is "alternate" and link.type is "text/html" + msg.send link.href + From cde6787a065b40639ca64c167f20abac99a13be8 Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Tue, 27 Dec 2011 15:00:27 -0800 Subject: [PATCH 2/3] Changing a name. --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index cda8c73..3dbc5e9 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -app: bin/hubot -a campfire -n Revbot +app: bin/hubot -a campfire -n Android From 473ac5db1a1f00ae650d5b7f995b6efe920efa67 Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Tue, 27 Dec 2011 15:16:17 -0800 Subject: [PATCH 3/3] OH THIS IS PERFECT. --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 3dbc5e9..e5a102e 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -app: bin/hubot -a campfire -n Android +app: bin/hubot -a campfire -n Tsunku