net-irc 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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