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.
@@ -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'
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'logger'
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+ require 'hashie'
6
+ require 'slack'
@@ -0,0 +1,3 @@
1
+ ENV['RACK_ENV'] ||= 'development'
2
+
3
+ require File.expand_path('../application', __FILE__)
@@ -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,5 @@
1
+ require_relative 'commands/base'
2
+ require_relative 'commands/about'
3
+ require_relative 'commands/help'
4
+ require_relative 'commands/hi'
5
+ require_relative 'commands/unknown'
@@ -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,14 @@
1
+ module SlackRespondent
2
+ module Commands
3
+ class Hi < Base
4
+ help do
5
+ title 'hi'
6
+ desc 'Says hello.'
7
+ end
8
+
9
+ def self.call(client, data, _match)
10
+ client.say(channel: data.channel, text: "Hi <@#{data.user}>!")
11
+ end
12
+ end
13
+ end
14
+ 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