net-irc2 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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
+