net-irc 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,327 @@
1
+ # Ruby client for the Lingr[http://www.lingr.com] API. For more details and tutorials, see the
2
+ # {Lingr API Reference}[http://wiki.lingr.com/dev/show/API+Reference] pages on the {Lingr Developer Wiki}[http://wiki.lingr.com].
3
+ #
4
+ # All methods return a hash with two keys:
5
+ # * :succeeded - <tt>true</tt> if the method succeeded, <tt>false</tt> otherwise
6
+ # * :response - a Hash version of the response document received from the server
7
+ #
8
+ # = api_client.rb
9
+ #
10
+ # Lingr API client
11
+ #
12
+ #
13
+ # Original written by Lingr.
14
+ # Modified by cho45 <cho45@lowreal.net>
15
+ # * Use json gem instead of gsub/eval.
16
+ # * Raise APIError when api fails.
17
+ # * Rename class name to Lingr::Client.
18
+
19
+ $KCODE = 'u' # used by json
20
+ require "rubygems"
21
+ require "net/http"
22
+ require "json"
23
+ require "uri"
24
+ require "timeout"
25
+
26
+ module Lingr
27
+ class Client
28
+ class ClientError < StandardError; end
29
+ class APIError < ClientError
30
+ def initialize(error)
31
+ @error = error || {
32
+ "message" => "socket error",
33
+ "code" => 0,
34
+ }
35
+ super(@error["message"])
36
+ end
37
+
38
+ def code
39
+ @error["code"]
40
+ end
41
+ end
42
+
43
+ attr_accessor :api_key
44
+ # 0 = quiet, 1 = some debug info, 2 = more debug info
45
+ attr_accessor :verbosity
46
+ attr_accessor :session
47
+ attr_accessor :timeout
48
+
49
+ def initialize(api_key, verbosity=0, hostname='www.lingr.com')
50
+ @api_key = api_key
51
+ @host = hostname
52
+ @verbosity = verbosity
53
+ @timeout = 60
54
+ end
55
+
56
+ # Create a new API session
57
+ #
58
+ def create_session(client_type='automaton')
59
+ if @session
60
+ @error_info = nil
61
+ raise ClientError, "already in a session"
62
+ end
63
+
64
+ ret = do_api :post, 'session/create', { :api_key => @api_key, :client_type => client_type }, false
65
+ @session = ret["session"]
66
+ ret
67
+ end
68
+
69
+ # Verify a session id. If no session id is passed, verifies the current session id for this ApiClient
70
+ #
71
+ def verify_session(session_id=nil)
72
+ do_api :get, 'session/verify', { :session => session_id || @session }, false
73
+ end
74
+
75
+ # Destroy the current API session
76
+ #
77
+ def destroy_session
78
+ ret = do_api :post, 'session/destroy', { :session => @session }
79
+ @session = nil
80
+ ret
81
+ end
82
+
83
+ # Get a list of the currently hot rooms
84
+ #
85
+ def get_hot_rooms(count=nil)
86
+ do_api :get, 'explore/get_hot_rooms', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false
87
+ end
88
+
89
+ # Get a list of the newest rooms
90
+ #
91
+ def get_new_rooms(count=nil)
92
+ do_api :get, 'explore/get_new_rooms', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false
93
+ end
94
+
95
+ # Get a list of the currently hot tags
96
+ #
97
+ def get_hot_tags(count=nil)
98
+ do_api :get, 'explore/get_hot_tags', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false
99
+ end
100
+
101
+ # Get a list of all tags
102
+ #
103
+ def get_all_tags(count=nil)
104
+ do_api :get, 'explore/get_all_tags', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false
105
+ end
106
+
107
+ # Search room name, description, and tags for keywords. Keywords can be a String or an Array.
108
+ #
109
+ def search(keywords)
110
+ do_api :get, 'explore/search', { :api_key => @api_key, :q => keywords.is_a?(Array) ? keywords.join(',') : keywords }, false
111
+ end
112
+
113
+ # Search room tags. Tagnames can be a String or an Array.
114
+ #
115
+ def search_tags(tagnames)
116
+ do_api :get, 'explore/search_tags', { :api_key => @api_key, :q => tagnames.is_a?(Array) ? tagnames.join(',') : tagnames }, false
117
+ end
118
+
119
+ # Search archives. If room_id is non-nil, the search is limited to the archives of that room.
120
+ #
121
+ def search_archives(query, room_id=nil)
122
+ params = { :api_key => @api_key, :q => query }
123
+ params.merge!({ :id => room_id }) if room_id
124
+ do_api :get, 'explore/search_archives', params, false
125
+ end
126
+
127
+ # Authenticate a user within the current API session
128
+ #
129
+ def login(email, password)
130
+ do_api :post, 'auth/login', { :session => @session, :email => email, :password => password }
131
+ end
132
+
133
+ # Log out the currently-authenticated user in the session, if any
134
+ #
135
+ def logout
136
+ do_api :post, 'auth/logout', { :session => @session }
137
+ end
138
+
139
+ # Get information about the currently-authenticated user
140
+ #
141
+ def get_user_info
142
+ do_api :get, 'user/get_info', { :session => @session }
143
+ end
144
+
145
+ # Start observing the currently-authenticated user
146
+ #
147
+ def start_observing_user
148
+ do_api :post, 'user/start_observing', { :session => @session }
149
+ end
150
+
151
+ # Observe the currently-authenticated user, watching for profile changes
152
+ #
153
+ def observe_user(ticket, counter)
154
+ do_api :get, 'user/observe', { :session => @session, :ticket => ticket, :counter => counter }
155
+ end
156
+
157
+ # Stop observing the currently-authenticated user
158
+ #
159
+ def stop_observing_user(ticket)
160
+ do_api :post, 'user/stop_observing', { :session => @session, :ticket =>ticket }
161
+ end
162
+
163
+ # Get information about a chatroom, including room description, current occupants, recent messages, etc.
164
+ #
165
+ def get_room_info(room_id, counter=nil, password=nil)
166
+ params = { :api_key => @api_key, :id => room_id }
167
+ params.merge!({ :counter => counter }) if counter
168
+ params.merge!({ :password => password }) if password
169
+ do_api :get, 'room/get_info', params, false
170
+ end
171
+
172
+ # Create a chatroom
173
+ #
174
+ # options is a Hash containing any of the parameters allowed for room.create. If the :image key is present
175
+ # in options, its value must be a hash with the keys :filename, :mime_type, and :io
176
+ #
177
+ def create_room(options)
178
+ do_api :post, 'room/create', options.merge({ :session => @session })
179
+ end
180
+
181
+ # Change the settings for a chatroom
182
+ #
183
+ # options is a Hash containing any of the parameters allowed for room.create. If the :image key is present
184
+ # in options, its value must be a hash with the keys :filename, :mime_type, and :io. To change the id for
185
+ # a room, use the key :new_id
186
+ #
187
+ def change_settings(room_id, options)
188
+ do_api :post, 'room/change_settings', options.merge({ :session => @session })
189
+ end
190
+
191
+ # Delete a chatroom
192
+ #
193
+ def delete_room(room_id)
194
+ do_api :post, 'room/delete', { :id => room_id, :session => @session }
195
+ end
196
+
197
+ # Enter a chatroom
198
+ #
199
+ def enter_room(room_id, nickname=nil, password=nil, idempotent=false)
200
+ params = { :session => @session, :id => room_id }
201
+ params.merge!({ :nickname => nickname }) if nickname
202
+ params.merge!({ :password => password }) if password
203
+ params.merge!({ :idempotent => 'true' }) if idempotent
204
+ do_api :post, 'room/enter', params
205
+ end
206
+
207
+ # Poll for messages in a chatroom
208
+ #
209
+ def get_messages(ticket, counter, user_messages_only=false)
210
+ do_api :get, 'room/get_messages', { :session => @session, :ticket => ticket, :counter => counter, :user_messages_only => user_messages_only }
211
+ end
212
+
213
+ # Observe a chatroom, waiting for events to occur in the room
214
+ #
215
+ def observe_room(ticket, counter)
216
+ do_api :get, 'room/observe', { :session => @session, :ticket => ticket, :counter => counter }
217
+ end
218
+
219
+ # Set your nickname in a chatroom
220
+ #
221
+ def set_nickname(ticket, nickname)
222
+ do_api :post, 'room/set_nickname', { :session => @session, :ticket => ticket, :nickname => nickname }
223
+ end
224
+
225
+ # Say something in a chatroom. If target_occupant_id is not nil, a private message
226
+ # is sent to the indicated occupant.
227
+ #
228
+ def say(ticket, msg, target_occupant_id = nil)
229
+ params = { :session => @session, :ticket => ticket, :message => msg }
230
+ params.merge!({ :occupant_id => target_occupant_id}) if target_occupant_id
231
+ do_api :post, 'room/say', params
232
+ end
233
+
234
+ # Exit a chatroom
235
+ #
236
+ def exit_room(ticket)
237
+ do_api :post, 'room/exit', { :session => @session, :ticket => ticket }
238
+ end
239
+
240
+ private
241
+
242
+ def do_api(method, path, parameters, require_session=true)
243
+ if require_session and !@session
244
+ raise ClientError, "not in a session"
245
+ end
246
+
247
+ response = Timeout.timeout(@timeout) {
248
+ JSON.parse(self.send(method, url_for(path), parameters.merge({ :format => 'json' })))
249
+ }
250
+
251
+ unless success?(response)
252
+ raise APIError, response["error"]
253
+ end
254
+
255
+ response
256
+ end
257
+
258
+ def url_for(method)
259
+ "http://#{@host}/#{@@PATH_BASE}#{method}"
260
+ end
261
+
262
+ def get(url, params)
263
+ uri = URI.parse(url)
264
+ path = uri.path
265
+ q = params.inject("?") {|s, p| s << "#{p[0].to_s}=#{URI.encode(p[1].to_s, /./)}&"}.chop
266
+ path << q if q.length > 0
267
+
268
+ Net::HTTP.start(uri.host, uri.port) do |http|
269
+ http.read_timeout = @timeout
270
+ req = Net::HTTP::Get.new(path)
271
+ req.basic_auth(uri.user, uri.password) if uri.user
272
+ parse_result http.request(req)
273
+ end
274
+ end
275
+
276
+ def post(url, params)
277
+ if !params.find {|p| p[1].is_a?(Hash)}
278
+ params = params.inject({}){|hash,(k,v)| hash[k.to_s] = v; hash}
279
+ parse_result Net::HTTP.post_form(URI.parse(url), params)
280
+ else
281
+ boundary = 'lingr-api-client' + (0x1000000 + rand(0x1000000).to_s(16))
282
+
283
+ query = params.collect { |p|
284
+ ret = ["--#{boundary}"]
285
+
286
+ if p[1].is_a?(Hash)
287
+ ret << "Content-Disposition: form-data; name=\"#{URI.encode(p[0].to_s)}\"; filename=\"#{p[1][:filename]}\""
288
+ ret << "Content-Transfer-Encoding: binary"
289
+ ret << "Content-Type: #{p[1][:mime_type]}"
290
+ ret << ""
291
+ ret << p[1][:io].read
292
+ else
293
+ ret << "Content-Disposition: form-data; name=\"#{URI.encode(p[0].to_s)}\""
294
+ ret << ""
295
+ ret << p[1]
296
+ end
297
+
298
+ ret.join("\r\n")
299
+ }.join('') + "--#{boundary}--\r\n"
300
+
301
+ uri = URI.parse(url)
302
+ Net::HTTP.start(uri.host, uri.port) do |http|
303
+ http.read_timeout = @timeout
304
+ parse_result http.post2(uri.path, query, "Content-Type" => "multipart/form-data; boundary=#{boundary}")
305
+ end
306
+ end
307
+ end
308
+
309
+ def parse_result(result)
310
+ return nil if !result || result.code != '200' || (!result['Content-Type'] || result['Content-Type'].index('text/javascript') != 0)
311
+ # puts
312
+ # puts
313
+ # puts result.body
314
+ # puts
315
+ # puts
316
+ result.body
317
+ end
318
+
319
+ def success?(response)
320
+ return false if !response
321
+ response["status"] and response["status"] == 'ok'
322
+ end
323
+
324
+
325
+ @@PATH_BASE = 'api/'
326
+ end
327
+ end
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- # vim:fileencoding=UTF-8:
2
+ # vim:encoding=UTF-8:
3
3
  =begin
4
4
 
5
5
 
@@ -12,7 +12,7 @@ Ruby's by cho45
12
12
  $LOAD_PATH << "lib"
13
13
  $LOAD_PATH << "../lib"
14
14
 
15
- $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
15
+ $KCODE = "u" unless defined? ::Encoding # json use this
16
16
 
17
17
  require "rubygems"
18
18
  require "json"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- # vim:fileencoding=UTF-8:
2
+ # vim:encoding=UTF-8:
3
3
  =begin
4
4
  # sig.rb
5
5
 
@@ -17,7 +17,7 @@ ServerLog IRC Gateway
17
17
  $LOAD_PATH << "lib"
18
18
  $LOAD_PATH << "../lib"
19
19
 
20
- $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
20
+ $KCODE = "u" unless defined? ::Encoding
21
21
 
22
22
  require "rubygems"
23
23
  require "net/irc"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
- # vim:fileencoding=UTF-8:
3
- $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
2
+ # vim:encoding=UTF-8:
3
+ $KCODE = "u" unless defined? ::Encoding # json use this
4
4
  =begin
5
5
 
6
6
  # tig.rb
@@ -145,8 +145,6 @@ Use IM instead of any APIs (e.g. post)
145
145
 
146
146
  ### api_source=<source>
147
147
 
148
- ### max_params_count=<number:3>
149
-
150
148
  ### check_friends_interval=<seconds:3600>
151
149
 
152
150
  ### check_updates_interval=<seconds:86400>
@@ -167,6 +165,10 @@ Set 0 to disable checking.
167
165
 
168
166
  ### shuffled_tmap
169
167
 
168
+ ### ll=<lat>,<long>
169
+
170
+ ### with_retweets
171
+
170
172
  ## Extended commands through the CTCP ACTION
171
173
 
172
174
  ### list (ls)
@@ -180,7 +182,7 @@ Set 0 to disable checking.
180
182
  /me fav! [ID...]
181
183
  /me fav NICK
182
184
 
183
- ### link (ln)
185
+ ### link (ln, url, u)
184
186
 
185
187
  /me link ID [ID...]
186
188
 
@@ -197,7 +199,7 @@ Set 0 to disable checking.
197
199
  /me reply ID blah, blah...
198
200
 
199
201
  ### retweet (rt)
200
-
202
+
201
203
  /me retweet ID (blah, blah...)
202
204
 
203
205
  ### utf7 (utf-7)
@@ -232,9 +234,10 @@ Ruby's by cho45
232
234
 
233
235
  =end
234
236
 
235
- if File.directory? "lib"
237
+ case
238
+ when File.directory?("lib")
236
239
  $LOAD_PATH << "lib"
237
- elsif File.directory? File.expand_path("lib", "..")
240
+ when File.directory?(File.expand_path("lib", ".."))
238
241
  $LOAD_PATH << File.expand_path("lib", "..")
239
242
  end
240
243
 
@@ -258,14 +261,25 @@ end
258
261
  module Net::IRC::Constants; RPL_WHOISBOT = "335"; RPL_CREATEONTIME = "329"; end
259
262
 
260
263
  class TwitterIrcGateway < Net::IRC::Server::Session
264
+ @@ctcp_action_commands = []
265
+
266
+ class << self
267
+ def ctcp_action(*commands, &block)
268
+ name = "+ctcp_action_#{commands.inspect}"
269
+ define_method(name, block)
270
+ commands.each do |command|
271
+ @@ctcp_action_commands << [command, name]
272
+ end
273
+ end
274
+ end
275
+
261
276
  def server_name
262
277
  "twittergw"
263
278
  end
264
279
 
265
280
  def server_version
266
- rev = %q$Revision: 34381 $.split[1]
267
- rev &&= "+r#{rev}"
268
- "0.0.0#{rev}"
281
+ head = `git rev-parse HEAD 2>/dev/null`
282
+ head.empty?? "unknown" : head
269
283
  end
270
284
 
271
285
  def available_user_modes
@@ -298,13 +312,16 @@ class TwitterIrcGateway < Net::IRC::Server::Session
298
312
 
299
313
  class APIFailed < StandardError; end
300
314
 
315
+ MAX_MODE_PARAMS = 3
316
+ WSP_REGEX = Regexp.new("\\r\\n|[\\r\\n\\t#{"\\u00A0\\u1680\\u180E\\u2002-\\u200D\\u202F\\u205F\\u2060\\uFEFF" if "\u0000" == "\000"}]")
317
+
301
318
  def initialize(*args)
302
319
  super
303
320
  @groups = {}
304
321
  @channels = [] # joined channels (groups)
305
322
  @nicknames = {}
306
323
  @drones = []
307
- @config = Pathname.new(ENV["HOME"]) + ".tig"
324
+ @config = Pathname.new(ENV["HOME"]) + ".tig" ### TODO マルチユーザに対応してない
308
325
  @etags = {}
309
326
  @consums = []
310
327
  @limit = hourly_limit
@@ -362,6 +379,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
362
379
  post server_name, MODE, @nick, "+o"
363
380
  post @prefix, JOIN, main_channel
364
381
  post server_name, MODE, main_channel, "+mto", @nick
382
+ post server_name, MODE, main_channel, "+q", @nick
365
383
  if @me.status
366
384
  @me.status.user = @me
367
385
  post @prefix, TOPIC, main_channel, generate_status_message(@me.status.text)
@@ -425,14 +443,20 @@ class TwitterIrcGateway < Net::IRC::Server::Session
425
443
 
426
444
  @timeline = TypableMap.new(@opts.tmap_size || 10_404,
427
445
  @opts.shuffled_tmap || false)
428
- @sources = @opts.clientspoofing ? fetch_sources : [[api_source, "tig.rb"]]
446
+
447
+ if @opts.clientspoofing
448
+ update_sources
449
+ else
450
+ @sources = [api_source]
451
+ end
429
452
 
430
453
  update_redundant_suffix
431
454
  @check_updates_thread = Thread.start do
432
- sleep @opts.check_updates_interval || 86400
455
+ sleep 30
433
456
 
434
457
  loop do
435
458
  begin
459
+ @log.info "check_updates"
436
460
  check_updates
437
461
  rescue Exception => e
438
462
  @log.error e.inspect
@@ -443,7 +467,9 @@ class TwitterIrcGateway < Net::IRC::Server::Session
443
467
  sleep 0.01 * (90 + rand(21)) *
444
468
  (@opts.check_updates_interval || 86400) # 0.9 ... 1.1 day
445
469
  end
446
- end if @opts.check_updates_interval != 0
470
+
471
+ sleep @opts.check_updates_interval || 86400
472
+ end
447
473
 
448
474
  @check_timeline_thread = Thread.start do
449
475
  sleep 2 * (@me.friends_count / 100.0).ceil
@@ -511,7 +537,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
511
537
  def on_privmsg(m)
512
538
  target, mesg = *m.params
513
539
 
514
- m.ctcps.each {|ctcp| on_ctcp target, ctcp } if m.ctcp?
540
+ m.ctcps.each {|ctcp| on_ctcp(target, ctcp) } if m.ctcp?
515
541
 
516
542
  return if mesg.empty?
517
543
  return on_ctcp_action(target, mesg) if mesg.sub!(/\A +/, "") #and @opts.direct_action
@@ -563,29 +589,32 @@ class TwitterIrcGateway < Net::IRC::Server::Session
563
589
  return
564
590
  end
565
591
 
566
- in_reply_to = nil
592
+ q = { :status => mesg, :source => source }
593
+
567
594
  if @opts.old_style_reply and mesg[/\A@(?>([A-Za-z0-9_]{1,15}))[^A-Za-z0-9_]/]
568
- screen_name = $1
569
- unless user = friend(screen_name)
570
- user = api("users/show/#{screen_name}")
571
- end
572
- if user and user.status
573
- in_reply_to = user.status.id
574
- elsif user
575
- user = api("users/show/#{user.id}", {}, { :authenticate => user.protected })
576
- in_reply_to = user.status.id if user.status
595
+ if user = friend($1) || api("users/show/#{$1}")
596
+ unless user.status
597
+ user = api("users/show/#{user.id}", {},
598
+ { :authenticate => user.protected })
599
+ end
600
+ if user.status
601
+ q.update :in_reply_to_status_id => user.status.id
602
+ end
577
603
  end
578
604
  end
605
+ if @opts.ll
606
+ lat, long = @opts.ll.split(",", 2)
607
+ q.update :lat => lat.to_f
608
+ q.update :long => long.to_f
609
+ end
579
610
 
580
- q = { :status => mesg, :source => source }
581
- q.update(:in_reply_to_status_id => in_reply_to) if in_reply_to
582
611
  ret = api("statuses/update", q)
583
612
  log oops(ret) if ret.truncated
584
613
  ret.user.status = ret
585
614
  @me = ret.user
586
615
  log "Status updated"
587
616
  end
588
- when target.nick? # Direct message
617
+ when target.screen_name? # Direct message
589
618
  ret = api("direct_messages/new", { :screen_name => target, :text => mesg })
590
619
  post server_name, NOTICE, @nick, "Your direct message has been sent to #{target}."
591
620
  else
@@ -604,7 +633,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
604
633
 
605
634
  def on_whois(m)
606
635
  nick = m.params[0]
607
- unless nick.nick?
636
+ unless nick.screen_name?
608
637
  post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
609
638
  return
610
639
  end
@@ -635,16 +664,33 @@ class TwitterIrcGateway < Net::IRC::Server::Session
635
664
  end
636
665
 
637
666
  def on_who(m)
638
- channel = m.params[0]
667
+ channel = m.params[0]
668
+ whoreply = Proc.new do |ch, user|
669
+ # "<channel> <user> <host> <server> <nick>
670
+ # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
671
+ # :<hopcount> <real name>"
672
+ prefix = prefix(user)
673
+ server = api_base.host
674
+ mode = case prefix.nick
675
+ when @nick then "~"
676
+ #when @drones.include?(user.id) then "%" # FIXME
677
+ else "+"
678
+ end
679
+ hop = prefix.host.count("/")
680
+ real = user.name
681
+ post server_name, RPL_WHOREPLY, @nick,
682
+ ch, prefix.user, prefix.host, server, prefix.nick, "H*#{mode}", "#{hop} #{real}"
683
+ end
684
+
639
685
  case
640
686
  when channel.casecmp(main_channel).zero?
641
687
  users = [@me]
642
688
  users.concat @friends.reverse if @friends
643
- users.each {|friend| whoreply channel, friend }
689
+ users.each {|friend| whoreply.call channel, friend }
644
690
  post server_name, RPL_ENDOFWHO, @nick, channel
645
691
  when (@groups.key?(channel) and @friends)
646
692
  @groups[channel].each do |nick|
647
- whoreply channel, friend(nick)
693
+ whoreply.call channel, friend(nick)
648
694
  end
649
695
  post server_name, RPL_ENDOFWHO, @nick, channel
650
696
  else
@@ -662,6 +708,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
662
708
  @channels.uniq!
663
709
  post @prefix, JOIN, channel
664
710
  post server_name, MODE, channel, "+mtio", @nick
711
+ post server_name, MODE, channel, "+q", @nick
665
712
  save_config
666
713
  end
667
714
  end
@@ -676,7 +723,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
676
723
 
677
724
  def on_invite(m)
678
725
  nick, channel = *m.params
679
- if not nick.nick? or @nick.casecmp(nick).zero?
726
+ if not nick.screen_name? or @nick.casecmp(nick).zero?
680
727
  post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" # or yourself
681
728
  return
682
729
  end
@@ -737,6 +784,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
737
784
  channel = m.params[0]
738
785
  return if not channel.casecmp(main_channel).zero? or @me.status.nil?
739
786
 
787
+ return if not @opts.mesautofix
740
788
  begin
741
789
  require "levenshtein"
742
790
  topic = m.params[1]
@@ -754,7 +802,8 @@ class TwitterIrcGateway < Net::IRC::Server::Session
754
802
  if distance < 0.5
755
803
  deleted = api("statuses/destroy/#{previous.id}")
756
804
  @timeline.delete_if {|tid, s| s.id == deleted.id }
757
- log "Fixed: #{status.text}"
805
+ log "Similar update in previous. Conclude that it has error."
806
+ log "And overwrite previous as new status: #{status.text}"
758
807
  else
759
808
  log "Status updated"
760
809
  end
@@ -766,12 +815,13 @@ class TwitterIrcGateway < Net::IRC::Server::Session
766
815
  channel = m.params[0]
767
816
 
768
817
  unless m.params[1]
769
- if channel.ch?
818
+ case
819
+ when channel.ch?
770
820
  mode = "+mt"
771
821
  mode += "i" unless channel.casecmp(main_channel).zero?
772
822
  post server_name, RPL_CHANNELMODEIS, @nick, channel, mode
773
823
  #post server_name, RPL_CREATEONTIME, @nick, channel, 0
774
- elsif channel.casecmp(@nick).zero?
824
+ when channel.casecmp(@nick).zero?
775
825
  post server_name, RPL_UMODEIS, @nick, @nick, "+o"
776
826
  end
777
827
  end
@@ -787,303 +837,390 @@ class TwitterIrcGateway < Net::IRC::Server::Session
787
837
  def on_ctcp_action(target, mesg)
788
838
  #return unless main_channel.casecmp(target).zero?
789
839
  command, *args = mesg.split(" ")
790
- case command.downcase
791
- when "call"
792
- if args.size < 2
793
- log "/me call <Twitter_screen_name> as <IRC_nickname>"
794
- return
840
+ if command
841
+ command.downcase!
842
+
843
+ @@ctcp_action_commands.each do |define, name|
844
+ if define === command
845
+ send(name, target, mesg, Regexp.last_match || command, args)
846
+ break
847
+ end
848
+ end
849
+ else
850
+ commands = @@ctcp_action_commands.map {|define, name|
851
+ define
852
+ }.select {|define|
853
+ define.is_a? String
854
+ }
855
+
856
+ log "[tig.rb] CTCP ACTION COMMANDS:"
857
+ commands.each_slice(5) do |c|
858
+ log c.join(" ")
795
859
  end
796
- screen_name = args[0]
797
- nickname = args[2] || args[1] # allow omitting "as"
798
- if nickname == "is" and
799
- deleted_nick = @nicknames.delete(screen_name)
800
- log %Q{Removed the nickname "#{deleted_nick}" for #{screen_name}}
860
+ end
861
+
862
+ rescue APIFailed => e
863
+ log e.inspect
864
+ rescue Exception => e
865
+ log e.inspect
866
+ e.backtrace.each do |l|
867
+ @log.error "\t#{l}"
868
+ end
869
+ end
870
+
871
+ ctcp_action "call" do |target, mesg, command, args|
872
+ if args.size < 2
873
+ log "/me call <Twitter_screen_name> as <IRC_nickname>"
874
+ return
875
+ end
876
+ screen_name = args[0]
877
+ nickname = args[2] || args[1] # allow omitting "as"
878
+ if nickname == "is" and
879
+ deleted_nick = @nicknames.delete(screen_name)
880
+ log %Q{Removed the nickname "#{deleted_nick}" for #{screen_name}}
881
+ else
882
+ @nicknames[screen_name] = nickname
883
+ log "Call #{screen_name} as #{nickname}"
884
+ end
885
+ #save_config
886
+ end
887
+
888
+ ctcp_action "debug" do |target, mesg, command, args|
889
+ code = args.join(" ")
890
+ begin
891
+ log instance_eval(code).inspect
892
+ rescue Exception => e
893
+ log e.inspect
894
+ end
895
+ end
896
+
897
+ ctcp_action "utf-7", "utf7" do |target, mesg, command, args|
898
+ unless defined? ::Iconv
899
+ log "Can't load iconv."
900
+ return
901
+ end
902
+ @utf7 = !@utf7
903
+ log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}"
904
+ end
905
+
906
+ ctcp_action "list", "ls" do |target, mesg, command, args|
907
+ if args.empty?
908
+ log "/me list <NICK> [<NUM>]"
909
+ return
910
+ end
911
+ nick = args.first
912
+ if not nick.screen_name? or
913
+ api("users/username_available", { :username => nick }).valid
914
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
915
+ return
916
+ end
917
+ id = nick
918
+ authenticate = false
919
+ if user = friend(nick)
920
+ id = user.id
921
+ nick = user.screen_name
922
+ authenticate = user.protected
923
+ end
924
+ unless (1..200).include?(count = args[1].to_i)
925
+ count = 20
926
+ end
927
+ begin
928
+ res = api("statuses/user_timeline/#{id}",
929
+ { :count => count }, { :authenticate => authenticate })
930
+ rescue APIFailed
931
+ #log "#{nick} has protected their updates."
932
+ return
933
+ end
934
+ res.reverse_each do |s|
935
+ message(s, target, nil, nil, NOTICE)
936
+ end
937
+ end
938
+
939
+ ctcp_action %r/\A(un)?fav(?:ou?rite)?(!)?\z/ do |target, mesg, command, args|
940
+ # fav, unfav, favorite, unfavorite, favourite, unfavourite
941
+ method = command[1].nil? ? "create" : "destroy"
942
+ force = !!command[2]
943
+ entered = command[0].capitalize
944
+ statuses = []
945
+ if args.empty?
946
+ if method == "create"
947
+ if status = @timeline.last
948
+ statuses << status
949
+ else
950
+ #log ""
951
+ return
952
+ end
801
953
  else
802
- @nicknames[screen_name] = nickname
803
- log "Call #{screen_name} as #{nickname}"
954
+ @favorites ||= api("favorites").reverse
955
+ if @favorites.empty?
956
+ log "You've never favorite yet. No favorites to unfavorite."
957
+ return
958
+ end
959
+ statuses.push @favorites.last
804
960
  end
805
- #save_config
806
- when /\Autf-?7\z/
807
- unless defined? ::Iconv
808
- log "Can't load iconv."
809
- return
961
+ else
962
+ args.each do |tid_or_nick|
963
+ case
964
+ when status = @timeline[tid = tid_or_nick]
965
+ statuses.push status
966
+ when friend = friend(nick = tid_or_nick)
967
+ if friend.status
968
+ statuses.push friend.status
969
+ else
970
+ log "#{tid_or_nick} has no status."
971
+ end
972
+ else
973
+ # PRIVMSG: fav nick
974
+ log "No such ID/NICK #{@opts.tid % tid_or_nick}"
975
+ end
810
976
  end
811
- @utf7 = !@utf7
812
- log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}"
813
- when "list", "ls"
814
- if args.empty?
815
- log "/me list <NICK> [<NUM>]"
816
- return
977
+ end
978
+ @favorites ||= []
979
+ statuses.each do |s|
980
+ if not force and method == "create" and
981
+ @favorites.find {|i| i.id == s.id }
982
+ log "The status is already favorited! <#{permalink(s)}>"
983
+ next
817
984
  end
818
- nick = args.first
819
- if not nick.nick? or
820
- api("users/username_available", { :username => nick }).valid
821
- post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
822
- return
985
+ res = api("favorites/#{method}/#{s.id}")
986
+ log "#{entered}: #{res.user.screen_name}: #{generate_status_message(res.text)}"
987
+ if method == "create"
988
+ @favorites.push res
989
+ else
990
+ @favorites.delete_if {|i| i.id == res.id }
823
991
  end
824
- id = nick
825
- authenticate = false
826
- if user = friend(nick)
827
- id = user.id
828
- nick = user.screen_name
829
- authenticate = user.protected
992
+ end
993
+ end
994
+
995
+ ctcp_action "link", "ln", /\Au(?:rl)?\z/ do |target, mesg, command, args|
996
+ args.each do |tid|
997
+ if status = @timeline[tid]
998
+ log "#{@opts.tid % tid}: #{permalink(status)}"
999
+ else
1000
+ log "No such ID #{@opts.tid % tid}"
830
1001
  end
831
- unless (1..200).include?(count = args[1].to_i)
832
- count = 20
1002
+ end
1003
+ end
1004
+
1005
+ ctcp_action "ratio", "ratios" do |target, mesg, command, args|
1006
+ unless args.empty?
1007
+ args = args.first.split(":") if args.size == 1
1008
+ case
1009
+ when @opts.dm && @opts.mentions && args.size < 3
1010
+ log "/me ratios <timeline> <dm> <mentions>"
1011
+ return
1012
+ when @opts.dm && args.size < 2
1013
+ log "/me ratios <timeline> <dm>"
1014
+ return
1015
+ when @opts.mentions && args.size < 2
1016
+ log "/me ratios <timeline> <mentions>"
1017
+ return
833
1018
  end
834
- begin
835
- res = api("statuses/user_timeline/#{id}",
836
- { :count => count }, { :authenticate => authenticate })
837
- rescue APIFailed
838
- #log "#{nick} has protected their updates."
1019
+ ratios = args.map {|ratio| ratio.to_f }
1020
+ if ratios.any? {|ratio| ratio <= 0.0 }
1021
+ log "Ratios must be greater than 0.0 and fractional values are permitted."
839
1022
  return
840
1023
  end
841
- res.reverse_each do |s|
842
- message(s, target, nil, nil, NOTICE)
1024
+ @ratio.timeline = ratios[0]
1025
+
1026
+ case
1027
+ when @opts.dm
1028
+ @ratio.dm = ratios[1]
1029
+ @ratio.mentions = ratios[2] if @opts.mentions
1030
+ when @opts.mentions
1031
+ @ratio.mentions = ratios[1]
843
1032
  end
844
- when /\A(un)?fav(?:ou?rite)?(!)?\z/
845
- # fav, unfav, favorite, unfavorite, favourite, unfavourite
846
- method = $1.nil? ? "create" : "destroy"
847
- force = !!$2
848
- entered = $&.capitalize
849
- statuses = []
850
- if args.empty?
851
- if method == "create"
852
- if status = @timeline.last
853
- statuses << status
854
- else
855
- #log ""
856
- return
857
- end
858
- else
859
- @favorites ||= api("favorites").reverse
860
- if @favorites.empty?
861
- log "You've never favorite yet. No favorites to unfavorite."
862
- return
863
- end
864
- statuses.push @favorites.last
865
- end
866
- else
867
- args.each do |tid_or_nick|
868
- case
869
- when status = @timeline[tid_or_nick]
1033
+ end
1034
+ log "Intervals: " + @ratio.zip([:timeline, :dm, :mentions]).map {|ratio, name| [name, "#{interval(ratio).round}sec"] }.inspect
1035
+ end
1036
+
1037
+ ctcp_action "rm", %r/\A(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))\z/ do |target, mesg, command, args|
1038
+ # destroy, delete, del, remove, rm, miss, oops
1039
+ statuses = []
1040
+ if args.empty? and @me.status
1041
+ statuses.push @me.status
1042
+ else
1043
+ args.each do |tid|
1044
+ if status = @timeline[tid]
1045
+ if status.user.id == @me.id
870
1046
  statuses.push status
871
- when friend = friend(tid_or_nick)
872
- if friend.status
873
- statuses.push friend.status
874
- else
875
- log "#{tid_or_nick} has no status."
876
- end
877
1047
  else
878
- # PRIVMSG: fav nick
879
- log "No such ID/NICK #{@opts.tid % tid_or_nick}"
1048
+ log "The status you specified by the ID #{@opts.tid % tid} is not yours."
880
1049
  end
881
- end
882
- end
883
- @favorites ||= []
884
- statuses.each do |s|
885
- if not force and method == "create" and
886
- @favorites.find {|i| i.id == s.id }
887
- log "The status is already favorited! <#{permalink(s)}>"
888
- next
889
- end
890
- res = api("favorites/#{method}/#{s.id}")
891
- log "#{entered}: #{res.user.screen_name}: #{generate_status_message(res.text)}"
892
- if method == "create"
893
- @favorites.push res
894
- else
895
- @favorites.delete_if {|i| i.id == res.id }
896
- end
897
- end
898
- when "link", "ln"
899
- args.each do |tid|
900
- if status = @timeline[tid]
901
- log "#{@opts.tid % tid}: #{permalink(status)}"
902
1050
  else
903
1051
  log "No such ID #{@opts.tid % tid}"
904
1052
  end
905
1053
  end
906
- when /\Aratios?\z/
907
- unless args.empty?
908
- args = args.first.split(":") if args.size == 1
909
- if @opts.dm and @opts.mentions and args.size < 3
910
- log "/me ratios <timeline> <dm> <mentions>"
911
- return
912
- elsif @opts.dm and args.size < 2
913
- log "/me ratios <timeline> <dm>"
914
- return
915
- elsif @opts.mentions and args.size < 2
916
- log "/me ratios <timeline> <mentions>"
917
- return
918
- end
919
- ratios = args.map {|ratio| ratio.to_f }
920
- if ratios.any? {|ratio| ratio <= 0.0 }
921
- log "Ratios must be greater than 0.0 and fractional values are permitted."
922
- return
923
- end
924
- @ratio.timeline = ratios[0]
925
- if @opts.dm
926
- @ratio.dm = ratios[1]
927
- @ratio.mentions = ratios[2] if @opts.mentions
928
- elsif @opts.mentions
929
- @ratio.mentions = ratios[1]
930
- end
931
- end
932
- log "Intervals: #{@ratio.map {|ratio| interval ratio }.inspect}"
933
- when /\A(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))\z/
934
- # destroy, delete, del, remove, rm, miss, oops
935
- statuses = []
936
- if args.empty? and @me.status
937
- statuses.push @me.status
938
- else
939
- args.each do |tid|
940
- if status = @timeline[tid]
941
- if status.user.id == @me.id
942
- statuses.push status
943
- else
944
- log "The status you specified by the ID #{@opts.tid % tid} is not yours."
945
- end
946
- else
947
- log "No such ID #{@opts.tid % tid}"
1054
+ end
1055
+ b = false
1056
+ statuses.each do |st|
1057
+ res = api("statuses/destroy/#{st.id}")
1058
+ @timeline.delete_if {|tid, s| s.id == res.id }
1059
+ b = @me.status && @me.status.id == res.id
1060
+ log "Destroyed: #{res.text}"
1061
+ end
1062
+ Thread.start do
1063
+ sleep 2
1064
+ @me = api("account/update_profile") #api("account/verify_credentials")
1065
+ if @me.status
1066
+ @me.status.user = @me
1067
+ msg = generate_status_message(@me.status.text)
1068
+ @timeline.any? do |tid, s|
1069
+ if s.id == @me.status.id
1070
+ msg << " " << @opts.tid % tid
948
1071
  end
949
1072
  end
1073
+ post @prefix, TOPIC, main_channel, msg
950
1074
  end
951
- b = false
952
- statuses.each do |st|
953
- res = api("statuses/destroy/#{st.id}")
954
- @timeline.delete_if {|tid, s| s.id == res.id }
955
- b = @me.status && @me.status.id == res.id
956
- log "Destroyed: #{res.text}"
957
- end
958
- Thread.start do
959
- sleep 2
960
- @me = api("account/update_profile") #api("account/verify_credentials")
961
- if @me.status
962
- @me.status.user = @me
963
- msg = generate_status_message(@me.status.text)
964
- @timeline.any? do |tid, s|
965
- if s.id == @me.status.id
966
- msg << " " << @opts.tid % tid
967
- end
968
- end
969
- post @prefix, TOPIC, main_channel, msg
970
- end
971
- end if b
972
- when "name"
973
- name = mesg.split(" ", 2)[1]
974
- unless name.nil?
975
- @me = api("account/update_profile", { :name => name })
976
- @me.status.user = @me if @me.status
977
- log "You are named #{@me.name}."
978
- end
979
- when "email"
980
- # FIXME
981
- email = args.first
982
- unless email.nil?
983
- @me = api("account/update_profile", { :email => email })
984
- @me.status.user = @me if @me.status
985
- end
986
- when "url"
987
- # FIXME
988
- url = args.first || ""
989
- @me = api("account/update_profile", { :url => url })
990
- @me.status.user = @me if @me.status
991
- when "in", "location"
992
- location = mesg.split(" ", 2)[1] || ""
993
- @me = api("account/update_profile", { :location => location })
1075
+ end if b
1076
+ end
1077
+
1078
+ ctcp_action "name" do |target, mesg, command, args|
1079
+ name = mesg.split(" ", 2)[1]
1080
+ unless name.nil?
1081
+ @me = api("account/update_profile", { :name => name })
994
1082
  @me.status.user = @me if @me.status
995
- location = (@me.location and @me.location.empty?) ? "nowhere" : "in #{@me.location}"
996
- log "You are #{location} now."
997
- when /\Adesc(?:ription)?\z/
998
- # FIXME
999
- description = mesg.split(" ", 2)[1] || ""
1000
- @me = api("account/update_profile", { :description => description })
1083
+ log "You are named #{@me.name}."
1084
+ end
1085
+ end
1086
+
1087
+ ctcp_action "email" do |target, mesg, command, args|
1088
+ # FIXME
1089
+ email = args.first
1090
+ unless email.nil?
1091
+ @me = api("account/update_profile", { :email => email })
1001
1092
  @me.status.user = @me if @me.status
1002
- #when /\Acolou?rs?\z/ # TODO
1003
- # # bg, text, link, fill and border
1004
- #when "image", "img" # TODO
1005
- # url = args.first
1006
- # # DCC SEND
1007
- #when "follow"# TODO
1008
- #when "leave" # TODO
1009
- when /\A(?:mention|re(?:ply)?)\z/ # reply, re, mention
1010
- tid = args.first
1011
- if status = @timeline[tid]
1012
- text = mesg.split(" ", 3)[2]
1013
- screen_name = "@#{status.user.screen_name}"
1014
- if text.nil? or not text.include?(screen_name)
1015
- text = "#{screen_name} #{text}"
1016
- end
1017
- ret = api("statuses/update", { :status => text, :source => source,
1018
- :in_reply_to_status_id => status.id })
1019
- log oops(ret) if ret.truncated
1020
- msg = generate_status_message(status.text)
1021
- url = permalink(status)
1022
- log "Status updated (In reply to #{@opts.tid % tid}: #{msg} <#{url}>)"
1023
- ret.user.status = ret
1024
- @me = ret.user
1025
- end
1026
- when /\Aspoo(o+)?f\z/
1093
+ end
1094
+ end
1095
+
1096
+ ctcp_action "url" do |target, mesg, command, args|
1097
+ # FIXME
1098
+ url = args.first || ""
1099
+ @me = api("account/update_profile", { :url => url })
1100
+ @me.status.user = @me if @me.status
1101
+ end
1102
+
1103
+ ctcp_action "in", "location" do |target, mesg, command, args|
1104
+ location = mesg.split(" ", 2)[1] || ""
1105
+ @me = api("account/update_profile", { :location => location })
1106
+ @me.status.user = @me if @me.status
1107
+ location = (@me.location and @me.location.empty?) ? "nowhere" : "in #{@me.location}"
1108
+ log "You are #{location} now."
1109
+ end
1110
+
1111
+ ctcp_action %r/\Adesc(?:ription)?\z/ do |target, mesg, command, args|
1112
+ # FIXME
1113
+ description = mesg.split(" ", 2)[1] || ""
1114
+ @me = api("account/update_profile", { :description => description })
1115
+ @me.status.user = @me if @me.status
1116
+ end
1117
+
1118
+ ctcp_action %r/\A(?:mention|re(?:ply)?)\z/ do |target, mesg, command, args|
1119
+ # reply, re, mention
1120
+ tid = args.first
1121
+ if status = @timeline[tid]
1122
+ text = mesg.split(" ", 3)[2]
1123
+ screen_name = "@#{status.user.screen_name}"
1124
+ if text.nil? or not text.include?(screen_name)
1125
+ text = "#{screen_name} #{text}"
1126
+ end
1127
+ ret = api("statuses/update", { :status => text, :source => source,
1128
+ :in_reply_to_status_id => status.id })
1129
+ log oops(ret) if ret.truncated
1130
+ msg = generate_status_message(status.text)
1131
+ url = permalink(status)
1132
+ log "Status updated (In reply to #{@opts.tid % tid}: #{msg} <#{url}>)"
1133
+ ret.user.status = ret
1134
+ @me = ret.user
1135
+ end
1136
+ end
1137
+
1138
+ ctcp_action %r/\Aspoo(o+)?f\z/ do |target, mesg, command, args|
1139
+ if args.empty?
1027
1140
  Thread.start do
1028
- @sources = args.empty? \
1029
- ? @sources.size == 1 || $1 ? fetch_sources($1 && $1.size) \
1030
- : [[api_source, "tig.rb"]] \
1031
- : args.map {|src| [src.upcase != "WEB" ? src : "", "=#{src}"] }
1032
- log @sources.map {|src| src[1] }.sort.join(", ")
1033
- end
1034
- when "bot", "drone"
1035
- if args.empty?
1036
- log "/me bot <NICK> [<NICK>...]"
1037
- return
1141
+ update_sources(command[1].nil?? 0 : command[1].size)
1038
1142
  end
1039
- args.each do |bot|
1040
- user = friend(bot)
1041
- unless user
1042
- post server_name, ERR_NOSUCHNICK, bot, "No such nick/channel"
1043
- next
1044
- end
1045
- if @drones.delete(user.id)
1046
- mode = "-#{mode}"
1047
- log "#{bot} is no longer a bot."
1048
- else
1049
- @drones << user.id
1050
- mode = "+#{mode}"
1051
- log "Marks #{bot} as a bot."
1052
- end
1143
+ return
1144
+ end
1145
+ names = []
1146
+ @sources = args.map do |arg|
1147
+ names << "=#{arg}"
1148
+ case arg.upcase
1149
+ when "WEB" then ""
1150
+ when "API" then nil
1151
+ else arg
1053
1152
  end
1054
- save_config
1055
- when "home", "h"
1056
- if args.empty?
1057
- log "/me home <NICK>"
1058
- return
1153
+ end
1154
+ log(names.inject([]) do |r, name|
1155
+ s = r.join(", ")
1156
+ if s.size < 400
1157
+ r << name
1158
+ else
1159
+ log s
1160
+ [name]
1059
1161
  end
1060
- nick = args.first
1061
- if not nick.nick? or
1062
- api("users/username_available", { :username => nick }).valid
1063
- post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
1064
- return
1162
+ end.join(", "))
1163
+ end
1164
+
1165
+ ctcp_action "bot", "drone" do |target, mesg, command, args|
1166
+ if args.empty?
1167
+ log "/me bot <NICK> [<NICK>...]"
1168
+ return
1169
+ end
1170
+ args.each do |bot|
1171
+ user = friend(bot)
1172
+ unless user
1173
+ post server_name, ERR_NOSUCHNICK, bot, "No such nick/channel"
1174
+ next
1065
1175
  end
1066
- log "http://twitter.com/#{nick}"
1067
- when "retweet", "rt"
1068
- tid = args.first
1069
- if status = @timeline[tid]
1070
- if args.size >= 2
1071
- comment = mesg.split(" ",3)[2] + " "
1072
- else
1073
- comment = ""
1074
- end
1075
- screen_name = "@#{status.user.screen_name}"
1076
- rt_message = generate_status_message(status.text)
1077
- text = "#{comment}RT #{screen_name}: #{rt_message}"
1078
- ret = api("statuses/update", { :status => text, :source => source })
1079
- log oops(ret) if ret.truncated
1080
- log "Status updated (RT to #{@opts.tid % tid}: #{text})"
1081
- ret.user.status = ret
1082
- @me = ret.user
1176
+ if @drones.delete(user.id)
1177
+ mode = "-#{mode}"
1178
+ log "#{bot} is no longer a bot."
1179
+ else
1180
+ @drones << user.id
1181
+ mode = "+#{mode}"
1182
+ log "Marks #{bot} as a bot."
1083
1183
  end
1084
- end unless command.nil?
1085
- rescue APIFailed => e
1086
- log e.inspect
1184
+ end
1185
+ save_config
1186
+
1187
+ end
1188
+
1189
+ ctcp_action "home", "h" do |target, mesg, command, args|
1190
+ if args.empty?
1191
+ log "/me home <NICK>"
1192
+ return
1193
+ end
1194
+ nick = args.first
1195
+ if not nick.screen_name? or
1196
+ api("users/username_available", { :username => nick }).valid
1197
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
1198
+ return
1199
+ end
1200
+ log "http://twitter.com/#{nick}"
1201
+ end
1202
+
1203
+ ctcp_action "retweet", "rt" do |target, mesg, command, args|
1204
+ if args.empty?
1205
+ log "/me #{command} <ID> blah blah"
1206
+ return
1207
+ end
1208
+ tid = args.first
1209
+ if status = @timeline[tid]
1210
+ if args.size >= 2
1211
+ comment = mesg.split(" ", 3)[2] + " "
1212
+ else
1213
+ comment = ""
1214
+ end
1215
+ screen_name = "@#{status.user.screen_name}"
1216
+ rt_message = generate_status_message(status.text)
1217
+ text = "#{comment}RT #{screen_name}: #{rt_message}"
1218
+ ret = api("statuses/update", { :status => text, :source => source })
1219
+ log oops(ret) if ret.truncated
1220
+ log "Status updated (RT to #{@opts.tid % tid}: #{text})"
1221
+ ret.user.status = ret
1222
+ @me = ret.user
1223
+ end
1087
1224
  end
1088
1225
 
1089
1226
  def on_ctcp_clientinfo(target, msg)
@@ -1129,7 +1266,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1129
1266
  rest = @friends.map do |i|
1130
1267
  prefix = "+" #@drones.include?(i.id) ? "%" : "+" # FIXME ~&%
1131
1268
  "#{prefix}#{i.screen_name}"
1132
- end.reverse.inject("@#{@nick}") do |r, nick|
1269
+ end.reverse.inject("~#{@nick}") do |r, nick|
1133
1270
  if r.size < 400
1134
1271
  r << " " << nick
1135
1272
  else
@@ -1166,15 +1303,20 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1166
1303
  end
1167
1304
 
1168
1305
  def check_timeline
1169
- cmd = PRIVMSG
1170
- q = { :count => 200 }
1171
- if @latest_id ||= nil
1306
+ cmd = PRIVMSG
1307
+ path = "statuses/#{@opts.with_retweets ? "home" : "friends"}_timeline"
1308
+ q = { :count => 200 }
1309
+ @latest_id ||= nil
1310
+
1311
+ case
1312
+ when @latest_id
1172
1313
  q.update(:since_id => @latest_id)
1173
- elsif not @me.statuses_count.zero? and not @me.friends_count.zero?
1174
- cmd = NOTICE
1314
+ when is_first_retrieve = !@me.statuses_count.zero? && !@me.friends_count.zero?
1315
+ # cmd = NOTICE # デバッグするときめんどくさいので
1316
+ q.update(:count => 20)
1175
1317
  end
1176
1318
 
1177
- api("statuses/friends_timeline", q).reverse_each do |status|
1319
+ api(path, q).reverse_each do |status|
1178
1320
  id = @latest_id = status.id
1179
1321
  next if @timeline.any? {|tid, s| s.id == id }
1180
1322
 
@@ -1196,6 +1338,9 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1196
1338
  b = false
1197
1339
  @friends.each_with_index do |friend, i|
1198
1340
  if b = friend.id == user.id
1341
+ if friend.screen_name != user.screen_name
1342
+ post prefix(friend), NICK, user.screen_name
1343
+ end
1199
1344
  @friends[i] = user
1200
1345
  break
1201
1346
  end
@@ -1265,43 +1410,41 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1265
1410
  def check_updates
1266
1411
  update_redundant_suffix
1267
1412
 
1268
- return unless /\+r(\d+)\z/ === server_version
1269
- rev = $1.to_i
1270
- uri = URI("http://svn.coderepos.org/share/lang/ruby/net-irc/trunk/examples/tig.rb")
1413
+ uri = URI("http://github.com/api/v1/json/cho45/net-irc/commits/master")
1271
1414
  @log.debug uri.inspect
1272
- res = http(uri).request(http_req(:head, uri))
1273
- @etags[uri.to_s] = res["ETag"]
1274
- return unless not res.is_a?(Net::HTTPNotModified) and
1275
- /\A"(\d+)/ === res["ETag"] and rev < $1.to_i
1276
- uri = URI("http://coderepos.org/share/log/lang/ruby/net-irc/trunk/examples/tig.rb")
1277
- uri.query = { :rev => $1, :stop_rev => rev, :verbose => "on" }.to_query_str(";")
1278
- log "\002New version is available.\017 <#{uri}>"
1415
+ res = http(uri).request(http_req(:get, uri))
1416
+
1417
+ latest = JSON.parse(res.body)['commits'][0]['id']
1418
+ unless server_version == latest
1419
+ log "\002New version is available.\017 run 'git pull'."
1420
+ end
1279
1421
  rescue Errno::ECONNREFUSED, Timeout::Error => e
1280
1422
  @log.error "Failed to get the latest revision of tig.rb from #{uri.host}: #{e.inspect}"
1281
1423
  end
1282
1424
 
1283
1425
  def interval(ratio)
1284
1426
  now = Time.now
1285
- max = @opts.maxlimit
1427
+ max = @opts.maxlimit || 0
1286
1428
  limit = 0.98 * @limit # 98% of the rate limit
1287
1429
  i = 3600.0 # an hour in seconds
1288
1430
  i *= @ratio.inject {|sum, r| sum.to_f + r.to_f } +
1289
1431
  @consums.delete_if {|t| t < now }.size
1290
1432
  i /= ratio.to_f
1291
- i /= (max and 0 < max and max < limit) ? max : limit
1433
+ i /= (0 < max && max < limit) ? max : limit
1434
+ i = 60 * 30 if i > 60 * 30 # 30分以上止まらないように。
1435
+ i
1292
1436
  rescue => e
1293
1437
  @log.error e.inspect
1294
1438
  100
1295
1439
  end
1296
1440
 
1297
1441
  def join(channel, users)
1298
- max_params_count = @opts.max_params_count || 3
1299
1442
  params = []
1300
1443
  users.each do |user|
1301
1444
  prefix = prefix(user)
1302
1445
  post prefix, JOIN, channel
1303
- params << prefix.nick
1304
- next if params.size < max_params_count
1446
+ params << prefix.nick if user.protected
1447
+ next if params.size < MAX_MODE_PARAMS
1305
1448
 
1306
1449
  post server_name, MODE, channel, "+#{"v" * params.size}", *params
1307
1450
  params = []
@@ -1347,22 +1490,6 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1347
1490
  end
1348
1491
  end
1349
1492
 
1350
- def whoreply(channel, user)
1351
- # "<channel> <user> <host> <server> <nick>
1352
- # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
1353
- # :<hopcount> <real name>"
1354
- prefix = prefix(user)
1355
- server = api_base.host
1356
- real = user.name
1357
- mode = case prefix.nick
1358
- when @nick then "@"
1359
- #when @drones.include?(user.id) then "%" # FIXME
1360
- else "+"
1361
- end
1362
- post server_name, RPL_WHOREPLY, @nick, channel,
1363
- prefix.user, prefix.host, server, prefix.nick, "H*#{mode}", "0 #{real}"
1364
- end
1365
-
1366
1493
  def save_config
1367
1494
  config = {
1368
1495
  :groups => @groups,
@@ -1397,6 +1524,10 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1397
1524
  }x === path
1398
1525
  end
1399
1526
 
1527
+ #def require_put?(path)
1528
+ # %r{ \A status(?:es)?/retweet (?:/|\z) }x === path
1529
+ #end
1530
+
1400
1531
  def api(path, query = {}, opts = {})
1401
1532
  path.sub!(%r{\A/+}, "")
1402
1533
  query = query.to_query_str
@@ -1407,7 +1538,6 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1407
1538
  uri.path += path
1408
1539
  uri.path += ".json" if path != "users/username_available"
1409
1540
  uri.query = query unless query.empty?
1410
- @log.debug uri.inspect
1411
1541
 
1412
1542
  header = {}
1413
1543
  credentials = authenticate ? [@real, @pass] : nil
@@ -1416,15 +1546,19 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1416
1546
  http_req :delete, uri, header, credentials
1417
1547
  when require_post?(path)
1418
1548
  http_req :post, uri, header, credentials
1549
+ #when require_put?(path)
1550
+ # http_req :put, uri, header, credentials
1419
1551
  else
1420
1552
  http_req :get, uri, header, credentials
1421
1553
  end
1422
1554
 
1555
+ @log.debug [req.method, uri.to_s]
1423
1556
  ret = http(uri, 30, 30).request req
1424
1557
 
1425
1558
  #@etags[uri.to_s] = ret["ETag"]
1426
1559
 
1427
- if authenticate
1560
+ case
1561
+ when authenticate
1428
1562
  hourly_limit = ret["X-RateLimit-Limit"].to_i
1429
1563
  unless hourly_limit.zero?
1430
1564
  if @limit != hourly_limit
@@ -1445,7 +1579,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1445
1579
  @consums << expired_on
1446
1580
  end
1447
1581
  end
1448
- elsif ret["X-RateLimit-Remaining"]
1582
+ when ret["X-RateLimit-Remaining"]
1449
1583
  @limit_remaining_for_ip = ret["X-RateLimit-Remaining"].to_i
1450
1584
  @log.debug "IP based limit: #{@limit_remaining_for_ip}"
1451
1585
  end
@@ -1470,8 +1604,10 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1470
1604
  when Net::HTTPBadRequest # 400: exceeded the rate limitation
1471
1605
  if ret.key?("X-RateLimit-Reset")
1472
1606
  s = ret["X-RateLimit-Reset"].to_i - Time.now.to_i
1473
- log "RateLimit: #{(s / 60.0).ceil} min remaining to get timeline"
1474
- sleep s
1607
+ if s > 0
1608
+ log "RateLimit: #{(s / 60.0).ceil} min remaining to get timeline"
1609
+ sleep (s > 60 * 10) ? 60 * 10 : s # 10 分に一回はとってくるように
1610
+ end
1475
1611
  end
1476
1612
  raise APIFailed, "#{ret.code}: #{ret.message}"
1477
1613
  when Net::HTTPUnauthorized # 401
@@ -1503,8 +1639,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1503
1639
  mesg.delete!("\000\001")
1504
1640
  mesg.gsub!("&gt;", ">")
1505
1641
  mesg.gsub!("&lt;", "<")
1506
- #mesg.gsub!(/\r\n|[\r\n\t\u00A0\u1680\u180E\u2002-\u200D\u202F\u205F\u2060\uFEFF]/, " ")
1507
- mesg.gsub!(/\r\n|[\r\n\t]/, " ")
1642
+ mesg.gsub!(WSP_REGEX, " ")
1508
1643
  mesg = untinyurl(mesg)
1509
1644
  mesg.sub!(@rsuffix_regex, "") if @rsuffix_regex
1510
1645
  mesg.strip
@@ -1563,7 +1698,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1563
1698
  end
1564
1699
 
1565
1700
  def decode_utf7(str)
1566
- return str unless defined?(::Iconv) and str.include?("+")
1701
+ return str unless defined? ::Iconv and str.include?("+")
1567
1702
 
1568
1703
  str.sub!(/\A(?:.+ > |.+\z)/) { Iconv.iconv("UTF-8", "UTF-7", $&).join }
1569
1704
  #FIXME str = "[utf7]: #{str}" if str =~ /[^a-z0-9\s]/i
@@ -1593,8 +1728,10 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1593
1728
  login, key, len = @opts.bitlify.split(":", 3) if @opts.bitlify
1594
1729
  len = (len || 20).to_i
1595
1730
  longurls = URI.extract(text, %w[http https]).uniq.map do |url|
1596
- URI.rstrip_unpaired_paren(url)
1597
- end.reject {|url| url.size < len }
1731
+ URI.rstrip url
1732
+ end.reject do |url|
1733
+ url.size < len
1734
+ end
1598
1735
  return text if longurls.empty?
1599
1736
 
1600
1737
  bitly = URI("http://api.bit.ly/shorten")
@@ -1637,7 +1774,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1637
1774
  size = unu_url.size
1638
1775
 
1639
1776
  text.gsub(URI.regexp(%w[http https])) do |url|
1640
- url = URI.rstrip_unpaired_paren(url)
1777
+ url = URI.rstrip url
1641
1778
  if url.size < size + 5 or url[0, size] == unu_url
1642
1779
  return url
1643
1780
  end
@@ -1684,12 +1821,12 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1684
1821
  end
1685
1822
 
1686
1823
  urls = []
1687
- (text.split(/[\s<>]+/) + [text]).each do |str|
1824
+ text.split(/[\s<>]+/).each do |str|
1688
1825
  next if /%[0-9A-Fa-f]{2}/ === str
1689
1826
  # URI::UNSAFE + "#"
1690
1827
  escaped_str = URI.escape(str, %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]#]})
1691
1828
  URI.extract(escaped_str, %w[http https]).each do |url|
1692
- uri = URI(URI.rstrip_unpaired_paren(url))
1829
+ uri = URI(URI.rstrip(url))
1693
1830
  if not urls.include?(uri.to_s) and exist_uri?(uri)
1694
1831
  urls << uri.to_s
1695
1832
  end
@@ -1764,21 +1901,40 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1764
1901
  uri
1765
1902
  end
1766
1903
 
1767
- def fetch_sources(n = nil)
1768
- n = n.to_i
1769
- uri = URI("http://wedata.net/databases/TwitterSources/items.json")
1904
+ def update_sources(n = 0)
1905
+ if @sources and @sources.size > 1 and n.zero?
1906
+ log "tig.rb"
1907
+ @sources = [api_source]
1908
+ return @sources
1909
+ end
1910
+
1911
+ uri = URI("http://wedata.net/databases/TwitterSources/items.json")
1770
1912
  @log.debug uri.inspect
1771
- json = http(uri).request(http_req(:get, uri)).body
1913
+ json = http(uri).request(http_req(:get, uri)).body
1772
1914
  sources = JSON.parse json
1773
- sources.map! {|item| [item["data"]["source"], item["name"]] }.push ["", "web"]
1774
- if (1 ... sources.size).include?(n)
1775
- sources = Array.new(n) { sources.delete_at(rand(sources.size)) }.compact
1776
- end
1777
- sources
1915
+ sources.map! {|item| [item["data"]["source"], item["name"]] }
1916
+ sources.push ["", "web"]
1917
+ sources.push [nil, "API"]
1918
+
1919
+ sources = Array.new(n) do
1920
+ sources.delete_at(rand(sources.size))
1921
+ end if (1 ... sources.size).include?(n)
1922
+
1923
+ log(sources.inject([]) do |r, src|
1924
+ s = r.join(", ")
1925
+ if s.size < 400
1926
+ r << src[1]
1927
+ else
1928
+ log s
1929
+ [src[1]]
1930
+ end
1931
+ end.join(", ")) if @sources
1932
+
1933
+ @sources = sources.map {|src| src[0] }
1778
1934
  rescue => e
1779
1935
  @log.error e.inspect
1780
1936
  log "An error occured while loading #{uri.host}."
1781
- @sources || [[api_source, "tig.rb"]]
1937
+ @sources ||= [api_source]
1782
1938
  end
1783
1939
 
1784
1940
  def update_redundant_suffix
@@ -1867,33 +2023,34 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1867
2023
  end
1868
2024
 
1869
2025
  def source
1870
- @sources[rand(@sources.size)].first
2026
+ @sources[rand(@sources.size)]
1871
2027
  end
1872
2028
 
1873
2029
  def initial_message
1874
2030
  super
1875
2031
  post server_name, RPL_ISUPPORT, @nick,
2032
+ "PREFIX=(qov)~@%+", "CHANTYPES=#", "CHANMODES=#{available_channel_modes}",
2033
+ "MODES=#{MAX_MODE_PARAMS}", "NICKLEN=15", "TOPICLEN=420", "CHANNELLEN=50",
1876
2034
  "NETWORK=Twitter",
1877
- "CHANTYPES=#", "CHANNELLEN=50", "CHANMODES=#{available_channel_modes}",
1878
- "NICKLEN=15", "TOPICLEN=420", "PREFIX=(hov)%@+",
1879
2035
  "are supported by this server"
1880
2036
  end
1881
2037
 
1882
2038
  User = Struct.new(:id, :name, :screen_name, :location, :description, :url,
1883
2039
  :following, :notifications, :protected, :time_zone,
1884
2040
  :utc_offset, :created_at, :friends_count, :followers_count,
1885
- :statuses_count, :favourites_count, :verified,
1886
- :verified_profile,
2041
+ :statuses_count, :favourites_count, :verified, :geo_enabled,
1887
2042
  :profile_image_url, :profile_background_color, :profile_text_color,
1888
2043
  :profile_link_color, :profile_sidebar_fill_color,
1889
2044
  :profile_sidebar_border_color, :profile_background_image_url,
1890
2045
  :profile_background_tile, :status)
1891
- Status = Struct.new(:id, :text, :source, :created_at, :truncated, :favorited,
2046
+ Status = Struct.new(:id, :text, :source, :created_at, :truncated, :favorited, :geo,
1892
2047
  :in_reply_to_status_id, :in_reply_to_user_id,
1893
2048
  :in_reply_to_screen_name, :user)
1894
2049
  DM = Struct.new(:id, :text, :created_at,
1895
2050
  :sender_id, :sender_screen_name, :sender,
1896
2051
  :recipient_id, :recipient_screen_name, :recipient)
2052
+ Geo = Struct.new(:type, :coordinates, :geometries, :geometry, :properties, :id,
2053
+ :crs, :name, :href, :bbox, :features)
1897
2054
 
1898
2055
  class TypableMap < Hash
1899
2056
  #Roman = %w[
@@ -1925,8 +2082,7 @@ class TwitterIrcGateway < Net::IRC::Server::Session
1925
2082
  if @seq.respond_to?(:shuffle!)
1926
2083
  @seq.shuffle!
1927
2084
  else
1928
- seq = @seq.dup
1929
- @seq.map! { seq.slice!(rand(seq.size)) }
2085
+ @seq = Array.new(@seq.size) { @seq.delete_at(rand(@seq.size)) }
1930
2086
  end
1931
2087
  @seq.freeze
1932
2088
  else
@@ -2005,11 +2161,14 @@ class Hash
2005
2161
  TwitterIrcGateway::Status.new
2006
2162
  when struct_of?(TwitterIrcGateway::DM)
2007
2163
  TwitterIrcGateway::DM.new
2164
+ when struct_of?(TwitterIrcGateway::Geo)
2165
+ TwitterIrcGateway::Geo.new
2008
2166
  else
2009
2167
  members = keys
2010
2168
  members.concat TwitterIrcGateway::User.members
2011
2169
  members.concat TwitterIrcGateway::Status.members
2012
2170
  members.concat TwitterIrcGateway::DM.members
2171
+ members.concat TwitterIrcGateway::Geo.members
2013
2172
  members.map! {|m| m.to_sym }
2014
2173
  members.uniq!
2015
2174
  Struct.new(*members).new
@@ -2020,10 +2179,10 @@ class Hash
2020
2179
  struct
2021
2180
  end
2022
2181
 
2023
- # { :f => nil } #=> "f"
2024
- # { "f" => "" } #=> "f="
2025
- # { "f" => "v" } #=> "f=v"
2182
+ # { :f => "v" } #=> "f=v"
2026
2183
  # { "f" => [1, 2] } #=> "f=1&f=2"
2184
+ # { "f" => "" } #=> "f="
2185
+ # { "f" => nil } #=> "f"
2027
2186
  def to_query_str separator = "&"
2028
2187
  inject([]) do |r, (k, v)|
2029
2188
  k = URI.encode_component k.to_s
@@ -2049,7 +2208,7 @@ class String
2049
2208
  /\A[&#+!][^ \007,]{1,50}\z/ === self
2050
2209
  end
2051
2210
 
2052
- def nick? # Twitter screen_name (username)
2211
+ def screen_name?
2053
2212
  /\A[A-Za-z0-9_]{1,15}\z/ === self
2054
2213
  end
2055
2214
 
@@ -2060,21 +2219,28 @@ class String
2060
2219
  end
2061
2220
 
2062
2221
  module URI::Escape
2063
- # URI.escape("あ1") #=> "%E3%81%82\xEF\xBC\x91"
2064
- # URI("file:///4") #=> #<URI::Generic:0x9d09db0 URL:file:/4>
2065
- # "\\d" -> "0-9" for Ruby 1.9
2066
2222
  alias :_orig_escape :escape
2067
- def escape str, unsafe = %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]]}
2068
- _orig_escape(str, unsafe)
2223
+
2224
+ if defined? ::RUBY_REVISION and RUBY_REVISION < 24544
2225
+ # URI.escape("あ1") #=> "%E3%81%82\xEF\xBC\x91"
2226
+ # URI("file:///4") #=> #<URI::Generic:0x9d09db0 URL:file:/4>
2227
+ # "\\d" -> "[0-9]" for Ruby 1.9
2228
+ def escape str, unsafe = %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]]}
2229
+ _orig_escape(str, unsafe)
2230
+ end
2231
+ alias :encode :escape
2069
2232
  end
2070
- alias :encode :escape
2071
2233
 
2072
2234
  def encode_component str, unsafe = /[^-_.!~*'()a-zA-Z0-9 ]/
2073
2235
  _orig_escape(str, unsafe).tr(" ", "+")
2074
2236
  end
2075
2237
 
2076
- def rstrip_unpaired_paren str
2077
- str.sub(%r{(/[^/()]*(?:\([^/()]*\)[^/()]*)*)\)[^/()]*\z}, "\\1")
2238
+ def rstrip str
2239
+ str.sub(%r{
2240
+ (?: ( / [^/?#()]* (?: \( [^/?#()]* \) [^/?#()]* )* ) \) [^/?#()]*
2241
+ | \.
2242
+ ) \z
2243
+ }x, "\\1")
2078
2244
  end
2079
2245
  end
2080
2246