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.
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!("&gt;", ">")
2012
+ mesg.gsub!("&lt;", "<")
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
+