net-irc 0.0.5 → 0.0.6
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/ChangeLog +7 -0
- data/Rakefile +1 -1
- data/examples/echo_bot.rb +31 -0
- data/examples/gmail.rb +201 -0
- data/examples/hatena-star-stream.rb +1 -1
- data/examples/hig.rb +733 -0
- data/examples/iig.rb +842 -0
- data/examples/lig.rb +2 -2
- data/examples/lingr.rb +1 -0
- data/examples/mixi.rb +23 -8
- data/examples/nig.rb +1 -1
- data/examples/sig.rb +5 -4
- data/examples/tig.rb +324 -124
- data/examples/wig.rb +107 -50
- data/lib/net/irc.rb +1 -1
- data/lib/net/irc/constants.rb +1 -1
- data/lib/net/irc/message.rb +1 -1
- data/lib/net/irc/pattern.rb +2 -2
- data/spec/channel_manager_spec.rb +5 -0
- data/spec/net-irc_spec.rb +15 -29
- metadata +6 -2
data/examples/iig.rb
ADDED
@@ -0,0 +1,842 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
=begin
|
3
|
+
|
4
|
+
# iig.rb
|
5
|
+
|
6
|
+
Identi.ca/Laconi.ca IRC gateway
|
7
|
+
|
8
|
+
## Launch
|
9
|
+
|
10
|
+
$ ruby iig.rb
|
11
|
+
|
12
|
+
If you want to help:
|
13
|
+
|
14
|
+
$ ruby iig.rb --help
|
15
|
+
|
16
|
+
## Configuration
|
17
|
+
|
18
|
+
Options specified by after IRC realname.
|
19
|
+
|
20
|
+
Configuration example for Tiarra <http://coderepos.org/share/wiki/Tiarra>.
|
21
|
+
|
22
|
+
identica {
|
23
|
+
host: localhost
|
24
|
+
port: 16672
|
25
|
+
name: username@example.com athack tid ratio=32:1 replies=6
|
26
|
+
password: password on Identi.ca
|
27
|
+
in-encoding: utf8
|
28
|
+
out-encoding: utf8
|
29
|
+
}
|
30
|
+
|
31
|
+
### athack
|
32
|
+
|
33
|
+
If `athack` client option specified,
|
34
|
+
all nick in join message is leading with @.
|
35
|
+
|
36
|
+
So if you complemente nicks (e.g. Irssi),
|
37
|
+
it's good for Identi.ca like reply command (@nick).
|
38
|
+
|
39
|
+
In this case, you will see torrent of join messages after connected,
|
40
|
+
because NAMES list can't send @ leading nick (it interpreted op.)
|
41
|
+
|
42
|
+
### tid[=<color>]
|
43
|
+
|
44
|
+
Apply ID to each message for make favorites by CTCP ACTION.
|
45
|
+
|
46
|
+
/me fav ID [ID...]
|
47
|
+
|
48
|
+
<color> can be
|
49
|
+
|
50
|
+
0 => white
|
51
|
+
1 => black
|
52
|
+
2 => blue navy
|
53
|
+
3 => green
|
54
|
+
4 => red
|
55
|
+
5 => brown maroon
|
56
|
+
6 => purple
|
57
|
+
7 => orange olive
|
58
|
+
8 => yellow
|
59
|
+
9 => lightgreen lime
|
60
|
+
10 => teal
|
61
|
+
11 => lightcyan cyan aqua
|
62
|
+
12 => lightblue royal
|
63
|
+
13 => pink lightpurple fuchsia
|
64
|
+
14 => grey
|
65
|
+
15 => lightgrey silver
|
66
|
+
|
67
|
+
|
68
|
+
### jabber=<jid>:<pass>
|
69
|
+
|
70
|
+
If `jabber=<jid>:<pass>` option specified,
|
71
|
+
use jabber to get friends timeline.
|
72
|
+
|
73
|
+
You must setup im notifing settings in the site and
|
74
|
+
install "xmpp4r-simple" gem.
|
75
|
+
|
76
|
+
$ sudo gem install xmpp4r-simple
|
77
|
+
|
78
|
+
Be careful for managing password.
|
79
|
+
|
80
|
+
### alwaysim
|
81
|
+
|
82
|
+
Use IM instead of any APIs (e.g. post)
|
83
|
+
|
84
|
+
### ratio=<timeline>:<friends>
|
85
|
+
|
86
|
+
### replies[=<ratio>]
|
87
|
+
|
88
|
+
### maxlimit=<hourly limit>
|
89
|
+
|
90
|
+
### checkrls=<interval seconds>
|
91
|
+
|
92
|
+
## License
|
93
|
+
|
94
|
+
Ruby's by cho45
|
95
|
+
|
96
|
+
=end
|
97
|
+
|
98
|
+
$LOAD_PATH << "lib"
|
99
|
+
$LOAD_PATH << "../lib"
|
100
|
+
|
101
|
+
$KCODE = "u" # json use this
|
102
|
+
|
103
|
+
require "rubygems"
|
104
|
+
require "net/irc"
|
105
|
+
require "net/http"
|
106
|
+
require "uri"
|
107
|
+
require "json"
|
108
|
+
require "socket"
|
109
|
+
require "time"
|
110
|
+
require "logger"
|
111
|
+
require "yaml"
|
112
|
+
require "pathname"
|
113
|
+
require "cgi"
|
114
|
+
|
115
|
+
Net::HTTP.version_1_2
|
116
|
+
|
117
|
+
class IdenticaIrcGateway < Net::IRC::Server::Session
|
118
|
+
def server_name
|
119
|
+
"identicagw"
|
120
|
+
end
|
121
|
+
|
122
|
+
def server_version
|
123
|
+
"0.0.0"
|
124
|
+
end
|
125
|
+
|
126
|
+
def main_channel
|
127
|
+
"#Identi.ca"
|
128
|
+
end
|
129
|
+
|
130
|
+
def api_base
|
131
|
+
URI("http://identi.ca/api/")
|
132
|
+
end
|
133
|
+
|
134
|
+
def api_source
|
135
|
+
"iig.rb"
|
136
|
+
end
|
137
|
+
|
138
|
+
def jabber_bot_id
|
139
|
+
"update@identi.ca"
|
140
|
+
end
|
141
|
+
|
142
|
+
def hourly_limit
|
143
|
+
60
|
144
|
+
end
|
145
|
+
|
146
|
+
class ApiFailed < StandardError; end
|
147
|
+
|
148
|
+
def initialize(*args)
|
149
|
+
super
|
150
|
+
@groups = {}
|
151
|
+
@channels = [] # joined channels (groups)
|
152
|
+
@user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)})"
|
153
|
+
@config = Pathname.new(ENV["HOME"]) + ".iig"
|
154
|
+
@map = nil
|
155
|
+
load_config
|
156
|
+
end
|
157
|
+
|
158
|
+
def on_user(m)
|
159
|
+
super
|
160
|
+
post @prefix, JOIN, main_channel
|
161
|
+
post server_name, MODE, main_channel, "+o", @prefix.nick
|
162
|
+
|
163
|
+
@real, *@opts = @opts.name || @real.split(/\s+/)
|
164
|
+
@opts = @opts.inject({}) {|r,i|
|
165
|
+
key, value = i.split("=")
|
166
|
+
r.update(key => value)
|
167
|
+
}
|
168
|
+
@tmap = TypableMap.new
|
169
|
+
|
170
|
+
if @opts["jabber"]
|
171
|
+
jid, pass = @opts["jabber"].split(":", 2)
|
172
|
+
@opts["jabber"].replace("jabber=#{jid}:********")
|
173
|
+
if jabber_bot_id
|
174
|
+
begin
|
175
|
+
require "xmpp4r-simple"
|
176
|
+
start_jabber(jid, pass)
|
177
|
+
rescue LoadError
|
178
|
+
log "Failed to start Jabber."
|
179
|
+
log 'Installl "xmpp4r-simple" gem or check your ID/pass.'
|
180
|
+
finish
|
181
|
+
end
|
182
|
+
else
|
183
|
+
@opts.delete("jabber")
|
184
|
+
log "This gateway does not support Jabber bot."
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
log "Client Options: #{@opts.inspect}"
|
189
|
+
@log.info "Client Options: #{@opts.inspect}"
|
190
|
+
|
191
|
+
@hourly_limit = hourly_limit
|
192
|
+
@ratio = Struct.new(:timeline, :friends, :replies).new(*(@opts["ratio"] || "10:3").split(":").map {|ratio| ratio.to_f })
|
193
|
+
@ratio[:replies] = @opts.key?("replies") ? (@opts["replies"] || 5).to_f : 0.0
|
194
|
+
|
195
|
+
footing = @ratio.inject {|sum, ratio| sum + ratio }
|
196
|
+
|
197
|
+
@ratio.each_pair {|m, v| @ratio[m] = v / footing }
|
198
|
+
|
199
|
+
@timeline = []
|
200
|
+
@check_friends_thread = Thread.start do
|
201
|
+
loop do
|
202
|
+
begin
|
203
|
+
check_friends
|
204
|
+
rescue ApiFailed => e
|
205
|
+
@log.error e.inspect
|
206
|
+
rescue Exception => e
|
207
|
+
@log.error e.inspect
|
208
|
+
e.backtrace.each do |l|
|
209
|
+
@log.error "\t#{l}"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
sleep freq(@ratio[:friends])
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
return if @opts["jabber"]
|
217
|
+
|
218
|
+
sleep 3
|
219
|
+
@check_timeline_thread = Thread.start do
|
220
|
+
loop do
|
221
|
+
begin
|
222
|
+
check_timeline
|
223
|
+
# check_direct_messages
|
224
|
+
rescue ApiFailed => e
|
225
|
+
@log.error e.inspect
|
226
|
+
rescue Exception => e
|
227
|
+
@log.error e.inspect
|
228
|
+
e.backtrace.each do |l|
|
229
|
+
@log.error "\t#{l}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
sleep freq(@ratio[:timeline])
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
return unless @opts.key?("replies")
|
237
|
+
|
238
|
+
sleep 10
|
239
|
+
@check_replies_thread = Thread.start do
|
240
|
+
loop do
|
241
|
+
begin
|
242
|
+
check_replies
|
243
|
+
rescue ApiFailed => e
|
244
|
+
@log.error e.inspect
|
245
|
+
rescue Exception => e
|
246
|
+
@log.error e.inspect
|
247
|
+
e.backtrace.each do |l|
|
248
|
+
@log.error "\t#{l}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
sleep freq(@ratio[:replies])
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def on_disconnected
|
257
|
+
@check_friends_thread.kill rescue nil
|
258
|
+
@check_replies_thread.kill rescue nil
|
259
|
+
@check_timeline_thread.kill rescue nil
|
260
|
+
@im_thread.kill rescue nil
|
261
|
+
@im.disconnect rescue nil
|
262
|
+
end
|
263
|
+
|
264
|
+
def on_privmsg(m)
|
265
|
+
return on_ctcp(m[0], ctcp_decoding(m[1])) if m.ctcp?
|
266
|
+
retry_count = 3
|
267
|
+
ret = nil
|
268
|
+
target, message = *m.params
|
269
|
+
begin
|
270
|
+
if target =~ /^#/
|
271
|
+
if @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post
|
272
|
+
ret = @im.deliver(jabber_bot_id, message)
|
273
|
+
post "#{nick}!#{nick}@#{api_base.host}", TOPIC, main_channel, untinyurl(message)
|
274
|
+
else
|
275
|
+
ret = api("statuses/update", {"status" => message})
|
276
|
+
end
|
277
|
+
else
|
278
|
+
# direct message
|
279
|
+
ret = api("direct_messages/new", {"user" => target, "text" => message})
|
280
|
+
end
|
281
|
+
raise ApiFailed, "API failed" unless ret
|
282
|
+
log "Status Updated"
|
283
|
+
rescue => e
|
284
|
+
@log.error [retry_count, e.inspect].inspect
|
285
|
+
if retry_count > 0
|
286
|
+
retry_count -= 1
|
287
|
+
@log.debug "Retry to setting status..."
|
288
|
+
retry
|
289
|
+
else
|
290
|
+
log "Some Error Happened on Sending #{message}. #{e}"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def on_ctcp(target, message)
|
296
|
+
_, command, *args = message.split(/\s+/)
|
297
|
+
case command
|
298
|
+
when "list", "ls"
|
299
|
+
nick = args[0]
|
300
|
+
unless (1..200).include?(count = args[1].to_i)
|
301
|
+
count = 20
|
302
|
+
end
|
303
|
+
@log.debug([ nick, message ])
|
304
|
+
res = api("statuses/user_timeline", {"id" => nick, "count" => "#{count}"}).reverse_each do |s|
|
305
|
+
@log.debug(s)
|
306
|
+
post nick, NOTICE, main_channel, "#{generate_status_message(s)}"
|
307
|
+
end
|
308
|
+
unless res
|
309
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
310
|
+
end
|
311
|
+
when /^(un)?fav(?:ou?rite)?$/
|
312
|
+
method, pfx = $1.nil? ? ["create", "F"] : ["destroy", "Unf"]
|
313
|
+
args.each_with_index do |tid, i|
|
314
|
+
st = @tmap[tid]
|
315
|
+
if st
|
316
|
+
sleep 1 if i > 0
|
317
|
+
res = api("favorites/#{method}/#{st["id"]}")
|
318
|
+
post server_name, NOTICE, main_channel, "#{pfx}av: #{res["user"]["screen_name"]}: #{res["text"]}"
|
319
|
+
else
|
320
|
+
post server_name, NOTICE, main_channel, "No such ID #{tid}"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
when "link", "ln"
|
324
|
+
args.each do |tid|
|
325
|
+
st = @tmap[tid]
|
326
|
+
if st
|
327
|
+
st["link"] = "http://#{api_base.host}/notice/#{st["id"]}" unless st["link"]
|
328
|
+
post server_name, NOTICE, main_channel, st["link"]
|
329
|
+
else
|
330
|
+
post server_name, NOTICE, main_channel, "No such ID #{tid}"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
# when /^ratios?$/
|
334
|
+
# if args[1].nil? ||
|
335
|
+
# @opts.key?("replies") && args[2].nil?
|
336
|
+
# return post server_name, NOTICE, main_channel, "/me ratios <timeline> <frends>[ <replies>]"
|
337
|
+
# end
|
338
|
+
# ratios = args.map {|ratio| ratio.to_f }
|
339
|
+
# if ratios.any? {|ratio| ratio <= 0.0 }
|
340
|
+
# return post server_name, NOTICE, main_channel, "Ratios must be greater than 0."
|
341
|
+
# end
|
342
|
+
# footing = ratios.inject {|sum, ratio| sum + ratio }
|
343
|
+
# @ratio[:timeline] = ratios[0]
|
344
|
+
# @ratio[:friends] = ratios[1]
|
345
|
+
# @ratio[:replies] = ratios[2] || 0.0
|
346
|
+
# @ratio.each_pair {|m, v| @ratio[m] = v / footing }
|
347
|
+
# intervals = @ratio.map {|ratio| freq ratio }
|
348
|
+
# post server_name, NOTICE, main_channel, "Intervals: #{intervals.join(", ")}"
|
349
|
+
when /^(?:de(?:stroy|l(?:ete)?)|remove|miss)$/
|
350
|
+
args.each_with_index do |tid, i|
|
351
|
+
st = @tmap[tid]
|
352
|
+
if st
|
353
|
+
sleep 1 if i > 0
|
354
|
+
res = api("statuses/destroy/#{st["id"]}")
|
355
|
+
post server_name, NOTICE, main_channel, "Destroyed: #{res["text"]}"
|
356
|
+
else
|
357
|
+
post server_name, NOTICE, main_channel, "No such ID #{tid}"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
when "in", "location"
|
361
|
+
location = args.join(" ")
|
362
|
+
api("account/update_location", {"location" => location})
|
363
|
+
location = location.empty? ? "nowhere" : "in #{location}"
|
364
|
+
post server_name, NOTICE, main_channel, "You are #{location} now."
|
365
|
+
end
|
366
|
+
rescue ApiFailed => e
|
367
|
+
log e.inspect
|
368
|
+
end
|
369
|
+
|
370
|
+
def on_whois(m)
|
371
|
+
nick = m.params[0]
|
372
|
+
f = (@friends || []).find {|i| i["screen_name"] == nick }
|
373
|
+
if f
|
374
|
+
post server_name, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}"
|
375
|
+
post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s
|
376
|
+
post server_name, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle"
|
377
|
+
post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list"
|
378
|
+
else
|
379
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def on_who(m)
|
384
|
+
channel = m.params[0]
|
385
|
+
case
|
386
|
+
when channel == main_channel
|
387
|
+
# "<channel> <user> <host> <server> <nick>
|
388
|
+
# ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
|
389
|
+
# :<hopcount> <real name>"
|
390
|
+
@friends.each do |f|
|
391
|
+
user = nick = f["screen_name"]
|
392
|
+
host = serv = api_base.host
|
393
|
+
real = f["name"]
|
394
|
+
post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
|
395
|
+
end
|
396
|
+
post server_name, RPL_ENDOFWHO, @nick, channel
|
397
|
+
when @groups.key?(channel)
|
398
|
+
@groups[channel].each do |name|
|
399
|
+
f = @friends.find {|i| i["screen_name"] == name }
|
400
|
+
user = nick = f["screen_name"]
|
401
|
+
host = serv = api_base.host
|
402
|
+
real = f["name"]
|
403
|
+
post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
|
404
|
+
end
|
405
|
+
post server_name, RPL_ENDOFWHO, @nick, channel
|
406
|
+
else
|
407
|
+
post server_name, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def on_join(m)
|
412
|
+
channels = m.params[0].split(/\s*,\s*/)
|
413
|
+
channels.each do |channel|
|
414
|
+
next if channel == main_channel
|
415
|
+
|
416
|
+
@channels << channel
|
417
|
+
@channels.uniq!
|
418
|
+
post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
|
419
|
+
post server_name, MODE, channel, "+o", @nick
|
420
|
+
save_config
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def on_part(m)
|
425
|
+
channel = m.params[0]
|
426
|
+
return if channel == main_channel
|
427
|
+
|
428
|
+
@channels.delete(channel)
|
429
|
+
post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet."
|
430
|
+
end
|
431
|
+
|
432
|
+
def on_invite(m)
|
433
|
+
nick, channel = *m.params
|
434
|
+
return if channel == main_channel
|
435
|
+
|
436
|
+
if (@friends || []).find {|i| i["screen_name"] == nick }
|
437
|
+
((@groups[channel] ||= []) << nick).uniq!
|
438
|
+
post "#{nick}!#{nick}@#{api_base.host}", JOIN, channel
|
439
|
+
post server_name, MODE, channel, "+o", nick
|
440
|
+
save_config
|
441
|
+
else
|
442
|
+
post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def on_kick(m)
|
447
|
+
channel, nick, mes = *m.params
|
448
|
+
return if channel == main_channel
|
449
|
+
|
450
|
+
if (@friends || []).find {|i| i["screen_name"] == nick }
|
451
|
+
(@groups[channel] ||= []).delete(nick)
|
452
|
+
post nick, PART, channel
|
453
|
+
save_config
|
454
|
+
else
|
455
|
+
post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
private
|
460
|
+
def check_timeline
|
461
|
+
api("statuses/friends_timeline", {:count => "117"}).reverse_each do |s|
|
462
|
+
id = s["id"]
|
463
|
+
next if id.nil? || @timeline.include?(id)
|
464
|
+
|
465
|
+
@timeline << id
|
466
|
+
nick = s["user"]["screen_name"]
|
467
|
+
mesg = generate_status_message(s)
|
468
|
+
tid = @tmap.push(s)
|
469
|
+
|
470
|
+
@log.debug [id, nick, mesg]
|
471
|
+
if nick == @nick # 自分のときは TOPIC に
|
472
|
+
post "#{nick}!#{nick}@#{api_base.host}", TOPIC, main_channel, untinyurl(mesg)
|
473
|
+
else
|
474
|
+
if @opts.key?("tid")
|
475
|
+
mesg = "%s \x03%s [%s]" % [mesg, @opts["tid"] || 10, tid]
|
476
|
+
end
|
477
|
+
message(nick, main_channel, mesg)
|
478
|
+
end
|
479
|
+
@groups.each do |channel, members|
|
480
|
+
next unless members.include?(nick)
|
481
|
+
if @opts.key?("tid")
|
482
|
+
mesg = "%s \x03%s [%s]" % [mesg, @opts["tid"] || 10, tid]
|
483
|
+
end
|
484
|
+
message(nick, channel, mesg)
|
485
|
+
end
|
486
|
+
end
|
487
|
+
@log.debug "@timeline.size = #{@timeline.size}"
|
488
|
+
@timeline = @timeline.last(117)
|
489
|
+
end
|
490
|
+
|
491
|
+
def generate_status_message(status)
|
492
|
+
s = status
|
493
|
+
mesg = s["text"]
|
494
|
+
@log.debug(mesg)
|
495
|
+
|
496
|
+
# time = Time.parse(s["created_at"]) rescue Time.now
|
497
|
+
m = {""" => "\"", "<" => "<", ">" => ">", "&" => "&", "\n" => " "}
|
498
|
+
mesg.gsub!(/#{m.keys.join("|")}/) { m[$&] }
|
499
|
+
mesg
|
500
|
+
end
|
501
|
+
|
502
|
+
def check_replies
|
503
|
+
time = @prev_time_r || Time.now
|
504
|
+
@prev_time_r = Time.now
|
505
|
+
api("statuses/replies").reverse_each do |s|
|
506
|
+
id = s["id"]
|
507
|
+
next if id.nil? || @timeline.include?(id)
|
508
|
+
|
509
|
+
created_at = Time.parse(s["created_at"]) rescue next
|
510
|
+
next if created_at < time
|
511
|
+
|
512
|
+
nick = s["user"]["screen_name"]
|
513
|
+
mesg = generate_status_message(s)
|
514
|
+
tid = @tmap.push(s)
|
515
|
+
|
516
|
+
@log.debug [id, nick, mesg]
|
517
|
+
if @opts.key?("tid")
|
518
|
+
mesg = "%s \x03%s [%s]" % [mesg, @opts["tid"] || 10, tid]
|
519
|
+
end
|
520
|
+
message nick, main_channel, mesg
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def check_direct_messages
|
525
|
+
time = @prev_time_d || Time.now
|
526
|
+
@prev_time_d = Time.now
|
527
|
+
api("direct_messages", {"since" => time.httpdate}).reverse_each do |s|
|
528
|
+
nick = s["sender_screen_name"]
|
529
|
+
mesg = s["text"]
|
530
|
+
time = Time.parse(s["created_at"])
|
531
|
+
@log.debug [nick, mesg, time].inspect
|
532
|
+
message(nick, @nick, mesg)
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
def check_friends
|
537
|
+
first = true unless @friends
|
538
|
+
@friends ||= []
|
539
|
+
friends = api("statuses/friends")
|
540
|
+
if first && !@opts.key?("athack")
|
541
|
+
@friends = friends
|
542
|
+
post server_name, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ")
|
543
|
+
post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
|
544
|
+
else
|
545
|
+
prv_friends = @friends.map {|i| i["screen_name"] }
|
546
|
+
now_friends = friends.map {|i| i["screen_name"] }
|
547
|
+
|
548
|
+
# Twitter API bug?
|
549
|
+
return if !first && (now_friends.length - prv_friends.length).abs > 10
|
550
|
+
|
551
|
+
(now_friends - prv_friends).each do |join|
|
552
|
+
join = "@#{join}" if @opts.key?("athack")
|
553
|
+
post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
|
554
|
+
end
|
555
|
+
(prv_friends - now_friends).each do |part|
|
556
|
+
part = "@#{part}" if @opts.key?("athack")
|
557
|
+
post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
|
558
|
+
end
|
559
|
+
@friends = friends
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
def check_downtime
|
564
|
+
@prev_downtime ||= nil
|
565
|
+
schedule = api("help/downtime_schedule", {}, {:avoid_error => true})["error"]
|
566
|
+
if @prev_downtime != schedule && @prev_downtime = schedule
|
567
|
+
msg = schedule.gsub(%r{[\r\n]|<style(?:\s[^>]*)?>.*?</style\s*>}m, "")
|
568
|
+
uris = URI.extract(msg)
|
569
|
+
uris.each do |uri|
|
570
|
+
msg << " #{uri}"
|
571
|
+
end
|
572
|
+
msg.gsub!(/<[^>]+>/, "")
|
573
|
+
log "\002\037#{msg}\017"
|
574
|
+
# TODO: sleeping for the downtime
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
def freq(ratio)
|
579
|
+
max = (@opts["maxlimit"] || 100).to_i
|
580
|
+
limit = @hourly_limit < max ? @hourly_limit : max
|
581
|
+
f = 3600 / (limit * ratio).round
|
582
|
+
@log.debug "Frequency: #{f}"
|
583
|
+
f
|
584
|
+
end
|
585
|
+
|
586
|
+
def start_jabber(jid, pass)
|
587
|
+
@log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}"
|
588
|
+
@im = Jabber::Simple.new(jid, pass)
|
589
|
+
@im.add(jabber_bot_id)
|
590
|
+
@im_thread = Thread.start do
|
591
|
+
loop do
|
592
|
+
begin
|
593
|
+
@im.received_messages.each do |msg|
|
594
|
+
@log.debug [msg.from, msg.body]
|
595
|
+
if msg.from.strip == jabber_bot_id
|
596
|
+
# Twitter -> 'id: msg'
|
597
|
+
body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "")
|
598
|
+
if Regexp.last_match
|
599
|
+
nick, id = Regexp.last_match.captures
|
600
|
+
body = CGI.unescapeHTML(body)
|
601
|
+
message(id || nick, main_channel, body)
|
602
|
+
end
|
603
|
+
end
|
604
|
+
end
|
605
|
+
rescue Exception => e
|
606
|
+
@log.error "Error on Jabber loop: #{e.inspect}"
|
607
|
+
e.backtrace.each do |l|
|
608
|
+
@log.error "\t#{l}"
|
609
|
+
end
|
610
|
+
end
|
611
|
+
sleep 1
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
def save_config
|
617
|
+
config = {
|
618
|
+
:channels => @channels,
|
619
|
+
:groups => @groups,
|
620
|
+
}
|
621
|
+
@config.open("w") do |f|
|
622
|
+
YAML.dump(config, f)
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def load_config
|
627
|
+
@config.open do |f|
|
628
|
+
config = YAML.load(f)
|
629
|
+
@channels = config[:channels]
|
630
|
+
@groups = config[:groups]
|
631
|
+
end
|
632
|
+
rescue Errno::ENOENT
|
633
|
+
end
|
634
|
+
|
635
|
+
def require_post?(path)
|
636
|
+
[
|
637
|
+
%r{^statuses/(?:update$|destroy/)},
|
638
|
+
"direct_messages/new",
|
639
|
+
"account/update_location",
|
640
|
+
%r{^favorites/},
|
641
|
+
].any? {|i| i === path }
|
642
|
+
end
|
643
|
+
|
644
|
+
def api(path, q = {}, opt = {})
|
645
|
+
ret = {}
|
646
|
+
headers = {"User-Agent" => @user_agent}
|
647
|
+
headers["If-Modified-Since"] = q["since"] if q.key?("since")
|
648
|
+
|
649
|
+
q["source"] ||= api_source
|
650
|
+
q = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^-.!~*'()\w]/n)}" : r }.join("&")
|
651
|
+
|
652
|
+
path = path.sub(%r{^/+}, "")
|
653
|
+
uri = api_base.dup
|
654
|
+
uri.path += "#{path}.json"
|
655
|
+
if require_post? path
|
656
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
657
|
+
req.body = q
|
658
|
+
else
|
659
|
+
uri.query = q
|
660
|
+
req = Net::HTTP::Get.new(uri.request_uri, headers)
|
661
|
+
end
|
662
|
+
req.basic_auth(@real, @pass)
|
663
|
+
@log.debug uri.inspect
|
664
|
+
|
665
|
+
ret = Net::HTTP.start(uri.host, uri.port) {|http| http.request(req) }
|
666
|
+
case ret
|
667
|
+
when Net::HTTPOK # 200
|
668
|
+
ret = JSON.parse(ret.body.gsub(/'(y(?:es)?|no?|true|false|null)'/, '"\1"'))
|
669
|
+
if ret.kind_of?(Hash) && !opt[:avoid_error] && ret["error"]
|
670
|
+
raise ApiFailed, "Server Returned Error: #{ret["error"]}"
|
671
|
+
end
|
672
|
+
ret
|
673
|
+
when Net::HTTPNotModified # 304
|
674
|
+
[]
|
675
|
+
when Net::HTTPBadRequest # 400
|
676
|
+
# exceeded the rate limitation
|
677
|
+
raise ApiFailed, "#{ret.code}: #{ret.message}"
|
678
|
+
else
|
679
|
+
raise ApiFailed, "Server Returned #{ret.code} #{ret.message}"
|
680
|
+
end
|
681
|
+
rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
|
682
|
+
raise ApiFailed, e.inspect
|
683
|
+
end
|
684
|
+
|
685
|
+
def message(sender, target, str)
|
686
|
+
# str.gsub!(/&#(x)?([0-9a-f]+);/i) do
|
687
|
+
# [$1 ? $2.hex : $2.to_i].pack("U")
|
688
|
+
# end
|
689
|
+
str = untinyurl(str)
|
690
|
+
sender = "#{sender}!#{sender}@#{api_base.host}"
|
691
|
+
post sender, PRIVMSG, target, str
|
692
|
+
end
|
693
|
+
|
694
|
+
def log(str)
|
695
|
+
str.gsub!(/\r?\n|\r/, " ")
|
696
|
+
post server_name, NOTICE, main_channel, str
|
697
|
+
end
|
698
|
+
|
699
|
+
def untinyurl(text)
|
700
|
+
text.gsub(%r|http://(preview\.)?tinyurl\.com/[0-9a-z=]+|i) {|m|
|
701
|
+
uri = URI(m)
|
702
|
+
uri.host = uri.host.sub($1, "") if $1
|
703
|
+
Net::HTTP.start(uri.host, uri.port) {|http|
|
704
|
+
http.open_timeout = 3
|
705
|
+
begin
|
706
|
+
http.head(uri.request_uri, {"User-Agent" => @user_agent})["Location"] || m
|
707
|
+
rescue Timeout::Error
|
708
|
+
m
|
709
|
+
end
|
710
|
+
}
|
711
|
+
}
|
712
|
+
end
|
713
|
+
|
714
|
+
class TypableMap < Hash
|
715
|
+
Roman = %w|k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q|.unshift("").map {|consonant|
|
716
|
+
case
|
717
|
+
when consonant.size > 1, consonant == "y"
|
718
|
+
%w|a u o|
|
719
|
+
when consonant == "q"
|
720
|
+
%w|a i e o|
|
721
|
+
else
|
722
|
+
%w|a i u e o|
|
723
|
+
end.map {|vowel| "#{consonant}#{vowel}" }
|
724
|
+
}.flatten
|
725
|
+
|
726
|
+
def initialize(size = 1)
|
727
|
+
@seq = Roman
|
728
|
+
@map = {}
|
729
|
+
@n = 0
|
730
|
+
@size = size
|
731
|
+
end
|
732
|
+
|
733
|
+
def generate(n)
|
734
|
+
ret = []
|
735
|
+
begin
|
736
|
+
n, r = n.divmod(@seq.size)
|
737
|
+
ret << @seq[r]
|
738
|
+
end while n > 0
|
739
|
+
ret.reverse.join
|
740
|
+
end
|
741
|
+
|
742
|
+
def push(obj)
|
743
|
+
id = generate(@n)
|
744
|
+
self[id] = obj
|
745
|
+
@n += 1
|
746
|
+
@n = @n % (@seq.size ** @size)
|
747
|
+
id
|
748
|
+
end
|
749
|
+
alias << push
|
750
|
+
|
751
|
+
def clear
|
752
|
+
@n = 0
|
753
|
+
super
|
754
|
+
end
|
755
|
+
|
756
|
+
private :[]=
|
757
|
+
undef update, merge, merge!, replace
|
758
|
+
end
|
759
|
+
|
760
|
+
|
761
|
+
end
|
762
|
+
|
763
|
+
if __FILE__ == $0
|
764
|
+
require "optparse"
|
765
|
+
|
766
|
+
opts = {
|
767
|
+
:port => 16672,
|
768
|
+
:host => "localhost",
|
769
|
+
:log => nil,
|
770
|
+
:debug => false,
|
771
|
+
:foreground => false,
|
772
|
+
}
|
773
|
+
|
774
|
+
OptionParser.new do |parser|
|
775
|
+
parser.instance_eval do
|
776
|
+
self.banner = <<-EOB.gsub(/^\t+/, "")
|
777
|
+
Usage: #{$0} [opts]
|
778
|
+
|
779
|
+
EOB
|
780
|
+
|
781
|
+
separator ""
|
782
|
+
|
783
|
+
separator "Options:"
|
784
|
+
on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
|
785
|
+
opts[:port] = port
|
786
|
+
end
|
787
|
+
|
788
|
+
on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
|
789
|
+
opts[:host] = host
|
790
|
+
end
|
791
|
+
|
792
|
+
on("-l", "--log LOG", "log file") do |log|
|
793
|
+
opts[:log] = log
|
794
|
+
end
|
795
|
+
|
796
|
+
on("--debug", "Enable debug mode") do |debug|
|
797
|
+
opts[:log] = $stdout
|
798
|
+
opts[:debug] = true
|
799
|
+
end
|
800
|
+
|
801
|
+
on("-f", "--foreground", "run foreground") do |foreground|
|
802
|
+
opts[:log] = $stdout
|
803
|
+
opts[:foreground] = true
|
804
|
+
end
|
805
|
+
|
806
|
+
on("-n", "--name [user name or email address]") do |name|
|
807
|
+
opts[:name] = name
|
808
|
+
end
|
809
|
+
|
810
|
+
parse!(ARGV)
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
opts[:logger] = Logger.new(opts[:log], "daily")
|
815
|
+
opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
|
816
|
+
|
817
|
+
# def daemonize(foreground = false)
|
818
|
+
# trap("SIGINT") { exit! 0 }
|
819
|
+
# trap("SIGTERM") { exit! 0 }
|
820
|
+
# trap("SIGHUP") { exit! 0 }
|
821
|
+
# return yield if $DEBUG || foreground
|
822
|
+
# Process.fork do
|
823
|
+
# Process.setsid
|
824
|
+
# Dir.chdir "/"
|
825
|
+
# File.open("/dev/null") {|f|
|
826
|
+
# STDIN.reopen f
|
827
|
+
# STDOUT.reopen f
|
828
|
+
# STDERR.reopen f
|
829
|
+
# }
|
830
|
+
# yield
|
831
|
+
# end
|
832
|
+
# exit! 0
|
833
|
+
# end
|
834
|
+
|
835
|
+
# daemonize(opts[:debug] || opts[:foreground]) do
|
836
|
+
Net::IRC::Server.new(opts[:host], opts[:port], IdenticaIrcGateway, opts).start
|
837
|
+
# end
|
838
|
+
end
|
839
|
+
|
840
|
+
# Local Variables:
|
841
|
+
# coding: utf-8
|
842
|
+
# End:
|