slackbot_frd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|