slack-ruby-bot-boilerplate 0.1.0
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 +5 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.rubocop_todo.yml +77 -0
- data/.travis.yml +28 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +139 -0
- data/DEPLOYMENT.md +33 -0
- data/Gemfile +9 -0
- data/LICENSE.md +22 -0
- data/README.md +688 -0
- data/Rakefile +19 -0
- data/TUTORIAL.md +218 -0
- data/UPGRADING.md +226 -0
- data/lib/config/application.rb +12 -0
- data/lib/config/boot.rb +6 -0
- data/lib/config/environment.rb +3 -0
- data/lib/slack_respondent.rb +25 -0
- data/lib/slack_respondent/client.rb +61 -0
- data/lib/slack_respondent/commands.rb +5 -0
- data/lib/slack_respondent/commands/about.rb +15 -0
- data/lib/slack_respondent/commands/base.rb +140 -0
- data/lib/slack_respondent/commands/help.rb +42 -0
- data/lib/slack_respondent/commands/hi.rb +14 -0
- data/lib/slack_respondent/commands/support/attrs.rb +34 -0
- data/lib/slack_respondent/commands/support/help.rb +81 -0
- data/lib/slack_respondent/commands/support/match.rb +22 -0
- data/lib/slack_respondent/commands/unknown.rb +13 -0
- data/lib/slack_respondent/config.rb +38 -0
- data/lib/slack_respondent/reactions.rb +38 -0
- data/lib/slack_respondent/support/loggable.rb +23 -0
- data/lib/slack_respondent/version.rb +3 -0
- data/slack-ruby-bot-boilerplate.gemspec +25 -0
- data/slack.png +0 -0
- metadata +189 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'boot'
|
5
|
+
|
6
|
+
Dir[File.expand_path('../../initializers', __FILE__) + '/**/*.rb'].each do |file|
|
7
|
+
require file
|
8
|
+
end
|
9
|
+
|
10
|
+
require File.expand_path('../application', __FILE__)
|
11
|
+
|
12
|
+
require 'slack_ruby_bot'
|
data/lib/config/boot.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'slack-ruby-client'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
|
5
|
+
require_relative 'slack_respondent/support/loggable'
|
6
|
+
require_relative 'slack_respondent/commands'
|
7
|
+
require_relative 'slack_respondent/reactions'
|
8
|
+
require_relative 'slack_respondent/client'
|
9
|
+
require_relative 'slack_respondent/config'
|
10
|
+
|
11
|
+
module SlackRespondent
|
12
|
+
ABOUT = <<-ABOUT
|
13
|
+
Bot name
|
14
|
+
ABOUT
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def configure
|
18
|
+
block_given? ? yield(Config) : Config
|
19
|
+
end
|
20
|
+
|
21
|
+
def config
|
22
|
+
Config
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module SlackRespondent
|
2
|
+
class ClientWrapper
|
3
|
+
include Loggable
|
4
|
+
attr_accessor :aliases
|
5
|
+
attr_accessor :send_gifs
|
6
|
+
attr_accessor :names
|
7
|
+
attr_reader :client
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@name = []
|
11
|
+
|
12
|
+
response = client.auth_test
|
13
|
+
|
14
|
+
SlackRespondent.configure do |config|
|
15
|
+
config.url = response.url
|
16
|
+
config.team = response.team
|
17
|
+
config.team_id = response.team_id
|
18
|
+
config.user = response.user
|
19
|
+
config.user_id = response.user_id
|
20
|
+
logger.info "Welcome #{response.user} to the #{response.team} team."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def names
|
25
|
+
[
|
26
|
+
SlackRespondent::Config.user,
|
27
|
+
SlackRespondent::Config.aliases ? SlackRespondent::Config.aliases.map(&:downcase) : nil,
|
28
|
+
SlackRespondent::Config.user_id ? "<@#{SlackRespondent::Config.user_id.downcase}>" : nil,
|
29
|
+
SlackRespondent::Config.user_id ? "<@#{SlackRespondent::Config.user_id.downcase}>:" : nil,
|
30
|
+
SlackRespondent::Config.user ? "#{SlackRespondent::Config.user}:" : nil
|
31
|
+
].compact.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
def name?(name)
|
35
|
+
name && names.include?(name.downcase)
|
36
|
+
end
|
37
|
+
|
38
|
+
def user_id
|
39
|
+
names.first
|
40
|
+
end
|
41
|
+
|
42
|
+
def name
|
43
|
+
SlackRespondent.config.user || (self.self && self.self.name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def url
|
47
|
+
SlackRespondent.config.url
|
48
|
+
end
|
49
|
+
|
50
|
+
def say(options = {})
|
51
|
+
filtered_options = options.slice(:channel, :text, :attachments)
|
52
|
+
client.chat_postMessage(filtered_options)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def client
|
58
|
+
@client ||= Slack::Web::Client.new
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SlackRespondent
|
2
|
+
module Commands
|
3
|
+
class About < Base
|
4
|
+
command 'about'
|
5
|
+
|
6
|
+
def self.call(client, data, _match)
|
7
|
+
client.say(channel: data.channel, text: SlackRespondent::ABOUT)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.pattern
|
11
|
+
/^#{bot_matcher}$/u
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require_relative 'support/match'
|
2
|
+
require_relative 'support/help'
|
3
|
+
|
4
|
+
module SlackRespondent
|
5
|
+
module Commands
|
6
|
+
class Base
|
7
|
+
include Loggable
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def help(&block)
|
11
|
+
Support::Help.instance.capture_help(self, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def command_name_from_class
|
15
|
+
name ? name.split(':').last.downcase : object_id.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def operator(*values, &block)
|
19
|
+
values = values.map { |value| Regexp.escape(value) }.join('|')
|
20
|
+
match Regexp.new("^(?<operator>#{values})(?<expression>.*)$", Regexp::IGNORECASE), &block
|
21
|
+
end
|
22
|
+
|
23
|
+
def command(*values, &block)
|
24
|
+
values = values.map { |value| value.is_a?(Regexp) ? value.source : Regexp.escape(value) }.join('|')
|
25
|
+
match Regexp.new("^#{bot_matcher}[\\s]+(?<command>#{values})([\\s]+(?<expression>.*)|)$", Regexp::IGNORECASE | Regexp::MULTILINE)
|
26
|
+
end
|
27
|
+
|
28
|
+
def invoke(client, data)
|
29
|
+
finalize_routes!
|
30
|
+
expression, text = parse(client, data)
|
31
|
+
return false unless expression || data.attachments
|
32
|
+
routes.each_pair do |route, options|
|
33
|
+
match_method = options[:match_method]
|
34
|
+
case match_method
|
35
|
+
when :match
|
36
|
+
next unless expression
|
37
|
+
match = route.match(expression)
|
38
|
+
match ||= route.match(text) if text
|
39
|
+
next unless match
|
40
|
+
next if match.names.include?('bot') && !client.name?(match['bot'])
|
41
|
+
match = Support::Match.new(match)
|
42
|
+
# when :scan
|
43
|
+
# next unless expression
|
44
|
+
# match = expression.scan(route)
|
45
|
+
# next unless match.any?
|
46
|
+
# when :attachment
|
47
|
+
# next unless data.attachments && !data.attachments.empty?
|
48
|
+
# match, attachment, field = match_attachments(data, route, options[:fields_to_scan])
|
49
|
+
# next unless match
|
50
|
+
# match = Support::Match.new(match, attachment, field)
|
51
|
+
end
|
52
|
+
call_command(client, data, match)
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def match(match)
|
59
|
+
routes[match] = { match_method: :match }
|
60
|
+
end
|
61
|
+
|
62
|
+
# def scan(match, &block)
|
63
|
+
# routes[match] = { match_method: :scan, block: block }
|
64
|
+
# end
|
65
|
+
|
66
|
+
def attachment(match, fields_to_scan = nil, &block)
|
67
|
+
fields_to_scan = [fields_to_scan] unless fields_to_scan.nil? || fields_to_scan.is_a?(Array)
|
68
|
+
routes[match] = {
|
69
|
+
match_method: :attachment,
|
70
|
+
block: block,
|
71
|
+
fields_to_scan: fields_to_scan
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def bot_matcher
|
76
|
+
'(?<bot>\S*)'
|
77
|
+
end
|
78
|
+
|
79
|
+
def routes
|
80
|
+
@routes ||= ActiveSupport::OrderedHash.new
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def call_command(client, data, match)
|
86
|
+
if respond_to?(:call)
|
87
|
+
send(:call, client, data, match) if permitted?(client, data, match)
|
88
|
+
else
|
89
|
+
raise NotImplementedError, data.text
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse(client, data)
|
94
|
+
text = data.text
|
95
|
+
return text unless direct_message?(data) && message_from_another_user?(data)
|
96
|
+
return text if message_begins_with_bot_mention?(text, client.names)
|
97
|
+
["#{client.name} #{text}", text]
|
98
|
+
end
|
99
|
+
|
100
|
+
def direct_message?(data)
|
101
|
+
data.channel && data.channel[0] == 'D'
|
102
|
+
end
|
103
|
+
|
104
|
+
def message_from_another_user?(data)
|
105
|
+
data.user && data.user != SlackRespondent.config.user_id
|
106
|
+
end
|
107
|
+
|
108
|
+
def message_begins_with_bot_mention?(text, bot_names)
|
109
|
+
bot_names = bot_names.join('|')
|
110
|
+
!!text.downcase.match(/\A(#{bot_names})\s|\A(#{bot_names})\z/)
|
111
|
+
end
|
112
|
+
|
113
|
+
def finalize_routes!
|
114
|
+
# return if routes && routes.any?
|
115
|
+
routes.clear
|
116
|
+
match(self.pattern) if respond_to?(:pattern)
|
117
|
+
command command_name_from_class
|
118
|
+
end
|
119
|
+
|
120
|
+
def match_attachments(data, route, fields_to_scan = nil)
|
121
|
+
fields_to_scan ||= %i[pretext text title]
|
122
|
+
data.attachments.each do |attachment|
|
123
|
+
fields_to_scan.each do |field|
|
124
|
+
next unless attachment[field]
|
125
|
+
match = route.match(attachment[field])
|
126
|
+
return match, attachment, field if match
|
127
|
+
end
|
128
|
+
end
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
# Intended to be overridden by subclasses to hook in an
|
133
|
+
# authorization mechanism.
|
134
|
+
def permitted?(_client, _data, _match)
|
135
|
+
true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module SlackRespondent
|
2
|
+
module Commands
|
3
|
+
class Help < Base
|
4
|
+
help do
|
5
|
+
title 'help'
|
6
|
+
desc 'Shows help information.'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.call(client, data, match)
|
10
|
+
|
11
|
+
command = match[:expression]
|
12
|
+
|
13
|
+
text = if command.present?
|
14
|
+
Support::Help.instance.command_full_desc(command)
|
15
|
+
else
|
16
|
+
general_text
|
17
|
+
end
|
18
|
+
|
19
|
+
client.say(channel: data.channel, text: text)
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
private
|
24
|
+
|
25
|
+
def general_text
|
26
|
+
bot_desc = Support::Help.instance.bot_desc_and_commands
|
27
|
+
other_commands_descs = Support::Help.instance.other_commands_descs
|
28
|
+
<<TEXT
|
29
|
+
#{bot_desc.join("\n")}
|
30
|
+
|
31
|
+
*Other commands:*
|
32
|
+
#{other_commands_descs.join("\n")}
|
33
|
+
|
34
|
+
For getting description of the command use: *help <command>*
|
35
|
+
|
36
|
+
For more information see https://github.com/slack-ruby/slack-ruby-bot, please.
|
37
|
+
TEXT
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SlackRespondent
|
2
|
+
module Commands
|
3
|
+
module Support
|
4
|
+
class Attrs
|
5
|
+
attr_accessor :command_name, :command_desc, :command_long_desc
|
6
|
+
attr_reader :klass, :commands
|
7
|
+
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
10
|
+
@commands = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def title(title)
|
14
|
+
self.command_name = title
|
15
|
+
end
|
16
|
+
|
17
|
+
def desc(desc)
|
18
|
+
self.command_desc = desc
|
19
|
+
end
|
20
|
+
|
21
|
+
def long_desc(long_desc)
|
22
|
+
self.command_long_desc = long_desc
|
23
|
+
end
|
24
|
+
|
25
|
+
def command(title, &block)
|
26
|
+
@commands << self.class.new(klass).tap do |k|
|
27
|
+
k.title(title)
|
28
|
+
k.instance_eval(&block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require_relative 'attrs'
|
3
|
+
|
4
|
+
module SlackRespondent
|
5
|
+
module Commands
|
6
|
+
module Support
|
7
|
+
class Help
|
8
|
+
include Singleton
|
9
|
+
attr_reader :commands_help_attrs
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@commands_help_attrs = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def capture_help(klass, &block)
|
16
|
+
k = Commands::Support::Attrs.new(klass)
|
17
|
+
k.instance_eval(&block)
|
18
|
+
@commands_help_attrs << k
|
19
|
+
end
|
20
|
+
|
21
|
+
def bot_desc_and_commands
|
22
|
+
collect_help_attrs(bot_help_attrs) do |help_attrs|
|
23
|
+
bot_commands_descs = collect_name_and_desc(help_attrs.commands)
|
24
|
+
"#{command_name_and_desc(help_attrs)}\n\n*Commands:*\n#{bot_commands_descs.join("\n")}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def other_commands_descs
|
29
|
+
collect_name_and_desc(other_commands_help_attrs)
|
30
|
+
end
|
31
|
+
|
32
|
+
def command_full_desc(name)
|
33
|
+
unescaped_name = Slack::Messages::Formatting.unescape(name)
|
34
|
+
help_attrs = find_command_help_attrs(unescaped_name)
|
35
|
+
return "There's no command *#{unescaped_name}*" unless help_attrs
|
36
|
+
return "There's no description for command *#{unescaped_name}*" if help_attrs.command_long_desc.blank?
|
37
|
+
"#{command_name_and_desc(help_attrs)}\n\n#{help_attrs.command_long_desc}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_command_help_attrs(name)
|
41
|
+
help_attrs = commands_help_attrs.find { |k| k.command_name == name }
|
42
|
+
return help_attrs if help_attrs
|
43
|
+
commands_help_attrs.each { |k| k.commands.each { |c| return c if c.command_name == name } }
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def collect_help_attrs(help_attrs)
|
50
|
+
help_attrs_with_present_names(help_attrs).map do |ha|
|
51
|
+
yield(ha)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def collect_name_and_desc(help_attrs)
|
56
|
+
collect_help_attrs(help_attrs) do |ha|
|
57
|
+
command_name_and_desc(ha)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def command_name_and_desc(help_attrs)
|
62
|
+
desc = help_attrs.command_desc.present? ? " - #{help_attrs.command_desc}" : ''
|
63
|
+
"*#{help_attrs.command_name}*#{desc}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def help_attrs_with_present_names(help_attrs)
|
67
|
+
help_attrs.select { |k| k.command_name.present? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def bot_help_attrs
|
71
|
+
commands_help_attrs #.select { |k| k.klass.ancestors.include?(SlackRespondent::Bot) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def other_commands_help_attrs
|
75
|
+
[]
|
76
|
+
# commands_help_attrs.select { |k| k.klass.ancestors.include?(SlackRespondent::Commands::Base) && !k.klass.ancestors.include?(SlackRespondent::Bot) }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|