├── .env.example ├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── assets └── slack-icon.png ├── bin ├── console_slacker.rb └── slacker.rb ├── lib ├── adapters │ ├── adapter.rb │ ├── http │ │ └── generic_http_client.rb │ ├── repl │ │ └── console_adapter.rb │ └── slack │ │ ├── slack_adapter.rb │ │ └── slack_http_client.rb ├── listener.rb ├── message.rb ├── plugins │ ├── coin_flip_plugin.rb │ ├── github │ │ ├── github_ensemble.rb │ │ ├── github_identity_manager.rb │ │ └── plugins │ │ │ └── github_identity_plugin.rb │ ├── graphite │ │ ├── graph_search_plugin.rb │ │ ├── graph_view_plugin.rb │ │ ├── graphite_api.rb │ │ └── graphite_ensemble.rb │ ├── jira │ │ ├── jira_datastore.rb │ │ ├── jira_integration.rb │ │ └── plugins │ │ │ ├── browse_issues_plugin.rb │ │ │ ├── issue_assignment_plugin.rb │ │ │ ├── issue_info_plugin.rb │ │ │ └── state_change_plugin.rb │ ├── plugin.rb │ ├── remember_plugin.rb │ ├── timezone_plugin.rb │ └── util_plugin.rb ├── robot.rb └── scoped_listener.rb └── spec ├── listener_spec.rb ├── plugins └── util_spec.rb └── spec_helper.rb /.env.example: -------------------------------------------------------------------------------- 1 | NAME=slacker 2 | SLACK_TOKEN= 3 | 4 | GITHUB_CLIENT_ID= 5 | GITHUB_CLIENT_SECRET= 6 | 7 | REDIS_HOST=127.0.0.1 8 | REDIS_DB=0 9 | REDIS_PORT=6379 10 | 11 | JIRA_URL= 12 | JIRA_DEFAULT_PROJECT= 13 | JIRA_USERNAME=admin 14 | JIRA_PASSWORD=admin 15 | 16 | JIRA_TRANSITION_RESOLVE=5 17 | JIRA_TRANSITION_REOPEN=3 18 | 19 | S3_BUCKET_NAME= 20 | S3_ACCESS_KEY_ID= 21 | S3_SECRET_KEY= 22 | 23 | GRAPHITE_API_HOST= 24 | GRAPHITE_API_PORT= 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | dump.rdb 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | notifications: 3 | email: false 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'activesupport' 5 | gem 'dotenv' 6 | gem 'redis' 7 | gem 'redis-namespace' 8 | gem 'json' 9 | gem 'websocket-eventmachine-client' 10 | gem 'colorize' 11 | gem 'octokit' 12 | gem 'jiralicious' 13 | gem 'terminal-table' 14 | gem 'image_suckr' 15 | gem 's3' 16 | 17 | group :test do 18 | gem 'rspec' 19 | end 20 | 21 | gem 'timerizer' 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.1) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | colorize (0.7.5) 11 | crack (0.1.8) 12 | diff-lcs (1.2.5) 13 | dotenv (1.0.2) 14 | eventmachine (1.0.7) 15 | faraday (0.9.0) 16 | multipart-post (>= 1.2, < 3) 17 | hashie (2.1.2) 18 | httparty (0.11.0) 19 | multi_json (~> 1.0) 20 | multi_xml (>= 0.5.2) 21 | i18n (0.7.0) 22 | jiralicious (0.5.0) 23 | crack (~> 0.1.8) 24 | hashie (>= 1.1, < 3.0.0) 25 | httparty (>= 0.10, < 0.12.0) 26 | json (>= 1.6, < 1.9.0) 27 | nokogiri (< 1.6) 28 | oauth 29 | json (1.8.1) 30 | minitest (5.5.1) 31 | multi_json (1.11.0) 32 | multi_xml (0.5.5) 33 | multipart-post (2.0.0) 34 | nokogiri (1.5.11) 35 | oauth (0.4.7) 36 | octokit (2.0.0) 37 | sawyer (~> 0.3.0) 38 | rake (10.3.2) 39 | redis (3.1.0) 40 | redis-namespace (1.5.1) 41 | redis (~> 3.0, >= 3.0.4) 42 | rspec (3.0.0) 43 | rspec-core (~> 3.0.0) 44 | rspec-expectations (~> 3.0.0) 45 | rspec-mocks (~> 3.0.0) 46 | rspec-core (3.0.4) 47 | rspec-support (~> 3.0.0) 48 | rspec-expectations (3.0.4) 49 | diff-lcs (>= 1.2.0, < 2.0) 50 | rspec-support (~> 3.0.0) 51 | rspec-mocks (3.0.4) 52 | rspec-support (~> 3.0.0) 53 | rspec-support (3.0.4) 54 | sawyer (0.3.0) 55 | faraday (~> 0.8, < 0.10) 56 | uri_template (~> 0.5.0) 57 | terminal-table (1.4.5) 58 | thread_safe (0.3.5) 59 | timecop (0.7.1) 60 | timerizer (0.1.4) 61 | tzinfo (1.2.2) 62 | thread_safe (~> 0.1) 63 | uri_template (0.5.3) 64 | websocket (1.2.1) 65 | websocket-eventmachine-base (1.1.0) 66 | eventmachine (~> 1.0) 67 | websocket (~> 1.0) 68 | websocket-native (~> 1.0) 69 | websocket-eventmachine-client (1.1.0) 70 | websocket-eventmachine-base (~> 1.0) 71 | websocket-native (1.0.0) 72 | 73 | PLATFORMS 74 | ruby 75 | 76 | DEPENDENCIES 77 | activesupport 78 | colorize 79 | dotenv 80 | jiralicious 81 | json 82 | octokit 83 | rake 84 | redis 85 | redis-namespace 86 | rspec 87 | terminal-table 88 | timecop 89 | timerizer 90 | websocket-eventmachine-client 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Taylor Blau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slacker 2.1 2 | [![Build Status](https://travis-ci.org/ttaylorr/slacker.svg?branch=improvements)](https://travis-ci.org/ttaylorr/slacker) 3 | 4 | slacker_icon 5 | Slacker is a collection of scripts for making [Slack](https://slack.com) a little better. 6 | Slacker is maintained primarily by [ttaylorr](http://ttaylorr.com) and sits on the [MCProHosting](https://mcprohosting.com) Slack room. 7 | 8 | ### contributing 9 | 10 | If you wish to add a new script, write a plugin in the `lib/plugins` directory (and an accompanying test). If anything about this is confusing, take a look at the existing patterns, and work from there. 11 | 12 | Slacker makes it really easy to respond to messages using Regex. Simply respond as in the following example: 13 | 14 | ```ruby 15 | @slacker.respond /regex/ do |message, match| 16 | # the match variables are contained in `match` 17 | message.write(response) 18 | end 19 | ``` 20 | 21 | Slacker also exposes a simple conversation API. Need more info from a user when responding to a message? The conversation API is perfect. 22 | 23 | ```ruby 24 | @slacker.respond /conversation test/ do |message| 25 | message << 'What is your username on X again?' 26 | 27 | # This listener doens't need the 'slacker' prefix, and will be ignored unless 28 | # a previous message has been sent matching the outer regex. 29 | message.expect_reply /(.*)/ do |reply, match| 30 | # Do something with the reply that the user has given back... 31 | reply << "Oh! Your username is #{match[1]}" 32 | 33 | # The neat thing here is that since this block wont actually be triggered until 34 | # a reply is received, you can register *nested* replies! Just repeat the 35 | # pattern above ad infinitum, and you can register a whole dialog! 36 | end 37 | end 38 | ``` 39 | 40 | 41 | ### installation 42 | 43 | Clone down this repo on the box that you will be running Slacker on. 44 | 45 | ``` 46 | $ git clone git@github.com:ttaylorr/slacker && cd slacker 47 | ``` 48 | 49 | Once cloned, you'll need to do a few things. 50 | 51 | 1. Install all necessary gems by running `bundle install`. 52 | 2. Create a `.env` file and fill it out with the necessary tokens and info, as in `.env.example`. (To make this easy, just `cp .env.example .env` and replace fill out the environment variables. 53 | 54 | There is only one thing you have to provide details for in the `.env` file, `SLACK_TOKEN`. If you're using Slack, you'll want to [create a bot user](https://api.slack.com/bot-users) and copy the token that slack gives you into the `.env` file. If you want to use [redis](https://redis.io), you can set the `REDIS_HOST` and `REDIS_PORT` configuration options in your `.env` appropriately. 55 | 56 | To start slacker invoke: 57 | 58 | ``` 59 | $ ruby bin/slacker.rb 60 | ``` 61 | 62 | ### configuration 63 | 64 | All configuration is contained in the `.env` file in the root of Slacker. 65 | 66 | ------ 67 | 68 | With :heart: from Slacker, enjoy. 69 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => [:spec] 2 | 3 | desc "Run rspec specs" 4 | task :spec do 5 | sh "rspec spec --color" 6 | end 7 | -------------------------------------------------------------------------------- /assets/slack-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttaylorr/slacker/5539644e7ff929b62246df28853a8b1e8fe82c27/assets/slack-icon.png -------------------------------------------------------------------------------- /bin/console_slacker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'dotenv' 4 | Dotenv.load 5 | 6 | require_relative '../lib/robot' 7 | require_relative '../lib/adapters/repl/console_adapter' 8 | require_relative '../lib/plugins/util_plugin' 9 | require_relative '../lib/plugins/jira/jira_integration' 10 | require_relative '../lib/plugins/coin_flip_plugin' 11 | 12 | r = Slacker::Robot.new(ENV["NAME"]) 13 | 14 | # Attach all the plugins 15 | r.plug(Slacker::Plugins::UtilPlugin.new) 16 | r.plug(Slacker::Plugins::JiraIntegration.new) 17 | r.plug(Slacker::Plugins::CoinFlipPlugin.new) 18 | 19 | # Plug in the adapter and run 20 | r.attach(Slacker::Adapters::ConsoleAdapter.new(r)) 21 | -------------------------------------------------------------------------------- /bin/slacker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | # Load all environment variables from @bkeeper's "dotenv" 4 | require 'dotenv' 5 | Dotenv.load 6 | 7 | require_relative '../lib/robot' 8 | require_relative '../lib/adapters/slack/slack_adapter' 9 | require_relative '../lib/plugins/util_plugin' 10 | require_relative '../lib/plugins/jira/jira_integration' 11 | require_relative '../lib/plugins/coin_flip_plugin' 12 | require_relative '../lib/plugins/timezone_plugin' 13 | require_relative '../lib/plugins/remember_plugin' 14 | require_relative '../lib/plugins/graphite/graphite_ensemble' 15 | require_relative '../lib/plugins/github/github_ensemble' 16 | 17 | [" __ __ ", 18 | " _____/ /___ ______/ /_____ _____ ", 19 | " / ___/ / __ `/ ___/ //_/ _ \\/ ___/ ", 20 | " (__ ) / /_/ / /__/ ,< / __/ / ", 21 | "/____/_/\\__,_/\\___/_/|_|\\___/_/ ", 22 | "", ].each do |segment| 23 | puts segment.green 24 | end 25 | 26 | r = Slacker::Robot.new(ENV["NAME"]) 27 | 28 | # Attach all the plugins 29 | r.plug(Slacker::Plugins::UtilPlugin.new) 30 | r.plug(Slacker::Plugins::JiraIntegration.new) 31 | r.plug(Slacker::Plugins::CoinFlipPlugin.new) 32 | r.plug(Slacker::Plugins::TimezonePlugin.new) 33 | r.plug(Slacker::Plugins::RememberPlugin.new) 34 | r.plug(Slacker::Plugins::GraphiteEnsemble.new) 35 | r.plug(Slacker::Plugins::GitHubEnsemble.new) 36 | 37 | # Plug in the adapter and run 38 | r.attach(Slacker::Adapters::SlackAdapter.new(r)) 39 | -------------------------------------------------------------------------------- /lib/adapters/adapter.rb: -------------------------------------------------------------------------------- 1 | module Slacker 2 | module Adapters 3 | class Adapter 4 | attr_reader :robot 5 | 6 | # Public - iniitalizes an adapter 7 | # 8 | # robot - the robot to relay messages to 9 | def initialize(robot) 10 | @robot = robot 11 | end 12 | 13 | # Public - sends a message back to the chat room 14 | # 15 | # Note: subclasses should implement this method 16 | # 17 | # message - the MessageData object to send back 18 | # 19 | # Returns a boolean indicating the success of sending 20 | # the message back to the server 21 | def send(message) 22 | raise NotImplementedError.new 23 | end 24 | 25 | # Public - proxy method to send a message back 26 | # to the robot 27 | # 28 | # message - the message that was heard 29 | # 30 | # Returns nothing 31 | def hear(message) 32 | @robot.hear(message) 33 | end 34 | 35 | # Public - method that is called after the adapter 36 | # is initialized 37 | # 38 | # Note: subclasses should implement this method 39 | # 40 | # Returns nothing 41 | def run 42 | raise NotImplementedError.new 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/adapters/http/generic_http_client.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'net/http' 3 | require 'json' 4 | require 'openssl' 5 | 6 | module Slacker 7 | module Adapters 8 | module Http 9 | class GenericHttpClient 10 | def initialize(base_path) 11 | @root = URI.parse(base_path) 12 | @http = Net::HTTP.new(@root.host, @root.port) 13 | 14 | @http.use_ssl = true 15 | @http.verify_mode = OpenSSL::SSL::VERIFY_NONE 16 | end 17 | 18 | def get(relative_path, args) 19 | uri = URI.join(@root, relative_path) 20 | uri.query = URI.encode_www_form(args) unless args.nil? 21 | 22 | request = Net::HTTP::Get.new(uri) 23 | 24 | @http.request(request) 25 | end 26 | 27 | def post(relative_path, args) 28 | request = Net::HTTP::Post.new(URI.join(@root, relative_path)) 29 | request.set_form_data(args) unless args.nil? 30 | 31 | @http.request(request) 32 | end 33 | 34 | def json_parse(response) 35 | JSON.parse(response.body) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/adapters/repl/console_adapter.rb: -------------------------------------------------------------------------------- 1 | require_relative '../adapter' 2 | 3 | module Slacker 4 | module Adapters 5 | class ConsoleAdapter < Adapter 6 | def initialize(robot) 7 | super 8 | end 9 | 10 | def run 11 | loop do 12 | print ">> " 13 | 14 | hear({ 15 | :text => gets.chomp, 16 | :channel => { name: 'Console' }, 17 | :user => { name: 'User' } 18 | }) 19 | end 20 | end 21 | 22 | def send(message) 23 | unless message.response.empty? 24 | puts "<< [#{@robot.name}] #{message.pretty_response}\n\n" 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/adapters/slack/slack_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'dotenv' 2 | require 'websocket-eventmachine-client' 3 | require 'json' 4 | 5 | require_relative '../adapter' 6 | require_relative '../../message' 7 | require_relative 'slack_http_client' 8 | 9 | module Slacker 10 | module Adapters 11 | class SlackAdapter < Adapter 12 | attr_reader :channels, :users, :username 13 | 14 | def initialize(robot) 15 | super 16 | @name, @token = 17 | ENV['NAME'], ENV['SLACK_TOKEN'] 18 | 19 | @api = SlackHttpClient.new(@name, @token) 20 | end 21 | 22 | def run 23 | @rtm_meta = @api.start_rtm 24 | 25 | @channels = (@rtm_meta["channels"] + @rtm_meta["groups"] + @rtm_meta["ims"]) 26 | @users = (@rtm_meta["users"] + @rtm_meta["bots"]) 27 | 28 | queue = Queue.new 29 | workers = create_worker_pool(queue, 16) 30 | 31 | start_websocket(queue) 32 | 33 | # Attach a bunch of workers to the queue and handle incoming messages 34 | workers.each(&:join) 35 | end 36 | 37 | def send(message) 38 | @socket.send({ 39 | :type => 'message', 40 | :channel => message.channel["id"], 41 | :text => message.pretty_response 42 | }.to_json) 43 | end 44 | 45 | def channel_by_id(channel_id) 46 | @channels.select { |channel| channel["id"] == channel_id }.first 47 | end 48 | 49 | def user_by_name(username) 50 | @users.select { |user| user["name"] == username }.first 51 | end 52 | 53 | def user_by_id(user_id) 54 | @users.select { |user| user["id"] == user_id }.first 55 | end 56 | 57 | private 58 | def create_worker_pool(queue, size=16) 59 | Array.new(16) do 60 | Thread.new do 61 | w = QueueWorker.new(queue, self) 62 | loop { w.work } 63 | end 64 | end 65 | end 66 | 67 | def start_websocket(queue) 68 | EM.run do 69 | @socket = WebSocket::EventMachine::Client.connect(:uri => @rtm_meta["url"]) 70 | 71 | @socket.onmessage do |msg, type| 72 | packet = JSON.parse(msg) 73 | if packet["type"] == "message" && packet["text"] 74 | # Replace Slack's username @ mention with their actual username 75 | packet["text"].gsub! /<@U(.{8})>/ do |match| 76 | matched_user = user_by_id("U#{$1}") 77 | matched_user["name"] if matched_user 78 | end 79 | 80 | queue << packet 81 | end 82 | end 83 | end 84 | end 85 | end 86 | 87 | class QueueWorker < Struct.new(:queue, :adapter) 88 | def work 89 | message = self.queue.pop 90 | 91 | begin 92 | self.adapter.hear({ 93 | :text => message["text"], 94 | :channel => self.adapter.channel_by_id(message["channel"]), 95 | :user => self.adapter.user_by_id(message["user"]) 96 | }) 97 | rescue Exception => e 98 | puts e 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/adapters/slack/slack_http_client.rb: -------------------------------------------------------------------------------- 1 | require_relative '../http/generic_http_client' 2 | 3 | module Slacker 4 | module Adapters 5 | class SlackHttpClient < Http::GenericHttpClient 6 | def initialize(name, token) 7 | super("https://slack.com/api/") 8 | 9 | @name, @token = 10 | name, token 11 | end 12 | 13 | def start_rtm 14 | response = json_parse self.get('rtm.start', { 15 | :token => @token 16 | }) 17 | raise "Invalid Slack authentication" if response["error"] == "invalid_auth" 18 | response 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/listener.rb: -------------------------------------------------------------------------------- 1 | module Slacker 2 | class Listener 3 | attr_reader :regex, :callback, :conversational 4 | attr_writer :conversational 5 | 6 | def initialize(regex, callback) 7 | @regex, @callback = regex, callback 8 | @conversational = false 9 | end 10 | 11 | def hears?(message) 12 | self.regex.match(message.text) 13 | end 14 | 15 | def hear!(message, match) 16 | self.callback.call(message, match) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/message.rb: -------------------------------------------------------------------------------- 1 | require_relative 'scoped_listener' 2 | 3 | module Slacker 4 | class Message 5 | attr_reader :text, :original_message, :channel, :user, :response, :response_listeners 6 | 7 | def initialize(text, original_message, channel, user) 8 | @text, @original_message, @channel, @user, @response, @response_listeners = 9 | text, original_message, channel, user, [], [] 10 | end 11 | 12 | def write(message) 13 | @response << message 14 | end 15 | alias_method :<<, :write 16 | 17 | def pretty_response 18 | @response.join("\n") 19 | end 20 | 21 | def expect_reply(regex, &block) 22 | @response_listeners << ::Slacker::ScopedListener.new(@user, regex, block) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/plugins/coin_flip_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative 'plugin' 2 | 3 | module Slacker 4 | module Plugins 5 | class CoinFlipPlugin < Plugin 6 | def ready(robot) 7 | robot.respond /(throw|flip|toss) a coin/i do |message| 8 | message << "I get... #{random_side}!" 9 | end 10 | end 11 | 12 | def random_side 13 | ["heads", "tails"].sample 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/plugins/github/github_ensemble.rb: -------------------------------------------------------------------------------- 1 | require_relative './plugins/github_identity_plugin' 2 | 3 | require_relative 'github_identity_manager' 4 | require_relative '../plugin' 5 | 6 | module Slacker 7 | module Plugins 8 | class GitHubEnsemble < Plugin 9 | def ready(robot) 10 | identity_manager = ::GitHubIdentityManager.new(robot) 11 | 12 | robot.plug(GitHubIdentityPlugin.new(identity_manager)) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/plugins/github/github_identity_manager.rb: -------------------------------------------------------------------------------- 1 | require 'octokit' 2 | 3 | class GitHubIdentityManager 4 | def initialize(robot) 5 | @robot = robot 6 | end 7 | 8 | def make_octokit_user(chat_user) 9 | token = @robot.redis.get(make_key(chat_user)) 10 | 11 | Octokit::Client.new(:access_token => token) 12 | end 13 | 14 | def revoke_token!(chat_user) 15 | @robot.redis.del(make_key(chat_user)) 16 | end 17 | 18 | def set_token!(chat_user, token) 19 | @robot.redis.set(make_key(chat_user), token[:token]) 20 | end 21 | 22 | private 23 | def make_key(chat_user) 24 | "github:#{chat_user["id"]}:oauth_token" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/plugins/github/plugins/github_identity_plugin.rb: -------------------------------------------------------------------------------- 1 | require 'dotenv' 2 | require 'octokit' 3 | require 'pp' 4 | require_relative '../../plugin' 5 | 6 | module Slacker 7 | module Plugins 8 | class GitHubIdentityPlugin < Plugin 9 | def initialize(identity_manager) 10 | @identity_manager = identity_manager 11 | end 12 | 13 | def ready(robot) 14 | robot.respond /^(?:(?:gh|github) me)|(who am i on (?:github|gh)?)/i do |message| 15 | begin 16 | github_user = @identity_manager.make_octokit_user(message.user).user 17 | message << "You are #{github_user.login} on GitHub! :octocat:" 18 | rescue Octokit::Unauthorized => e 19 | message << "I don't know who you are on GitHub. (You can authenticate with \"slacker I am on GitHub\"" 20 | end 21 | end 22 | 23 | robot.respond /(forget|erase|delete|remove) me ((?:on|from) (?:github|gh))/i do |message| 24 | @identity_manager.revoke_token!(message.user) 25 | message << "OK, I have no idea who you are on GitHub anymore." 26 | end 27 | 28 | robot.respond /i am (.*) on github/i do |message, match| 29 | github_username = match[1] 30 | 31 | message << "Okay, I'm going to authenticate you as #{github_username} on GitHub." 32 | message << "You can send me a direct message with your GitHub password so I can make an access token for you. I promise I wont store it!" 33 | 34 | message.expect_reply /(.*)/ do |reply, password| 35 | user = Octokit::Client.new(:login => github_username, 36 | :password => password) 37 | 38 | if user.nil? 39 | reply << "Uh-oh! I couldn't manage to authenticate you with those credentials. Check your password and try again!" 40 | else 41 | begin 42 | # First login attempt 43 | set_token(robot, user, generate_oauth_token(user, nil), message) 44 | rescue Octokit::Unauthorized => e 45 | # Either the password was incorrect, (handled in the second rescue block), 46 | # or the user has 2FA enabled and hasn't sent down their token yet 47 | reply << "Great! Could you send your 2FA token over as well? If you don't have one, just say \"no token\"." 48 | 49 | reply.expect_reply /(.*)/ do |token_reply, token_match| 50 | otp_token = token_match[1] 51 | # Second login attempt, this time with either an incorrect password and no token 52 | # (fail), or a correct password and a valid token (success) 53 | begin 54 | set_token(robot, 55 | user, 56 | generate_oauth_token(user, otp_token), 57 | message) 58 | rescue Octokit::Unauthorized => e 59 | reply << "Hm, I wasn't able to authenticate you with those credentials. Sorry!" 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | def generate_oauth_token(user, otp_token) 69 | authorization_args = {:scopes => ["user", "repo", ], 70 | :note => "slacker integration token", 71 | :client_id => ENV["GITHUB_CLIENT_ID"], 72 | :client_secret => ENV["GITHUB_CLIENT_SECRET"]} 73 | 74 | if otp_token 75 | authorization_args[:headers] = {"X-GitHub-OTP" => otp_token} 76 | end 77 | 78 | user.create_authorization(authorization_args) 79 | end 80 | 81 | def set_token(robot, gh_user, token, message) 82 | @identity_manager.set_token!(message.user, token) 83 | 84 | robot.send_message "You are #{gh_user.login} on GitHub! :octocat:", message.channel 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/plugins/graphite/graph_search_plugin.rb: -------------------------------------------------------------------------------- 1 | require 'dotenv' 2 | 3 | module Slacker 4 | class GraphSearchPlugin 5 | def initialize(graphite_api) 6 | @graphite_api = graphite_api 7 | end 8 | 9 | def ready(robot) 10 | robot.respond /(?:(?:(?:show me|query)\s)?graphs matching|(?:graph\s)?search me) (.*)/i do |message, match| 11 | query = match[1] 12 | matching_graphs = @graphite_api.expand(query) 13 | 14 | if matching_graphs.empty? 15 | message << "I couldn't find any graphs matching `#{query}`" 16 | else 17 | message << "I found #{matching_graphs.length} graphs matching the query `#{query}`" 18 | rows = matching_graphs.each_with_index.map do |id, i| 19 | [(i+1), id] 20 | end 21 | 22 | table = Terminal::Table.new(:headings => ["#", "Graph ID"], :rows => rows) 23 | message << "```#{table}```" 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plugins/graphite/graph_view_plugin.rb: -------------------------------------------------------------------------------- 1 | require 'dotenv' 2 | require 'json' 3 | require "open-uri" 4 | require "s3" 5 | 6 | module Slacker 7 | class GraphViewPlugin 8 | def initialize(graphite_api) 9 | @graphite_api = graphite_api 10 | end 11 | 12 | def ready(robot) 13 | robot.respond /(?:(?:graph me)|(?:(?:show|get|fetch) me a graph of)) (.*)/ do |message, match| 14 | graph_id = match[1] 15 | graphite_url = @graphite_api.graph_url_for graph_id 16 | 17 | message << "Okay, here's a graph of `#{graph_id}` for you! :chart_with_upwards_trend:" 18 | 19 | if storage_bucket.nil? 20 | # If we're not re-hosting images, just send the link directly 21 | message << graphite_url 22 | else 23 | # Otherwise, do these things things, in this order: 24 | # 1) download the image data ourselves 25 | # 2) place the data into the bucket 26 | # 3) send the bucket-link into chat 27 | bucket = storage_bucket 28 | img_name = "graphs/#{graph_object_name}.png" 29 | 30 | object = bucket.objects.build(img_name) 31 | object.content = open(graphite_url).read 32 | object.save 33 | 34 | message << "https://s3.amazonaws.com/#{ENV['S3_BUCKET_NAME']}/#{img_name}" 35 | end 36 | end 37 | end 38 | 39 | private 40 | def storage_bucket 41 | @s3 ||= S3::Service.new(:access_key_id => ENV['S3_ACCESS_KEY_ID'], 42 | :secret_access_key => ENV['S3_SECRET_KEY']) 43 | @s3.buckets.find(ENV['S3_BUCKET_NAME']) unless @s3.nil? 44 | end 45 | 46 | def graph_object_name 47 | return "graph:#{Time.now.to_i}" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/plugins/graphite/graphite_api.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'json' 3 | require 'uri' 4 | 5 | class GraphiteAPI 6 | def initialize(host, port) 7 | @uri = URI.parse("http://#{host}:#{port}") 8 | @graphite = Net::HTTP.new(@uri.host, @uri.port) 9 | end 10 | 11 | def graph_url_for(id) 12 | "#{@uri.to_s}/render?target=#{id}.png&width=1000&height=300" 13 | end 14 | 15 | def expand(query) 16 | get("metrics/expand", {:query => query})["results"] 17 | end 18 | 19 | private 20 | def get(endpoint, data) 21 | request_uri = @uri.merge(endpoint) 22 | request_uri.query = URI.encode_www_form(data) 23 | 24 | request = Net::HTTP::Get.new(request_uri) 25 | 26 | get_json_from request 27 | end 28 | 29 | def get_json_from(request) 30 | JSON.parse(@graphite.request(request).body) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/plugins/graphite/graphite_ensemble.rb: -------------------------------------------------------------------------------- 1 | require_relative '../plugin' 2 | 3 | require_relative './graphite_api.rb' 4 | 5 | require_relative './graph_view_plugin.rb' 6 | require_relative './graph_search_plugin.rb' 7 | 8 | module Slacker 9 | module Plugins 10 | class GraphiteEnsemble < Plugin 11 | def ready(robot) 12 | graphite_api = GraphiteAPI.new(ENV["GRAPHITE_API_HOST"], ENV["GRAPHITE_API_PORT"]) 13 | 14 | robot.plug(GraphViewPlugin.new(graphite_api)) 15 | robot.plug(GraphSearchPlugin.new(graphite_api)) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/plugins/jira/jira_datastore.rb: -------------------------------------------------------------------------------- 1 | module Slacker 2 | class JiraDatastore 3 | def initialize(robot) 4 | @robot = robot 5 | end 6 | 7 | def set_jira_username(user, username) 8 | @robot.redis.set(make_jira_key(user), username) 9 | @robot.redis.set(make_slack_key(username), user["name"]) 10 | end 11 | 12 | def get_jira_username(user) 13 | @robot.redis.get(make_jira_key(user)) 14 | end 15 | 16 | def get_slack_username(user) 17 | @robot.redis.get(make_slack_key(user)) 18 | end 19 | 20 | def handle_username_change(username, message) 21 | begin 22 | jira_user = Jiralicious::User.find(username) 23 | set_jira_username(message.user, jira_user.name) 24 | 25 | @robot.send_message("OK, you are *#{jira_user.name}* on JIRA", message.channel) 26 | rescue Exception => e 27 | message << "I couldn't find a user named #{username} on JIRA :disappointed:" 28 | end 29 | end 30 | 31 | private 32 | def make_slack_key(jira_username) 33 | "jira:slack:#{jira_username}:username" 34 | end 35 | 36 | def make_jira_key(user) 37 | "jira:#{user["id"]}:username" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/plugins/jira/jira_integration.rb: -------------------------------------------------------------------------------- 1 | require_relative '../plugin' 2 | require_relative './jira_datastore' 3 | 4 | # Require all of the plugins associated with JIRA 5 | require_relative './plugins/browse_issues_plugin.rb' 6 | require_relative './plugins/issue_assignment_plugin.rb' 7 | require_relative './plugins/issue_info_plugin.rb' 8 | require_relative './plugins/state_change_plugin.rb' 9 | 10 | module Slacker 11 | module Plugins 12 | class JiraIntegration < Plugin 13 | def ready(robot) 14 | configure_jiralicious 15 | datastore = ::Slacker::JiraDatastore.new(robot) 16 | 17 | # Plug them all in! 18 | robot.plug(BrowseIssuesPlugin.new(datastore)) 19 | robot.plug(IssueAssignementPlugin.new(datastore)) 20 | robot.plug(IssueInfoPlugin.new(datastore)) 21 | robot.plug(StateChangePlugin.new) 22 | end 23 | 24 | def configure_jiralicious 25 | Jiralicious.configure do |config| 26 | config.username = ENV["JIRA_USERNAME"] 27 | config.password = ENV["JIRA_PASSWORD"] 28 | config.uri = ENV["JIRA_URL"] 29 | config.api_version = "latest" 30 | config.auth_type = :basic 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/plugins/jira/plugins/browse_issues_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../plugin' 2 | 3 | require 'dotenv' 4 | require 'jiralicious' 5 | require 'terminal-table' 6 | require 'active_support/all' 7 | 8 | module Slacker 9 | module Plugins 10 | class BrowseIssuesPlugin < Plugin 11 | def initialize(username_datastore) 12 | @jira = Jiralicious 13 | @usernames = username_datastore 14 | end 15 | 16 | def ready(robot) 17 | robot.respond /i am (.*) on jira/i do |message, match| 18 | username = match.captures[0] 19 | @usernames.handle_username_change(username, message) 20 | end 21 | 22 | robot.respond /(?:show me)?(?:my issues\s?)(?:on JIRA\s)?(?:(?:on|in|affecting)\s(.*))?/i do |message, match| 23 | jira_opts = { 24 | :username => @usernames.get_jira_username(message.user), 25 | :project => match[1] || ENV["JIRA_DEFAULT_PROJECT"] 26 | } 27 | 28 | if jira_opts[:username].nil? 29 | message << "What's your JIRA username, again?" 30 | 31 | message.expect_reply /.*/ do |reply, match| 32 | @usernames.handle_username_change(match, reply) 33 | 34 | jira_opts[:username] = match 35 | 36 | write_jira_issues_to(reply, jira_opts, robot) 37 | end 38 | else 39 | write_jira_issues_to(message, jira_opts, robot) 40 | end 41 | end 42 | end 43 | 44 | private 45 | def write_jira_issues_to(message, opts, robot) 46 | robot.send_message("Okay, looking for JIRA issues assigned to you on #{opts[:project]}...", message.channel) 47 | 48 | issues_query = "PROJECT=\"#{opts[:project]}\" AND assignee in (#{opts[:username]}) AND status=Open" 49 | response = @jira.search(issues_query) 50 | 51 | message << "I found #{response.issues.length} issues assigned to you on #{opts[:project]}:" 52 | 53 | rows = response.issues.map do |issue| 54 | [issue.jira_key, 55 | issue.summary.truncate(69), 56 | "#{ENV["JIRA_URL"]}/browse/#{issue.jira_key}"] 57 | end 58 | 59 | headers = ['ID', 'Summary', 'URL'] 60 | table = Terminal::Table.new(:headings => headers, :rows => rows) 61 | 62 | message << "```#{table}```" 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/plugins/jira/plugins/issue_assignment_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../plugin' 2 | 3 | require 'dotenv' 4 | require 'jiralicious' 5 | 6 | module Slacker 7 | module Plugins 8 | class IssueAssignementPlugin < Plugin 9 | def initialize(username_datastore) 10 | @jira = Jiralicious 11 | @usernames = username_datastore 12 | end 13 | 14 | def ready(robot) 15 | robot.respond /assign (\w{3,4}-\d*) to @?(.*)/i do |message, match| 16 | issue_key = match.captures[0] 17 | assignee = resolve_assignee(match.captures[1], message, robot) 18 | 19 | if assignee.nil? 20 | message << "Uh-oh! I can't find @#{match.captures[1]} in Slack anywhere :disappointed:" 21 | else 22 | jira_assignee = @usernames.get_jira_username(assignee) 23 | if jira_assignee.nil? 24 | message << "Oh-noes! I don't know who @#{assignee["name"]} is on JIRA :disappointed:" 25 | else 26 | begin 27 | issue = @jira::Issue.find(issue_key) 28 | issue.set_assignee(jira_assignee) 29 | 30 | assigned_to = assignee["id"] == message.user["id"] ? "you." : "<@#{assignee["name"]}>" 31 | issue_url = "#{ENV["JIRA_URL"]}/browse/#{issue_key}" 32 | 33 | message << "Boom! I assigned #{issue_key} to #{assigned_to} (#{issue_url})" 34 | rescue Jiralicious::IssueNotFound => e 35 | message << "Hmm... I couldn't find an issue tagged #{issue_key} on JIRA." 36 | end 37 | end 38 | end 39 | end 40 | end 41 | 42 | private 43 | def resolve_assignee(capture, message, robot) 44 | username = capture.downcase === 'me' ? message.user["name"] : capture 45 | robot.adapter.user_by_name(username) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/plugins/jira/plugins/issue_info_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../plugin' 2 | 3 | require 'jiralicious' 4 | require 'terminal-table' 5 | 6 | module Slacker 7 | module Plugins 8 | class IssueInfoPlugin < Plugin 9 | def initialize(user_datastore) 10 | @jira = Jiralicious 11 | @users = user_datastore 12 | end 13 | 14 | def ready(robot) 15 | robot.respond /(?:info me|(?:show info|tell me) about|what is)\s?(\w{3,4}-\d*)/i do |message, match| 16 | issue_key = match[1].upcase 17 | begin 18 | robot.send_message("Looking for info about #{issue_key}...", message.channel) 19 | 20 | issue = @jira::Issue.find(issue_key) 21 | 22 | assigner = @users.get_slack_username(issue['fields']['creator']['name']) || issue['fields']['creator']['name'] 23 | assignee = @users.get_slack_username(issue['fields']['assignee']['name']) || issue['fields']['assignee']['name'] 24 | 25 | status = issue['fields']['status']['name'].downcase 26 | issue_url = "#{ENV["JIRA_URL"]}/browse/#{issue_key}" 27 | 28 | rows = [] 29 | rows << [issue_key, "@#{assigner}", "@#{assignee}", issue.summary, status] 30 | table = Terminal::Table.new(:headings => ['ID', 'Assigner', 'Assignee', 'Description', 'Status'], :rows => rows) 31 | 32 | message << "```#{table}```" 33 | rescue Jiralicious::IssueNotFound => e 34 | message << "I don't know anything about #{issue_key}." 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/plugins/jira/plugins/state_change_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../plugin' 2 | 3 | require 'dotenv' 4 | require 'jiralicious' 5 | require 'terminal-table' 6 | 7 | module Slacker 8 | module Plugins 9 | class StateChangePlugin < Plugin 10 | def initialize 11 | @jira = Jiralicious 12 | @transition_ids = { 13 | :reopen => ENV["JIRA_TRANSITION_REOPEN"].to_i, 14 | :resolve => ENV["JIRA_TRANSITION_RESOLVE"].to_i 15 | } 16 | end 17 | 18 | def ready(robot) 19 | robot.respond /(?:mark (\w{3,4}-\d*) as (?:completed|done|resolved))|(?:(?:close|resolve) (\w{3,4}-\d*))/i do |message, match| 20 | issue_id = (match[1] || match[2]).upcase 21 | 22 | if preform_transition(issue_id, :resolve, message).nil? 23 | message << "Cool, I marked #{issue_id} as resolved for you :sunglasses:" 24 | end 25 | end 26 | 27 | robot.respond /(?:(?:open|reopen) (\w{3,4}-\d*))|(?:mark (\w{3,4}-\d*) as open(?:ed)?)/i do |message, match| 28 | issue_id = (match[1] || match[2]).upcase 29 | 30 | if preform_transition(issue_id, :reopen, message).nil? 31 | message << "Alrighty, I re-opened #{issue_id} for you. Get to work!" 32 | end 33 | end 34 | end 35 | 36 | def preform_transition(issue_id, state, message) 37 | begin 38 | transition = @jira::Issue::Transitions.go(issue_id, @transition_ids[state]) 39 | rescue Jiralicious::TransitionError => e 40 | message << "Uh-oh! JIRA isn't letting me preform that transition for you right now." 41 | rescue Jiralicious::IssueNotFound => e 42 | message << "Hm, I can't find anything about #{issue_id.upcase} on JIRA." 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/plugins/plugin.rb: -------------------------------------------------------------------------------- 1 | module Slacker 2 | module Plugins 3 | class Plugin 4 | # Public - called when the plugin is ready to be attached 5 | # to the robot 6 | # 7 | # Returns nothing 8 | def ready(robot) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/plugins/remember_plugin.rb: -------------------------------------------------------------------------------- 1 | require 'terminal-table' 2 | 3 | require_relative 'plugin' 4 | 5 | module Slacker 6 | module Plugins 7 | class RememberPlugin < Plugin 8 | def ready(robot) 9 | robot.respond /remember (.*) is (.*)/i do |message, match| 10 | key, value = match[1], match[2] 11 | remember(key, value, robot) 12 | 13 | message << "Okay, I'll remember that #{key} is #{value}." 14 | end 15 | 16 | robot.respond /what is (.*)/i do |message, match| 17 | key = match[1] 18 | value = recall(key, robot) 19 | 20 | message << (value ? value : "I don't remember what #{key} is...") 21 | end 22 | 23 | robot.respond /forget (.*)/i do |message, match| 24 | key = match[1] 25 | forget(key, robot) 26 | 27 | message << "I suddenly forgot what #{key} is!" 28 | end 29 | 30 | robot.respond /what do you (?:remember|know)/i do |message| 31 | memories = robot.redis.keys(make_key('*')) 32 | if memories.empty? 33 | message << "I don't know anything!" 34 | else 35 | rows = memories.each_with_index.map do |key, i| 36 | name = key.match(/memory:(.*)/i)[1] 37 | [i + 1, name, recall(name, robot)] 38 | end 39 | 40 | message << "Here are some things that I remember:" 41 | table = Terminal::Table.new(:headings => ['#', 'Name', 'Memory'], 42 | :rows => rows) 43 | 44 | message << "```#{table.to_s}```" 45 | end 46 | end 47 | end 48 | 49 | def remember(key, value, robot) 50 | robot.redis.set(make_key(key), value) 51 | end 52 | 53 | def forget(key, robot) 54 | robot.redis.del(make_key(key)) 55 | end 56 | 57 | def recall(key, robot) 58 | robot.redis.get(make_key(key)) 59 | end 60 | 61 | private 62 | def make_key(key) 63 | "memory:#{key}".downcase 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/plugins/timezone_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative 'plugin' 2 | 3 | module Slacker 4 | module Plugins 5 | class TimezonePlugin < Plugin 6 | def ready(robot) 7 | robot.respond /I live in GMT(\+|-)(\d)/i do |message, matches| 8 | time_offset = "#{matches[1]}#{matches[2]}" 9 | 10 | set_timezone(message.user, robot, matches[1], matches[2]) 11 | message << "Okay! I'll remember that you live in GMT#{time_offset}." 12 | end 13 | 14 | robot.respond /what time is it for (\@?(.*))/i do |message, matches| 15 | target_user = robot.adapter.user_by_name(matches[1]) 16 | time_offset = get_timezone(target_user, robot) 17 | 18 | if time_offset 19 | local_time = Time.now.getlocal(time_offset) 20 | formatted_time = local_time.strftime("%H:%M%p") 21 | 22 | time_comment = case local_time.hour 23 | when 0..7 24 | "It's pretty early! " 25 | when 19..23 26 | "It's pretty late! " 27 | end 28 | 29 | message << "#{time_comment}Looks like it's #{formatted_time} for @#{target_user["name"]}." 30 | else 31 | message << "Hmm... I don't know what time it is for @#{matches[1]}" 32 | end 33 | end 34 | end 35 | 36 | def set_timezone(user, robot, sign, hours) 37 | robot.redis.set(make_timezone_key(user), "#{sign}0#{hours}:00") 38 | end 39 | 40 | def get_timezone(target, robot) 41 | robot.redis.get(make_timezone_key(target)) 42 | end 43 | 44 | private 45 | def make_timezone_key(user) 46 | "timezone:#{user["id"] || user}" 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/plugins/util_plugin.rb: -------------------------------------------------------------------------------- 1 | require_relative 'plugin' 2 | 3 | module Slacker 4 | module Plugins 5 | class UtilPlugin 6 | def ready(robot) 7 | robot.respond /ping/ do |message| 8 | message.write("Pong!") 9 | end 10 | 11 | robot.respond /echo (.*)$/ do |message, match| 12 | message.write(match[1]) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/robot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'listener' 2 | require_relative 'message' 3 | 4 | require 'redis' 5 | require 'redis-namespace' 6 | require 'colorize' 7 | 8 | module Slacker 9 | class Robot 10 | attr_reader :name, :redis, :adapter 11 | 12 | def initialize(name) 13 | @name, @listeners, @adapter = 14 | (name || ENV["NAME"]), [], nil 15 | 16 | redis_connection = Redis.new(:host => (ENV["REDIS_HOST"] || "127.0.0.1"), 17 | :db => (ENV["REDIS_DB"] || 0), 18 | :port => (ENV["REDIS_PORT"] || 6739)) 19 | @redis = Redis::Namespace.new(:ns => :slacker, :redis => redis_connection) 20 | end 21 | 22 | def respond(regex, &callback) 23 | @listeners << Listener.new(regex, callback) 24 | end 25 | 26 | def address_pattern 27 | /^@?(#{@name})[:-;\s]?/i 28 | end 29 | 30 | def hear(raw_message) 31 | original_text = raw_message[:text] 32 | message = Message.new(original_text.gsub(address_pattern, ""), 33 | original_text, 34 | raw_message[:channel], 35 | raw_message[:user]) 36 | 37 | # Duplicate the @listeners array so we can drop all of the conversational listeners 38 | # if they've been satisfied 39 | @listeners.dup.each do |listener| 40 | match = listener.hears?(message) 41 | 42 | if match && (listener.conversational ? true : message.original_message =~ address_pattern) 43 | listener.hear!(message, match) 44 | 45 | # If there is a match and the listener is conversational, we can delete it because 46 | # it has been satisfied 47 | if listener.conversational 48 | @listeners.delete(listener) 49 | end 50 | end 51 | end 52 | 53 | # Add all of the response listeners to the message 54 | @listeners = (@listeners + message.response_listeners) 55 | 56 | @adapter.send(message) if @adapter 57 | 58 | return message 59 | end 60 | 61 | def send_message(reply, channel) 62 | message = Message.new(nil, nil, channel, nil) 63 | message << reply 64 | 65 | @adapter.send(message) 66 | end 67 | 68 | def attach(adapter) 69 | @adapter = adapter 70 | 71 | begin 72 | (Thread.new { adapter.run }).join 73 | rescue Exception => e 74 | puts e.message 75 | end 76 | end 77 | 78 | def plug(plugin) 79 | plugin.ready(self) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/scoped_listener.rb: -------------------------------------------------------------------------------- 1 | module Slacker 2 | class ScopedListener < Listener 3 | attr_reader :user 4 | 5 | def initialize(user, regex, callback) 6 | super(regex, callback) 7 | 8 | @user = user 9 | @conversational = true 10 | end 11 | 12 | # Override the behavior of the superclass Listener to ensure that the 13 | # user also matches 14 | def hears?(message) 15 | if @user["name"] == message.user["name"] 16 | self.regex.match(message.original_message) 17 | else 18 | nil 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/listener_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require_relative '../lib/listener' 3 | 4 | include Slacker::SpecHelper 5 | 6 | describe Slacker::Robot do 7 | let(:responder_block) { Proc.new { |message| }} 8 | 9 | it "dispatches :call to matching messages" do 10 | @robot.respond(/^message$/, &responder_block) 11 | 12 | expect(responder_block).to receive(:call) 13 | @robot.hear(construct_message("slacker message")) 14 | end 15 | 16 | it "doesn't dispatch :call to messages that don't match" do 17 | @robot.respond(/foo/, &responder_block) 18 | 19 | expect(responder_block).not_to receive(:call) 20 | @robot.hear(construct_message("hubot foo")) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/plugins/util_spec.rb: -------------------------------------------------------------------------------- 1 | require 'timecop' 2 | require 'timerizer' 3 | 4 | require_relative '../spec_helper' 5 | require_relative '../../lib/plugins/util_plugin' 6 | 7 | include Slacker::SpecHelper 8 | 9 | describe Slacker::Plugins::UtilPlugin do 10 | before(:each) do 11 | @robot.plug(Slacker::Plugins::UtilPlugin.new) 12 | end 13 | 14 | it "replies 'pong' to messages matching /ping/" do 15 | expect(@robot.hear(construct_message("#{bot_name} ping")).response).to include("Pong!") 16 | end 17 | 18 | it "does not reply to messages not matching /ping/" do 19 | expect(@robot.hear(construct_message("#{bot_name} pong")).response).not_to include("Pong!") 20 | end 21 | 22 | it "echos a given message" do 23 | message = "some message" 24 | other_message = "some other message" 25 | 26 | expect(@robot.hear(construct_message("#{bot_name} echo #{message}")).response).to include(message) 27 | expect(@robot.hear(construct_message("#{bot_name} echo #{message}")).response).not_to include(other_message) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'dotenv' 2 | require 'rspec' 3 | 4 | require_relative '../lib/robot' 5 | 6 | Dotenv.load 7 | 8 | module Slacker 9 | module SpecHelper 10 | RSpec.configure do |rspec| 11 | rspec.before(:each) do 12 | @robot = Slacker::Robot.new(bot_name) 13 | end 14 | end 15 | 16 | # Public - returns the name of the robot as 17 | # specified by the .env configuration file 18 | # (or "slacker" if none is given) 19 | # 20 | # Returns the name 21 | def bot_name 22 | ENV["NAME"] || "slacker" 23 | end 24 | 25 | def construct_message(message, channel=nil, user=nil) 26 | { 27 | :text => message, 28 | :channel => channel, 29 | :user => user 30 | } 31 | end 32 | end 33 | end 34 | --------------------------------------------------------------------------------