bub_bot 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/bin/bub_bot +5 -0
- data/braindump.md +28 -0
- data/bub_bot.gemspec +37 -0
- data/example_config.yml +56 -0
- data/lib/bub_bot/cli.rb +42 -0
- data/lib/bub_bot/configuration.rb +34 -0
- data/lib/bub_bot/deploy_manager.rb +131 -0
- data/lib/bub_bot/redis_connection.rb +16 -0
- data/lib/bub_bot/repo.rb +115 -0
- data/lib/bub_bot/server_manager.rb +76 -0
- data/lib/bub_bot/slack/client.rb +15 -0
- data/lib/bub_bot/slack/command.rb +100 -0
- data/lib/bub_bot/slack/command_parser.rb +37 -0
- data/lib/bub_bot/slack/commands/claim.rb +89 -0
- data/lib/bub_bot/slack/commands/deploy.rb +123 -0
- data/lib/bub_bot/slack/commands/echo.rb +5 -0
- data/lib/bub_bot/slack/commands/list.rb +26 -0
- data/lib/bub_bot/slack/commands/release.rb +26 -0
- data/lib/bub_bot/slack/response.rb +21 -0
- data/lib/bub_bot/version.rb +3 -0
- data/lib/bub_bot/web_server.rb +92 -0
- data/lib/bub_bot.rb +54 -0
- metadata +257 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'bub_bot/redis_connection'
|
2
|
+
|
3
|
+
class BubBot::ServerManager
|
4
|
+
ROOT_KEY = 'bub_server_list'.freeze
|
5
|
+
|
6
|
+
# Options:
|
7
|
+
# - duration (1.hour)
|
8
|
+
# - server_name (cannoli)
|
9
|
+
# - user (kevin)
|
10
|
+
#
|
11
|
+
# All options required. Do your own defaulting, you lazy bum!
|
12
|
+
def take(options)
|
13
|
+
puts 'takin'
|
14
|
+
|
15
|
+
server_name, duration, user = options.values_at(:server_name, :duration, :user)
|
16
|
+
expires_at = duration.from_now
|
17
|
+
|
18
|
+
data = {
|
19
|
+
'user' => user,
|
20
|
+
'expires_at' => expires_at
|
21
|
+
}
|
22
|
+
redis.hset(ROOT_KEY, server_name, data.to_json)
|
23
|
+
|
24
|
+
data.merge(server: server_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def release(server_name)
|
28
|
+
redis.hdel(ROOT_KEY, server_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def names
|
32
|
+
known_server_names
|
33
|
+
end
|
34
|
+
|
35
|
+
def list
|
36
|
+
claims = redis.hgetall(ROOT_KEY).to_h
|
37
|
+
|
38
|
+
known_server_names.each_with_object({}) do |server_name, claim_map|
|
39
|
+
claim_map[server_name] =
|
40
|
+
if claim = claims[server_name]
|
41
|
+
claim_data = JSON.parse(claim)
|
42
|
+
expires_at = DateTime.parse(claim_data['expires_at'])
|
43
|
+
|
44
|
+
# Filter out expired claims
|
45
|
+
if expires_at > Time.now
|
46
|
+
claim_data['expires_at'] = expires_at
|
47
|
+
claim_data
|
48
|
+
else
|
49
|
+
{}
|
50
|
+
end
|
51
|
+
else
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def claimed_by(username)
|
58
|
+
list
|
59
|
+
.select { |server, claim_data| claim_data['user'] == username }
|
60
|
+
.keys
|
61
|
+
end
|
62
|
+
|
63
|
+
def first_unclaimed
|
64
|
+
list.key({})
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def known_server_names
|
70
|
+
BubBot.configuration.servers
|
71
|
+
end
|
72
|
+
|
73
|
+
def redis
|
74
|
+
BubBot::RedisConnection.instance
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
# Just a simple wrapper around a slack connection. Call .instance on this, then
|
4
|
+
# call anything you'd normally call on a Slack Client on object on this instead. Eg
|
5
|
+
# `Client.instance.chat_postMessage`.
|
6
|
+
class BubBot::Slack::Client
|
7
|
+
include Singleton
|
8
|
+
def method_missing(method, *args, &block)
|
9
|
+
client.respond_to?(method) ? client.public_send(method, *args, &block) : super
|
10
|
+
end
|
11
|
+
|
12
|
+
def client
|
13
|
+
@client ||= Slack::Web::Client.new(token: BubBot.configuration.bot_oauth_token)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'bub_bot/server_manager.rb'
|
2
|
+
require 'bub_bot/deploy_manager.rb'
|
3
|
+
require 'slack-ruby-client'
|
4
|
+
|
5
|
+
class BubBot::Slack::Command
|
6
|
+
def self.can_handle?(command)
|
7
|
+
aliases.include?(command)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.aliases
|
11
|
+
# Guess the command name from the class name
|
12
|
+
[self.name.demodulize.downcase]
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
@options = options
|
17
|
+
puts "initialized a command"
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
name = self.class.name
|
22
|
+
raise "Your command #{name} needs to implement 'run'"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def servers
|
28
|
+
@@servers ||= BubBot::ServerManager.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def deployer
|
32
|
+
@@deployer ||= BubBot::DeployManager.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def client
|
36
|
+
BubBot::Slack::Client.instance
|
37
|
+
end
|
38
|
+
|
39
|
+
def source_user_id
|
40
|
+
@options['user']
|
41
|
+
end
|
42
|
+
|
43
|
+
def source_user_name
|
44
|
+
# TODO: cache these, since it's probably the same few people most of the time.
|
45
|
+
client.users_info(user: source_user_id)&.dig('user', 'name')
|
46
|
+
end
|
47
|
+
|
48
|
+
def tokens
|
49
|
+
@tokens ||= @options['text'].split(' ')
|
50
|
+
end
|
51
|
+
|
52
|
+
# Takes either a string or some options
|
53
|
+
def respond(options)
|
54
|
+
BubBot::Slack::Response.new(options, client)
|
55
|
+
end
|
56
|
+
|
57
|
+
def bot_name
|
58
|
+
BubBot.configuration.bot_name
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns an iterator over the token list that returns nil when out of tokens.
|
62
|
+
#
|
63
|
+
# Eg if the tokens are `aaa bbb ccc`:
|
64
|
+
#
|
65
|
+
# iterator = create_token_iterator
|
66
|
+
# iterator.next # aaa
|
67
|
+
# iterator.next # bbb
|
68
|
+
# iterator.next # ccc
|
69
|
+
# iterator.next # nil
|
70
|
+
#
|
71
|
+
# A good way to use this is for parsing order-agnostic commands:
|
72
|
+
#
|
73
|
+
# iterator = create_token_iterator
|
74
|
+
# while token = iterator.next
|
75
|
+
# if token == 'bake'
|
76
|
+
# recipe = iterator.next
|
77
|
+
# raise "bad recipe" unless %w(bread cookies).include?(recipe)
|
78
|
+
# bake(iterator.next)
|
79
|
+
# elsif token == 'order'
|
80
|
+
# raise 'missing type' unless food_type = iterator.next
|
81
|
+
# raise 'missing when' unless when = iterator.next
|
82
|
+
# order(food_type, when)
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# Warning: don't use to_a on this iterator - it'll result in an infinite loop.
|
87
|
+
# In retrospect, this was a bad pattern. TODO: refactor this.
|
88
|
+
def create_token_iterator
|
89
|
+
unsafe_iterator = tokens.each
|
90
|
+
|
91
|
+
return Enumerator.new do |yielder|
|
92
|
+
loop do
|
93
|
+
yielder.yield unsafe_iterator.next
|
94
|
+
end
|
95
|
+
loop do
|
96
|
+
yielder.yield nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module BubBot::Slack
|
2
|
+
end
|
3
|
+
|
4
|
+
require 'bub_bot/slack/command'
|
5
|
+
Dir[File.dirname(__FILE__) + '/commands/*.rb'].each {|file| require file }
|
6
|
+
|
7
|
+
class BubBot::Slack::CommandParser
|
8
|
+
def self.get_command(string_input)
|
9
|
+
puts "Parsing #{string_input}"
|
10
|
+
#puts "options: #{command_classes}"
|
11
|
+
|
12
|
+
# Strip the bot name out
|
13
|
+
string_input.sub!(/^#{BubBot.configuration.bot_name} /, '')
|
14
|
+
|
15
|
+
command = string_input.split(' ').first
|
16
|
+
|
17
|
+
if command
|
18
|
+
command_classes.find do |command_class|
|
19
|
+
command_class.can_handle?(command)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.command_classes
|
27
|
+
|
28
|
+
# Get all the classes under BubBot::Slack::Command::*
|
29
|
+
@_command_classes ||= BubBot::Slack::Command.constants.map do |constant|
|
30
|
+
(BubBot::Slack::Command.to_s + "::" + constant.to_s).safe_constantize
|
31
|
+
end.select do |constant|
|
32
|
+
|
33
|
+
# Get only the constants that are classes and inherit from Command
|
34
|
+
constant.is_a?(Class) && constant < BubBot::Slack::Command
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'action_view/helpers'
|
3
|
+
|
4
|
+
class BubBot::Slack::Command::Claim < BubBot::Slack::Command
|
5
|
+
include ActionView::Helpers::DateHelper
|
6
|
+
|
7
|
+
DEFAULT_DURATION = 1.hour.freeze
|
8
|
+
INCREMENTS = %w(minute hour day week month)
|
9
|
+
|
10
|
+
# bub take cannoli for 20 minutes deploy master
|
11
|
+
def self.aliases
|
12
|
+
%w(claim take gimmee canhaz)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
server = duration = deploy = nil
|
17
|
+
iterator = create_token_iterator
|
18
|
+
|
19
|
+
# Skip the command name itself
|
20
|
+
iterator.next
|
21
|
+
|
22
|
+
while token = iterator.peek
|
23
|
+
if token == 'for'
|
24
|
+
puts 'got for'
|
25
|
+
iterator.next
|
26
|
+
duration = parse_duration(iterator)
|
27
|
+
elsif token.to_i > 0
|
28
|
+
puts 'got int'
|
29
|
+
duration = parse_duration(iterator)
|
30
|
+
elsif servers.names.include?(token)
|
31
|
+
puts 'got server'
|
32
|
+
server = iterator.next
|
33
|
+
elsif token == 'deploy'
|
34
|
+
puts 'got deploy'
|
35
|
+
raise RespondableError.new('Use the new deploy command to deploy, not the take command.')
|
36
|
+
else
|
37
|
+
raise RespondableError.new("I'm not sure what '#{token}' means.")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Default to the first unclaimed server if no server was specified. If there
|
42
|
+
# are no unclaimed servers, error.
|
43
|
+
unless server
|
44
|
+
unless server = servers.first_unclaimed
|
45
|
+
raise RespondableError.new("No available servers.")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
duration ||= DEFAULT_DURATION
|
50
|
+
|
51
|
+
take_options = {
|
52
|
+
server_name: server,
|
53
|
+
duration: duration,
|
54
|
+
user: source_user_name
|
55
|
+
}.compact
|
56
|
+
|
57
|
+
result = servers.take(take_options)
|
58
|
+
|
59
|
+
time_ago = time_ago_in_words(result['expires_at'])
|
60
|
+
respond("#{source_user_name} has #{server} for the next #{time_ago}")
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def parse_duration(iterator)
|
66
|
+
value = iterator.next.to_i
|
67
|
+
unless increment = iterator.next
|
68
|
+
raise RespondableError.new("Missing increment. Do you mean '#{value} hours'?")
|
69
|
+
end
|
70
|
+
unless valid_increments.include?(increment)
|
71
|
+
raise RespondableError.new("I don't know the increment '#{increment}'. Try one of these instead: #{INCREMENTS.join(', ')}")
|
72
|
+
end
|
73
|
+
|
74
|
+
# 5.minutes
|
75
|
+
value.public_send(increment)
|
76
|
+
end
|
77
|
+
|
78
|
+
def should_deploy?
|
79
|
+
false # todo: parse from command, also support buttons
|
80
|
+
end
|
81
|
+
|
82
|
+
def valid_increments
|
83
|
+
@@valid_increments ||= INCREMENTS.reduce([]) do |valid, increment|
|
84
|
+
valid << increment + 's'
|
85
|
+
valid << increment
|
86
|
+
valid
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
class BubBot::Slack::Command::Deploy < BubBot::Slack::Command
|
2
|
+
def self.aliases
|
3
|
+
%w(deploy ship push)
|
4
|
+
end
|
5
|
+
|
6
|
+
# bub deploy cannoli core kk_foo
|
7
|
+
def run
|
8
|
+
puts 'deploying'
|
9
|
+
server = nil
|
10
|
+
deploys = {}
|
11
|
+
iterator = create_token_iterator
|
12
|
+
|
13
|
+
# Skip the command name itself
|
14
|
+
iterator.next
|
15
|
+
|
16
|
+
while token = iterator.peek
|
17
|
+
puts "token loop #{token}"
|
18
|
+
if token == 'and'
|
19
|
+
iterator.next
|
20
|
+
next
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: strip trailing commas (need to do this in the iterator)
|
24
|
+
#token = token.sub!(/,$/, '')
|
25
|
+
|
26
|
+
if servers.names.include?(token)
|
27
|
+
puts 'servers.names'
|
28
|
+
server = iterator.next
|
29
|
+
|
30
|
+
# Handle 'kk_some_fix [to] core'
|
31
|
+
elsif branches.include?(token)
|
32
|
+
puts 'branches.include'
|
33
|
+
|
34
|
+
branch = iterator.next
|
35
|
+
target = iterator.next
|
36
|
+
|
37
|
+
# Skip connector words
|
38
|
+
target = iterator.next if %w(to on with).include?(target)
|
39
|
+
|
40
|
+
deploys[target] = branch
|
41
|
+
|
42
|
+
# Handle 'core kk_some_fix'
|
43
|
+
elsif targets.include?(token)
|
44
|
+
puts 'else targets.inclue'
|
45
|
+
target = iterator.next
|
46
|
+
branch = iterator.next
|
47
|
+
deploys[target] = branch
|
48
|
+
else
|
49
|
+
raise RespondableError.new("I didn't recognize #{token}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Validate all the potential deploys before we start
|
54
|
+
deploys.each do |target, branch|
|
55
|
+
puts 'deploys.each'
|
56
|
+
# TODO: infer targets, etc
|
57
|
+
unless targets.include?(target)
|
58
|
+
raise RespondableError.new("Unknown deploy target #{token}. Try one of #{targets.join(', ')}")
|
59
|
+
end
|
60
|
+
unless branches(target).include?(branch)
|
61
|
+
raise RespondableError.new("Deploy target #{target} doesn't have a branch named #{branch}. Maybe you forgot to push that branch?")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
server = servers.first_unclaimed unless server
|
66
|
+
|
67
|
+
unless server
|
68
|
+
return respond('No servers available')
|
69
|
+
end
|
70
|
+
|
71
|
+
unless deploys.any?
|
72
|
+
return respond("Please specify a target and a branch, eg `#{bot_name} deploy #{server} core kk_add_lasers`");
|
73
|
+
end
|
74
|
+
|
75
|
+
# TODO:
|
76
|
+
# - default to deploying develop
|
77
|
+
|
78
|
+
claim_data = servers.list[server]
|
79
|
+
|
80
|
+
if claim_data['user'] && claim_data['user'] != source_user_name
|
81
|
+
raise RespondableError.new("Server already claimed by #{claim_data['user']}. Use the 'take' command first to override their claim.")
|
82
|
+
elsif
|
83
|
+
# Extend our current claim on this server to 1 hour from now unless we
|
84
|
+
# already have it claimed for longer
|
85
|
+
if !claim_data['expires_at'] || claim_data['expires_at'] < 1.hour.from_now
|
86
|
+
servers.take(
|
87
|
+
user: source_user_name,
|
88
|
+
duration: 1.hour,
|
89
|
+
server_name: server
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
message_segments = deploys.map do |target, branch|
|
95
|
+
"branch '#{branch}' to target '#{target}'"
|
96
|
+
end
|
97
|
+
respond("Deploying on server #{server}: #{message_segments.join('; ')}").deliver
|
98
|
+
|
99
|
+
deploys.each do |target, branch|
|
100
|
+
deployer.deploy(server, target, branch)
|
101
|
+
end
|
102
|
+
|
103
|
+
respond("Finished deploying on server #{server}: #{message_segments.join('; ')}");
|
104
|
+
end
|
105
|
+
|
106
|
+
# All known branches for the given target. Returns all branches for *all*
|
107
|
+
# targets if target is nil.
|
108
|
+
def branches(target = nil)
|
109
|
+
puts 'deploy.branches'
|
110
|
+
@_branch_cache ||= {}
|
111
|
+
if target == nil
|
112
|
+
targets.flat_map do |target|
|
113
|
+
branches(target)
|
114
|
+
end
|
115
|
+
else
|
116
|
+
@_branch_cache[target] ||= deployer.branches(target)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def targets
|
121
|
+
deployer.target_names
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'action_view/helpers'
|
3
|
+
|
4
|
+
class BubBot::Slack::Command::List < BubBot::Slack::Command
|
5
|
+
include ActionView::Helpers::DateHelper
|
6
|
+
|
7
|
+
def self.aliases
|
8
|
+
%w(list status all wazup)
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
list_strings = servers.list.map do |server, claim|
|
13
|
+
if claim['expires_at']
|
14
|
+
time_ago = time_ago_in_words(claim['expires_at'])
|
15
|
+
"#{server}: *#{claim['user']}'s* for the next #{time_ago}"
|
16
|
+
else
|
17
|
+
"#{server}: *free*"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
respond(list_strings.join("\n"))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class BubBot::Slack::Command::Release < BubBot::Slack::Command
|
2
|
+
def run
|
3
|
+
puts "Running release"
|
4
|
+
servers_to_release = tokens.drop(1)
|
5
|
+
puts "servers_to_release: #{servers_to_release}"
|
6
|
+
|
7
|
+
my_servers = servers.claimed_by(source_user_name)
|
8
|
+
servers_to_release =
|
9
|
+
if servers_to_release.empty?
|
10
|
+
my_servers
|
11
|
+
else
|
12
|
+
servers_to_release & my_servers
|
13
|
+
end
|
14
|
+
|
15
|
+
if (unknown_servers = servers_to_release - servers.names).any?
|
16
|
+
raise RespondableError.new("Unknown server(s): #{unknown_servers.join(', ')}. Nothing released.")
|
17
|
+
end
|
18
|
+
|
19
|
+
servers_to_release.each do |server|
|
20
|
+
servers.release(server)
|
21
|
+
end
|
22
|
+
|
23
|
+
released = servers_to_release.any? ? servers_to_release.join(', ') : 'nothing'
|
24
|
+
respond("Released #{released}")
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class BubBot::Slack::Response
|
2
|
+
attr_accessor :text
|
3
|
+
|
4
|
+
def initialize(options, client)
|
5
|
+
@text = options.is_a?(String) ? options : options[:text]
|
6
|
+
@client = client
|
7
|
+
end
|
8
|
+
|
9
|
+
def deliver
|
10
|
+
body = {
|
11
|
+
text: text,
|
12
|
+
username: BubBot.configuration.bot_name
|
13
|
+
}
|
14
|
+
# TODO: configure channel
|
15
|
+
@client.chat_postMessage(channel: '#' + channel, text: text, as_user: true)
|
16
|
+
end
|
17
|
+
|
18
|
+
def channel
|
19
|
+
BubBot.configuration.slack_channel
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'bub_bot/slack/command_parser'
|
2
|
+
require 'bub_bot/slack/response'
|
3
|
+
require 'bub_bot/slack/client'
|
4
|
+
require 'faraday'
|
5
|
+
|
6
|
+
class BubError < StandardError
|
7
|
+
end
|
8
|
+
|
9
|
+
# An error that should should result in a user-facing response (and not a non-200
|
10
|
+
# http response code)
|
11
|
+
class RespondableError < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
class BubBot::WebServer
|
15
|
+
def call(env)
|
16
|
+
puts ' --- Got request ---'
|
17
|
+
|
18
|
+
# Ignore retries for now.
|
19
|
+
if env['HTTP_X_SLACK_RETRY_NUM']
|
20
|
+
puts "Ignoring retry: #{env['HTTP_X_SLACK_RETRY_NUM']}, because #{env['HTTP_X_SLACK_RETRY_REASON']}"
|
21
|
+
return [200, {}, ['ok']]
|
22
|
+
end
|
23
|
+
|
24
|
+
request = Rack::Request.new(env)
|
25
|
+
|
26
|
+
# For easily checking if the server's up
|
27
|
+
if request.path == '/' && request.get?
|
28
|
+
return [200, {}, ['ok']]
|
29
|
+
|
30
|
+
# When slack sends us a challenge request
|
31
|
+
elsif request.path == '/' && request.post?
|
32
|
+
params = parse_params(request)
|
33
|
+
return [200, {}, [params[:challenge]]] if params[:challenge]
|
34
|
+
|
35
|
+
event = params[:event]
|
36
|
+
|
37
|
+
# Skip messages from bots
|
38
|
+
return [200, {}, []] if event[:subtype] == 'bot_message'
|
39
|
+
|
40
|
+
# Make sure this is in the form of 'bub foo'
|
41
|
+
unless event[:text].starts_with?(BubBot.configuration.bot_name + ' ')
|
42
|
+
puts "skipping non-bub message"
|
43
|
+
return [200, {}, []]
|
44
|
+
end
|
45
|
+
|
46
|
+
command = BubBot::Slack::CommandParser.get_command(event[:text])
|
47
|
+
|
48
|
+
puts " --- Running command #{command}"
|
49
|
+
|
50
|
+
# Slack will retry any message that takes longer than 3 seconds to complete,
|
51
|
+
# so do all message processing in a thread.
|
52
|
+
command_thread = Thread.new do
|
53
|
+
response =
|
54
|
+
begin
|
55
|
+
if command
|
56
|
+
command.new(event).run
|
57
|
+
else
|
58
|
+
BubBot::Slack::Response.new("unknown command", slack_client)
|
59
|
+
end
|
60
|
+
rescue RespondableError => e
|
61
|
+
BubBot::Slack::Response.new(e.message, slack_client)
|
62
|
+
end
|
63
|
+
|
64
|
+
response.deliver
|
65
|
+
end
|
66
|
+
command_thread.abort_on_exception = true
|
67
|
+
|
68
|
+
return [200, {}, []]
|
69
|
+
|
70
|
+
#elsif request.path == '/heroku_hook' && request.post?
|
71
|
+
#HerokuInterface.new.handle_heroku_webhook(request.body.read)
|
72
|
+
#return [200, {}, []]
|
73
|
+
else
|
74
|
+
raise BubError, "Failed request: #{request.request_method} #{request.path}"
|
75
|
+
err 'invalid request'
|
76
|
+
end
|
77
|
+
rescue BubError => e
|
78
|
+
puts "Err: #{e.message}"
|
79
|
+
return [400, {}, [e.message]]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def parse_params(request)
|
85
|
+
JSON.parse(request.body.read)
|
86
|
+
.with_indifferent_access
|
87
|
+
end
|
88
|
+
|
89
|
+
def slack_client
|
90
|
+
BubBot::Slack::Client.instance
|
91
|
+
end
|
92
|
+
end
|
data/lib/bub_bot.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'bub_bot/version'
|
3
|
+
require 'bub_bot/web_server'
|
4
|
+
require 'bub_bot/configuration'
|
5
|
+
require 'bub_bot/cli'
|
6
|
+
require 'pry-byebug'
|
7
|
+
require 'active_support'
|
8
|
+
require 'active_support/core_ext'
|
9
|
+
|
10
|
+
module BubBot
|
11
|
+
class << self
|
12
|
+
attr_accessor :configuration
|
13
|
+
# From a config.ru file you can do `run BubBot`. TODO: maybe not. That would
|
14
|
+
# skip the running background thread.
|
15
|
+
#
|
16
|
+
# Handle an individual web request. You shouldn't call this method directly.
|
17
|
+
# Instead, give BubBot to Rack and let it call this method.
|
18
|
+
def call(env)
|
19
|
+
(@web_server ||= WebServer.new).call(env)
|
20
|
+
end
|
21
|
+
|
22
|
+
# This method starts a listening web server. Call from the cli or wherever
|
23
|
+
# else you want to kick off a running BubBot process.
|
24
|
+
def start
|
25
|
+
puts 'Booting BubBot'
|
26
|
+
Thread.new do
|
27
|
+
loop do
|
28
|
+
#puts "Checking for servers to shutdown"
|
29
|
+
# TODO: actually do that ^
|
30
|
+
sleep 10# * 60
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
app = Rack::Builder.new do
|
35
|
+
# if development (TODO)
|
36
|
+
use Rack::Reloader
|
37
|
+
# end
|
38
|
+
run BubBot
|
39
|
+
end.to_app
|
40
|
+
|
41
|
+
Rack::Handler::Thin.run(app, BubBot.configuration.rack_options_hash)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Used for setting config options:
|
45
|
+
# BubBot.configure do |config|
|
46
|
+
# config.bot_name 'lillian'
|
47
|
+
# config.redis_host 'localhost:6379'
|
48
|
+
# end
|
49
|
+
def configure
|
50
|
+
self.configuration ||= Configuration.new
|
51
|
+
yield configuration
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|