bub_bot 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|