slackbot_frd 0.0.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/bin/slackbot-frd +196 -0
- data/lib/slackbot_frd.rb +3 -0
- data/lib/slackbot_frd/initializer/bot_starter.rb +52 -0
- data/lib/slackbot_frd/initializer/bot_starter_cli.rb +11 -0
- data/lib/slackbot_frd/lib/bot.rb +18 -0
- data/lib/slackbot_frd/lib/errors.rb +21 -0
- data/lib/slackbot_frd/lib/log.rb +60 -0
- data/lib/slackbot_frd/lib/slack_connection.rb +271 -0
- data/lib/slackbot_frd/lib/user_channel_callbacks.rb +53 -0
- data/lib/slackbot_frd/slack_methods/channels_list.rb +38 -0
- data/lib/slackbot_frd/slack_methods/chat_post_message.rb +48 -0
- data/lib/slackbot_frd/slack_methods/im_channels_list.rb +38 -0
- data/lib/slackbot_frd/slack_methods/rtm_start.rb +31 -0
- data/lib/slackbot_frd/slack_methods/users_list.rb +38 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c5c6be64e04fc7a8be46b5dacab72fee1612e5b5
|
4
|
+
data.tar.gz: 9180a45cb3f7acbe6db6fb9cd74c4ca0f85c85e4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 395ab58d08a47f062b529998bd0fe4a515930d3013176b0e60a58ffba5d587fc18e51c5b4c5629cb2c03c027d464dd867637c4041b012d4c629a509221eaeffe
|
7
|
+
data.tar.gz: e767617e733d4c580d08fb04a82e944f46d0cfb388f5e4265e5fd7c933cbb4684d3476befc95210bff29c85d56c81d41fe6464e399d5abce550ab1abce169242
|
data/bin/slackbot-frd
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require 'slackbot_frd/initializer/bot_starter'
|
7
|
+
require 'slackbot_frd/lib/slack_connection'
|
8
|
+
require 'slackbot_frd/lib/bot'
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'byebug'
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
|
15
|
+
DEBUG = true
|
16
|
+
|
17
|
+
PID_FILE_WATCHER = "/tmp/slackbot-frd-watcher.pid"
|
18
|
+
PID_FILE_CONNECTION = "/tmp/slackbot-frd-connection.pid"
|
19
|
+
BOT_LIST_FILE = "/tmp/slackbot-frd-bot-list.pid"
|
20
|
+
ERROR_FILE = "/tmp/slackbot-frd-error-file.pid"
|
21
|
+
DEFAULT_CONFIG_FILE = "slackbot-frd.conf"
|
22
|
+
LOG_FILE = "slackbot-frd.log"
|
23
|
+
|
24
|
+
class SlackbotFrdBin < Thor
|
25
|
+
desc "list", "List all bots"
|
26
|
+
long_desc <<-LONGDESC
|
27
|
+
list will print out all available bots
|
28
|
+
|
29
|
+
> $ slackbot-frd list -- List all installed bots
|
30
|
+
LONGDESC
|
31
|
+
def list
|
32
|
+
# TODO
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "start [bot1] [bot2] [botx...]", "Start all specified bots, or all bots"
|
36
|
+
long_desc <<-LONGDESC
|
37
|
+
start [bot1] [bot2] [botx...] will start the specified bots.
|
38
|
+
If no bots are specified, all available bots will be run.
|
39
|
+
|
40
|
+
params set via explicit flags will overwrite conflicting environment variables,
|
41
|
+
and environment variables will overwrite conflicting config file params.
|
42
|
+
|
43
|
+
> $ slackbot-frd start -- Start all available bots
|
44
|
+
LONGDESC
|
45
|
+
option :daemonize, type: :boolean, aliases: 'd'
|
46
|
+
option :botdir, type: :string, aliases: 'b'
|
47
|
+
option 'config-file'.to_sym, type: :string, aliases: 'c'
|
48
|
+
option :token, type: :string, aliases: 't'
|
49
|
+
def start(*bots)
|
50
|
+
config_file = options['config-file'.to_sym]
|
51
|
+
config_file = "#{Dir.pwd}/#{DEFAULT_CONFIG_FILE}" unless config_file
|
52
|
+
json = config_file_json(config_file) if config_file
|
53
|
+
json ||= {}
|
54
|
+
|
55
|
+
daemonize = false
|
56
|
+
daemonize = json["daemonize"] if json["daemonize"]
|
57
|
+
daemonize = ENV["SLACKBOT_FRD_DAEMONIZE"] if ENV["SLACKBOT_FRD_DAEMONIZE"]
|
58
|
+
daemonize = options[:daemonize] if options[:daemonize]
|
59
|
+
|
60
|
+
botdir = Dir.pwd
|
61
|
+
botdir = json["botdir"] if json["botdir"]
|
62
|
+
botdir = ENV["SLACKBOT_FRD_BOTDIR"] if ENV["SLACKBOT_FRD_BOTDIR"]
|
63
|
+
botdir = options[:botdir] if options[:botdir]
|
64
|
+
botdir = File.expand_path(botdir)
|
65
|
+
|
66
|
+
SlackbotFrd::Log.logfile = "#{botdir}/#{LOG_FILE}"
|
67
|
+
SlackbotFrd::Log.info("Logging to file '#{SlackbotFrd::Log.logfile}'")
|
68
|
+
|
69
|
+
token = json["token"]
|
70
|
+
token = ENV["SLACKBOT_FRD_TOKEN"] if ENV["SLACKBOT_FRD_TOKEN"]
|
71
|
+
token = options[:token] if options[:token]
|
72
|
+
unless token
|
73
|
+
SlackbotFrd::Log.error("No token found. Cannot authenticate to Slack")
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
if daemonize
|
78
|
+
set_watcher_pid(Process.fork{ watch_connection(bots, token, botdir, true) })
|
79
|
+
else
|
80
|
+
watch_connection(bots, token, botdir)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
desc "stop", "Stop all bots"
|
85
|
+
long_desc <<-LONGDESC
|
86
|
+
stop will stop all bots
|
87
|
+
|
88
|
+
> $ slackbot-frd stop -- Stop all running bots
|
89
|
+
LONGDESC
|
90
|
+
|
91
|
+
def stop
|
92
|
+
# first kill the watcher, then kill the connection
|
93
|
+
kill_pid(watcher_pid)
|
94
|
+
kill_pid(connection_pid)
|
95
|
+
end
|
96
|
+
|
97
|
+
desc "restart", "Stop all bots and restart them"
|
98
|
+
long_desc <<-LONGDESC
|
99
|
+
restart will restart all bots
|
100
|
+
|
101
|
+
> $ slackbot-frd restart -- Restart all running bots
|
102
|
+
LONGDESC
|
103
|
+
def restart
|
104
|
+
stop
|
105
|
+
start(set_bots)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
def config_file_json(config_file)
|
110
|
+
if File.exists?(config_file)
|
111
|
+
content = File.read(config_file)
|
112
|
+
return JSON.parse(content)
|
113
|
+
end
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
def bots_from_file
|
119
|
+
File.read(BOT_LIST_FILE).split
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
def set_bots_in_file(bots)
|
124
|
+
File.write(BOT_LIST_FILE, "#{bots.join("\n")}")
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
def kill_pid(pid)
|
129
|
+
# try 3 times to SIGINT the pid, then SIGKILL it
|
130
|
+
3.times do
|
131
|
+
break unless running?(pid)
|
132
|
+
Process.kill('SIGINT', pid)
|
133
|
+
end
|
134
|
+
Process.kill('SIGKILL', pid) if running?(pid)
|
135
|
+
# TODO log if the process is still running
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
def watch_connection(bots, token, botdir, daemonize = false)
|
140
|
+
until errors
|
141
|
+
pid = Process.fork do
|
142
|
+
Process.daemon if daemonize
|
143
|
+
loop { BotStarter.start_bots(ERROR_FILE, token, botdir, bots) }
|
144
|
+
end
|
145
|
+
set_connection_pid(pid)
|
146
|
+
Process.wait(pid)
|
147
|
+
end
|
148
|
+
if errors
|
149
|
+
puts "Could not start connection: #{errors}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
def errors
|
155
|
+
return File.read(ERROR_FILE).split if File.exists?(ERROR_FILE)
|
156
|
+
nil
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
def running?(pid)
|
161
|
+
begin
|
162
|
+
Process.getpgid(pid.to_i)
|
163
|
+
return true
|
164
|
+
rescue Errno::ESRCH
|
165
|
+
return false
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
def watcher_pid
|
171
|
+
File.read(PID_FILE_WATCHER).to_i
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
def connection_pid
|
176
|
+
File.read(PID_FILE_CONNECTION).to_i
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
def set_watcher_pid(pid)
|
181
|
+
File.write(PID_FILE_WATCHER, pid)
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
def set_connection_pid(pid)
|
186
|
+
File.write(PID_FILE_CONNECTION, pid)
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
def delete_pid_files
|
191
|
+
File.delete(PID_FILE_WATCHER)
|
192
|
+
File.delete(PID_FILE_CONNECTION)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
SlackbotFrdBin.start(ARGV)
|
data/lib/slackbot_frd.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
require 'slackbot_frd/lib/slack_connection'
|
6
|
+
require 'slackbot_frd/lib/bot'
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'byebug'
|
10
|
+
rescue LoadError
|
11
|
+
end
|
12
|
+
|
13
|
+
class BotStarter
|
14
|
+
def self.start_bots(errors_file, token, botdir, bots)
|
15
|
+
bot_enabled = ->(bot) { bots.empty? || bots.include?(bot) }
|
16
|
+
|
17
|
+
# Create a new Connection to pass to the bot classes
|
18
|
+
slack_connection = SlackbotFrd::SlackConnection.new(token)
|
19
|
+
|
20
|
+
load_bot_files(botdir)
|
21
|
+
|
22
|
+
bots = []
|
23
|
+
# instantiate them, and then call their add_callbacks method
|
24
|
+
ObjectSpace.each_object(Class).select do |klass|
|
25
|
+
if klass != SlackbotFrd::Bot && klass.ancestors.include?(SlackbotFrd::Bot) && bot_enabled.call(klass.name)
|
26
|
+
SlackbotFrd::Log.debug("Instantiating class '#{klass.to_s}'")
|
27
|
+
b = klass.new
|
28
|
+
SlackbotFrd::Log.debug("Adding callbacks to bot '#{klass}'")
|
29
|
+
b.add_callbacks(slack_connection)
|
30
|
+
bots.push(b)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if bots.count == 0
|
35
|
+
@error ||= []
|
36
|
+
@error.push("No bots loaded")
|
37
|
+
SlackbotFrd::Log.error("Not starting: no bots found")
|
38
|
+
else
|
39
|
+
SlackbotFrd::Log.debug("Starting SlackConnection")
|
40
|
+
slack_connection.start
|
41
|
+
SlackbotFrd::Log.debug("Connection closed")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def self.load_bot_files(top_level_dir)
|
47
|
+
Dir["#{File.expand_path(top_level_dir)}/**/*.rb"].each do |f|
|
48
|
+
SlackbotFrd::Log.debug("Loading bot file '#{f}'")
|
49
|
+
load(f)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'slackbot_frd/initializer/bot_starter'
|
2
|
+
|
3
|
+
class BotStarterCli < Thor
|
4
|
+
option :token, type: :string, required: true, aliases: 't'
|
5
|
+
option :botdir, type: :string, required: true, aliases: ['b', 'd']
|
6
|
+
def start(*bots)
|
7
|
+
BotStarter.start_bots(options[:token], options[:botdir], bots)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
BotStarterCli.start(ARGV)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Subclass the bot class to have your bot loaded and run
|
2
|
+
module SlackbotFrd
|
3
|
+
class Bot
|
4
|
+
def self.only(*bots)
|
5
|
+
@bots ||= []
|
6
|
+
@bots.push(bots).flatten!
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :bots
|
11
|
+
end
|
12
|
+
|
13
|
+
# This is where the bot adds all of their callbacks to the bpbot
|
14
|
+
def add_callbacks(slack_connection)
|
15
|
+
raise StandardError.new("You must override the define() method for your bot to do anything")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module SlackbotFrd
|
3
|
+
class NoTokenError < StandardError
|
4
|
+
def initialize(message = nil)
|
5
|
+
if message
|
6
|
+
super(message)
|
7
|
+
else
|
8
|
+
super("An API token is required for authenticating to the Slack API")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class AuthenticationFailedError < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
class InvalidUserError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
class InvalidChannelError < StandardError
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module SlackbotFrd
|
4
|
+
class Log
|
5
|
+
@levels = {verbose: 1, debug: 2, info: 3, warn: 4, error: 5}
|
6
|
+
@default_level = :debug
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_writer :level
|
10
|
+
attr_accessor :logfile
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.level
|
14
|
+
return @default_level unless @level
|
15
|
+
@level
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.level_on(level)
|
19
|
+
@levels[level] >= @levels[self.level]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.colors
|
23
|
+
[:green, :red, :yellow, :none, :blue]
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.error(message)
|
27
|
+
log('Error', message, :red) if level_on(:error)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.warn(message)
|
31
|
+
log('Warn', message, :yellow) if level_on(:warn)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.debug(message)
|
35
|
+
log('Debug', message, :green) if level_on(:debug)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.info(message)
|
39
|
+
log('Info', message, :blue) if level_on(:info)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.verbose(message)
|
43
|
+
log('Verbose', message, :magenta) if level_on(:verbose)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.log(loglevel, message, color = :none)
|
47
|
+
om = "#{DateTime.now.strftime('%Y-%m-%e %H:%M:%S.%L %z')}: [#{loglevel}]: #{message}\n"
|
48
|
+
print om.send(color)
|
49
|
+
begin
|
50
|
+
raise StandardError.new("No log file specified. (Set with SlackbotFrd::Log.logfile=)") unless @logfile
|
51
|
+
File.open(@logfile, 'a') do |f|
|
52
|
+
f.write(om)
|
53
|
+
end
|
54
|
+
rescue StandardError => e
|
55
|
+
puts "OH NO! ERROR WRITING TO LOG FILE!: #{e}"
|
56
|
+
end
|
57
|
+
om
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'faye/websocket'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'slackbot_frd/lib/errors'
|
6
|
+
require 'slackbot_frd/lib/user_channel_callbacks'
|
7
|
+
require 'slackbot_frd/lib/log'
|
8
|
+
|
9
|
+
require 'slackbot_frd/slack_methods/rtm_start'
|
10
|
+
require 'slackbot_frd/slack_methods/chat_post_message'
|
11
|
+
require 'slackbot_frd/slack_methods/im_channels_list'
|
12
|
+
require 'slackbot_frd/slack_methods/channels_list'
|
13
|
+
require 'slackbot_frd/slack_methods/users_list'
|
14
|
+
|
15
|
+
module SlackbotFrd
|
16
|
+
class SlackConnection
|
17
|
+
FILE_PATH = File.expand_path(__FILE__)
|
18
|
+
APP_ROOT = File.expand_path(File.dirname(File.dirname(FILE_PATH)))
|
19
|
+
FILE_DIR = File.dirname(FILE_PATH)
|
20
|
+
LOG_FILE = "#{APP_ROOT}/bp-slackbot.log"
|
21
|
+
PID_FILE_NAME = "#{APP_ROOT}/bp-slackbot.pid"
|
22
|
+
|
23
|
+
attr_accessor :token
|
24
|
+
|
25
|
+
def initialize(token)
|
26
|
+
unless token
|
27
|
+
SlackbotFrd::Log::error("No token passed to #{self.class}")
|
28
|
+
raise NoTokenError.new
|
29
|
+
end
|
30
|
+
|
31
|
+
@token = token
|
32
|
+
@event_id = 0
|
33
|
+
@on_connected_callbacks = []
|
34
|
+
@on_disconnected_callbacks = []
|
35
|
+
@on_message_callbacks = UserChannelCallbacks.new
|
36
|
+
@on_channel_left_callbacks = UserChannelCallbacks.new
|
37
|
+
@on_channel_joined_callbacks = UserChannelCallbacks.new
|
38
|
+
|
39
|
+
# These hashes are used to map ids to names efficiently
|
40
|
+
@user_id_to_name = {}
|
41
|
+
@user_name_to_id = {}
|
42
|
+
@channel_id_to_name = {}
|
43
|
+
@channel_name_to_id = {}
|
44
|
+
|
45
|
+
restrict_actions_to_channels_joined
|
46
|
+
SlackbotFrd::Log::debug("Done initializing #{self.class}")
|
47
|
+
end
|
48
|
+
|
49
|
+
def start
|
50
|
+
# Write pid file
|
51
|
+
File.write(PID_FILE_NAME, "#{Process.pid}")
|
52
|
+
|
53
|
+
SlackbotFrd::Log::info("#{self.class}: starting event machine")
|
54
|
+
|
55
|
+
EM.run do
|
56
|
+
wss_url = SlackbotFrd::SlackMethods::RtmStart.wss_url(@token)
|
57
|
+
unless wss_url
|
58
|
+
str = "No Real Time stream opened by slack. Check for correct authentication token"
|
59
|
+
SlackbotFrd::Log.error(str)
|
60
|
+
File.append(@errors_file, str)
|
61
|
+
return
|
62
|
+
end
|
63
|
+
@ws = Faye::WebSocket::Client.new(wss_url)
|
64
|
+
|
65
|
+
@on_connected_callbacks.each { |callback| @ws.on(:open, &callback) }
|
66
|
+
@on_disconnected_callbacks.each { |callback| @ws.on(:close, &callback) }
|
67
|
+
@ws.on(:message) { |event| process_message_received(event) }
|
68
|
+
|
69
|
+
# Clean up our pid file
|
70
|
+
@ws.on(:close) { |event| File.delete(PID_FILE_NAME) }
|
71
|
+
end
|
72
|
+
|
73
|
+
SlackbotFrd::Log::debug("#{self.class}: event machine started")
|
74
|
+
end
|
75
|
+
|
76
|
+
def event_id
|
77
|
+
@event_id += 1
|
78
|
+
@event_id
|
79
|
+
end
|
80
|
+
|
81
|
+
def on_connected(&block)
|
82
|
+
@on_connected_callbacks.push(block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def on_close(&block)
|
86
|
+
@on_disconnected_callbacks.push(block)
|
87
|
+
end
|
88
|
+
|
89
|
+
def on_message(user = :any, channel = :any, &block)
|
90
|
+
@on_message_callbacks.add(user_name_to_id(user), channel_name_to_id(channel), block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def on_channel_left(user = :any, channel = :any, &block)
|
94
|
+
@on_channel_left_callbacks.add(user_name_to_id(user), channel_name_to_id(channel), block)
|
95
|
+
end
|
96
|
+
|
97
|
+
def on_channel_joined(user = :any, channel = :any, &block)
|
98
|
+
u = user_name_to_id(user)
|
99
|
+
c = channel_name_to_id(channel)
|
100
|
+
@on_channel_joined_callbacks.add(u, c, block)
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_message_as_user(channel, message)
|
104
|
+
unless @ws
|
105
|
+
SlackbotFrd::Log::error("Cannot send message '#{message}' as user to channel '#{channel}' because not connected to wss stream")
|
106
|
+
raise NotConnectedError.new("Not connected to wss stream")
|
107
|
+
end
|
108
|
+
|
109
|
+
resp = @ws.send({
|
110
|
+
id: event_id,
|
111
|
+
type: "message",
|
112
|
+
channel: channel_name_to_id(channel),
|
113
|
+
text: message
|
114
|
+
}.to_json)
|
115
|
+
|
116
|
+
SlackbotFrd::Log::debug("#{self.class}: sending message '#{message}' as user to channel '#{channel}'. Response: #{resp}")
|
117
|
+
end
|
118
|
+
|
119
|
+
def send_message(channel, message, username, avatar, avatar_is_emoji)
|
120
|
+
resp = SlackbotFrd::SlackMethods::ChatPostMessage.postMessage(
|
121
|
+
@token,
|
122
|
+
channel_name_to_id(channel),
|
123
|
+
message,
|
124
|
+
username,
|
125
|
+
avatar,
|
126
|
+
avatar_is_emoji
|
127
|
+
)
|
128
|
+
SlackbotFrd::Log::debug("#{self.class}: sending message '#{message}' as user '#{username}' to channel '#{channel}'. Response: #{resp}")
|
129
|
+
end
|
130
|
+
|
131
|
+
def restrict_actions_to_channels_joined(value = true)
|
132
|
+
@restrict_actions_to_channels_joined = value
|
133
|
+
end
|
134
|
+
|
135
|
+
def user_id_to_name(user_id)
|
136
|
+
return user_id if user_id == :any || user_id == :bot
|
137
|
+
unless @user_id_to_name && @user_id_to_name.has_key?(user_id)
|
138
|
+
refresh_user_info
|
139
|
+
end
|
140
|
+
SlackbotFrd::Log::warn("#{self.class}: User id '#{user_id}' not found") unless @user_id_to_name.include?(user_id)
|
141
|
+
@user_id_to_name[user_id]
|
142
|
+
end
|
143
|
+
|
144
|
+
def user_name_to_id(user_name)
|
145
|
+
return user_name if user_name == :any || user_name == :bot
|
146
|
+
unless @user_name_to_id && @user_name_to_id.has_key?(user_name)
|
147
|
+
refresh_user_info
|
148
|
+
end
|
149
|
+
SlackbotFrd::Log::warn("#{self.class}: User name '#{user_name}' not found") unless @user_name_to_id.include?(user_name)
|
150
|
+
@user_name_to_id[user_name]
|
151
|
+
end
|
152
|
+
|
153
|
+
def channel_id_to_name(channel_id)
|
154
|
+
unless @channel_id_to_name && @channel_id_to_name.has_key?(channel_id)
|
155
|
+
refresh_channel_info
|
156
|
+
end
|
157
|
+
SlackbotFrd::Log::warn("#{self.class}: Channel id '#{channel_id}' not found") unless @channel_id_to_name.include?(channel_id)
|
158
|
+
@channel_id_to_name[channel_id]
|
159
|
+
end
|
160
|
+
|
161
|
+
def channel_name_to_id(channel_name)
|
162
|
+
return channel_name if channel_name == :any
|
163
|
+
nc = normalize_channel_name(channel_name)
|
164
|
+
unless @channel_name_to_id && @channel_name_to_id.has_key?(nc)
|
165
|
+
refresh_channel_info
|
166
|
+
end
|
167
|
+
SlackbotFrd::Log::warn("#{self.class}: Channel name '#{nc}' not found") unless @channel_name_to_id.include?(nc)
|
168
|
+
@channel_name_to_id[nc]
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
def normalize_channel_name(channel_name)
|
173
|
+
return channel_name[1..-1] if channel_name.start_with?('#')
|
174
|
+
channel_name
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
def process_message_received(event)
|
179
|
+
message = JSON.parse(event.data)
|
180
|
+
SlackbotFrd::Log::verbose("#{self.class}: Message received: #{message}")
|
181
|
+
if message["type"] == "message"
|
182
|
+
if message["subtype"] == "channel_join"
|
183
|
+
process_join_message(message)
|
184
|
+
elsif message["subtype"] == "channel_leave"
|
185
|
+
process_leave_message(message)
|
186
|
+
elsif message["subtype"] == "file_share"
|
187
|
+
process_file_share(message)
|
188
|
+
else
|
189
|
+
process_chat_message(message)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
def process_file_share(message)
|
196
|
+
SlackbotFrd::Log::verbose("#{self.class}: Processing file share: #{message}")
|
197
|
+
SlackbotFrd::Log::debug("#{self.class}: Not processing file share because it is not implemented:")
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
def process_chat_message(message)
|
202
|
+
SlackbotFrd::Log::verbose("#{self.class}: Processing chat message: #{message}")
|
203
|
+
|
204
|
+
user = message["user"]
|
205
|
+
user = :bot if message["subtype"] == "bot_message"
|
206
|
+
channel = message["channel"]
|
207
|
+
text = message["text"]
|
208
|
+
|
209
|
+
unless user
|
210
|
+
SlackbotFrd::Log::warn("#{self.class}: Chat message doesn't include user! message: #{message}")
|
211
|
+
return
|
212
|
+
end
|
213
|
+
|
214
|
+
unless channel
|
215
|
+
SlackbotFrd::Log::warn("#{self.class}: Chat message doesn't include channel! message: #{message}")
|
216
|
+
return
|
217
|
+
end
|
218
|
+
|
219
|
+
@on_message_callbacks.where_include_all(user, channel).each do |callback|
|
220
|
+
# instance_exec allows the user to call send_message and send_message_as_user
|
221
|
+
# without prefixing like this: slack_connection.send_message()
|
222
|
+
#
|
223
|
+
# However, it makes calling functions defined in the class not work, so
|
224
|
+
# for now we aren't going to do it
|
225
|
+
#
|
226
|
+
#instance_exec(user_id_to_name(user), channel_id_to_name(channel), text, &callback)
|
227
|
+
callback.call(user_id_to_name(user), channel_id_to_name(channel), text)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
def process_join_message(message)
|
233
|
+
SlackbotFrd::Log::verbose("#{self.class}: Processing join message: #{message}")
|
234
|
+
user = message["user"]
|
235
|
+
user = :bot if message["subtype"] == "bot_message"
|
236
|
+
channel = message["channel"]
|
237
|
+
@on_channel_joined_callbacks.where_include_all(user, channel).each do |callback|
|
238
|
+
callback.call(user_id_to_name(user), channel_id_to_name(channel))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
def process_leave_message(message)
|
244
|
+
SlackbotFrd::Log::verbose("#{self.class}: Processing leave message: #{message}")
|
245
|
+
user = message["user"]
|
246
|
+
user = :bot if message["subtype"] == "bot_message"
|
247
|
+
channel = message["channel"]
|
248
|
+
@on_channel_left_callbacks.where_include_all(user, channel).each do |callback|
|
249
|
+
callback.call(user_id_to_name(user), channel_id_to_name(channel))
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
def refresh_user_info
|
255
|
+
users_list = SlackbotFrd::SlackMethods::UsersList.new(@token).connect
|
256
|
+
@user_id_to_name = users_list.ids_to_names
|
257
|
+
@user_name_to_id = users_list.names_to_ids
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
def refresh_channel_info
|
262
|
+
channels_list = SlackbotFrd::SlackMethods::ChannelsList.new(@token).connect
|
263
|
+
@channel_id_to_name = channels_list.ids_to_names
|
264
|
+
@channel_name_to_id = channels_list.names_to_ids
|
265
|
+
|
266
|
+
im_channels_list = SlackbotFrd::SlackMethods::ImChannelsList.new(@token).connect
|
267
|
+
@channel_id_to_name.merge!(im_channels_list.ids_to_names)
|
268
|
+
@channel_name_to_id.merge!(im_channels_list.names_to_ids)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'slackbot_frd/lib/log'
|
2
|
+
|
3
|
+
module SlackbotFrd
|
4
|
+
class UserChannelCallbacks
|
5
|
+
def initialize
|
6
|
+
@conditions = {}
|
7
|
+
@conditions[:any] = {}
|
8
|
+
@conditions[:any][:any] = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def init(user, channel)
|
12
|
+
unless user
|
13
|
+
Log::error("#{self.class}: Invalid user '#{user}'")
|
14
|
+
raise InvalidUserError.new
|
15
|
+
end
|
16
|
+
unless channel
|
17
|
+
Log::error("#{self.class}: Invalid channel '#{channel}'")
|
18
|
+
raise InvalidChannelError.new
|
19
|
+
end
|
20
|
+
@conditions[user] ||= {}
|
21
|
+
@conditions[user][:any] ||= []
|
22
|
+
@conditions[:any][channel] ||= []
|
23
|
+
@conditions[user][channel] ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(user, channel, callback)
|
27
|
+
init(user, channel)
|
28
|
+
@conditions[user][channel].push(callback)
|
29
|
+
end
|
30
|
+
|
31
|
+
def where(user, channel)
|
32
|
+
init(user, channel)
|
33
|
+
@conditions[user][channel] || []
|
34
|
+
end
|
35
|
+
|
36
|
+
def where_all
|
37
|
+
@conditions[:any][:any] || []
|
38
|
+
end
|
39
|
+
|
40
|
+
def where_include_all(user, channel)
|
41
|
+
init(user, channel)
|
42
|
+
retval = @conditions[:any][:any].dup || []
|
43
|
+
retval.concat(@conditions[user][:any] || [])
|
44
|
+
retval.concat(@conditions[:any][channel] || [])
|
45
|
+
retval.concat(@conditions[user][channel] || [])
|
46
|
+
retval
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"#{@conditions}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SlackbotFrd
|
5
|
+
module SlackMethods
|
6
|
+
class ChannelsList
|
7
|
+
include HTTParty
|
8
|
+
base_uri 'https://slack.com/api/channels.list'
|
9
|
+
|
10
|
+
attr_reader :response
|
11
|
+
|
12
|
+
def initialize(token)
|
13
|
+
@token = token
|
14
|
+
end
|
15
|
+
|
16
|
+
def connect
|
17
|
+
@response = JSON.parse(self.class.post('', :body => { token: @token } ).body)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def ids_to_names
|
22
|
+
retval = {}
|
23
|
+
@response["channels"].each do |channel|
|
24
|
+
retval[channel["id"]] = channel["name"]
|
25
|
+
end
|
26
|
+
retval
|
27
|
+
end
|
28
|
+
|
29
|
+
def names_to_ids
|
30
|
+
retval = {}
|
31
|
+
@response["channels"].each do |channel|
|
32
|
+
retval[channel["name"]] = channel["id"]
|
33
|
+
end
|
34
|
+
retval
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SlackbotFrd
|
5
|
+
module SlackMethods
|
6
|
+
class ChatPostMessage
|
7
|
+
include HTTParty
|
8
|
+
base_uri 'https://slack.com/api/chat.postMessage'
|
9
|
+
|
10
|
+
def self.postMessage(token, channel, message, username = nil, avatar = nil, avatar_is_emoji = nil)
|
11
|
+
r = ChatPostMessage.new(token, channel, message, username, avatar, avatar_is_emoji)
|
12
|
+
r.postMessage
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(token, channel, message, username = nil, avatar = nil, avatar_is_emoji = nil)
|
16
|
+
@token = token
|
17
|
+
@channel = channel
|
18
|
+
@message = message
|
19
|
+
@username = username
|
20
|
+
@avatar = avatar
|
21
|
+
@avatar_is_emoji = avatar_is_emoji
|
22
|
+
end
|
23
|
+
|
24
|
+
def postMessage
|
25
|
+
body = {
|
26
|
+
token: @token,
|
27
|
+
channel: @channel,
|
28
|
+
text: @message,
|
29
|
+
}
|
30
|
+
|
31
|
+
if @username
|
32
|
+
body.merge!({ username: @username })
|
33
|
+
|
34
|
+
if @avatar_is_emoji
|
35
|
+
body.merge!({ icon_emoji: @avatar })
|
36
|
+
else
|
37
|
+
body.merge!({ icon_url: @avatar })
|
38
|
+
end
|
39
|
+
else
|
40
|
+
body.merge!({ as_user: true })
|
41
|
+
end
|
42
|
+
|
43
|
+
@response = self.class.post('', :body => body)
|
44
|
+
@response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SlackbotFrd
|
5
|
+
module SlackMethods
|
6
|
+
class ImChannelsList
|
7
|
+
include HTTParty
|
8
|
+
base_uri 'https://slack.com/api/im.list'
|
9
|
+
|
10
|
+
attr_reader :response
|
11
|
+
|
12
|
+
def initialize(token)
|
13
|
+
@token = token
|
14
|
+
end
|
15
|
+
|
16
|
+
def connect
|
17
|
+
@response = JSON.parse(self.class.post('', :body => { token: @token } ).body)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def ids_to_names
|
22
|
+
retval = {}
|
23
|
+
@response["ims"].each do |im|
|
24
|
+
retval[im["id"]] = im["user"]
|
25
|
+
end
|
26
|
+
retval
|
27
|
+
end
|
28
|
+
|
29
|
+
def names_to_ids
|
30
|
+
retval = {}
|
31
|
+
@response["ims"].each do |im|
|
32
|
+
retval[im["user"]] = im["id"]
|
33
|
+
end
|
34
|
+
retval
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SlackbotFrd
|
5
|
+
module SlackMethods
|
6
|
+
class RtmStart
|
7
|
+
include HTTParty
|
8
|
+
base_uri 'https://slack.com/api/rtm.start'
|
9
|
+
|
10
|
+
def self.wss_url(token)
|
11
|
+
r = RtmStart.new(token)
|
12
|
+
r.connect
|
13
|
+
r.wss_url
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(token)
|
17
|
+
@token = token
|
18
|
+
end
|
19
|
+
|
20
|
+
def connect
|
21
|
+
@response = JSON.parse(self.class.post('', :body => { token: @token } ).body)
|
22
|
+
@response
|
23
|
+
end
|
24
|
+
|
25
|
+
def wss_url
|
26
|
+
#return "ERR" unless @response.has_key?("url")
|
27
|
+
@response["url"]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SlackbotFrd
|
5
|
+
module SlackMethods
|
6
|
+
class UsersList
|
7
|
+
include HTTParty
|
8
|
+
base_uri 'https://slack.com/api/users.list'
|
9
|
+
|
10
|
+
attr_reader :response
|
11
|
+
|
12
|
+
def initialize(token)
|
13
|
+
@token = token
|
14
|
+
end
|
15
|
+
|
16
|
+
def connect
|
17
|
+
@response = JSON.parse(self.class.post('', :body => { token: @token } ).body)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def ids_to_names
|
22
|
+
retval = {}
|
23
|
+
@response["members"].each do |user|
|
24
|
+
retval[user["id"]] = user["name"]
|
25
|
+
end
|
26
|
+
retval
|
27
|
+
end
|
28
|
+
|
29
|
+
def names_to_ids
|
30
|
+
retval = {}
|
31
|
+
@response["members"].each do |user|
|
32
|
+
retval[user["name"]] = user["id"]
|
33
|
+
end
|
34
|
+
retval
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slackbot_frd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Porter
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httparty
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.13'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faye-websocket
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.9'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.9'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: colorize
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.7'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: thor
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.19'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.19'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: daemons
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.2'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: json
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.8'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.8'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: byebug
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.1'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.1'
|
139
|
+
description: The slack web api is good, but very raw. What you need is a great ruby
|
140
|
+
framework to abstract away all that. This is it! This framework allows you to
|
141
|
+
write bots easily by providing methods that are easy to call. Behind the scenes,
|
142
|
+
the framework is negotiating your real time stream, converting channel names and
|
143
|
+
user names to and from IDs so you can use the names instead, and parsing/classifying
|
144
|
+
the real time messages into useful types that you can hook into. Don't write your
|
145
|
+
bot without this.
|
146
|
+
email: BenjaminPorter86@gmail.com
|
147
|
+
executables:
|
148
|
+
- slackbot-frd
|
149
|
+
extensions: []
|
150
|
+
extra_rdoc_files: []
|
151
|
+
files:
|
152
|
+
- bin/slackbot-frd
|
153
|
+
- lib/slackbot_frd.rb
|
154
|
+
- lib/slackbot_frd/initializer/bot_starter.rb
|
155
|
+
- lib/slackbot_frd/initializer/bot_starter_cli.rb
|
156
|
+
- lib/slackbot_frd/lib/bot.rb
|
157
|
+
- lib/slackbot_frd/lib/errors.rb
|
158
|
+
- lib/slackbot_frd/lib/log.rb
|
159
|
+
- lib/slackbot_frd/lib/slack_connection.rb
|
160
|
+
- lib/slackbot_frd/lib/user_channel_callbacks.rb
|
161
|
+
- lib/slackbot_frd/slack_methods/channels_list.rb
|
162
|
+
- lib/slackbot_frd/slack_methods/chat_post_message.rb
|
163
|
+
- lib/slackbot_frd/slack_methods/im_channels_list.rb
|
164
|
+
- lib/slackbot_frd/slack_methods/rtm_start.rb
|
165
|
+
- lib/slackbot_frd/slack_methods/users_list.rb
|
166
|
+
homepage: http://rubygems.org/gems/slackbot_frd
|
167
|
+
licenses:
|
168
|
+
- MIT
|
169
|
+
metadata: {}
|
170
|
+
post_install_message:
|
171
|
+
rdoc_options: []
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
requirements: []
|
185
|
+
rubyforge_project:
|
186
|
+
rubygems_version: 2.2.2
|
187
|
+
signing_key:
|
188
|
+
specification_version: 4
|
189
|
+
summary: slackbot_frd provides a dirt-simple framework for implementing one or more
|
190
|
+
slack bots
|
191
|
+
test_files: []
|