├── .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 | [](https://travis-ci.org/ttaylorr/slacker)
3 |
4 |
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 |
--------------------------------------------------------------------------------