net-irc2 0.0.10
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/AUTHORS.txt +33 -0
- data/ChangeLog +95 -0
- data/README +91 -0
- data/Rakefile +69 -0
- data/examples/2ch.rb +225 -0
- data/examples/2ig.rb +267 -0
- data/examples/client.rb +23 -0
- data/examples/echo_bot.rb +31 -0
- data/examples/echo_bot_celluloid.rb +33 -0
- data/examples/gig.rb +192 -0
- data/examples/gmail.rb +202 -0
- data/examples/gtig.rb +420 -0
- data/examples/hatena-star-stream.rb +270 -0
- data/examples/hcig.rb +285 -0
- data/examples/hig.rb +771 -0
- data/examples/iig.rb +819 -0
- data/examples/ircd.rb +358 -0
- data/examples/lig.rb +551 -0
- data/examples/lingr.rb +327 -0
- data/examples/mixi.rb +252 -0
- data/examples/sig.rb +188 -0
- data/examples/tig.rb +2712 -0
- data/lib/net/irc/client/channel_manager.rb +144 -0
- data/lib/net/irc/client.rb +117 -0
- data/lib/net/irc/constants.rb +214 -0
- data/lib/net/irc/message/modeparser.rb +85 -0
- data/lib/net/irc/message/serverconfig.rb +30 -0
- data/lib/net/irc/message.rb +109 -0
- data/lib/net/irc/pattern.rb +68 -0
- data/lib/net/irc/server.rb +186 -0
- data/lib/net/irc.rb +77 -0
- data/spec/channel_manager_spec.rb +184 -0
- data/spec/modeparser_spec.rb +165 -0
- data/spec/net-irc_spec.rb +337 -0
- data/spec/spec.opts +1 -0
- metadata +91 -0
data/examples/tig.rb
ADDED
@@ -0,0 +1,2712 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
$KCODE = "u" unless defined? ::Encoding # json use this
|
4
|
+
=begin
|
5
|
+
|
6
|
+
# tig.rb
|
7
|
+
|
8
|
+
Ruby version of TwitterIrcGateway
|
9
|
+
<http://www.misuzilla.org/dist/net/twitterircgateway/>
|
10
|
+
|
11
|
+
## Launch
|
12
|
+
|
13
|
+
$ ruby tig.rb
|
14
|
+
|
15
|
+
If you want to help:
|
16
|
+
|
17
|
+
$ ruby tig.rb --help
|
18
|
+
|
19
|
+
## Configuration
|
20
|
+
|
21
|
+
Options specified by after IRC realname.
|
22
|
+
|
23
|
+
Configuration example for Tiarra <http://coderepos.org/share/wiki/Tiarra>.
|
24
|
+
|
25
|
+
general {
|
26
|
+
server-in-encoding: utf8
|
27
|
+
server-out-encoding: utf8
|
28
|
+
client-in-encoding: utf8
|
29
|
+
client-out-encoding: utf8
|
30
|
+
}
|
31
|
+
|
32
|
+
networks {
|
33
|
+
name: tig
|
34
|
+
}
|
35
|
+
|
36
|
+
tig {
|
37
|
+
server: localhost 16668
|
38
|
+
password: password on Twitter
|
39
|
+
# Recommended
|
40
|
+
name: username mentions tid
|
41
|
+
}
|
42
|
+
|
43
|
+
### athack
|
44
|
+
|
45
|
+
If `athack` client option specified,
|
46
|
+
all nick in join message is leading with @.
|
47
|
+
|
48
|
+
So if you complemente nicks (e.g. Irssi),
|
49
|
+
it's good for Twitter like reply command (@nick).
|
50
|
+
|
51
|
+
In this case, you will see torrent of join messages after connected,
|
52
|
+
because NAMES list can't send @ leading nick (it interpreted op.)
|
53
|
+
|
54
|
+
### tid[=<color:10>[,<bgcolor>]]
|
55
|
+
|
56
|
+
Apply ID to each message for make favorites by CTCP ACTION.
|
57
|
+
|
58
|
+
/me fav [ID...]
|
59
|
+
|
60
|
+
<color> and <bgcolor> can be
|
61
|
+
|
62
|
+
0 => white
|
63
|
+
1 => black
|
64
|
+
2 => blue navy
|
65
|
+
3 => green
|
66
|
+
4 => red
|
67
|
+
5 => brown maroon
|
68
|
+
6 => purple
|
69
|
+
7 => orange olive
|
70
|
+
8 => yellow
|
71
|
+
9 => lightgreen lime
|
72
|
+
10 => teal
|
73
|
+
11 => lightcyan cyan aqua
|
74
|
+
12 => lightblue royal
|
75
|
+
13 => pink lightpurple fuchsia
|
76
|
+
14 => grey
|
77
|
+
15 => lightgrey silver
|
78
|
+
|
79
|
+
### ratio=<timeline>:<dm>[:<mentions>] (obsolete)
|
80
|
+
|
81
|
+
"121:6:20" by default.
|
82
|
+
|
83
|
+
/me ratios
|
84
|
+
|
85
|
+
Ratio | Timeline | DM | Mentions |
|
86
|
+
---------+----------+-------+----------|
|
87
|
+
1 | 24s | N/A | N/A |
|
88
|
+
141:6 | 26s | 10m OR N/A |
|
89
|
+
135:12 | 27s | 5m OR N/A |
|
90
|
+
135:6:6 | 27s | 10m | 10m |
|
91
|
+
---------+----------+-------+----------|
|
92
|
+
121:6:20 | 30s | 10m | 3m |
|
93
|
+
---------+----------+-------+----------|
|
94
|
+
4:1 | 31s | 2m1s | N/A |
|
95
|
+
50:5:12 | 49s | 8m12s | 3m25s |
|
96
|
+
20:5:6 | 57s | 3m48s | 3m10s |
|
97
|
+
30:5:12 | 58s | 5m45s | 2m24s |
|
98
|
+
1:1:1 | 1m13s | 1m13s | 1m13s |
|
99
|
+
---------------------------------------+
|
100
|
+
(Hourly limit: 150)
|
101
|
+
|
102
|
+
### dm[=<ratio>]
|
103
|
+
|
104
|
+
### mentions[=<ratio>]
|
105
|
+
|
106
|
+
### maxlimit=<hourly_limit>
|
107
|
+
|
108
|
+
### clientspoofing
|
109
|
+
|
110
|
+
### httpproxy=[<user>[:<password>]@]<address>[:<port>]
|
111
|
+
|
112
|
+
### main_channel=<channel:#twitter>
|
113
|
+
|
114
|
+
### api_source=<source>
|
115
|
+
|
116
|
+
### check_friends_interval=<seconds:3600>
|
117
|
+
|
118
|
+
### check_updates_interval=<seconds:86400>
|
119
|
+
|
120
|
+
Set 0 to disable checking.
|
121
|
+
|
122
|
+
### old_style_reply
|
123
|
+
|
124
|
+
### tmap_size=<number:10404>
|
125
|
+
|
126
|
+
### strftime=<format:%m-%d %H:%M>
|
127
|
+
|
128
|
+
### untiny_whole_urls
|
129
|
+
|
130
|
+
### bitlify=<username>:<apikey>:<minlength:20>
|
131
|
+
|
132
|
+
### unuify
|
133
|
+
|
134
|
+
### shuffled_tmap
|
135
|
+
|
136
|
+
### ll=<lat>,<long>
|
137
|
+
|
138
|
+
### with_retweets
|
139
|
+
|
140
|
+
### without_lists
|
141
|
+
|
142
|
+
## Extended commands through the CTCP ACTION
|
143
|
+
|
144
|
+
### list (ls)
|
145
|
+
|
146
|
+
/me list NICK [NUMBER]
|
147
|
+
|
148
|
+
### fav (favorite, favourite, unfav, unfavorite, unfavourite)
|
149
|
+
|
150
|
+
/me fav [ID...]
|
151
|
+
/me unfav [ID...]
|
152
|
+
/me fav! [ID...]
|
153
|
+
/me fav NICK
|
154
|
+
|
155
|
+
### link (ln, url, u)
|
156
|
+
|
157
|
+
/me link ID [ID...]
|
158
|
+
|
159
|
+
### destroy (del, delete, miss, oops, remove, rm)
|
160
|
+
|
161
|
+
/me destroy [ID...]
|
162
|
+
|
163
|
+
### in (location)
|
164
|
+
|
165
|
+
/me in Sugamo, Tokyo, Japan
|
166
|
+
|
167
|
+
### reply (re, mention)
|
168
|
+
|
169
|
+
/me reply ID blah, blah...
|
170
|
+
|
171
|
+
### retweet (rt)
|
172
|
+
|
173
|
+
/me retweet ID (blah, blah...)
|
174
|
+
|
175
|
+
### utf7 (utf-7)
|
176
|
+
|
177
|
+
/me utf7
|
178
|
+
|
179
|
+
### name
|
180
|
+
|
181
|
+
/me name My Name
|
182
|
+
|
183
|
+
### description (desc)
|
184
|
+
|
185
|
+
/me description blah, blah...
|
186
|
+
|
187
|
+
### spoof
|
188
|
+
|
189
|
+
/me spoof
|
190
|
+
/me spoo[o...]f
|
191
|
+
/me spoof tigrb twitterircgateway twitt web mobileweb
|
192
|
+
|
193
|
+
### bot (drone)
|
194
|
+
|
195
|
+
/me bot NICK [NICK...]
|
196
|
+
|
197
|
+
### spam
|
198
|
+
report user as spammer
|
199
|
+
|
200
|
+
/me spam <NICK>|<ID>
|
201
|
+
|
202
|
+
## Feed
|
203
|
+
|
204
|
+
<http://coderepos.org/share/log/lang/ruby/net-irc/trunk/examples/tig.rb?limit=100&mode=stop_on_copy&format=rss>
|
205
|
+
|
206
|
+
## License
|
207
|
+
|
208
|
+
Ruby's by cho45
|
209
|
+
|
210
|
+
=end
|
211
|
+
|
212
|
+
case
|
213
|
+
when File.directory?("lib")
|
214
|
+
$LOAD_PATH << "lib"
|
215
|
+
when File.directory?(File.expand_path("lib", ".."))
|
216
|
+
$LOAD_PATH << File.expand_path("lib", "..")
|
217
|
+
end
|
218
|
+
|
219
|
+
require "pp"
|
220
|
+
require "rubygems"
|
221
|
+
require "net/irc"
|
222
|
+
require "net/https"
|
223
|
+
require "uri"
|
224
|
+
require "time"
|
225
|
+
require "logger"
|
226
|
+
require "yaml"
|
227
|
+
require "pathname"
|
228
|
+
require "ostruct"
|
229
|
+
require "json"
|
230
|
+
require "oauth"
|
231
|
+
|
232
|
+
begin
|
233
|
+
require "iconv"
|
234
|
+
require "punycode"
|
235
|
+
rescue LoadError
|
236
|
+
end
|
237
|
+
|
238
|
+
module Net::IRC::Constants; RPL_WHOISBOT = "335"; RPL_CREATEONTIME = "329"; end
|
239
|
+
|
240
|
+
class TwitterIrcGateway < Net::IRC::Server::Session
|
241
|
+
CONFIG_DIR = Pathname.new("~/.tig.rb").expand_path
|
242
|
+
|
243
|
+
CONSUMER_KEY = 'ZxRg3rGeqE68Tqkz9nhmA'
|
244
|
+
CONSUMER_SECRET = 'GaJsr2jfjUYIHaPc01UqiqMlvUJPCL5z5uPQM5T418'
|
245
|
+
|
246
|
+
class UnauthorizedException < Exception; end
|
247
|
+
|
248
|
+
@@ctcp_action_commands = []
|
249
|
+
|
250
|
+
class << self
|
251
|
+
def ctcp_action(*commands, &block)
|
252
|
+
name = "+ctcp_action_#{commands.inspect}"
|
253
|
+
define_method(name, block)
|
254
|
+
commands.each do |command|
|
255
|
+
@@ctcp_action_commands << [command, name]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def server_name
|
261
|
+
"twittergw"
|
262
|
+
end
|
263
|
+
|
264
|
+
def server_version
|
265
|
+
@server_version ||= instance_eval {
|
266
|
+
head = `git rev-parse HEAD 2>/dev/null`.chomp
|
267
|
+
head.empty?? "unknown" : head
|
268
|
+
}
|
269
|
+
end
|
270
|
+
|
271
|
+
def available_user_modes
|
272
|
+
"o"
|
273
|
+
end
|
274
|
+
|
275
|
+
def available_channel_modes
|
276
|
+
"mntiovah"
|
277
|
+
end
|
278
|
+
|
279
|
+
def main_channel
|
280
|
+
@opts.main_channel || "#twitter"
|
281
|
+
end
|
282
|
+
|
283
|
+
def api_base(secure = true)
|
284
|
+
URI("http#{"s" if secure}://api.twitter.com/")
|
285
|
+
end
|
286
|
+
|
287
|
+
def api_source
|
288
|
+
"#{@opts.api_source || "tigrb"}"
|
289
|
+
end
|
290
|
+
|
291
|
+
def hourly_limit
|
292
|
+
150
|
293
|
+
end
|
294
|
+
|
295
|
+
class APIFailed < StandardError; end
|
296
|
+
|
297
|
+
MAX_MODE_PARAMS = 3
|
298
|
+
WSP_REGEX = Regexp.new("\\r\\n|[\\r\\n\\t#{"\\u00A0\\u1680\\u180E\\u2002-\\u200D\\u202F\\u205F\\u2060\\uFEFF" if "\u0000" == "\000"}]")
|
299
|
+
|
300
|
+
def initialize(*args)
|
301
|
+
super
|
302
|
+
@channels = {}
|
303
|
+
@nicknames = {}
|
304
|
+
@drones = []
|
305
|
+
@etags = {}
|
306
|
+
@consums = []
|
307
|
+
@follower_ids = []
|
308
|
+
@limit = hourly_limit
|
309
|
+
@friends =
|
310
|
+
@sources =
|
311
|
+
@rsuffix_regex =
|
312
|
+
@im =
|
313
|
+
@im_thread =
|
314
|
+
@utf7 =
|
315
|
+
@httpproxy = nil
|
316
|
+
@ratelimit = RateLimit.new(150)
|
317
|
+
@cert_store = OpenSSL::X509::Store.new
|
318
|
+
@cert_store.set_default_paths
|
319
|
+
end
|
320
|
+
|
321
|
+
def config(&block)
|
322
|
+
# merge local (user) config and global config
|
323
|
+
merged = {}
|
324
|
+
global = {}
|
325
|
+
local = {}
|
326
|
+
|
327
|
+
global_config = CONFIG_DIR + "config"
|
328
|
+
begin
|
329
|
+
global = eval(global_config.read) || {}
|
330
|
+
rescue Errno::ENOENT
|
331
|
+
end
|
332
|
+
|
333
|
+
local_config = @real ? CONFIG_DIR + "#{@real}/config" : nil
|
334
|
+
if local_config
|
335
|
+
begin
|
336
|
+
local = eval(local_config.read) || {}
|
337
|
+
rescue Errno::ENOENT
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
merged.update(global)
|
342
|
+
merged.update(local)
|
343
|
+
|
344
|
+
if block
|
345
|
+
merged.instance_eval(&block)
|
346
|
+
merged.each do |k, v|
|
347
|
+
unless global[k] == v
|
348
|
+
local[k] = v
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
if local_config
|
353
|
+
local_config.parent.mkpath
|
354
|
+
local_config.open('w') do |f|
|
355
|
+
PP.pp(local, f)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
merged
|
361
|
+
end
|
362
|
+
|
363
|
+
def on_user(m)
|
364
|
+
super
|
365
|
+
|
366
|
+
@real, *@opts = (@opts.name || @real).split(" ")
|
367
|
+
@opts = @opts.inject({}) do |r, i|
|
368
|
+
key, value = i.split("=", 2)
|
369
|
+
key = "mentions" if key == "replies" # backcompat
|
370
|
+
r.update key => case value
|
371
|
+
when nil then true
|
372
|
+
when /\A\d+\z/ then value.to_i
|
373
|
+
when /\A(?:\d+\.\d*|\.\d+)\z/ then value.to_f
|
374
|
+
else value
|
375
|
+
end
|
376
|
+
end
|
377
|
+
@opts = OpenStruct.new(@opts)
|
378
|
+
@opts.httpproxy.sub!(/\A(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?\z/) do
|
379
|
+
@httpproxy = OpenStruct.new({
|
380
|
+
:user => $1, :password => $2, :address => $3, :port => $4.to_i,
|
381
|
+
})
|
382
|
+
$&.sub(/[^:@]+(?=@)/, "********")
|
383
|
+
end if @opts.httpproxy
|
384
|
+
|
385
|
+
@timeline = TypableMap.new(@opts.tmap_size || 200,
|
386
|
+
@opts.shuffled_tmap || false)
|
387
|
+
|
388
|
+
@consumer = OAuth::Consumer.new(
|
389
|
+
CONSUMER_KEY,
|
390
|
+
CONSUMER_SECRET,
|
391
|
+
:site => 'https://api.twitter.com'
|
392
|
+
)
|
393
|
+
|
394
|
+
@log.debug config.inspect
|
395
|
+
if config['access_token']
|
396
|
+
@access_token = OAuth::AccessToken.new(@consumer, config['access_token'], config['access_token_secret'])
|
397
|
+
on_authorized
|
398
|
+
else
|
399
|
+
begin
|
400
|
+
@access_token = @consumer.get_access_token(nil, {}, {
|
401
|
+
:x_auth_mode => "client_auth",
|
402
|
+
:x_auth_username => @real,
|
403
|
+
:x_auth_password => @pass,
|
404
|
+
})
|
405
|
+
on_authorized
|
406
|
+
rescue OAuth::Unauthorized
|
407
|
+
log 'Failed trying xAuth'
|
408
|
+
oauth_request
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def oauth_request
|
414
|
+
@request_token = @consumer.get_request_token
|
415
|
+
log 'Access following URL: %s' % @request_token.authorize_url
|
416
|
+
log 'and send /me oauth <PIN>'
|
417
|
+
end
|
418
|
+
|
419
|
+
def on_authorized
|
420
|
+
retry_count = 0
|
421
|
+
begin
|
422
|
+
@me = api("account/update_profile") #api("account/verify_credentials")
|
423
|
+
rescue APIFailed => e
|
424
|
+
@log.error e.inspect
|
425
|
+
sleep 1
|
426
|
+
retry_count += 1
|
427
|
+
retry if retry_count < 3
|
428
|
+
log "Failed to access API 3 times." <<
|
429
|
+
" Please retry oauth verification or" <<
|
430
|
+
" Twitter Status <http://status.twitter.com/> and try again later."
|
431
|
+
oauth_request
|
432
|
+
end
|
433
|
+
|
434
|
+
@prefix = prefix(@me)
|
435
|
+
@user = @prefix.user
|
436
|
+
@host = @prefix.host
|
437
|
+
|
438
|
+
#post NICK, @me.screen_name if @nick != @me.screen_name
|
439
|
+
post server_name, MODE, @nick, "+o"
|
440
|
+
post @prefix, JOIN, main_channel
|
441
|
+
post server_name, MODE, main_channel, "+mto", @nick
|
442
|
+
post server_name, MODE, main_channel, "+q", @nick
|
443
|
+
if @me.status
|
444
|
+
post @prefix, TOPIC, main_channel, generate_status_message(@me.status.text)
|
445
|
+
end
|
446
|
+
|
447
|
+
log "Client options: #{@opts.marshal_dump.inspect}"
|
448
|
+
@log.info "Client options: #{@opts.inspect}"
|
449
|
+
|
450
|
+
@opts.tid = begin
|
451
|
+
c = @opts.tid # expect: 0..15, true, "0,1"
|
452
|
+
b = nil
|
453
|
+
c, b = c.split(",", 2).map {|i| i.to_i } if c.respond_to? :split
|
454
|
+
c = 10 unless (0 .. 15).include? c # 10: teal
|
455
|
+
if (0 .. 15).include?(b)
|
456
|
+
"\003%.2d,%.2d[%%s]\017" % [c, b]
|
457
|
+
else
|
458
|
+
"\003%.2d[%%s]\017" % c
|
459
|
+
end
|
460
|
+
end if @opts.tid
|
461
|
+
|
462
|
+
check_friends
|
463
|
+
@ratelimit.register(:check_friends, 3600)
|
464
|
+
@check_friends_thread = Thread.start do
|
465
|
+
loop do
|
466
|
+
sleep @ratelimit.interval(:check_friends)
|
467
|
+
begin
|
468
|
+
check_friends
|
469
|
+
rescue APIFailed => e
|
470
|
+
@log.error e.inspect
|
471
|
+
rescue Exception => e
|
472
|
+
@log.error e.inspect
|
473
|
+
e.backtrace.each do |l|
|
474
|
+
@log.error "\t#{l}"
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
if @opts.clientspoofing
|
481
|
+
update_sources
|
482
|
+
else
|
483
|
+
@sources = [api_source]
|
484
|
+
end
|
485
|
+
|
486
|
+
start_timeline_thread(@opts.chirp)
|
487
|
+
|
488
|
+
update_redundant_suffix
|
489
|
+
@check_updates_thread = Thread.start do
|
490
|
+
sleep 30
|
491
|
+
|
492
|
+
loop do
|
493
|
+
begin
|
494
|
+
@log.info "check_updates"
|
495
|
+
update_redundant_suffix
|
496
|
+
check_updates
|
497
|
+
rescue Exception => e
|
498
|
+
@log.error e.inspect
|
499
|
+
e.backtrace.each do |l|
|
500
|
+
@log.error "\t#{l}"
|
501
|
+
end
|
502
|
+
end
|
503
|
+
sleep 0.01 * (90 + rand(21)) *
|
504
|
+
(@opts.check_updates_interval || 86400) # 0.9 ... 1.1 day
|
505
|
+
end
|
506
|
+
|
507
|
+
sleep @opts.check_updates_interval || 86400
|
508
|
+
end
|
509
|
+
|
510
|
+
@ratelimit.register(:dm, 600)
|
511
|
+
@check_dms_thread = Thread.start do
|
512
|
+
loop do
|
513
|
+
begin
|
514
|
+
if check_direct_messages
|
515
|
+
@ratelimit.incr(:dm)
|
516
|
+
else
|
517
|
+
@ratelimit.decr(:dm)
|
518
|
+
end
|
519
|
+
rescue APIFailed => e
|
520
|
+
@log.error e.inspect
|
521
|
+
rescue Exception => e
|
522
|
+
@log.error e.inspect
|
523
|
+
e.backtrace.each do |l|
|
524
|
+
@log.error "\t#{l}"
|
525
|
+
end
|
526
|
+
end
|
527
|
+
sleep @ratelimit.interval(:dm)
|
528
|
+
end
|
529
|
+
end if @opts.dm
|
530
|
+
|
531
|
+
@ratelimit.register(:mentions, 180)
|
532
|
+
@check_mentions_thread = Thread.start do
|
533
|
+
sleep @ratelimit.interval(:timeline)
|
534
|
+
|
535
|
+
loop do
|
536
|
+
begin
|
537
|
+
if check_mentions
|
538
|
+
@ratelimit.incr(:mentions)
|
539
|
+
else
|
540
|
+
@ratelimit.decr(:mentions)
|
541
|
+
end
|
542
|
+
rescue APIFailed => e
|
543
|
+
@log.error e.inspect
|
544
|
+
rescue Exception => e
|
545
|
+
@log.error e.inspect
|
546
|
+
e.backtrace.each do |l|
|
547
|
+
@log.error "\t#{l}"
|
548
|
+
end
|
549
|
+
end
|
550
|
+
sleep @ratelimit.interval(:mentions)
|
551
|
+
end
|
552
|
+
end if @opts.mentions
|
553
|
+
|
554
|
+
@ratelimit.register(:lists, 60 * 60)
|
555
|
+
@check_lists_thread = Thread.start do
|
556
|
+
sleep 60
|
557
|
+
Thread.current[:last_updated] = Time.at(0)
|
558
|
+
loop do
|
559
|
+
begin
|
560
|
+
@log.info "LISTS update now..."
|
561
|
+
if check_lists
|
562
|
+
@ratelimit.incr(:lists)
|
563
|
+
else
|
564
|
+
@ratelimit.decr(:lists)
|
565
|
+
end
|
566
|
+
Thread.current[:last_updated] = Time.now
|
567
|
+
|
568
|
+
sleep @ratelimit.interval(:lists)
|
569
|
+
rescue Exception => e
|
570
|
+
@log.error e.inspect
|
571
|
+
e.backtrace.each do |l|
|
572
|
+
@log.error "\t#{l}"
|
573
|
+
end
|
574
|
+
sleep 60
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end unless @opts.without_lists
|
578
|
+
|
579
|
+
@ratelimit.register(:lists_status, 60 * 5)
|
580
|
+
@check_lists_status_thread = Thread.start do
|
581
|
+
Thread.current[:last_updated] = Time.at(0)
|
582
|
+
loop do
|
583
|
+
begin
|
584
|
+
@log.info "lists/status update now... #{@channels.size}"
|
585
|
+
## TODO 各リストにつき limit が必要
|
586
|
+
if check_lists_status
|
587
|
+
@ratelimit.incr(:lists_status)
|
588
|
+
else
|
589
|
+
@ratelimit.decr(:lists_status)
|
590
|
+
end
|
591
|
+
Thread.current[:last_updated] = Time.now
|
592
|
+
rescue Exception => e
|
593
|
+
@log.error e.inspect
|
594
|
+
e.backtrace.each do |l|
|
595
|
+
@log.error "\t#{l}"
|
596
|
+
end
|
597
|
+
end
|
598
|
+
sleep @ratelimit.interval(:lists_status)
|
599
|
+
end
|
600
|
+
end unless @opts.without_lists
|
601
|
+
end
|
602
|
+
|
603
|
+
def start_timeline_thread(chirp=false)
|
604
|
+
@log.info "start_timeline_thread: chirp=#{chirp}"
|
605
|
+
@check_timeline_thread.kill rescue nil
|
606
|
+
@chirp_thread.kill rescue nil
|
607
|
+
if chirp
|
608
|
+
@chirp_thread = Thread.start do
|
609
|
+
retry_count = 0
|
610
|
+
begin
|
611
|
+
uri = URI.parse('https://userstream.twitter.com/2/user.json?replies=all')
|
612
|
+
|
613
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
614
|
+
http.use_ssl = true
|
615
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
616
|
+
http.cert_store = @cert_store
|
617
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
618
|
+
req.oauth!(http, @consumer, @access_token)
|
619
|
+
|
620
|
+
@chirp_timer_thread.kill rescue nil
|
621
|
+
@chirp_timer_thread = Thread.start do
|
622
|
+
loop do
|
623
|
+
@log.info "check unresponsive_time"
|
624
|
+
unresponsive_time = Time.now - Thread.current[:timer]
|
625
|
+
if unresponsive_time > 90
|
626
|
+
@log.info "stream api timeout: re-start_timeline_thread"
|
627
|
+
start_timeline_thread(true)
|
628
|
+
else
|
629
|
+
sleep 90 - unresponsive_time
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
@chirp_timer_thread[:timer] = Time.now
|
634
|
+
|
635
|
+
http.request(req) do |res|
|
636
|
+
raise UnauthorizedException if res.code.to_i == 401
|
637
|
+
raise res.code unless res.code.to_i == 200
|
638
|
+
|
639
|
+
buf = ""
|
640
|
+
res.read_body do |str|
|
641
|
+
@chirp_timer_thread[:timer] = Time.now # update timer
|
642
|
+
buf << str
|
643
|
+
buf.gsub!(/[\s\S]+?\r\n/) do |chunk|
|
644
|
+
data = JSON.parse(chunk) rescue {}
|
645
|
+
struct = TwitterStruct.make(data)
|
646
|
+
|
647
|
+
begin
|
648
|
+
case
|
649
|
+
when data['text']
|
650
|
+
status = struct
|
651
|
+
id = @latest_id = status.id
|
652
|
+
unless @timeline.any? {|tid, s| s.id == id }
|
653
|
+
user = status.user
|
654
|
+
tid = @timeline.push(status)
|
655
|
+
tid = nil unless @opts.tid
|
656
|
+
|
657
|
+
if user.id == @me.id
|
658
|
+
mesg = generate_status_message(status.text)
|
659
|
+
mesg << " " << @opts.tid % tid if tid
|
660
|
+
post @prefix, TOPIC, main_channel, mesg
|
661
|
+
|
662
|
+
@me = user
|
663
|
+
else
|
664
|
+
if @friends
|
665
|
+
@friends.each_with_index do |friend, i|
|
666
|
+
if friend.id == user.id
|
667
|
+
if friend.screen_name != user.screen_name
|
668
|
+
post prefix(friend), NICK, user.screen_name
|
669
|
+
end
|
670
|
+
@friends[i] = user
|
671
|
+
break
|
672
|
+
end
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
message(status, main_channel, tid, nil, PRIVMSG)
|
677
|
+
end
|
678
|
+
@channels.each do |name, channel|
|
679
|
+
if channel[:members].find{|m| m.screen_name == user.screen_name }
|
680
|
+
message(status, name, tid, nil, (user.id == @me.id) ? NOTICE : PRIVMSG)
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
when data['friends']
|
685
|
+
when data['delete']
|
686
|
+
# TODO
|
687
|
+
when data['event'] == 'follow'
|
688
|
+
message(struct, main_channel, nil, "\00311follow\017 => @%s http://twitter.com/%s" % [
|
689
|
+
data['target']['screen_name'],
|
690
|
+
data['target']['screen_name']
|
691
|
+
])
|
692
|
+
when data['event'] == 'retweet'
|
693
|
+
# status event include this event
|
694
|
+
when data['event'] == 'favorite'
|
695
|
+
next if data['source']['screen_name'] == "amachang" # CAY (countermeasures against youpy)
|
696
|
+
message(struct, main_channel, nil, "\00311favorite\017 => @%s : %s http://twitter.com/%s" % [
|
697
|
+
data['target_object']['user']['screen_name'],
|
698
|
+
data['target_object']['text'],
|
699
|
+
data['target_object']['user']['screen_name']
|
700
|
+
])
|
701
|
+
when data['event'] == 'unfavorite'
|
702
|
+
message(struct, main_channel, nil, "\00305unfavorite =>\017 @%s : %s http://twitter.com/%s" % [
|
703
|
+
data['target_object']['user']['screen_name'],
|
704
|
+
data['target_object']['text'],
|
705
|
+
data['target_object']['user']['screen_name']
|
706
|
+
])
|
707
|
+
else
|
708
|
+
end
|
709
|
+
rescue Exception => e
|
710
|
+
@log.error e.inspect
|
711
|
+
e.backtrace.each do |l|
|
712
|
+
@log.error "\t#{l}"
|
713
|
+
end
|
714
|
+
end
|
715
|
+
''
|
716
|
+
end
|
717
|
+
end
|
718
|
+
end
|
719
|
+
rescue TimeoutError => e
|
720
|
+
@log.info "stream api timeout: retry"
|
721
|
+
retry
|
722
|
+
rescue Exception => e
|
723
|
+
@log.error e.inspect
|
724
|
+
e.backtrace.each do |l|
|
725
|
+
@log.error "\t#{l}"
|
726
|
+
end
|
727
|
+
sleep 1
|
728
|
+
retry_count += 1
|
729
|
+
if retry_count < 3
|
730
|
+
retry
|
731
|
+
else
|
732
|
+
@chirp_thread = nil
|
733
|
+
on_disconnected
|
734
|
+
on_authorized
|
735
|
+
end
|
736
|
+
end
|
737
|
+
end
|
738
|
+
else
|
739
|
+
@ratelimit.register(:timeline, 30)
|
740
|
+
@check_timeline_thread = Thread.start do
|
741
|
+
sleep 2 * (@me.friends_count / 100.0).ceil
|
742
|
+
sleep 10
|
743
|
+
|
744
|
+
loop do
|
745
|
+
begin
|
746
|
+
if check_timeline
|
747
|
+
@ratelimit.incr(:timeline)
|
748
|
+
else
|
749
|
+
@ratelimit.decr(:timeline)
|
750
|
+
end
|
751
|
+
rescue APIFailed => e
|
752
|
+
@log.error e.inspect
|
753
|
+
rescue Exception => e
|
754
|
+
@log.error e.inspect
|
755
|
+
e.backtrace.each do |l|
|
756
|
+
@log.error "\t#{l}"
|
757
|
+
end
|
758
|
+
end
|
759
|
+
sleep @ratelimit.interval(:timeline)
|
760
|
+
end
|
761
|
+
end
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
def on_disconnected
|
766
|
+
@check_friends_thread.kill rescue nil
|
767
|
+
@check_timeline_thread.kill rescue nil
|
768
|
+
@check_mentions_thread.kill rescue nil
|
769
|
+
@check_dms_thread.kill rescue nil
|
770
|
+
@check_updates_thread.kill rescue nil
|
771
|
+
@check_lists_thread.kill rescue nil
|
772
|
+
@check_lists_status_thread.kill rescue nil
|
773
|
+
@chirp_thread.kill rescue nil
|
774
|
+
@chirp_timer_thread.kill rescue nil
|
775
|
+
end
|
776
|
+
|
777
|
+
def on_privmsg(m)
|
778
|
+
target, mesg = *m.params
|
779
|
+
m.ctcps.each {|ctcp| on_ctcp(target, ctcp) } if m.ctcp?
|
780
|
+
|
781
|
+
return if mesg.empty?
|
782
|
+
return on_ctcp_action(target, mesg) if mesg.sub!(/\A +/, "") #and @opts.direct_action
|
783
|
+
|
784
|
+
if include_ngword?(mesg)
|
785
|
+
log "The message includes NG words, was ignored."
|
786
|
+
return
|
787
|
+
end
|
788
|
+
|
789
|
+
|
790
|
+
command, params = mesg.split(" ", 2)
|
791
|
+
case command.downcase # TODO: escape recursive
|
792
|
+
when "d", "dm"
|
793
|
+
screen_name, mesg = params.split(" ", 2)
|
794
|
+
unless screen_name or mesg
|
795
|
+
log 'Send "d NICK message" to send a direct (private) message.' <<
|
796
|
+
" You may reply to a direct message the same way."
|
797
|
+
return
|
798
|
+
end
|
799
|
+
m.params[0] = screen_name.sub(/\A@/, "")
|
800
|
+
m.params[1] = mesg #.rstrip
|
801
|
+
return on_privmsg(m)
|
802
|
+
# TODO
|
803
|
+
#when "f", "follow"
|
804
|
+
#when "on"
|
805
|
+
#when "off" # BUG if no args
|
806
|
+
#when "g", "get"
|
807
|
+
#when "w", "whois"
|
808
|
+
#when "n", "nudge" # BUG if no args
|
809
|
+
#when "*", "fav"
|
810
|
+
#when "delete"
|
811
|
+
#when "stats" # no args
|
812
|
+
#when "leave"
|
813
|
+
#when "invite"
|
814
|
+
end unless command.nil?
|
815
|
+
|
816
|
+
mesg = escape_http_urls(mesg)
|
817
|
+
mesg = @opts.unuify ? unuify(mesg) : bitlify(mesg)
|
818
|
+
mesg = Iconv.iconv("UTF-7", "UTF-8", mesg).join.encoding!("ASCII-8BIT") if @utf7
|
819
|
+
|
820
|
+
ret = nil
|
821
|
+
retry_count = 3
|
822
|
+
begin
|
823
|
+
case
|
824
|
+
when target.ch?
|
825
|
+
previous = @me.status
|
826
|
+
if previous and
|
827
|
+
((Time.now - Time.parse(previous.created_at)).to_i < 60 rescue true) and
|
828
|
+
mesg.strip == previous.text
|
829
|
+
log "You can't submit the same status twice in a row."
|
830
|
+
return
|
831
|
+
end
|
832
|
+
|
833
|
+
q = { :status => mesg }
|
834
|
+
|
835
|
+
if @opts.old_style_reply and mesg[/\A@(?>([A-Za-z0-9_]{1,15}))[^A-Za-z0-9_]/]
|
836
|
+
if user = friend($1) || api("users/show/#{$1}")
|
837
|
+
unless user.status
|
838
|
+
user = api("users/show/#{user.id}", {},
|
839
|
+
{ :authenticate => user.protected })
|
840
|
+
end
|
841
|
+
if user.status
|
842
|
+
q.update :in_reply_to_status_id => user.status.id
|
843
|
+
end
|
844
|
+
end
|
845
|
+
end
|
846
|
+
if @opts.ll
|
847
|
+
lat, long = @opts.ll.split(",", 2)
|
848
|
+
q.update :lat => lat.to_f
|
849
|
+
q.update :long => long.to_f
|
850
|
+
end
|
851
|
+
|
852
|
+
ret = api("statuses/update", q)
|
853
|
+
log oops(ret) if ret.truncated
|
854
|
+
ret.user.status = ret
|
855
|
+
@me = ret.user
|
856
|
+
log "Status updated"
|
857
|
+
when target.screen_name? # Direct message
|
858
|
+
ret = api("direct_messages/new", { :screen_name => target, :text => mesg })
|
859
|
+
post server_name, NOTICE, @nick, "Your direct message has been sent to #{target}."
|
860
|
+
else
|
861
|
+
post server_name, ERR_NOSUCHNICK, target, "No such nick/channel"
|
862
|
+
end
|
863
|
+
rescue => e
|
864
|
+
@log.error [retry_count, e.inspect, e.backtrace].inspect
|
865
|
+
if retry_count > 0
|
866
|
+
retry_count -= 1
|
867
|
+
@log.debug "Retry to setting status..."
|
868
|
+
retry
|
869
|
+
end
|
870
|
+
log "Some Error Happened on Sending #{mesg}. #{e}"
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
874
|
+
def on_whois(m)
|
875
|
+
nick = m.params[0]
|
876
|
+
unless nick.screen_name?
|
877
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
878
|
+
return
|
879
|
+
end
|
880
|
+
|
881
|
+
unless user = user(nick)
|
882
|
+
if api("users/username_available", { :username => nick }).valid
|
883
|
+
# TODO: 404 suspended
|
884
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
885
|
+
return
|
886
|
+
end
|
887
|
+
user = api("users/show/#{nick}", {}, { :authenticate => false })
|
888
|
+
end
|
889
|
+
|
890
|
+
prefix = prefix(user)
|
891
|
+
desc = user.name
|
892
|
+
desc = "#{desc} / #{user.description}".gsub(/\s+/, " ") if user.description and not user.description.empty?
|
893
|
+
signon_at = Time.parse(user.created_at).to_i rescue 0
|
894
|
+
idle_sec = (Time.now - (user.status ? Time.parse(user.status.created_at) : signon_at)).to_i rescue 0
|
895
|
+
location = user.location
|
896
|
+
location = "SoMa neighborhood of San Francisco, CA" if location.nil? or location.empty?
|
897
|
+
post server_name, RPL_WHOISUSER, @nick, nick, prefix.user, prefix.host, "*", desc
|
898
|
+
post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, location
|
899
|
+
post server_name, RPL_WHOISIDLE, @nick, nick, "#{idle_sec}", "#{signon_at}", "seconds idle, signon time"
|
900
|
+
post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list"
|
901
|
+
if @drones.include?(user.id)
|
902
|
+
post server_name, RPL_WHOISBOT, @nick, nick, "is a \002Bot\002 on #{server_name}"
|
903
|
+
end
|
904
|
+
end
|
905
|
+
|
906
|
+
def on_who(m)
|
907
|
+
channel = m.params[0]
|
908
|
+
whoreply = Proc.new do |ch, user|
|
909
|
+
# "<channel> <user> <host> <server> <nick>
|
910
|
+
# ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
|
911
|
+
# :<hopcount> <real name>"
|
912
|
+
prefix = prefix(user)
|
913
|
+
server = api_base.host
|
914
|
+
mode = case prefix.nick
|
915
|
+
when @nick then "~"
|
916
|
+
#when @drones.include?(user.id) then "%" # FIXME
|
917
|
+
else "+"
|
918
|
+
end
|
919
|
+
hop = prefix.host.count("/")
|
920
|
+
real = user.name
|
921
|
+
post server_name, RPL_WHOREPLY, @nick,
|
922
|
+
ch, prefix.user, prefix.host, server, prefix.nick, "H*#{mode}", "#{hop} #{real}"
|
923
|
+
end
|
924
|
+
|
925
|
+
case
|
926
|
+
when channel.casecmp(main_channel).zero?
|
927
|
+
users = [@me]
|
928
|
+
users.concat @friends.reverse if @friends
|
929
|
+
users.each {|friend| whoreply.call channel, friend }
|
930
|
+
post server_name, RPL_ENDOFWHO, @nick, channel
|
931
|
+
when (@channels.key?(channel) and @friends)
|
932
|
+
@channels[channel][:members].each do |user|
|
933
|
+
whoreply.call channel, user
|
934
|
+
end
|
935
|
+
post server_name, RPL_ENDOFWHO, @nick, channel
|
936
|
+
else
|
937
|
+
post server_name, ERR_NOSUCHNICK, @nick, "No such nick/channel"
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
def on_join(m)
|
942
|
+
channels = m.params[0].split(/ *, */)
|
943
|
+
channels.each do |channel|
|
944
|
+
channel = channel.split(" ", 2).first
|
945
|
+
next if channel.casecmp(main_channel).zero?
|
946
|
+
|
947
|
+
# auto rejoin のとき勝手に作って困るのでコメントアウト。
|
948
|
+
# create するまえに、必ず check_lists するようにしないと。
|
949
|
+
# name = channel[1..-1]
|
950
|
+
# unless @channels.find{|c| c.slug == name }
|
951
|
+
# @log.info "create list: #{name}"
|
952
|
+
# api("1/#{@me.screen_name}/lists",{'name' => name })
|
953
|
+
# end
|
954
|
+
# post @prefix, JOIN, channel
|
955
|
+
# post server_name, MODE, channel, "+mtio", @nick
|
956
|
+
# post server_name, MODE, channel, "+q", @nick
|
957
|
+
end
|
958
|
+
end
|
959
|
+
|
960
|
+
def on_part(m)
|
961
|
+
channel = m.params[0]
|
962
|
+
return if channel.casecmp(main_channel).zero?
|
963
|
+
|
964
|
+
# いきなり delete とか危険なのでコメントアウト
|
965
|
+
# IRC Gateway 側に流れない、という挙動にし、delete するには ctcp を必要に
|
966
|
+
# name = channel[1..-1]
|
967
|
+
# @log.info "delete list: #{name}"
|
968
|
+
# api("1/#{@me.screen_name}/lists/#{name}",{'_method' => 'DELETE' }) rescue nil
|
969
|
+
# post @prefix, PART, channel, "Ignore group #{channel}, but setting is alive yet."
|
970
|
+
end
|
971
|
+
|
972
|
+
def on_invite(m)
|
973
|
+
nick, channel = *m.params
|
974
|
+
if not nick.screen_name? or @nick.casecmp(nick).zero?
|
975
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" # or yourself
|
976
|
+
return
|
977
|
+
end
|
978
|
+
|
979
|
+
friend = friend(nick)
|
980
|
+
|
981
|
+
case
|
982
|
+
when channel.casecmp(main_channel).zero?
|
983
|
+
case
|
984
|
+
when friend #TODO
|
985
|
+
when api("users/username_available", { :username => nick }).valid
|
986
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
987
|
+
else
|
988
|
+
user = api("friendships/create/#{nick}")
|
989
|
+
join main_channel, [user]
|
990
|
+
@friends << user if @friends
|
991
|
+
@me.friends_count += 1
|
992
|
+
end
|
993
|
+
when friend
|
994
|
+
slug = channel[1..-1]
|
995
|
+
api("/1/#{@me.screen_name}/#{slug}/members",{'id'=>friend.id})
|
996
|
+
@channels[channel][:members] << friend
|
997
|
+
join(channel, [friend])
|
998
|
+
else
|
999
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
1000
|
+
end
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
def on_kick(m)
|
1004
|
+
channel, nick, msg = *m.params
|
1005
|
+
|
1006
|
+
if channel.casecmp(main_channel).zero?
|
1007
|
+
@friends.delete_if do |friend|
|
1008
|
+
if friend.screen_name.casecmp(nick).zero?
|
1009
|
+
user = api("friendships/destroy/#{friend.id}")
|
1010
|
+
post prefix(user), PART, main_channel, "Removed: #{msg}"
|
1011
|
+
@me.friends_count -= 1
|
1012
|
+
end
|
1013
|
+
end if @friends
|
1014
|
+
else
|
1015
|
+
friend = friend(nick)
|
1016
|
+
if friend
|
1017
|
+
slug = channel[1..-1]
|
1018
|
+
api("/1/#{@me.screen_name}/#{slug}/members",{'id'=>friend.id, '_method'=>'DELETE'})
|
1019
|
+
@channels[channel][:members].delete_if{|u| u.screen_name == friend.screen_name }
|
1020
|
+
post prefix(friend), PART, channel, "Removed: #{msg}"
|
1021
|
+
else
|
1022
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
1023
|
+
end
|
1024
|
+
end
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
#def on_nick(m)
|
1028
|
+
# @nicknames[@nick] = m.params[0]
|
1029
|
+
#end
|
1030
|
+
|
1031
|
+
def on_topic(m)
|
1032
|
+
channel = m.params[0]
|
1033
|
+
return if not channel.casecmp(main_channel).zero? or @me.status.nil?
|
1034
|
+
|
1035
|
+
return if not @opts.mesautofix
|
1036
|
+
begin
|
1037
|
+
require "levenshtein"
|
1038
|
+
topic = m.params[1]
|
1039
|
+
previous = @me.status
|
1040
|
+
return unless previous
|
1041
|
+
|
1042
|
+
distance = Levenshtein.normalized_distance(previous.text, topic)
|
1043
|
+
return if distance.zero?
|
1044
|
+
|
1045
|
+
status = api("statuses/update", { :status => topic, :source => source })
|
1046
|
+
log oops(ret) if status.truncated
|
1047
|
+
status.user.status = status
|
1048
|
+
@me = status.user
|
1049
|
+
|
1050
|
+
if distance < 0.5
|
1051
|
+
deleted = api("statuses/destroy/#{previous.id}")
|
1052
|
+
@timeline.delete_if {|tid, s| s.id == deleted.id }
|
1053
|
+
log "Similar update in previous. Conclude that it has error."
|
1054
|
+
log "And overwrite previous as new status: #{status.text}"
|
1055
|
+
else
|
1056
|
+
log "Status updated"
|
1057
|
+
end
|
1058
|
+
rescue LoadError
|
1059
|
+
end
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
def on_mode(m)
|
1063
|
+
channel = m.params[0]
|
1064
|
+
|
1065
|
+
unless m.params[1]
|
1066
|
+
case
|
1067
|
+
when channel.ch?
|
1068
|
+
mode = "+mt"
|
1069
|
+
mode += "i" unless channel.casecmp(main_channel).zero?
|
1070
|
+
post server_name, RPL_CHANNELMODEIS, @nick, channel, mode
|
1071
|
+
#post server_name, RPL_CREATEONTIME, @nick, channel, 0
|
1072
|
+
when channel.casecmp(@nick).zero?
|
1073
|
+
post server_name, RPL_UMODEIS, @nick, @nick, "+o"
|
1074
|
+
end
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
private
|
1079
|
+
def on_ctcp(target, mesg)
|
1080
|
+
type, mesg = mesg.split(" ", 2)
|
1081
|
+
method = "on_ctcp_#{type.downcase}".to_sym
|
1082
|
+
send(method, target, mesg) if respond_to? method, true
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
def on_ctcp_action(target, mesg)
|
1086
|
+
#return unless main_channel.casecmp(target).zero?
|
1087
|
+
command, *args = mesg.split(" ")
|
1088
|
+
if command
|
1089
|
+
command.downcase!
|
1090
|
+
|
1091
|
+
@@ctcp_action_commands.each do |define, name|
|
1092
|
+
if define === command
|
1093
|
+
send(name, target, mesg, Regexp.last_match || command, args)
|
1094
|
+
break
|
1095
|
+
end
|
1096
|
+
end
|
1097
|
+
else
|
1098
|
+
commands = @@ctcp_action_commands.map {|define, name|
|
1099
|
+
define
|
1100
|
+
}.select {|define|
|
1101
|
+
define.is_a? String
|
1102
|
+
}
|
1103
|
+
|
1104
|
+
log "[tig.rb] CTCP ACTION COMMANDS:"
|
1105
|
+
commands.each_slice(5) do |c|
|
1106
|
+
log c.join(" ")
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
rescue APIFailed => e
|
1111
|
+
log e.inspect
|
1112
|
+
rescue Exception => e
|
1113
|
+
log e.inspect
|
1114
|
+
e.backtrace.each do |l|
|
1115
|
+
@log.error "\t#{l}"
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
def include_ngword?(msg)
|
1120
|
+
msg = msg.dup.encoding!("UTF-8")
|
1121
|
+
if config['ngword'] && config['ngword'].size > 0
|
1122
|
+
msg =~ /#{config['ngword'].map {|i| Regexp.quote(i) }.join('|')}/
|
1123
|
+
else
|
1124
|
+
false
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
ctcp_action "oauth" do |target, mesg, command, args|
|
1129
|
+
if args.length == 1
|
1130
|
+
pin = args.first
|
1131
|
+
begin
|
1132
|
+
access_token = @request_token.get_access_token(
|
1133
|
+
:oauth_verifier => pin
|
1134
|
+
)
|
1135
|
+
config {
|
1136
|
+
self['access_token'] = access_token.token
|
1137
|
+
self['access_token_secret'] = access_token.secret
|
1138
|
+
}
|
1139
|
+
log "Congrats! OAuth Verified: #{access_token.params[:screen_name]}"
|
1140
|
+
@access_token = access_token
|
1141
|
+
on_authorized
|
1142
|
+
rescue OAuth::Unauthorized
|
1143
|
+
log "Invalid PIN was input. Please retry"
|
1144
|
+
oauth_request
|
1145
|
+
end
|
1146
|
+
else
|
1147
|
+
oauth_request
|
1148
|
+
end
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
ctcp_action "ngword" do |target, mesg, command, args|
|
1152
|
+
meth, word = *args
|
1153
|
+
case meth
|
1154
|
+
when 'add'
|
1155
|
+
config {
|
1156
|
+
(self['ngword'] ||= []) << word
|
1157
|
+
}
|
1158
|
+
when 'del'
|
1159
|
+
config {
|
1160
|
+
(self['ngword'] ||= []).reject! {|w| w == word }
|
1161
|
+
}
|
1162
|
+
when 'inc?'
|
1163
|
+
if word =~ /#{(config['ngword'] || []).map {|i| Regexp.quote(i) }.join('|')}/
|
1164
|
+
log "#{word} is included"
|
1165
|
+
else
|
1166
|
+
log "#{word} is not included"
|
1167
|
+
end
|
1168
|
+
end
|
1169
|
+
end
|
1170
|
+
|
1171
|
+
ctcp_action "tl_method" do |target, mesg, command, args|
|
1172
|
+
@opts.chirp = !@opts.chirp
|
1173
|
+
log "Changed Timeline retrieving method: Using ChripUserStream: #{@opts.chirp}"
|
1174
|
+
start_timeline_thread(@opts.chirp)
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
ctcp_action "reload" do |target, mesg, command, args|
|
1178
|
+
load File.expand_path(__FILE__)
|
1179
|
+
current = server_version
|
1180
|
+
@server_version = nil
|
1181
|
+
log "Reloaded tig.rb. New: #{server_version} <- Old: #{current}"
|
1182
|
+
initial_message
|
1183
|
+
end
|
1184
|
+
|
1185
|
+
ctcp_action "call" do |target, mesg, command, args|
|
1186
|
+
if args.size < 2
|
1187
|
+
log "/me call <Twitter_screen_name> as <IRC_nickname>"
|
1188
|
+
return
|
1189
|
+
end
|
1190
|
+
screen_name = args[0]
|
1191
|
+
nickname = args[2] || args[1] # allow omitting "as"
|
1192
|
+
if nickname == "is" and
|
1193
|
+
deleted_nick = @nicknames.delete(screen_name)
|
1194
|
+
log %Q{Removed the nickname "#{deleted_nick}" for #{screen_name}}
|
1195
|
+
else
|
1196
|
+
@nicknames[screen_name] = nickname
|
1197
|
+
log "Call #{screen_name} as #{nickname}"
|
1198
|
+
end
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
ctcp_action "debug" do |target, mesg, command, args|
|
1202
|
+
code = args.join(" ")
|
1203
|
+
begin
|
1204
|
+
log instance_eval(code).inspect
|
1205
|
+
rescue Exception => e
|
1206
|
+
log e.inspect
|
1207
|
+
end
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
ctcp_action "utf-7", "utf7" do |target, mesg, command, args|
|
1211
|
+
unless defined? ::Iconv
|
1212
|
+
log "Can't load iconv."
|
1213
|
+
return
|
1214
|
+
end
|
1215
|
+
@utf7 = !@utf7
|
1216
|
+
log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}"
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
ctcp_action "list", "ls" do |target, mesg, command, args|
|
1220
|
+
if args.empty?
|
1221
|
+
log "/me list <NICK> [<NUM>]"
|
1222
|
+
return
|
1223
|
+
end
|
1224
|
+
nick = args.first
|
1225
|
+
if not nick.screen_name? or
|
1226
|
+
api("users/username_available", { :username => nick }).valid
|
1227
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
1228
|
+
return
|
1229
|
+
end
|
1230
|
+
id = nick
|
1231
|
+
authenticate = false
|
1232
|
+
if user = friend(nick)
|
1233
|
+
id = user.id
|
1234
|
+
nick = user.screen_name
|
1235
|
+
authenticate = user.protected
|
1236
|
+
end
|
1237
|
+
unless (1..200).include?(count = args[1].to_i)
|
1238
|
+
count = 20
|
1239
|
+
end
|
1240
|
+
begin
|
1241
|
+
res = api("statuses/user_timeline/#{id}",
|
1242
|
+
{ :count => count }, { :authenticate => authenticate })
|
1243
|
+
rescue APIFailed
|
1244
|
+
#log "#{nick} has protected their updates."
|
1245
|
+
return
|
1246
|
+
end
|
1247
|
+
res.reverse_each do |s|
|
1248
|
+
message(s, target, nil, nil, NOTICE)
|
1249
|
+
end
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
ctcp_action %r/\A(un)?fav(?:ou?rite)?(!)?\z/ do |target, mesg, command, args|
|
1253
|
+
# fav, unfav, favorite, unfavorite, favourite, unfavourite
|
1254
|
+
method = command[1].nil? ? "create" : "destroy"
|
1255
|
+
force = !!command[2]
|
1256
|
+
entered = command[0].capitalize
|
1257
|
+
statuses = []
|
1258
|
+
if args.empty?
|
1259
|
+
if method == "create"
|
1260
|
+
if status = @timeline.last
|
1261
|
+
statuses << status
|
1262
|
+
else
|
1263
|
+
#log ""
|
1264
|
+
return
|
1265
|
+
end
|
1266
|
+
else
|
1267
|
+
@favorites ||= api("favorites").reverse
|
1268
|
+
if @favorites.empty?
|
1269
|
+
log "You've never favorite yet. No favorites to unfavorite."
|
1270
|
+
return
|
1271
|
+
end
|
1272
|
+
statuses.push @favorites.last
|
1273
|
+
end
|
1274
|
+
else
|
1275
|
+
args.each do |tid_or_nick|
|
1276
|
+
case
|
1277
|
+
when status = @timeline[tid = tid_or_nick]
|
1278
|
+
statuses.push status
|
1279
|
+
when friend = friend(nick = tid_or_nick)
|
1280
|
+
if friend.status
|
1281
|
+
statuses.push friend.status
|
1282
|
+
else
|
1283
|
+
log "#{tid_or_nick} has no status."
|
1284
|
+
end
|
1285
|
+
else
|
1286
|
+
# PRIVMSG: fav nick
|
1287
|
+
log "No such ID/NICK #{@opts.tid % tid_or_nick}"
|
1288
|
+
end
|
1289
|
+
end
|
1290
|
+
end
|
1291
|
+
@favorites ||= []
|
1292
|
+
statuses.each do |s|
|
1293
|
+
if not force and method == "create" and
|
1294
|
+
@favorites.find {|i| i.id == s.id }
|
1295
|
+
log "The status is already favorited! <#{permalink(s)}>"
|
1296
|
+
next
|
1297
|
+
end
|
1298
|
+
res = api("favorites/#{method}", { :id => s.id })
|
1299
|
+
log "#{entered}: #{res.user.screen_name}: #{generate_status_message(res.text)}"
|
1300
|
+
if method == "create"
|
1301
|
+
@favorites.push res
|
1302
|
+
else
|
1303
|
+
@favorites.delete_if {|i| i.id == res.id }
|
1304
|
+
end
|
1305
|
+
end
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
ctcp_action "link", "ln", /\Au(?:rl)?\z/ do |target, mesg, command, args|
|
1309
|
+
args.each do |tid|
|
1310
|
+
if status = @timeline[tid]
|
1311
|
+
log "#{@opts.tid % tid}: #{permalink(status)}"
|
1312
|
+
else
|
1313
|
+
log "No such ID #{@opts.tid % tid}"
|
1314
|
+
end
|
1315
|
+
end
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
ctcp_action "ratio", "ratios" do |target, mesg, command, args|
|
1319
|
+
log "Intervals: #{@ratelimit.inspect}"
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
ctcp_action "rm", %r/\A(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))\z/ do |target, mesg, command, args|
|
1323
|
+
# destroy, delete, del, remove, rm, miss, oops
|
1324
|
+
statuses = []
|
1325
|
+
if args.empty? and @me.status
|
1326
|
+
statuses.push @me.status
|
1327
|
+
else
|
1328
|
+
args.each do |tid|
|
1329
|
+
if status = @timeline[tid]
|
1330
|
+
if status.user.id == @me.id
|
1331
|
+
statuses.push status
|
1332
|
+
else
|
1333
|
+
log "The status you specified by the ID #{@opts.tid % tid} is not yours."
|
1334
|
+
end
|
1335
|
+
else
|
1336
|
+
log "No such ID #{@opts.tid % tid}"
|
1337
|
+
end
|
1338
|
+
end
|
1339
|
+
end
|
1340
|
+
b = false
|
1341
|
+
statuses.each do |st|
|
1342
|
+
res = api("statuses/destroy/#{st.id}")
|
1343
|
+
@timeline.delete_if {|tid, s| s.id == res.id }
|
1344
|
+
b = @me.status && @me.status.id == res.id
|
1345
|
+
log "Destroyed: #{res.text}"
|
1346
|
+
end
|
1347
|
+
Thread.start do
|
1348
|
+
sleep 2
|
1349
|
+
@me = api("account/update_profile") #api("account/verify_credentials")
|
1350
|
+
if @me.status
|
1351
|
+
@me.status.user = @me
|
1352
|
+
msg = generate_status_message(@me.status.text)
|
1353
|
+
@timeline.any? do |tid, s|
|
1354
|
+
if s.id == @me.status.id
|
1355
|
+
msg << " " << @opts.tid % tid
|
1356
|
+
end
|
1357
|
+
end
|
1358
|
+
post @prefix, TOPIC, main_channel, msg
|
1359
|
+
end
|
1360
|
+
end if b
|
1361
|
+
end
|
1362
|
+
|
1363
|
+
ctcp_action "name" do |target, mesg, command, args|
|
1364
|
+
name = mesg.split(" ", 2)[1]
|
1365
|
+
unless name.nil?
|
1366
|
+
@me = api("account/update_profile", { :name => name })
|
1367
|
+
@me.status.user = @me if @me.status
|
1368
|
+
log "You are named #{@me.name}."
|
1369
|
+
end
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
ctcp_action "email" do |target, mesg, command, args|
|
1373
|
+
# FIXME
|
1374
|
+
email = args.first
|
1375
|
+
unless email.nil?
|
1376
|
+
@me = api("account/update_profile", { :email => email })
|
1377
|
+
@me.status.user = @me if @me.status
|
1378
|
+
end
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
ctcp_action "url" do |target, mesg, command, args|
|
1382
|
+
# FIXME
|
1383
|
+
url = args.first || ""
|
1384
|
+
@me = api("account/update_profile", { :url => url })
|
1385
|
+
@me.status.user = @me if @me.status
|
1386
|
+
end
|
1387
|
+
|
1388
|
+
ctcp_action "in", "location" do |target, mesg, command, args|
|
1389
|
+
location = mesg.split(" ", 2)[1] || ""
|
1390
|
+
@me = api("account/update_profile", { :location => location })
|
1391
|
+
@me.status.user = @me if @me.status
|
1392
|
+
location = (@me.location and @me.location.empty?) ? "nowhere" : "in #{@me.location}"
|
1393
|
+
log "You are #{location} now."
|
1394
|
+
end
|
1395
|
+
|
1396
|
+
ctcp_action %r/\Adesc(?:ription)?\z/ do |target, mesg, command, args|
|
1397
|
+
# FIXME
|
1398
|
+
description = mesg.split(" ", 2)[1] || ""
|
1399
|
+
@me = api("account/update_profile", { :description => description })
|
1400
|
+
@me.status.user = @me if @me.status
|
1401
|
+
end
|
1402
|
+
|
1403
|
+
ctcp_action %r/\A(?:mention|re(?:ply)?)\z/ do |target, mesg, command, args|
|
1404
|
+
# reply, re, mention
|
1405
|
+
tid = args.first
|
1406
|
+
if status = @timeline[tid]
|
1407
|
+
text = mesg.split(" ", 3)[2]
|
1408
|
+
screen_name = "@#{status.user.screen_name}"
|
1409
|
+
if text.nil? or not text.include?(screen_name)
|
1410
|
+
text = "#{screen_name} #{text}"
|
1411
|
+
end
|
1412
|
+
ret = api("statuses/update", {
|
1413
|
+
:status => text,
|
1414
|
+
:source => source,
|
1415
|
+
:in_reply_to_status_id => status.id
|
1416
|
+
})
|
1417
|
+
log oops(ret) if ret.truncated
|
1418
|
+
msg = generate_status_message(status.text)
|
1419
|
+
url = permalink(status)
|
1420
|
+
log "Status updated (In reply to #{@opts.tid % tid}: #{msg} <#{url}>)"
|
1421
|
+
ret.user.status = ret
|
1422
|
+
@me = ret.user
|
1423
|
+
end
|
1424
|
+
end
|
1425
|
+
|
1426
|
+
ctcp_action %r/\Aspoo(o+)?f\z/ do |target, mesg, command, args|
|
1427
|
+
if args.empty?
|
1428
|
+
Thread.start do
|
1429
|
+
update_sources(command[1].nil?? 0 : command[1].size)
|
1430
|
+
end
|
1431
|
+
return
|
1432
|
+
end
|
1433
|
+
names = []
|
1434
|
+
@sources = args.map do |arg|
|
1435
|
+
names << "=#{arg}"
|
1436
|
+
case arg.upcase
|
1437
|
+
when "WEB" then ""
|
1438
|
+
when "API" then nil
|
1439
|
+
else arg
|
1440
|
+
end
|
1441
|
+
end
|
1442
|
+
log(names.inject([]) do |r, name|
|
1443
|
+
s = r.join(", ")
|
1444
|
+
if s.size < 400
|
1445
|
+
r << name
|
1446
|
+
else
|
1447
|
+
log s
|
1448
|
+
[name]
|
1449
|
+
end
|
1450
|
+
end.join(", "))
|
1451
|
+
end
|
1452
|
+
|
1453
|
+
ctcp_action "bot", "drone" do |target, mesg, command, args|
|
1454
|
+
if args.empty?
|
1455
|
+
log "/me bot <NICK> [<NICK>...]"
|
1456
|
+
return
|
1457
|
+
end
|
1458
|
+
args.each do |bot|
|
1459
|
+
user = friend(bot)
|
1460
|
+
unless user
|
1461
|
+
post server_name, ERR_NOSUCHNICK, bot, "No such nick/channel"
|
1462
|
+
next
|
1463
|
+
end
|
1464
|
+
if @drones.delete(user.id)
|
1465
|
+
mode = "-#{mode}"
|
1466
|
+
log "#{bot} is no longer a bot."
|
1467
|
+
else
|
1468
|
+
@drones << user.id
|
1469
|
+
mode = "+#{mode}"
|
1470
|
+
log "Marks #{bot} as a bot."
|
1471
|
+
end
|
1472
|
+
end
|
1473
|
+
end
|
1474
|
+
|
1475
|
+
ctcp_action "home", "h" do |target, mesg, command, args|
|
1476
|
+
if args.empty?
|
1477
|
+
log "/me home <NICK>"
|
1478
|
+
return
|
1479
|
+
end
|
1480
|
+
nick = args.first
|
1481
|
+
if not nick.screen_name? or
|
1482
|
+
api("users/username_available", { :username => nick }).valid
|
1483
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
1484
|
+
return
|
1485
|
+
end
|
1486
|
+
log "http://twitter.com/#{nick}"
|
1487
|
+
end
|
1488
|
+
|
1489
|
+
ctcp_action "retweet", "rt" do |target, mesg, command, args|
|
1490
|
+
if args.empty?
|
1491
|
+
log "/me #{command} <ID> blah blah"
|
1492
|
+
return
|
1493
|
+
end
|
1494
|
+
tid = args.first
|
1495
|
+
if status = @timeline[tid]
|
1496
|
+
if args.size >= 2
|
1497
|
+
comment = mesg.split(" ", 3)[2] + " "
|
1498
|
+
else
|
1499
|
+
comment = ""
|
1500
|
+
end
|
1501
|
+
screen_name = "@#{status.user.screen_name}"
|
1502
|
+
rt_message = generate_status_message(status.text)
|
1503
|
+
text = "#{comment}RT #{screen_name}: #{rt_message}"
|
1504
|
+
ret = api("statuses/update", { :status => text, :source => source })
|
1505
|
+
log oops(ret) if ret.truncated
|
1506
|
+
log "Status updated (RT to #{@opts.tid % tid}: #{text})"
|
1507
|
+
ret.user.status = ret
|
1508
|
+
@me = ret.user
|
1509
|
+
end
|
1510
|
+
end
|
1511
|
+
|
1512
|
+
ctcp_action "o_retweet", "ort" do |target, mesg, command, args|
|
1513
|
+
if args.empty?
|
1514
|
+
log "/me #{command} <ID>"
|
1515
|
+
return
|
1516
|
+
end
|
1517
|
+
tid = args.first
|
1518
|
+
if status = @timeline[tid]
|
1519
|
+
ret = api("statuses/retweet/#{status.id}",{ :source => source })
|
1520
|
+
log oops(ret) if ret.truncated
|
1521
|
+
log "Status updated (RT to #{@opts.tid % tid}: #{status.text})"
|
1522
|
+
ret.user.status = ret
|
1523
|
+
@me = ret.user
|
1524
|
+
end
|
1525
|
+
end
|
1526
|
+
|
1527
|
+
|
1528
|
+
ctcp_action "spam" do |target, mesg, command, args|
|
1529
|
+
if args.empty?
|
1530
|
+
log "/me spam <NICK>|<ID>"
|
1531
|
+
return
|
1532
|
+
end
|
1533
|
+
nick_or_tid = args.first
|
1534
|
+
if status = @timeline[nick_or_tid]
|
1535
|
+
screen_name = status.user.screen_name
|
1536
|
+
else
|
1537
|
+
if not nick.screen_name? or
|
1538
|
+
api("users/username_available", { :username => nick }).valid
|
1539
|
+
post server_name, ERR_NOSUCHNICK, nick, "No such nick"
|
1540
|
+
return
|
1541
|
+
end
|
1542
|
+
screen_name = nick_or_tid
|
1543
|
+
end
|
1544
|
+
api("report_spam", { :screen_name => screen_name })
|
1545
|
+
log "reported user \"#{screen_name}\" as spammer"
|
1546
|
+
end
|
1547
|
+
|
1548
|
+
def on_ctcp_clientinfo(target, msg)
|
1549
|
+
if user = user(target)
|
1550
|
+
post prefix(user), NOTICE, @nick, ctcp_encode("CLIENTINFO :CLIENTINFO USERINFO VERSION TIME")
|
1551
|
+
end
|
1552
|
+
end
|
1553
|
+
|
1554
|
+
def on_ctcp_userinfo(target, msg)
|
1555
|
+
user = user(target)
|
1556
|
+
if user and not user.description.empty?
|
1557
|
+
post prefix(user), NOTICE, @nick, ctcp_encode("USERINFO :#{user.description}")
|
1558
|
+
end
|
1559
|
+
end
|
1560
|
+
|
1561
|
+
def on_ctcp_version(target, msg)
|
1562
|
+
user = user(target)
|
1563
|
+
if user and user.status
|
1564
|
+
source = user.status.source
|
1565
|
+
version = source.gsub(/<[^>]*>/, "").strip
|
1566
|
+
version << " <#{$1}>" if / href="([^"]+)/ === source
|
1567
|
+
post prefix(user), NOTICE, @nick, ctcp_encode("VERSION :#{version}")
|
1568
|
+
end
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
def on_ctcp_time(target, msg)
|
1572
|
+
if user = user(target)
|
1573
|
+
offset = user.utc_offset
|
1574
|
+
post prefix(user), NOTICE, @nick, ctcp_encode("TIME :%s%s (%s)" % [
|
1575
|
+
(Time.now + offset).utc.iso8601[0, 19],
|
1576
|
+
"%+.2d:%.2d" % (offset/60).divmod(60),
|
1577
|
+
user.time_zone,
|
1578
|
+
])
|
1579
|
+
end
|
1580
|
+
end
|
1581
|
+
|
1582
|
+
def check_lists
|
1583
|
+
updated = false
|
1584
|
+
until @friends
|
1585
|
+
@log.debug "waiting retrieving friends..."
|
1586
|
+
sleep 1
|
1587
|
+
end
|
1588
|
+
|
1589
|
+
lists = page("1/#{@me.screen_name}/lists", :lists, true)
|
1590
|
+
|
1591
|
+
# expend lists.size API count
|
1592
|
+
channels = {}
|
1593
|
+
lists.each do |list|
|
1594
|
+
begin
|
1595
|
+
name = (list.user.screen_name == @me.screen_name) ?
|
1596
|
+
"##{list.slug}" :
|
1597
|
+
"##{list.user.screen_name}^#{list.slug}"
|
1598
|
+
members = page("1/#{@me.screen_name}/#{list.slug}/members", :users, true)
|
1599
|
+
@log.debug "Miss match member_count '%s', lists:%d vs members:%s" % [ list.slug, list.member_count, members.size ] unless list.member_count == members.size
|
1600
|
+
if list.member_count - members.size > 10
|
1601
|
+
@log.debug "Miss match count is over 10. skip this list: #{list.slug}"
|
1602
|
+
next
|
1603
|
+
end
|
1604
|
+
|
1605
|
+
channel = {
|
1606
|
+
:name => name,
|
1607
|
+
:list => list,
|
1608
|
+
:members => members,
|
1609
|
+
:inclusion => (members - @friends).empty?
|
1610
|
+
}
|
1611
|
+
|
1612
|
+
new = channel[:members]
|
1613
|
+
old = @channels.fetch(channel[:name], { :members => [] })[:members]
|
1614
|
+
|
1615
|
+
# deleted user
|
1616
|
+
(old - new).each do|user|
|
1617
|
+
post prefix(user), PART, name, "Removed: #{user.screen_name}"
|
1618
|
+
updated = true
|
1619
|
+
end
|
1620
|
+
|
1621
|
+
# new user
|
1622
|
+
joined = join(name, new - old)
|
1623
|
+
updated = true unless joined.empty?
|
1624
|
+
|
1625
|
+
channels[name] = channel
|
1626
|
+
rescue APIFailed => e
|
1627
|
+
log e.inspect
|
1628
|
+
end
|
1629
|
+
end
|
1630
|
+
|
1631
|
+
# unfollowed
|
1632
|
+
(@channels.keys - channels.keys).each do |name|
|
1633
|
+
post @prefix, PART, name, "No longer follow the list #{name}"
|
1634
|
+
updated = true
|
1635
|
+
end
|
1636
|
+
|
1637
|
+
# followed
|
1638
|
+
(channels.keys - @channels.keys).each do |name|
|
1639
|
+
post @prefix, JOIN, name
|
1640
|
+
post server_name, MODE, name, "+mtio", @nick
|
1641
|
+
post server_name, MODE, name, "+q", @nick
|
1642
|
+
updated = true
|
1643
|
+
end
|
1644
|
+
|
1645
|
+
@channels = channels
|
1646
|
+
updated
|
1647
|
+
end
|
1648
|
+
|
1649
|
+
def check_lists_status
|
1650
|
+
friends = @friends || []
|
1651
|
+
@channels.each do |name, channel|
|
1652
|
+
# タイムラインに全員含まれているならとってこなくてもよいが
|
1653
|
+
# そうでなければ個別にとってくる必要がある。
|
1654
|
+
next if channel[:inclusion]
|
1655
|
+
|
1656
|
+
list = channel[:list]
|
1657
|
+
@log.debug "retrieve #{name} statuses"
|
1658
|
+
res = api("1/#{list.user.screen_name}/lists/#{list.id}/statuses", {
|
1659
|
+
:since_id => channel[:last_id]
|
1660
|
+
})
|
1661
|
+
res.reverse_each do |s|
|
1662
|
+
next if channel[:members].include? s.user
|
1663
|
+
command = (s.user.id == @me.id) ? NOTICE : PRIVMSG
|
1664
|
+
command = channel[:last_id] ? command : NOTICE
|
1665
|
+
# TODO tid
|
1666
|
+
message(s, name, nil, nil, command)
|
1667
|
+
end
|
1668
|
+
channel[:last_id] = res.first.id if res.first
|
1669
|
+
end
|
1670
|
+
end
|
1671
|
+
|
1672
|
+
def check_friends
|
1673
|
+
@follower_ids = page("followers/ids/#{@me.id}", :ids)
|
1674
|
+
|
1675
|
+
if @friends.nil?
|
1676
|
+
@friends = page("statuses/friends/#{@me.id}", :users)
|
1677
|
+
if @opts.athack
|
1678
|
+
join main_channel, @friends
|
1679
|
+
else
|
1680
|
+
rest = @friends.map do |i|
|
1681
|
+
prefix = "+" #@drones.include?(i.id) ? "%" : "+" # FIXME ~&%
|
1682
|
+
"#{prefix}#{i.screen_name}"
|
1683
|
+
end.reverse.inject("~#{@nick}") do |r, nick|
|
1684
|
+
if r.size < 400
|
1685
|
+
r << " " << nick
|
1686
|
+
else
|
1687
|
+
post server_name, RPL_NAMREPLY, @nick, "=", main_channel, r
|
1688
|
+
nick
|
1689
|
+
end
|
1690
|
+
end
|
1691
|
+
post server_name, RPL_NAMREPLY, @nick, "=", main_channel, rest
|
1692
|
+
post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
|
1693
|
+
end
|
1694
|
+
else
|
1695
|
+
@me = api("account/update_profile") #api("account/verify_credentials")
|
1696
|
+
if @me.friends_count != @friends.size
|
1697
|
+
new_ids = page("friends/ids/#{@me.id}", :ids)
|
1698
|
+
friend_ids = @friends.reverse.map {|friend| friend.id }
|
1699
|
+
|
1700
|
+
(friend_ids - new_ids).each do |id|
|
1701
|
+
@friends.delete_if do |friend|
|
1702
|
+
if friend.id == id
|
1703
|
+
post prefix(friend), PART, main_channel, ""
|
1704
|
+
end
|
1705
|
+
end
|
1706
|
+
end
|
1707
|
+
|
1708
|
+
new_ids -= friend_ids
|
1709
|
+
unless new_ids.empty?
|
1710
|
+
new_friends = page("statuses/friends/#{@me.id}", :users)
|
1711
|
+
join main_channel, new_friends.delete_if {|friend|
|
1712
|
+
@friends.any? {|i| i.id == friend.id }
|
1713
|
+
}.reverse
|
1714
|
+
@friends.concat new_friends
|
1715
|
+
end
|
1716
|
+
end
|
1717
|
+
end
|
1718
|
+
end
|
1719
|
+
|
1720
|
+
def check_timeline
|
1721
|
+
updated = false
|
1722
|
+
|
1723
|
+
cmd = PRIVMSG
|
1724
|
+
path = "statuses/#{@opts.with_retweets ? "home" : "friends"}_timeline"
|
1725
|
+
q = { :count => 200 }
|
1726
|
+
@latest_id ||= nil
|
1727
|
+
|
1728
|
+
case
|
1729
|
+
when @latest_id
|
1730
|
+
q.update(:since_id => @latest_id)
|
1731
|
+
when is_first_retrieve = !@me.statuses_count.zero? && !@me.friends_count.zero?
|
1732
|
+
# cmd = NOTICE # デバッグするときめんどくさいので
|
1733
|
+
q.update(:count => 20)
|
1734
|
+
end
|
1735
|
+
|
1736
|
+
api(path, q).reverse_each do |status|
|
1737
|
+
id = @latest_id = status.id
|
1738
|
+
next if @timeline.any? {|tid, s| s.id == id }
|
1739
|
+
|
1740
|
+
user = status.user
|
1741
|
+
tid = @timeline.push(status)
|
1742
|
+
tid = nil unless @opts.tid
|
1743
|
+
|
1744
|
+
@log.debug [id, user.screen_name, status.text].inspect
|
1745
|
+
|
1746
|
+
if user.id == @me.id
|
1747
|
+
mesg = generate_status_message(status.text)
|
1748
|
+
mesg << " " << @opts.tid % tid if tid
|
1749
|
+
post @prefix, TOPIC, main_channel, mesg
|
1750
|
+
|
1751
|
+
@me = user
|
1752
|
+
else
|
1753
|
+
if @friends
|
1754
|
+
@friends.each_with_index do |friend, i|
|
1755
|
+
if friend.id == user.id
|
1756
|
+
if friend.screen_name != user.screen_name
|
1757
|
+
post prefix(friend), NICK, user.screen_name
|
1758
|
+
end
|
1759
|
+
@friends[i] = user
|
1760
|
+
break
|
1761
|
+
end
|
1762
|
+
end
|
1763
|
+
end
|
1764
|
+
|
1765
|
+
message(status, main_channel, tid, nil, cmd)
|
1766
|
+
end
|
1767
|
+
@channels.each do |name, channel|
|
1768
|
+
if channel[:members].find{|m| m.screen_name == user.screen_name }
|
1769
|
+
message(status, name, tid, nil, (user.id == @me.id) ? NOTICE : cmd)
|
1770
|
+
end
|
1771
|
+
end
|
1772
|
+
updated = true
|
1773
|
+
end
|
1774
|
+
|
1775
|
+
updated
|
1776
|
+
end
|
1777
|
+
|
1778
|
+
def check_direct_messages
|
1779
|
+
updated = false
|
1780
|
+
@prev_dm_id ||= nil
|
1781
|
+
q = @prev_dm_id ? { :count => 200, :since_id => @prev_dm_id } \
|
1782
|
+
: { :count => 1 }
|
1783
|
+
api("direct_messages", q).reverse_each do |mesg|
|
1784
|
+
unless @prev_dm_id &&= mesg.id
|
1785
|
+
@prev_dm_id = mesg.id
|
1786
|
+
next
|
1787
|
+
end
|
1788
|
+
|
1789
|
+
id = mesg.id
|
1790
|
+
user = mesg.sender
|
1791
|
+
tid = nil
|
1792
|
+
text = mesg.text
|
1793
|
+
@log.debug [id, user.screen_name, text].inspect
|
1794
|
+
message(user, @nick, tid, text)
|
1795
|
+
updated = true
|
1796
|
+
end
|
1797
|
+
updated
|
1798
|
+
end
|
1799
|
+
|
1800
|
+
def check_mentions
|
1801
|
+
updated = false
|
1802
|
+
|
1803
|
+
return if @timeline.empty?
|
1804
|
+
@prev_mention_id ||= @timeline.last.id
|
1805
|
+
api("statuses/mentions", {
|
1806
|
+
:count => 200,
|
1807
|
+
:since_id => @prev_mention_id
|
1808
|
+
}).reverse_each do |mention|
|
1809
|
+
id = @prev_mention_id = mention.id
|
1810
|
+
next if @timeline.any? {|tid, s| s.id == id }
|
1811
|
+
|
1812
|
+
mention.user.status = mention
|
1813
|
+
user = mention.user
|
1814
|
+
tid = @timeline.push(mention)
|
1815
|
+
tid = nil unless @opts.tid
|
1816
|
+
|
1817
|
+
@log.debug [id, user.screen_name, mention.text].inspect
|
1818
|
+
message(mention, main_channel, tid)
|
1819
|
+
|
1820
|
+
@friends.each_with_index do |friend, i|
|
1821
|
+
if friend.id == user.id
|
1822
|
+
@friends[i] = user
|
1823
|
+
break
|
1824
|
+
end
|
1825
|
+
end if @friends
|
1826
|
+
updated = true
|
1827
|
+
end
|
1828
|
+
updated
|
1829
|
+
end
|
1830
|
+
|
1831
|
+
def check_updates
|
1832
|
+
uri = URI("https://api.github.com/repos/cho45/net-irc/commits/master")
|
1833
|
+
@log.debug uri.inspect
|
1834
|
+
res = http(uri).request(http_req(:get, uri))
|
1835
|
+
|
1836
|
+
latest = JSON.parse(res.body)['sha']
|
1837
|
+
|
1838
|
+
raise "github API changed?" unless latest
|
1839
|
+
|
1840
|
+
is_in_local_repos = system("git rev-parse --verify #{latest} > /dev/null 2>&1")
|
1841
|
+
unless is_in_local_repos
|
1842
|
+
log "\002New version is available.\017 run 'git pull'."
|
1843
|
+
end
|
1844
|
+
rescue Errno::ECONNREFUSED, Timeout::Error => e
|
1845
|
+
@log.error "Failed to get the latest revision of tig.rb from #{uri.host}: #{e.inspect}"
|
1846
|
+
end
|
1847
|
+
|
1848
|
+
def join(channel, users)
|
1849
|
+
params = []
|
1850
|
+
users.each do |user|
|
1851
|
+
prefix = prefix(user)
|
1852
|
+
post prefix, JOIN, channel
|
1853
|
+
case
|
1854
|
+
when user.protected
|
1855
|
+
params << ["v", prefix.nick]
|
1856
|
+
when ! @follower_ids.include?(user.id)
|
1857
|
+
params << ["o", prefix.nick]
|
1858
|
+
end
|
1859
|
+
next if params.size < MAX_MODE_PARAMS
|
1860
|
+
|
1861
|
+
post server_name, MODE, channel, "+#{params.map {|m,_| m }.join}", *params.map {|_,n| n}
|
1862
|
+
params = []
|
1863
|
+
end
|
1864
|
+
post server_name, MODE, channel, "+#{params.map {|m,_| m }.join}", *params.map {|_,n| n} unless params.empty?
|
1865
|
+
users
|
1866
|
+
end
|
1867
|
+
|
1868
|
+
def require_post?(path, query)
|
1869
|
+
case path.sub(/\.json$/, '')
|
1870
|
+
when %r{
|
1871
|
+
\A/
|
1872
|
+
(?: 1/ | 1\.1/ )?
|
1873
|
+
(?: status(?:es)?/update \z
|
1874
|
+
| direct_messages/new \z
|
1875
|
+
| friendships/create/
|
1876
|
+
| account/(?: end_session \z | update_ )
|
1877
|
+
| favou?ri(?: ing | tes )/create
|
1878
|
+
| notifications/
|
1879
|
+
| statuses/retweet/
|
1880
|
+
| blocks/create/
|
1881
|
+
| report_spam )
|
1882
|
+
}x
|
1883
|
+
true
|
1884
|
+
when %r{
|
1885
|
+
\A/
|
1886
|
+
(?: 1(\.1)?/#{@me.screen_name} )
|
1887
|
+
}x
|
1888
|
+
query.key? 'name' or query.key? '_method' or query.key? 'id'
|
1889
|
+
end
|
1890
|
+
end
|
1891
|
+
|
1892
|
+
#def require_put?(path)
|
1893
|
+
# %r{ \A status(?:es)?/retweet (?:/|\z) }x === path
|
1894
|
+
#end
|
1895
|
+
|
1896
|
+
def api(path, query = {}, opts = {})
|
1897
|
+
path.sub!(%r{\A/+}, "")
|
1898
|
+
|
1899
|
+
authenticate = opts.fetch(:authenticate, true)
|
1900
|
+
|
1901
|
+
path = '/1.1/' + path
|
1902
|
+
path += ".json" if path != "users/username_available"
|
1903
|
+
|
1904
|
+
header = {}
|
1905
|
+
credentials = authenticate ? [@real, @pass] : nil
|
1906
|
+
|
1907
|
+
ret = nil
|
1908
|
+
begin
|
1909
|
+
case
|
1910
|
+
when path.include?("/destroy/")
|
1911
|
+
path += '?' + query.to_query_str unless query.empty?
|
1912
|
+
@log.debug [:delete, path]
|
1913
|
+
ret = @access_token.delete(path, header)
|
1914
|
+
when require_post?(path, query)
|
1915
|
+
@log.debug [:post, path]
|
1916
|
+
ret = @access_token.post(path, query, header)
|
1917
|
+
else
|
1918
|
+
path += '?' + query.to_query_str unless query.empty?
|
1919
|
+
@log.debug [:get, path]
|
1920
|
+
ret = @access_token.get(path, header)
|
1921
|
+
end
|
1922
|
+
rescue OpenSSL::SSL::SSLError => e
|
1923
|
+
@log.error e.inspect
|
1924
|
+
log "Fatal SSL error was happened #{e.inspect}"
|
1925
|
+
raise e.inspect
|
1926
|
+
end
|
1927
|
+
|
1928
|
+
#@etags[uri.to_s] = ret["ETag"]
|
1929
|
+
|
1930
|
+
case
|
1931
|
+
when authenticate
|
1932
|
+
hourly_limit = ret["X-RateLimit-Limit"].to_i
|
1933
|
+
unless hourly_limit.zero?
|
1934
|
+
if @limit != hourly_limit
|
1935
|
+
msg = "The rate limit per hour was changed: #{@limit} to #{hourly_limit}"
|
1936
|
+
@log.info msg
|
1937
|
+
@limit = hourly_limit
|
1938
|
+
end
|
1939
|
+
|
1940
|
+
#if req.is_a?(Net::HTTP::Get) and not %w{
|
1941
|
+
if not %w{
|
1942
|
+
statuses/friends_timeline
|
1943
|
+
direct_messages
|
1944
|
+
statuses/mentions
|
1945
|
+
}.include?(path) and not ret.is_a?(Net::HTTPServerError)
|
1946
|
+
expired_on = Time.parse(ret["Date"]) rescue Time.now
|
1947
|
+
expired_on += 3636 # 1.01 hours in seconds later
|
1948
|
+
@consums << expired_on
|
1949
|
+
end
|
1950
|
+
end
|
1951
|
+
when ret["X-RateLimit-Remaining"]
|
1952
|
+
@limit_remaining_for_ip = ret["X-RateLimit-Remaining"].to_i
|
1953
|
+
@log.debug "IP based limit: #{@limit_remaining_for_ip}"
|
1954
|
+
end
|
1955
|
+
|
1956
|
+
case ret
|
1957
|
+
when Net::HTTPOK # 200
|
1958
|
+
# Avoid Twitter's invalid JSON
|
1959
|
+
json = ret.body.strip.sub(/\A(?:false|true)\z/, "[\\&]")
|
1960
|
+
|
1961
|
+
res = JSON.parse(json)
|
1962
|
+
if res.is_a?(Hash) && res["error"] # and not res["response"]
|
1963
|
+
if @error != res["error"]
|
1964
|
+
@error = res["error"]
|
1965
|
+
log @error
|
1966
|
+
end
|
1967
|
+
raise APIFailed, res["error"]
|
1968
|
+
end
|
1969
|
+
|
1970
|
+
TwitterStruct.make(res)
|
1971
|
+
when Net::HTTPNoContent, # 204
|
1972
|
+
Net::HTTPNotModified # 304
|
1973
|
+
[]
|
1974
|
+
when Net::HTTPBadRequest # 400: exceeded the rate limitation
|
1975
|
+
if ret.key?("X-RateLimit-Reset")
|
1976
|
+
s = ret["X-RateLimit-Reset"].to_i - Time.now.to_i
|
1977
|
+
if s > 0
|
1978
|
+
log "RateLimit: #{(s / 60.0).ceil} min remaining to get timeline"
|
1979
|
+
sleep (s > 60 * 10) ? 60 * 10 : s # 10 分に一回はとってくるように
|
1980
|
+
end
|
1981
|
+
end
|
1982
|
+
raise APIFailed, "#{ret.code}: #{ret.message}"
|
1983
|
+
when Net::HTTPUnauthorized # 401
|
1984
|
+
raise APIFailed, "#{ret.code}: #{ret.message}"
|
1985
|
+
else
|
1986
|
+
raise APIFailed, "Server Returned #{ret.code} #{ret.message}"
|
1987
|
+
end
|
1988
|
+
rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
|
1989
|
+
raise APIFailed, e.inspect
|
1990
|
+
end
|
1991
|
+
|
1992
|
+
def page(path, name, authenticate = false, &block)
|
1993
|
+
@limit_remaining_for_ip ||= 52
|
1994
|
+
limit = 0.98 * @limit_remaining_for_ip # 98% of IP based rate limit
|
1995
|
+
r = []
|
1996
|
+
cursor = -1
|
1997
|
+
1.upto(limit) do |num|
|
1998
|
+
# next_cursor にアクセスするとNot found が返ってくることがあるので,その時はbreak
|
1999
|
+
ret = api(path, { :cursor => cursor }, { :authenticate => authenticate }) rescue break
|
2000
|
+
arr = ret[name.to_s]
|
2001
|
+
r.concat arr
|
2002
|
+
cursor = ret[:next_cursor]
|
2003
|
+
break if cursor.zero?
|
2004
|
+
end
|
2005
|
+
r
|
2006
|
+
end
|
2007
|
+
|
2008
|
+
def generate_status_message(mesg)
|
2009
|
+
mesg = decode_utf7(mesg)
|
2010
|
+
mesg.delete!("\000\001")
|
2011
|
+
mesg.gsub!(">", ">")
|
2012
|
+
mesg.gsub!("<", "<")
|
2013
|
+
mesg.gsub!(WSP_REGEX, " ")
|
2014
|
+
mesg = untinyurl(mesg)
|
2015
|
+
mesg.sub!(@rsuffix_regex, "") if @rsuffix_regex
|
2016
|
+
mesg.strip
|
2017
|
+
end
|
2018
|
+
|
2019
|
+
def friend(id)
|
2020
|
+
return nil unless @friends
|
2021
|
+
if id.is_a? String
|
2022
|
+
@friends.find {|i| i.screen_name.casecmp(id).zero? }
|
2023
|
+
else
|
2024
|
+
@friends.find {|i| i.id == id }
|
2025
|
+
end
|
2026
|
+
end
|
2027
|
+
|
2028
|
+
def user(id)
|
2029
|
+
if id.is_a? String
|
2030
|
+
@nick.casecmp(id).zero? ? @me : friend(id)
|
2031
|
+
else
|
2032
|
+
@me.id == id ? @me : friend(id)
|
2033
|
+
end
|
2034
|
+
end
|
2035
|
+
|
2036
|
+
def prefix(u)
|
2037
|
+
nick = u.screen_name
|
2038
|
+
nick = "@#{nick}" if @opts.athack
|
2039
|
+
user = "id=%.9d" % u.id
|
2040
|
+
host = api_base.host
|
2041
|
+
host += "/protected" if u.protected
|
2042
|
+
host += "/bot" if @drones.include?(u.id)
|
2043
|
+
|
2044
|
+
Prefix.new("#{nick}!#{user}@#{host}")
|
2045
|
+
end
|
2046
|
+
|
2047
|
+
def message(struct, target, tid = nil, str = nil, command = PRIVMSG)
|
2048
|
+
unless str
|
2049
|
+
status = struct.status || struct
|
2050
|
+
str = status.text
|
2051
|
+
str = "\00310♺ \017" + str if status.retweeted_status
|
2052
|
+
if command != PRIVMSG
|
2053
|
+
time = Time.parse(status.created_at) rescue Time.now
|
2054
|
+
str = "#{time.strftime(@opts.strftime || "%m-%d %H:%M")} #{str}" # TODO: color
|
2055
|
+
end
|
2056
|
+
end
|
2057
|
+
user = struct.user || (struct.source && struct.source.screen_name && struct.source)|| struct
|
2058
|
+
screen_name = user.screen_name
|
2059
|
+
|
2060
|
+
user.screen_name = @nicknames[screen_name] || screen_name
|
2061
|
+
prefix = prefix(user)
|
2062
|
+
str = generate_status_message(str)
|
2063
|
+
str = "#{str} #{@opts.tid % tid}" if tid
|
2064
|
+
|
2065
|
+
post prefix, command, target, str
|
2066
|
+
end
|
2067
|
+
|
2068
|
+
def log(str)
|
2069
|
+
post server_name, NOTICE, main_channel, str.gsub(/\r\n|[\r\n]/, " ")
|
2070
|
+
end
|
2071
|
+
|
2072
|
+
def decode_utf7(str)
|
2073
|
+
return str unless defined? ::Iconv and str.include?("+")
|
2074
|
+
|
2075
|
+
str.sub!(/\A(?:.+ > |.+\z)/) { Iconv.iconv("UTF-8", "UTF-7", $&).join }
|
2076
|
+
#FIXME str = "[utf7]: #{str}" if str =~ /[^a-z0-9\s]/i
|
2077
|
+
str
|
2078
|
+
rescue Iconv::IllegalSequence
|
2079
|
+
str
|
2080
|
+
rescue => e
|
2081
|
+
@log.error e
|
2082
|
+
str
|
2083
|
+
end
|
2084
|
+
|
2085
|
+
def untinyurl(text)
|
2086
|
+
text.gsub(@opts.untiny_whole_urls ? URI.regexp(%w[http https]) : %r{
|
2087
|
+
http:// (?:
|
2088
|
+
(?: bit\.ly | (?: tin | rub) yurl\.com | j\.mp
|
2089
|
+
| is\.gd | cli\.gs | tr\.im | u\.nu | airme\.us
|
2090
|
+
| ff\.im | twurl.nl | bkite\.com | tumblr\.com
|
2091
|
+
| pic\.gd | sn\.im | digg\.com | t\.co)
|
2092
|
+
/ [0-9a-z=-]+ |
|
2093
|
+
blip\.fm/~ (?> [0-9a-z]+) (?! /) |
|
2094
|
+
flic\.kr/[a-z0-9/]+
|
2095
|
+
)
|
2096
|
+
}ix) {|url|
|
2097
|
+
expanded = resolve_http_redirect(URI(url))
|
2098
|
+
if %w|http https|.include? expanded.scheme
|
2099
|
+
expanded.to_s
|
2100
|
+
else
|
2101
|
+
"#{expanded.scheme}: #{url}"
|
2102
|
+
end
|
2103
|
+
}
|
2104
|
+
end
|
2105
|
+
|
2106
|
+
def bitlify(text)
|
2107
|
+
login, key, len = @opts.bitlify.split(":", 3) if @opts.bitlify
|
2108
|
+
len = (len || 20).to_i
|
2109
|
+
longurls = URI.extract(text, %w[http https]).uniq.map do |url|
|
2110
|
+
URI.rstrip url
|
2111
|
+
end.reject do |url|
|
2112
|
+
url.size < len || url =~ %r{http://(?:bit\.ly)}
|
2113
|
+
end
|
2114
|
+
return text if longurls.empty?
|
2115
|
+
|
2116
|
+
bitly = URI("http://api.bit.ly/v3/shorten")
|
2117
|
+
if login and key
|
2118
|
+
bitly.query = {
|
2119
|
+
:format => "json", :longUrl => longurls,
|
2120
|
+
}.to_query_str(";")
|
2121
|
+
@log.debug bitly
|
2122
|
+
req = http_req(:get, bitly, {}, [login, key])
|
2123
|
+
res = http(bitly, 5, 10).request(req)
|
2124
|
+
res = JSON.parse(res.body)
|
2125
|
+
res = res["results"]
|
2126
|
+
|
2127
|
+
longurls.each do |longurl|
|
2128
|
+
text.gsub!(longurl) do
|
2129
|
+
res[$&] && res[$&]["shortUrl"] || $&
|
2130
|
+
end
|
2131
|
+
end
|
2132
|
+
end
|
2133
|
+
|
2134
|
+
text
|
2135
|
+
rescue => e
|
2136
|
+
@log.error e
|
2137
|
+
text
|
2138
|
+
end
|
2139
|
+
|
2140
|
+
def unuify(text)
|
2141
|
+
unu_url = "http://u.nu/"
|
2142
|
+
unu = URI("#{unu_url}unu-api-simple")
|
2143
|
+
size = unu_url.size
|
2144
|
+
|
2145
|
+
text.gsub(URI.regexp(%w[http https])) do |url|
|
2146
|
+
url = URI.rstrip url
|
2147
|
+
if url.size < size + 5 or url[0, size] == unu_url
|
2148
|
+
return url
|
2149
|
+
end
|
2150
|
+
|
2151
|
+
unu.query = { :url => url }.to_query_str
|
2152
|
+
@log.debug unu
|
2153
|
+
|
2154
|
+
res = http(unu, 5, 5).request(http_req(:get, unu)).body
|
2155
|
+
|
2156
|
+
if res[0, 12] == unu_url
|
2157
|
+
res
|
2158
|
+
else
|
2159
|
+
raise res.split("|")
|
2160
|
+
end
|
2161
|
+
end
|
2162
|
+
rescue => e
|
2163
|
+
@log.error e
|
2164
|
+
text
|
2165
|
+
end
|
2166
|
+
|
2167
|
+
def escape_http_urls(text)
|
2168
|
+
original_text = text.encoding!("UTF-8").dup
|
2169
|
+
|
2170
|
+
if defined? ::Punycode
|
2171
|
+
# TODO: Nameprep
|
2172
|
+
text.gsub!(%r{(https?://)([^\x00-\x2C\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+)}) do
|
2173
|
+
domain = $2
|
2174
|
+
# Dots:
|
2175
|
+
# * U+002E (full stop) * U+3002 (ideographic full stop)
|
2176
|
+
# * U+FF0E (fullwidth full stop) * U+FF61 (halfwidth ideographic full stop)
|
2177
|
+
# => /[.\u3002\uFF0E\uFF61] # Ruby 1.9 /x
|
2178
|
+
$1 + domain.split(/\.|\343\200\202|\357\274\216|\357\275\241/).map do |label|
|
2179
|
+
break [domain] if /\A-|[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]|-\z/ === label
|
2180
|
+
next label unless /[^-A-Za-z0-9]/ === label
|
2181
|
+
punycode = Punycode.encode(label)
|
2182
|
+
break [domain] if punycode.size > 59
|
2183
|
+
"xn--#{punycode}"
|
2184
|
+
end.join(".")
|
2185
|
+
end
|
2186
|
+
if text != original_text
|
2187
|
+
log "Punycode encoded: #{text}"
|
2188
|
+
original_text = text.dup
|
2189
|
+
end
|
2190
|
+
end
|
2191
|
+
|
2192
|
+
urls = []
|
2193
|
+
text.split(/[\s<>]+/).each do |str|
|
2194
|
+
next if /%[0-9A-Fa-f]{2}/ === str
|
2195
|
+
# URI::UNSAFE + "#"
|
2196
|
+
escaped_str = URI.escape(str, %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]#]})
|
2197
|
+
URI.extract(escaped_str, %w[http https]).each do |url|
|
2198
|
+
uri = URI(URI.rstrip(url))
|
2199
|
+
if not urls.include?(uri.to_s) and exist_uri?(uri)
|
2200
|
+
urls << uri.to_s
|
2201
|
+
end
|
2202
|
+
end if escaped_str != str
|
2203
|
+
end
|
2204
|
+
urls.each do |url|
|
2205
|
+
unescaped_url = URI.unescape(url).encoding!("UTF-8")
|
2206
|
+
text.gsub!(unescaped_url, url)
|
2207
|
+
end
|
2208
|
+
log "Percent encoded: #{text}" if text != original_text
|
2209
|
+
|
2210
|
+
text.encoding!("ASCII-8BIT")
|
2211
|
+
rescue => e
|
2212
|
+
@log.error e
|
2213
|
+
text
|
2214
|
+
end
|
2215
|
+
|
2216
|
+
def exist_uri?(uri, limit = 1)
|
2217
|
+
ret = nil
|
2218
|
+
#raise "Not supported." unless uri.is_a?(URI::HTTP)
|
2219
|
+
return ret if limit.zero? or uri.nil? or not uri.is_a?(URI::HTTP)
|
2220
|
+
@log.debug uri.inspect
|
2221
|
+
|
2222
|
+
req = http_req :head, uri
|
2223
|
+
http(uri, 3, 2).request(req) do |res|
|
2224
|
+
ret = case res
|
2225
|
+
when Net::HTTPSuccess
|
2226
|
+
true
|
2227
|
+
when Net::HTTPRedirection
|
2228
|
+
uri = resolve_http_redirect(uri)
|
2229
|
+
exist_uri?(uri, limit - 1)
|
2230
|
+
when Net::HTTPClientError
|
2231
|
+
false
|
2232
|
+
#when Net::HTTPServerError
|
2233
|
+
# nil
|
2234
|
+
else
|
2235
|
+
nil
|
2236
|
+
end
|
2237
|
+
end
|
2238
|
+
|
2239
|
+
ret
|
2240
|
+
rescue => e
|
2241
|
+
@log.error e.inspect
|
2242
|
+
ret
|
2243
|
+
end
|
2244
|
+
|
2245
|
+
def resolve_http_redirect(uri, limit = 3)
|
2246
|
+
return uri if limit.zero? or uri.nil?
|
2247
|
+
@log.debug uri.inspect
|
2248
|
+
|
2249
|
+
req = http_req :head, uri
|
2250
|
+
http(uri, 3, 2).request(req) do |res|
|
2251
|
+
break if not res.is_a?(Net::HTTPRedirection) or
|
2252
|
+
not res.key?("Location")
|
2253
|
+
begin
|
2254
|
+
location = URI(res["Location"])
|
2255
|
+
rescue URI::InvalidURIError
|
2256
|
+
end
|
2257
|
+
unless location.is_a? URI::HTTP
|
2258
|
+
begin
|
2259
|
+
location = URI.join(uri.to_s, res["Location"])
|
2260
|
+
rescue URI::InvalidURIError, URI::BadURIError
|
2261
|
+
# FIXME
|
2262
|
+
end
|
2263
|
+
end
|
2264
|
+
uri = resolve_http_redirect(location, limit - 1)
|
2265
|
+
end
|
2266
|
+
|
2267
|
+
uri
|
2268
|
+
rescue => e
|
2269
|
+
@log.error e.inspect
|
2270
|
+
uri
|
2271
|
+
end
|
2272
|
+
|
2273
|
+
def update_sources(n = 0)
|
2274
|
+
if @sources and @sources.size > 1 and n.zero?
|
2275
|
+
log "tig.rb"
|
2276
|
+
@sources = [api_source]
|
2277
|
+
return @sources
|
2278
|
+
end
|
2279
|
+
|
2280
|
+
uri = URI("http://wedata.net/databases/TwitterSources/items.json")
|
2281
|
+
@log.debug uri.inspect
|
2282
|
+
json = http(uri).request(http_req(:get, uri)).body
|
2283
|
+
sources = JSON.parse json
|
2284
|
+
sources.map! {|item| [item["data"]["source"], item["name"]] }
|
2285
|
+
sources.push ["", "web"]
|
2286
|
+
sources.push [nil, "API"]
|
2287
|
+
|
2288
|
+
sources = Array.new(n) do
|
2289
|
+
sources.delete_at(rand(sources.size))
|
2290
|
+
end if (1 ... sources.size).include?(n)
|
2291
|
+
|
2292
|
+
log(sources.inject([]) do |r, src|
|
2293
|
+
s = r.join(", ")
|
2294
|
+
if s.size < 400
|
2295
|
+
r << src[1]
|
2296
|
+
else
|
2297
|
+
log s
|
2298
|
+
[src[1]]
|
2299
|
+
end
|
2300
|
+
end.join(", ")) if @sources
|
2301
|
+
|
2302
|
+
@sources = sources.map {|src| src[0] }
|
2303
|
+
rescue => e
|
2304
|
+
@log.error e.inspect
|
2305
|
+
log "An error occured while loading #{uri.host}."
|
2306
|
+
@sources ||= [api_source]
|
2307
|
+
end
|
2308
|
+
|
2309
|
+
def update_redundant_suffix
|
2310
|
+
uri = URI("http://svn.coderepos.org/share/platform/twitterircgateway/suffixesblacklist.txt")
|
2311
|
+
@log.debug uri.inspect
|
2312
|
+
res = http(uri).request(http_req(:get, uri))
|
2313
|
+
@etags[uri.to_s] = res["ETag"]
|
2314
|
+
return if res.is_a? Net::HTTPNotModified
|
2315
|
+
source = res.body
|
2316
|
+
source.encoding!("UTF-8") if source.respond_to?(:encoding) and source.encoding == Encoding::BINARY
|
2317
|
+
@rsuffix_regex = /#{Regexp.union(*source.split)}\z/
|
2318
|
+
rescue Errno::ECONNREFUSED, Timeout::Error => e
|
2319
|
+
@log.error "Failed to get the redundant suffix blacklist from #{uri.host}: #{e.inspect}"
|
2320
|
+
end
|
2321
|
+
|
2322
|
+
def http(uri, open_timeout = nil, read_timeout = 60)
|
2323
|
+
http = case
|
2324
|
+
when @httpproxy
|
2325
|
+
Net::HTTP.new(uri.host, uri.port, @httpproxy.address, @httpproxy.port,
|
2326
|
+
@httpproxy.user, @httpproxy.password)
|
2327
|
+
when ENV["HTTP_PROXY"], ENV["http_proxy"]
|
2328
|
+
proxy = URI(ENV["HTTP_PROXY"] || ENV["http_proxy"])
|
2329
|
+
Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port,
|
2330
|
+
proxy.user, proxy.password)
|
2331
|
+
else
|
2332
|
+
Net::HTTP.new(uri.host, uri.port)
|
2333
|
+
end
|
2334
|
+
http.open_timeout = open_timeout if open_timeout # nil by default
|
2335
|
+
http.read_timeout = read_timeout if read_timeout # 60 by default
|
2336
|
+
if uri.is_a? URI::HTTPS
|
2337
|
+
http.use_ssl = true
|
2338
|
+
http.cert_store = @cert_store
|
2339
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
2340
|
+
end
|
2341
|
+
http
|
2342
|
+
rescue => e
|
2343
|
+
@log.error e
|
2344
|
+
end
|
2345
|
+
|
2346
|
+
def http_req(method, uri, header = {}, credentials = nil)
|
2347
|
+
accepts = ["*/*"]
|
2348
|
+
#require "mime/types"; accepts.unshift MIME::Types.of(uri.path).first.simplified
|
2349
|
+
types = { "json" => "application/json", "txt" => "text/plain" }
|
2350
|
+
ext = uri.path[/[^.]+\z/]
|
2351
|
+
accepts.unshift types[ext] if types.key?(ext)
|
2352
|
+
user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)}; net-irc) Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
|
2353
|
+
|
2354
|
+
header["User-Agent"] ||= user_agent
|
2355
|
+
header["Accept"] ||= accepts.join(",")
|
2356
|
+
header["Accept-Charset"] ||= "UTF-8,*;q=0.0" if ext != "json"
|
2357
|
+
#header["Accept-Language"] ||= @opts.lang # "en-us,en;q=0.9,ja;q=0.5"
|
2358
|
+
header["If-None-Match"] ||= @etags[uri.to_s] if @etags[uri.to_s]
|
2359
|
+
|
2360
|
+
req = case method.to_s.downcase.to_sym
|
2361
|
+
when :get
|
2362
|
+
Net::HTTP::Get.new uri.request_uri, header
|
2363
|
+
when :head
|
2364
|
+
Net::HTTP::Head.new uri.request_uri, header
|
2365
|
+
when :post
|
2366
|
+
Net::HTTP::Post.new uri.path, header
|
2367
|
+
when :put
|
2368
|
+
Net::HTTP::Put.new uri.path, header
|
2369
|
+
when :delete
|
2370
|
+
Net::HTTP::Delete.new uri.request_uri, header
|
2371
|
+
else # raise ""
|
2372
|
+
end
|
2373
|
+
if req.request_body_permitted?
|
2374
|
+
req["Content-Type"] ||= "application/x-www-form-urlencoded"
|
2375
|
+
req.body = uri.query
|
2376
|
+
end
|
2377
|
+
req.basic_auth(*credentials) if credentials
|
2378
|
+
req
|
2379
|
+
rescue => e
|
2380
|
+
@log.error e
|
2381
|
+
end
|
2382
|
+
|
2383
|
+
def oops(status)
|
2384
|
+
"Oops! Your update was over 140 characters. We sent the short version" <<
|
2385
|
+
" to your friends (they can view the entire update on the Web <" <<
|
2386
|
+
permalink(status) << ">)."
|
2387
|
+
end
|
2388
|
+
|
2389
|
+
def permalink(struct)
|
2390
|
+
"http://twitter.com/#{struct.user.screen_name}/statuses/#{struct.id}"
|
2391
|
+
end
|
2392
|
+
|
2393
|
+
def source
|
2394
|
+
@sources[rand(@sources.size)]
|
2395
|
+
end
|
2396
|
+
|
2397
|
+
def initial_message
|
2398
|
+
super
|
2399
|
+
post server_name, RPL_ISUPPORT, @nick,
|
2400
|
+
"PREFIX=(qaohv)~&@%+", "CHANTYPES=#", "CHANMODES=,,,mnti",
|
2401
|
+
"MODES=#{MAX_MODE_PARAMS}", "NICKLEN=15", "TOPICLEN=420", "CHANNELLEN=50",
|
2402
|
+
"NETWORK=Twitter",
|
2403
|
+
"are supported by this server"
|
2404
|
+
end
|
2405
|
+
|
2406
|
+
class TwitterStruct
|
2407
|
+
def self.make(obj)
|
2408
|
+
case obj
|
2409
|
+
when Hash
|
2410
|
+
obj = obj.dup
|
2411
|
+
obj.each do |k, v|
|
2412
|
+
obj[k] = TwitterStruct.make(v)
|
2413
|
+
end
|
2414
|
+
TwitterStruct.new(obj)
|
2415
|
+
when Array
|
2416
|
+
obj.map {|i| TwitterStruct.make(i) }
|
2417
|
+
else
|
2418
|
+
obj
|
2419
|
+
end
|
2420
|
+
end
|
2421
|
+
|
2422
|
+
def initialize(obj)
|
2423
|
+
@obj = obj
|
2424
|
+
end
|
2425
|
+
|
2426
|
+
def id
|
2427
|
+
@obj["id"]
|
2428
|
+
end
|
2429
|
+
|
2430
|
+
def [](name)
|
2431
|
+
@obj[name.to_s]
|
2432
|
+
end
|
2433
|
+
|
2434
|
+
def hash
|
2435
|
+
self.id ? self.id.hash : super
|
2436
|
+
end
|
2437
|
+
|
2438
|
+
def eql?(other)
|
2439
|
+
self.hash == other.hash
|
2440
|
+
end
|
2441
|
+
|
2442
|
+
def ==(other)
|
2443
|
+
self.hash == other.hash
|
2444
|
+
end
|
2445
|
+
|
2446
|
+
def method_missing(sym, *args)
|
2447
|
+
# XXX
|
2448
|
+
@obj[sym.to_s]
|
2449
|
+
end
|
2450
|
+
end
|
2451
|
+
|
2452
|
+
class TypableMap < Hash
|
2453
|
+
#Roman = %w[
|
2454
|
+
# 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
|
2455
|
+
#].unshift("").map do |consonant|
|
2456
|
+
# case consonant
|
2457
|
+
# when "h", "q" then %w|a i e o|
|
2458
|
+
# when /[hy]$/ then %w|a u o|
|
2459
|
+
# else %w|a i u e o|
|
2460
|
+
# end.map {|vowel| "#{consonant}#{vowel}" }
|
2461
|
+
#end.flatten
|
2462
|
+
Roman = %w[
|
2463
|
+
a i u e o ka ki ku ke ko sa shi su se so
|
2464
|
+
ta chi tsu te to na ni nu ne no ha hi fu he ho
|
2465
|
+
ma mi mu me mo ya yu yo ra ri ru re ro
|
2466
|
+
wa wo n
|
2467
|
+
ga gi gu ge go za ji zu ze zo da de do
|
2468
|
+
ba bi bu be bo pa pi pu pe po
|
2469
|
+
kya kyu kyo sha shu sho cha chu cho
|
2470
|
+
nya nyu nyo hya hyu hyo mya myu myo
|
2471
|
+
rya ryu ryo
|
2472
|
+
gya gyu gyo ja ju jo bya byu byo
|
2473
|
+
pya pyu pyo
|
2474
|
+
].freeze
|
2475
|
+
|
2476
|
+
def initialize(size = nil, shuffle = false)
|
2477
|
+
if shuffle
|
2478
|
+
@seq = Roman.dup
|
2479
|
+
if @seq.respond_to?(:shuffle!)
|
2480
|
+
@seq.shuffle!
|
2481
|
+
else
|
2482
|
+
@seq = Array.new(@seq.size) { @seq.delete_at(rand(@seq.size)) }
|
2483
|
+
end
|
2484
|
+
@seq.freeze
|
2485
|
+
else
|
2486
|
+
@seq = Roman
|
2487
|
+
end
|
2488
|
+
@n = 0
|
2489
|
+
@size = size || @seq.size
|
2490
|
+
end
|
2491
|
+
|
2492
|
+
def generate(n)
|
2493
|
+
ret = []
|
2494
|
+
begin
|
2495
|
+
n, r = n.divmod(@seq.size)
|
2496
|
+
ret << @seq[r]
|
2497
|
+
end while n > 0
|
2498
|
+
ret.reverse.join #.gsub(/n(?=[bmp])/, "m")
|
2499
|
+
end
|
2500
|
+
|
2501
|
+
def push(obj)
|
2502
|
+
id = generate(@n)
|
2503
|
+
self[id] = obj
|
2504
|
+
@n += 1
|
2505
|
+
@n %= @size
|
2506
|
+
id
|
2507
|
+
end
|
2508
|
+
alias :<< :push
|
2509
|
+
|
2510
|
+
def clear
|
2511
|
+
@n = 0
|
2512
|
+
super
|
2513
|
+
end
|
2514
|
+
|
2515
|
+
def first
|
2516
|
+
@size.times do |i|
|
2517
|
+
id = generate((@n + i) % @size)
|
2518
|
+
return self[id] if key? id
|
2519
|
+
end unless empty?
|
2520
|
+
nil
|
2521
|
+
end
|
2522
|
+
|
2523
|
+
def last
|
2524
|
+
@size.times do |i|
|
2525
|
+
id = generate((@n - 1 - i) % @size)
|
2526
|
+
return self[id] if key? id
|
2527
|
+
end unless empty?
|
2528
|
+
nil
|
2529
|
+
end
|
2530
|
+
|
2531
|
+
private :[]=
|
2532
|
+
end
|
2533
|
+
|
2534
|
+
class RateLimit
|
2535
|
+
def initialize(limit)
|
2536
|
+
@limit = limit
|
2537
|
+
@rates = {}
|
2538
|
+
end
|
2539
|
+
|
2540
|
+
def register(name, init_second=60)
|
2541
|
+
@rates[name.to_sym] = {
|
2542
|
+
:init => init_second.to_f,
|
2543
|
+
:rate => init_second.to_f,
|
2544
|
+
}
|
2545
|
+
end
|
2546
|
+
|
2547
|
+
def unregister(name)
|
2548
|
+
@rates.delete(name)
|
2549
|
+
end
|
2550
|
+
|
2551
|
+
def inspect
|
2552
|
+
"#<%s:0x%08x %s>" % [self.class, self.__id__,
|
2553
|
+
@rates.keys.map {|name| "#{name}:#{interval(name)}" }.join(' ')
|
2554
|
+
]
|
2555
|
+
end
|
2556
|
+
|
2557
|
+
def interval(name)
|
2558
|
+
rate = (3600.0 / @rates[name][:rate]) / @rates.values.inject(0) {|r,i| r + 3600.0 / i[:rate] }
|
2559
|
+
count = @limit * rate
|
2560
|
+
(3600 / count).to_i
|
2561
|
+
end
|
2562
|
+
|
2563
|
+
def incr(name)
|
2564
|
+
@rates[name][:rate] /= 2
|
2565
|
+
@rates[name][:rate] = 10 if @rates[name][:rate] < 10
|
2566
|
+
end
|
2567
|
+
|
2568
|
+
def decr(name)
|
2569
|
+
@rates[name][:rate] *= 2
|
2570
|
+
@rates[name][:rate] = 3600 if @rates[name][:rate] > 3600
|
2571
|
+
end
|
2572
|
+
end
|
2573
|
+
|
2574
|
+
end
|
2575
|
+
|
2576
|
+
class Hash
|
2577
|
+
# { :f => "v" } #=> "f=v"
|
2578
|
+
# { "f" => [1, 2] } #=> "f=1&f=2"
|
2579
|
+
# { "f" => "" } #=> "f="
|
2580
|
+
# { "f" => nil } #=> "f"
|
2581
|
+
def to_query_str separator = "&"
|
2582
|
+
inject([]) do |r, (k, v)|
|
2583
|
+
k = URI.encode_component k.to_s
|
2584
|
+
(v.is_a?(Array) ? v : [v]).each do |i|
|
2585
|
+
if i.nil?
|
2586
|
+
r << k
|
2587
|
+
else
|
2588
|
+
r << "#{k}=#{URI.encode_component i.to_s}"
|
2589
|
+
end
|
2590
|
+
end
|
2591
|
+
r
|
2592
|
+
end.join separator
|
2593
|
+
end
|
2594
|
+
end
|
2595
|
+
|
2596
|
+
class String
|
2597
|
+
def ch?
|
2598
|
+
/\A[&#+!][^ \007,]{1,50}\z/ === self
|
2599
|
+
end
|
2600
|
+
|
2601
|
+
def screen_name?
|
2602
|
+
/\A[A-Za-z0-9_]{1,15}\z/ === self
|
2603
|
+
end
|
2604
|
+
|
2605
|
+
def encoding! enc
|
2606
|
+
return self unless respond_to? :force_encoding
|
2607
|
+
force_encoding enc
|
2608
|
+
end
|
2609
|
+
end
|
2610
|
+
|
2611
|
+
module URI::Escape
|
2612
|
+
# alias :_orig_escape :escape
|
2613
|
+
#
|
2614
|
+
# if defined? ::RUBY_REVISION and RUBY_REVISION < 24544
|
2615
|
+
# # URI.escape("あ1") #=> "%E3%81%82\xEF\xBC\x91"
|
2616
|
+
# # URI("file:///4") #=> #<URI::Generic:0x9d09db0 URL:file:/4>
|
2617
|
+
# # "\\d" -> "[0-9]" for Ruby 1.9
|
2618
|
+
# def escape str, unsafe = %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]]}
|
2619
|
+
# _orig_escape(str, unsafe)
|
2620
|
+
# end
|
2621
|
+
# alias :encode :escape
|
2622
|
+
# end
|
2623
|
+
|
2624
|
+
def encode_component str, unsafe = /[^-_.!~*'()a-zA-Z0-9 ]/
|
2625
|
+
escape(str, unsafe).tr(" ", "+")
|
2626
|
+
end
|
2627
|
+
|
2628
|
+
def rstrip str
|
2629
|
+
str.sub(%r{
|
2630
|
+
(?: ( / [^/?#()]* (?: \( [^/?#()]* \) [^/?#()]* )* ) \) [^/?#()]*
|
2631
|
+
| \.
|
2632
|
+
) \z
|
2633
|
+
}x, "\\1")
|
2634
|
+
end
|
2635
|
+
end
|
2636
|
+
|
2637
|
+
if __FILE__ == $0
|
2638
|
+
require "optparse"
|
2639
|
+
|
2640
|
+
opts = {
|
2641
|
+
:port => 16668,
|
2642
|
+
:host => "localhost",
|
2643
|
+
:log => nil,
|
2644
|
+
:debug => false,
|
2645
|
+
:foreground => false,
|
2646
|
+
}
|
2647
|
+
|
2648
|
+
OptionParser.new do |parser|
|
2649
|
+
parser.instance_eval do
|
2650
|
+
self.banner = <<-EOB.gsub(/^\t+/, "")
|
2651
|
+
Usage: #{$0} [opts]
|
2652
|
+
|
2653
|
+
EOB
|
2654
|
+
|
2655
|
+
separator ""
|
2656
|
+
|
2657
|
+
separator "Options:"
|
2658
|
+
on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
|
2659
|
+
opts[:port] = port
|
2660
|
+
end
|
2661
|
+
|
2662
|
+
on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
|
2663
|
+
opts[:host] = host
|
2664
|
+
end
|
2665
|
+
|
2666
|
+
on("-l", "--log LOG", "log file") do |log|
|
2667
|
+
opts[:log] = log
|
2668
|
+
end
|
2669
|
+
|
2670
|
+
on("--debug", "Enable debug mode") do |debug|
|
2671
|
+
opts[:log] = $stdout
|
2672
|
+
opts[:debug] = true
|
2673
|
+
end
|
2674
|
+
|
2675
|
+
on("-f", "--foreground", "run foreground") do |foreground|
|
2676
|
+
opts[:log] = $stdout
|
2677
|
+
opts[:foreground] = true
|
2678
|
+
end
|
2679
|
+
|
2680
|
+
on("-n", "--name [user name or email address]") do |name|
|
2681
|
+
opts[:name] = name
|
2682
|
+
end
|
2683
|
+
|
2684
|
+
parse!(ARGV)
|
2685
|
+
end
|
2686
|
+
end
|
2687
|
+
|
2688
|
+
opts[:logger] = Logger.new(opts[:log], "daily")
|
2689
|
+
opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
|
2690
|
+
opts[:logger].level = Logger::INFO
|
2691
|
+
|
2692
|
+
#def daemonize(foreground = false)
|
2693
|
+
# [:INT, :TERM, :HUP].each do |sig|
|
2694
|
+
# Signal.trap sig, "EXIT"
|
2695
|
+
# end
|
2696
|
+
# return yield if $DEBUG or foreground
|
2697
|
+
# Process.fork do
|
2698
|
+
# Process.setsid
|
2699
|
+
# Dir.chdir "/"
|
2700
|
+
# STDIN.reopen "/dev/null"
|
2701
|
+
# STDOUT.reopen "/dev/null", "a"
|
2702
|
+
# STDERR.reopen STDOUT
|
2703
|
+
# yield
|
2704
|
+
# end
|
2705
|
+
# exit! 0
|
2706
|
+
#end
|
2707
|
+
|
2708
|
+
#daemonize(opts[:debug] || opts[:foreground]) do
|
2709
|
+
Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start
|
2710
|
+
#end
|
2711
|
+
end
|
2712
|
+
|