squab-bot 1.3.2
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.
- data/LICENSE.md +13 -0
- data/README.md +4 -0
- data/bin/squab-bot +338 -0
- data/bin/squab-bot.yaml +40 -0
- metadata +114 -0
data/LICENSE.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2013 Square Inc.
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
data/bin/squab-bot
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
#!/usr/bin/ruby20
|
2
|
+
require 'net/http'
|
3
|
+
require 'yaml'
|
4
|
+
require 'squab-client'
|
5
|
+
|
6
|
+
# Global bot, but populate the namespace local
|
7
|
+
# Needed to work with threads
|
8
|
+
require 'isaac/bot'
|
9
|
+
$bot = Isaac::Bot.new
|
10
|
+
%w(configure helpers on).each do |method|
|
11
|
+
eval(<<-EOF)
|
12
|
+
def #{method}(*args, &block)
|
13
|
+
$bot.#{method}(*args, &block)
|
14
|
+
end
|
15
|
+
EOF
|
16
|
+
end
|
17
|
+
|
18
|
+
squab_confs = %w(/etc/squab/squab-bot.yaml /etc/squab-bot.yaml)
|
19
|
+
local_squab_conf = File.join(File.dirname(__FILE__), 'squab-bot.yaml')
|
20
|
+
squab_confs.push(local_squab_conf)
|
21
|
+
|
22
|
+
$config = nil
|
23
|
+
squab_confs.each do |cf|
|
24
|
+
begin
|
25
|
+
puts "Looking at " + cf
|
26
|
+
if File.exists?(cf)
|
27
|
+
$config = YAML.load_file(cf)
|
28
|
+
break
|
29
|
+
end
|
30
|
+
rescue
|
31
|
+
next
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
if $config.nil?
|
36
|
+
puts "No parsable config found"
|
37
|
+
exit()
|
38
|
+
end
|
39
|
+
|
40
|
+
def mainloop()
|
41
|
+
configure do |c|
|
42
|
+
c.nick = $config['irc_nick']
|
43
|
+
c.server = $config['irc_server']
|
44
|
+
c.port = $config['irc_port'].to_i
|
45
|
+
c.password = $config['irc_password']
|
46
|
+
c.ssl = $config['irc_ssl']
|
47
|
+
c.verbose = $config['irc_verbose']
|
48
|
+
end
|
49
|
+
|
50
|
+
on :connect do
|
51
|
+
if not $config['sub_channels'].member?($config['home_channel'])
|
52
|
+
$config['sub_channels'][$config['home_channel']] = /.*/
|
53
|
+
end
|
54
|
+
for irc_chan in $config['sub_channels'].keys
|
55
|
+
$config['sub_channels'][irc_chan] = /#{$config['sub_channels'][irc_chan]}/
|
56
|
+
join irc_chan
|
57
|
+
end
|
58
|
+
for irc_chan in $config['join_channels']
|
59
|
+
join irc_chan
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
on :channel, /^#{$config['irc_nick']}: help/i do
|
64
|
+
msg channel, "You can post things to Squab by typing:"
|
65
|
+
msg channel, ".sq <thing to say>"
|
66
|
+
msg channel, "Other commands:"
|
67
|
+
msg channel, "info -- state what channels and topics are being output"
|
68
|
+
msg channel, 'speak [on|off] -- Output messages from squab to the channel'
|
69
|
+
msg channel, 'filter <regex> -- Filter the output to this channel based'
|
70
|
+
msg channel, ' on a regex. Use .* for everything'
|
71
|
+
msg channel, 'post <msg> -- Post a message to squab'
|
72
|
+
msg channel, 'search <thing> -- search for a thing in a squab message'
|
73
|
+
msg channel, 'join <channel> -- Join a channel'
|
74
|
+
msg channel, 'leave -- Leave this channel'
|
75
|
+
end
|
76
|
+
|
77
|
+
on :channel, /^#{$config['irc_nick']}: info/i do
|
78
|
+
if $config['sub_channels'].member?(channel)
|
79
|
+
msg channel, "Your filter is " + $config['sub_channels'][channel].source
|
80
|
+
sc = Squab::Client.new()
|
81
|
+
sources = JSON.parse(sc.list_sources)
|
82
|
+
source_list = []
|
83
|
+
sources.each do |s|
|
84
|
+
if $config['sub_channels'][channel].match(s)
|
85
|
+
source_list.push(s)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
msg channel, "Based on the filter regex I will output the following sources to this channel:"
|
89
|
+
msg channel, source_list.join(', ')
|
90
|
+
else
|
91
|
+
msg channel, "I don't currently output anything to this channel"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
on :channel, /^#{$config['irc_nick']}: speak (\S+)/i do
|
96
|
+
if match[0] =~ /^on$/i
|
97
|
+
if $config['sub_channels'].member?(channel)
|
98
|
+
msg channel, "I already speak in this channel."
|
99
|
+
else
|
100
|
+
$config['sub_channels'][channel] = /.*/
|
101
|
+
msg channel, "Ok, I'll start sending all squab output to this channel."
|
102
|
+
msg channel, "Use 'filter' if you'd like to limit what I output."
|
103
|
+
end
|
104
|
+
elsif match[0] =~ /^off$/i
|
105
|
+
if channel == $config['home_channel']
|
106
|
+
msg channel, "Sorry, I can't stop talking in my home channel"
|
107
|
+
else
|
108
|
+
$config['sub_channels'].delete(channel)
|
109
|
+
msg channel, "Ok, I'll stop talking"
|
110
|
+
end
|
111
|
+
else
|
112
|
+
msg channel, "I accept 'on' and 'off' for this command, you said '#{match[0]}'"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
on :channel, /^#{$config['irc_nick']}: filter (.*)/i do
|
117
|
+
if channel == $config['home_channel']
|
118
|
+
msg channel, "#{channel} always outputs all sources"
|
119
|
+
else
|
120
|
+
filter_re = /#{match[0]}/
|
121
|
+
$config['sub_channels'][channel] = filter_re
|
122
|
+
msg channel, "Adding filter #{match[0]} to #{channel}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
on :channel, /^(\.sq|#{$config['irc_nick']}: post)\s+(.*)$/i do
|
127
|
+
sc = Squab::Client.new()
|
128
|
+
sc.source = channel
|
129
|
+
sc.uid = nick
|
130
|
+
try_count = 0
|
131
|
+
begin
|
132
|
+
sc.send(match[1])
|
133
|
+
msg channel, "Posted to Squab"
|
134
|
+
rescue SendEventFailed
|
135
|
+
try_count += 1
|
136
|
+
if try_count > 3
|
137
|
+
msg channel, "Couldn't send event right now, please retry later"
|
138
|
+
else
|
139
|
+
sleep 1
|
140
|
+
retry
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
on :channel, /^#{$config['irc_nick']}: search\s+(.*)$/i do
|
146
|
+
sc = Squab::Client.new()
|
147
|
+
begin
|
148
|
+
res = JSON.parse(sc.simple_search(match[1]))
|
149
|
+
src = event['source']
|
150
|
+
usr = event['uid']
|
151
|
+
val = event['value'][0..255]
|
152
|
+
url = event['url']
|
153
|
+
res.each do |event|
|
154
|
+
msg channel, "[#{src}][#{usr}] #{value} #{url}"
|
155
|
+
end
|
156
|
+
rescue => e
|
157
|
+
msg channel, "Search failed: #{e}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
on :channel, /^#{$config['irc_nick']}: leave$/i do
|
162
|
+
if channel === $config['home_channel']
|
163
|
+
msg channel, "I can't leave this channel, it's where I say stuff"
|
164
|
+
else
|
165
|
+
msg channel, "Bye!"
|
166
|
+
part channel
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
on :private, /leave (\S+)/i do
|
171
|
+
if match[0] == $config['home_channel']
|
172
|
+
msg nick, "I can't leave that channel, that's my home!"
|
173
|
+
elsif match[0] =~ /^#/
|
174
|
+
msg match[0], "#{nick} asked me to leave, bye!"
|
175
|
+
msg nick, "Alright, leaving #{match[0]}"
|
176
|
+
part match[0]
|
177
|
+
else
|
178
|
+
msg channel, "That doesn't seem like a channel name"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
on :channel, /^#{$config['irc_nick']}: join (\S+)/ do
|
183
|
+
if match[0] =~ /^#/
|
184
|
+
msg channel, "Alright, heading over to #{match[0]}"
|
185
|
+
join match[0]
|
186
|
+
else
|
187
|
+
msg channel, "That doesn't seem like a channel name"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
on :private, /^join (\S+)/ do
|
192
|
+
if match[0] =~ /^#/
|
193
|
+
msg nick, "Alright, heading over to #{match[0]}"
|
194
|
+
join match[0]
|
195
|
+
else
|
196
|
+
msg nick, "That doesn't seem like a channel name"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
on :channel, /^#{$config['irc_nick']}:/ do
|
201
|
+
msg channel, "Hey, #{nick}, I'm a bot. You can ask me for 'help'"
|
202
|
+
end
|
203
|
+
|
204
|
+
# This will repeat everything said in the channel so you can see what
|
205
|
+
# squab-bot is actually matching on. Be careful with this, it will ruin
|
206
|
+
# real channels, so use a test channel
|
207
|
+
on :channel, /^(.*)$/ do
|
208
|
+
if $config['debug']
|
209
|
+
msg channel, "DEBUG: " + match[0]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
on :private do
|
214
|
+
msg nick, "Hi! I'm a bot! I live in #{$config['home_channel']}"
|
215
|
+
msg nick, "You can go there and ask me for 'help'"
|
216
|
+
msg nick, "I can also join another channel if you say 'join <channel>'"
|
217
|
+
msg nick, "I can also leave a channel if you say 'leave <channel>'"
|
218
|
+
end
|
219
|
+
|
220
|
+
on :extern_message do
|
221
|
+
msg channel, @message
|
222
|
+
end
|
223
|
+
|
224
|
+
$bot.start
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
# This is a parallel thread that calls to squab and gets recent messages
|
229
|
+
# to play back into channels. The home channel recieves everything and
|
230
|
+
# subscribed channels may apply filters for what sources are output there
|
231
|
+
def secondloop()
|
232
|
+
puts "Starting Squab Reader Loop"
|
233
|
+
|
234
|
+
sub_channels = $config['sub_channels']
|
235
|
+
message_file = $config['message_file']
|
236
|
+
api_url = $config['api_url']
|
237
|
+
strict_ssl = $config.fetch('strict_ssl', true)
|
238
|
+
squab_client = Squab::Client.new(:api => api_url)
|
239
|
+
latest_msg = 0
|
240
|
+
messages = []
|
241
|
+
if File.exists?(message_file)
|
242
|
+
File.open(message_file, 'r').each_line do |line|
|
243
|
+
latest_msg = line.strip.to_i
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Wait for the other thread to join irc
|
248
|
+
sleep 30
|
249
|
+
loop do
|
250
|
+
puts "Polling Squab..."
|
251
|
+
begin
|
252
|
+
if latest_msg == 0
|
253
|
+
# Replay the default number of messages if we don't have a latest
|
254
|
+
messages = squab_client.get
|
255
|
+
else
|
256
|
+
messages = squab_client.get_from_event(latest_msg)
|
257
|
+
end
|
258
|
+
messages = JSON.parse(messages)
|
259
|
+
rescue OpenSSL::SSL::SSLError => e
|
260
|
+
puts "SSL Error: #{e}"
|
261
|
+
if not strict_ssl
|
262
|
+
# Disable ssl if we're not being strict about it
|
263
|
+
puts "Disabling strict ssl verification"
|
264
|
+
squab_client.ssl_verify = false
|
265
|
+
else
|
266
|
+
sleep 10
|
267
|
+
end
|
268
|
+
retry
|
269
|
+
rescue => e
|
270
|
+
puts "Couldn't get messages from squab: #{e}"
|
271
|
+
sleep 10
|
272
|
+
retry
|
273
|
+
end
|
274
|
+
|
275
|
+
messages.reverse.each do |message|
|
276
|
+
if message.has_key?('id')
|
277
|
+
id = message['id']
|
278
|
+
if id.to_i >= latest_msg
|
279
|
+
latest_msg = id.to_i + 1
|
280
|
+
end
|
281
|
+
value = message['value']
|
282
|
+
source = message['source']
|
283
|
+
uid = message['uid']
|
284
|
+
url = message['url']
|
285
|
+
|
286
|
+
# Limit to Tweet sized
|
287
|
+
# IRC message size is usually 510 bytes, but you don't want
|
288
|
+
# giant messages in your channels. Add a url if you need more
|
289
|
+
# details
|
290
|
+
if value.length > 140
|
291
|
+
value = value[0,140] + ' [message too long]'
|
292
|
+
end
|
293
|
+
|
294
|
+
irc_m = nil
|
295
|
+
|
296
|
+
# Only print source if it's the same case-insensitive string as uid
|
297
|
+
id_tag = if uid =~ /^#{Regexp.escape(source)}$/i
|
298
|
+
"[#{source}]"
|
299
|
+
else
|
300
|
+
"[#{uid}][#{source}]"
|
301
|
+
end
|
302
|
+
|
303
|
+
irc_m = if url.nil?
|
304
|
+
"#{id_tag} #{value}"
|
305
|
+
else
|
306
|
+
"#{id_tag} #{value} (#{url})"
|
307
|
+
end
|
308
|
+
|
309
|
+
begin
|
310
|
+
sub_channels.each do |ch, filter|
|
311
|
+
if source =~ filter
|
312
|
+
m = Isaac::Message.new(":squab PRIVMSG " + ch + " :" + irc_m)
|
313
|
+
$bot.dispatch(:extern_message, m)
|
314
|
+
puts "Posting to #{ch}: #{message}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
# Don't flood the channel
|
318
|
+
sleep 1
|
319
|
+
rescue
|
320
|
+
puts "Can't send to bot"
|
321
|
+
sleep 10
|
322
|
+
retry
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
File.open('/tmp/squab.msg', 'w') {|fh| fh.write(latest_msg)}
|
327
|
+
sleep 10
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
|
332
|
+
t1 = Thread.new{mainloop()}
|
333
|
+
t2 = Thread.new{secondloop()}
|
334
|
+
|
335
|
+
t1.join
|
336
|
+
t2.join
|
337
|
+
|
338
|
+
puts "ok its done"
|
data/bin/squab-bot.yaml
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
---
|
2
|
+
# Subchannels will be joined, with bot notifications on. The filters will
|
3
|
+
# be applied as regex matches to the sources
|
4
|
+
sub_channels:
|
5
|
+
"#subchannel":
|
6
|
+
- "this|that"
|
7
|
+
"#subchannel2":
|
8
|
+
- "onlythis"
|
9
|
+
"#subchannel2":
|
10
|
+
- "com(pli|cat)ed.*regex"
|
11
|
+
# The squab bot will join these channels, but not speak
|
12
|
+
# This allows channels to post, but not have bot chatter in them
|
13
|
+
join_channels:
|
14
|
+
- "#posthere"
|
15
|
+
- "#orhere"
|
16
|
+
# The home channel is where the bot always lives. The bot will not leave
|
17
|
+
# this channel and always outputs all messages to here regardless of source
|
18
|
+
home_channel: "#squab"
|
19
|
+
# Where to save the most recently seen messages. If this file is missing
|
20
|
+
# the bot replays the last 5 messages (if the filter applies) and records
|
21
|
+
# the latest ID
|
22
|
+
message_file: /tmp/squab.msg
|
23
|
+
# This should be the address of your squab web interface
|
24
|
+
api_url: http://squab.example.com:8082/
|
25
|
+
# If your IRC server has a password, put it here
|
26
|
+
irc_password: secret_password
|
27
|
+
# What the bot should call itself. It makes sense to call it squab, but
|
28
|
+
# if you hate that, don't!
|
29
|
+
irc_nick: squab
|
30
|
+
# If your IRC server is protected by SSL, use this
|
31
|
+
irc_ssl: "true"
|
32
|
+
# The port to your irc server
|
33
|
+
irc_port: "6697"
|
34
|
+
# The name of your IRC server
|
35
|
+
irc_server: irc.example.com
|
36
|
+
# If you should output all the IRC stuff that the bot sees to the console
|
37
|
+
irc_verbose: "true"
|
38
|
+
# If you want debug on... the bot will output everything it sees in a channel
|
39
|
+
# you probably only want this for development purposes
|
40
|
+
debug: "false"
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: squab-bot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 3
|
9
|
+
- 2
|
10
|
+
version: 1.3.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Grier Johnson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-12-02 00:00:00 -05:00
|
19
|
+
default_executable: squab-bot
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: isaac
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: squab-client
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rangeclient
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
description: An IRC bot that allows reading and posting to Squab
|
64
|
+
email:
|
65
|
+
- github@squareup.com
|
66
|
+
executables:
|
67
|
+
- squab-bot
|
68
|
+
extensions: []
|
69
|
+
|
70
|
+
extra_rdoc_files:
|
71
|
+
- LICENSE.md
|
72
|
+
files:
|
73
|
+
- bin/squab-bot
|
74
|
+
- bin/squab-bot.yaml
|
75
|
+
- README.md
|
76
|
+
- LICENSE.md
|
77
|
+
has_rdoc: true
|
78
|
+
homepage: http://github.com/square/squab
|
79
|
+
licenses:
|
80
|
+
- Apache 2.0
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options:
|
83
|
+
- --charset=UTF-8
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 3
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
hash: 23
|
101
|
+
segments:
|
102
|
+
- 1
|
103
|
+
- 3
|
104
|
+
- 6
|
105
|
+
version: 1.3.6
|
106
|
+
requirements: []
|
107
|
+
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.6.2
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: An IRC interface to Squab
|
113
|
+
test_files: []
|
114
|
+
|