net-irc 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:fileencoding=UTF-8:
3
+
4
+ require 'rubygems'
5
+ require 'net/irc'
6
+
7
+ class NetIrcServer < Net::IRC::Server::Session
8
+ def server_name
9
+ "net-irc"
10
+ end
11
+
12
+ def server_version
13
+ "0.0.0"
14
+ end
15
+
16
+ def available_user_modes
17
+ "iosw"
18
+ end
19
+
20
+ def default_user_modes
21
+ ""
22
+ end
23
+
24
+ def available_channel_modes
25
+ "om"
26
+ end
27
+
28
+ def default_channel_modes
29
+ ""
30
+ end
31
+
32
+ def initialize(*args)
33
+ super
34
+ @@channels ||= {}
35
+ @@users ||= {}
36
+ @ping = false
37
+ end
38
+
39
+ def on_pass(m)
40
+ end
41
+
42
+ def on_user(m)
43
+ @user, @real = m.params[0], m.params[3]
44
+ @host = @socket.peeraddr[2]
45
+ @prefix = Prefix.new("#{@nick}!#{@user}@#{@host}")
46
+ @joined_on = @updated_on = Time.now.to_i
47
+
48
+ post @socket, @prefix, NICK, nick
49
+ @nick = nick
50
+ @prefix = "#{@nick}!#{@user}@#{@host}"
51
+
52
+ time = Time.now.to_i
53
+ @@users[@nick.downcase] = {
54
+ :nick => @nick,
55
+ :user => @user,
56
+ :host => @host,
57
+ :real => @real,
58
+ :prefix => @prefix,
59
+ :socket => @socket,
60
+ :joined_on => time,
61
+ :updated_on => time
62
+ }
63
+
64
+ initial_message
65
+
66
+ start_ping
67
+ end
68
+
69
+ def on_join(m)
70
+ channels = m.params[0].split(/\s*,\s*/)
71
+ password = m.params[1]
72
+
73
+ channels.each do |channel|
74
+ unless channel.downcase =~ /^#/
75
+ post @socket, server_name, ERR_NOSUCHCHANNEL, @nick, channel, "No such channel"
76
+ next
77
+ end
78
+
79
+ unless @@channels.key?(channel.downcase)
80
+ channel_create(channel)
81
+ else
82
+ return if @@channels[channel.downcase][:users].key?(@nick.downcase)
83
+
84
+ @@channels[channel.downcase][:users][@nick.downcase] = []
85
+ end
86
+
87
+ mode = @@channels[channel.downcase][:mode].empty? ? "" : "+" + @@channels[channel.downcase][:mode]
88
+ post @socket, server_name, RPL_CHANNELMODEIS, @nick, @@channels[channel.downcase][:alias], mode
89
+
90
+ channel_users = ""
91
+ @@channels[channel.downcase][:users].each do |nick, m|
92
+ post @@users[nick][:socket], @prefix, JOIN, @@channels[channel.downcase][:alias]
93
+
94
+ case
95
+ when m.index("@")
96
+ f = "@"
97
+ when m.index("+")
98
+ f = "+"
99
+ else
100
+ f = ""
101
+ end
102
+ channel_users += "#{f}#{@@users[nick.downcase][:nick]} "
103
+ end
104
+ post @socket, server_name, RPL_NAMREPLY, @@users[nick][:nick], "=", @@channels[channel.downcase][:alias], "#{channel_users.strip}"
105
+ post @socket, server_name, RPL_ENDOFNAMES, @@users[nick][:nick], @@channels[channel.downcase][:alias], "End of /NAMES list"
106
+ end
107
+ end
108
+
109
+ def on_part(m)
110
+ channel, message = *m.params
111
+
112
+ @@channels[channel.downcase][:users].each do |nick, f|
113
+ post @@users[nick][:socket], @prefix, PART, @@channels[channel.downcase][:alias], message
114
+ end
115
+ channel_part(channel)
116
+ end
117
+
118
+ def on_quit(m)
119
+ message = m.params[0]
120
+ @@channels.each do |channel, f|
121
+ if f[:users].key?(@nick.downcase)
122
+ channel_part(channel)
123
+ f[:users].each do |nick, m|
124
+ post @@users[nick][:socket], @prefix, QUIT, message
125
+ end
126
+ end
127
+ end
128
+ finish
129
+ end
130
+
131
+ def on_disconnected
132
+ super
133
+ @@channels.each do |channel, f|
134
+ if f[:users].key?(@nick.downcase)
135
+ channel_part(channel)
136
+ f[:users].each do |nick, m|
137
+ post @@users[nick][:socket], @prefix, QUIT, "disconnect"
138
+ end
139
+ end
140
+ end
141
+ channel_part_all
142
+ @@users.delete(@nick.downcase)
143
+ end
144
+
145
+ def on_who(m)
146
+ channel = m.params[0]
147
+ return unless channel
148
+
149
+ c = channel.downcase
150
+ case
151
+ when @@channels.key?(c)
152
+ @@channels[c][:users].each do |nickname, m|
153
+ nick = @@users[nickname][:nick]
154
+ user = @@users[nickname][:user]
155
+ host = @@users[nickname][:host]
156
+ real = @@users[nickname][:real]
157
+ case
158
+ when m.index("@")
159
+ f = "@"
160
+ when m.index("+")
161
+ f = "+"
162
+ else
163
+ f = ""
164
+ end
165
+ post @socket, server_name, RPL_WHOREPLY, @nick, @@channels[c][:alias], user, host, server_name, nick, "H#{f}", "0 #{real}"
166
+ end
167
+ post @socket, server_name, RPL_ENDOFWHO, @nick, @@channels[c][:alias], "End of /WHO list"
168
+ end
169
+ end
170
+
171
+ def on_mode(m)
172
+ end
173
+
174
+ def on_privmsg(m)
175
+ while (Time.now.to_i - @updated_on < 2)
176
+ sleep 2
177
+ end
178
+ idle_update
179
+
180
+ return on_ctcp(m[0], ctcp_decoding(m[1])) if m.ctcp?
181
+
182
+ target, message = *m.params
183
+ t = target.downcase
184
+
185
+ case
186
+ when @@channels.key?(t)
187
+ if @@channels[t][:users].key?(@nick.downcase)
188
+ @@channels[t][:users].each do |nick, m|
189
+ post @@users[nick][:socket], @prefix, PRIVMSG, @@channels[t][:alias], message unless nick == @nick.downcase
190
+ end
191
+ else
192
+ post @socket, nil, ERR_CANNOTSENDTOCHAN, @nick, target, "Cannot send to channel"
193
+ end
194
+ when @@users.key?(t)
195
+ post @@users[nick][:socket], @prefix, PRIVMSG, @@users[t][:nick], message
196
+ else
197
+ post @socket, nil, ERR_NOSUCHNICK, @nick, target, "No such nick/channel"
198
+ end
199
+ end
200
+
201
+ def on_ping(m)
202
+ post @socket, server_name, PONG, m.params[0]
203
+ end
204
+
205
+ def on_pong(m)
206
+ @ping = true
207
+ end
208
+
209
+ def idle_update
210
+ @updated_on = Time.now.to_i
211
+ if logged_in?
212
+ @@users[@nick.downcase][:updated_on] = @updated_on
213
+ end
214
+ end
215
+
216
+ def channel_create(channel)
217
+ @@channels[channel.downcase] = {
218
+ :alias => channel,
219
+ :topic => "",
220
+ :mode => default_channel_modes,
221
+ :users => {@nick.downcase => ["@"]},
222
+ }
223
+ end
224
+
225
+ def channel_part(channel)
226
+ @@channels[channel.downcase][:users].delete(@nick.downcase)
227
+ channel_delete(channel.downcase) if @@channels[channel.downcase][:users].size == 0
228
+ end
229
+
230
+ def channel_part_all
231
+ @@channels.each do |c|
232
+ channel_part(c)
233
+ end
234
+ end
235
+
236
+ def channel_delete(channel)
237
+ @@channels.delete(channel.downcase)
238
+ end
239
+
240
+ def post(socket, prefix, command, *params)
241
+ m = Message.new(prefix, command, params.map{|s|
242
+ s.gsub(/[\r\n]/, "")
243
+ })
244
+ socket << m
245
+ rescue
246
+ finish
247
+ end
248
+
249
+ def start_ping
250
+ Thread.start do
251
+ loop do
252
+ @ping = false
253
+ time = Time.now.to_i
254
+ if @ping == false && (time - @updated_on > 60)
255
+ post @socket, server_name, PING, @prefix
256
+ loop do
257
+ sleep 1
258
+ if @ping
259
+ break
260
+ end
261
+ if 60 < Time.now.to_i - time
262
+ Thread.stop
263
+ finish
264
+ end
265
+ end
266
+ end
267
+ sleep 60
268
+ end
269
+ end
270
+ end
271
+
272
+ # Call when client connected.
273
+ # Send RPL_WELCOME sequence. If you want to customize, override this method at subclass.
274
+ def initial_message
275
+ post @socket, server_name, RPL_WELCOME, @nick, "Welcome to the Internet Relay Network #{@prefix}"
276
+ post @socket, server_name, RPL_YOURHOST, @nick, "Your host is #{server_name}, running version #{server_version}"
277
+ post @socket, server_name, RPL_CREATED, @nick, "This server was created #{Time.now}"
278
+ post @socket, server_name, RPL_MYINFO, @nick, "#{server_name} #{server_version} #{available_user_modes} #{available_channel_modes}"
279
+ end
280
+
281
+ end
282
+
283
+
284
+ if __FILE__ == $0
285
+ require "optparse"
286
+
287
+ opts = {
288
+ :port => 6969,
289
+ :host => "localhost",
290
+ :log => nil,
291
+ :debug => false,
292
+ :foreground => false,
293
+ }
294
+
295
+ OptionParser.new do |parser|
296
+ parser.instance_eval do
297
+ self.banner = <<-EOB.gsub(/^\t+/, "")
298
+ Usage: #{$0} [opts]
299
+
300
+ EOB
301
+
302
+ separator ""
303
+
304
+ separator "Options:"
305
+ on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
306
+ opts[:port] = port
307
+ end
308
+
309
+ on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
310
+ opts[:host] = host
311
+ end
312
+
313
+ on("-l", "--log LOG", "log file") do |log|
314
+ opts[:log] = log
315
+ end
316
+
317
+ on("--debug", "Enable debug mode") do |debug|
318
+ opts[:log] = $stdout
319
+ opts[:debug] = true
320
+ end
321
+
322
+ on("-f", "--foreground", "run foreground") do |foreground|
323
+ opts[:log] = $stdout
324
+ opts[:foreground] = true
325
+ end
326
+
327
+ on("-n", "--name [user name or email address]") do |name|
328
+ opts[:name] = name
329
+ end
330
+
331
+ parse!(ARGV)
332
+ end
333
+ end
334
+
335
+ opts[:logger] = Logger.new(opts[:log], "daily")
336
+ opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
337
+
338
+ #def daemonize(foreground = false)
339
+ # [:INT, :TERM, :HUP].each do |sig|
340
+ # Signal.trap sig, "EXIT"
341
+ # end
342
+ # return yield if $DEBUG or foreground
343
+ # Process.fork do
344
+ # Process.setsid
345
+ # Dir.chdir "/"
346
+ # STDIN.reopen "/dev/null"
347
+ # STDOUT.reopen "/dev/null", "a"
348
+ # STDERR.reopen STDOUT
349
+ # yield
350
+ # end
351
+ # exit! 0
352
+ #end
353
+
354
+ #daemonize(opts[:debug] || opts[:foreground]) do
355
+ Net::IRC::Server.new(opts[:host], opts[:port], NetIrcServer, opts).start
356
+ #end
357
+ end
358
+
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # vim:fileencoding=UTF-8:
2
3
  =begin
3
4
 
4
5
 
@@ -11,7 +12,7 @@ Ruby's by cho45
11
12
  $LOAD_PATH << "lib"
12
13
  $LOAD_PATH << "../lib"
13
14
 
14
- $KCODE = "u" # json use this
15
+ $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
15
16
 
16
17
  require "rubygems"
17
18
  require "json"
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # vim:fileencoding=UTF-8:
2
3
  =begin
3
4
  # sig.rb
4
5
 
@@ -16,7 +17,7 @@ ServerLog IRC Gateway
16
17
  $LOAD_PATH << "lib"
17
18
  $LOAD_PATH << "../lib"
18
19
 
19
- $KCODE = "u" # json use this
20
+ $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
20
21
 
21
22
  require "rubygems"
22
23
  require "net/irc"
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
- # vim:fileencoding=utf-8:
3
- # -*- coding: utf-8 -*-
2
+ # vim:fileencoding=UTF-8:
3
+ $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
4
4
  =begin
5
5
 
6
6
  # tig.rb
7
7
 
8
8
  Ruby version of TwitterIrcGateway
9
- ( http://www.misuzilla.org/dist/net/twitterircgateway/ )
9
+ <http://www.misuzilla.org/dist/net/twitterircgateway/>
10
10
 
11
11
  ## Launch
12
12
 
@@ -20,15 +20,40 @@ If you want to help:
20
20
 
21
21
  Options specified by after IRC realname.
22
22
 
23
- Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ).
23
+ Configuration example for Tiarra <http://coderepos.org/share/wiki/Tiarra>.
24
24
 
25
- twitter {
26
- host: localhost
27
- port: 16668
28
- name: username@example.com athack jabber=username@example.com:jabberpasswd tid ratio=32:1 replies=6 maxlimit=70
25
+ general {
26
+ server-in-encoding: utf8
27
+ server-out-encoding: utf8
28
+ client-in-encoding: utf8
29
+ client-out-encoding: utf8
30
+ }
31
+
32
+ networks {
33
+ name: tig
34
+ }
35
+
36
+ tig {
37
+ server: localhost 16668
29
38
  password: password on Twitter
30
- in-encoding: utf8
31
- out-encoding: utf8
39
+ # Recommended
40
+ name: username mentions tid
41
+
42
+ # Same as TwitterIrcGateway.exe.config.sample
43
+ # (90, 360 and 300 seconds)
44
+ #name: username dm ratio=4:1 maxlimit=50
45
+ #name: username dm ratio=20:5:6 maxlimit=62 mentions
46
+ #
47
+ # <http://cheebow.info/chemt/archives/2009/04/posttwit.html>
48
+ # (60, 360 and 150 seconds)
49
+ #name: username dm ratio=30:5:12 maxlimit=94 mentions
50
+ #
51
+ # <http://cheebow.info/chemt/archives/2009/07/api150rhtwit.html>
52
+ # (36, 360 and 150 seconds)
53
+ #name: username dm ratio=50:5:12 maxlimit=134 mentions
54
+ #
55
+ # for Jabber
56
+ #name: username jabber=username@example.com:jabberpasswd
32
57
  }
33
58
 
34
59
  ### athack
@@ -42,13 +67,13 @@ it's good for Twitter like reply command (@nick).
42
67
  In this case, you will see torrent of join messages after connected,
43
68
  because NAMES list can't send @ leading nick (it interpreted op.)
44
69
 
45
- ### tid[=<color>]
70
+ ### tid[=<color:10>[,<bgcolor>]]
46
71
 
47
72
  Apply ID to each message for make favorites by CTCP ACTION.
48
73
 
49
- /me fav ID [ID...]
74
+ /me fav [ID...]
50
75
 
51
- <color> can be
76
+ <color> and <bgcolor> can be
52
77
 
53
78
  0 => white
54
79
  1 => black
@@ -67,11 +92,10 @@ Apply ID to each message for make favorites by CTCP ACTION.
67
92
  14 => grey
68
93
  15 => lightgrey silver
69
94
 
70
-
71
95
  ### jabber=<jid>:<pass>
72
96
 
73
97
  If `jabber=<jid>:<pass>` option specified,
74
- use jabber to get friends timeline.
98
+ use Jabber to get friends timeline.
75
99
 
76
100
  You must setup im notifing settings in the site and
77
101
  install "xmpp4r-simple" gem.
@@ -84,28 +108,77 @@ Be careful for managing password.
84
108
 
85
109
  Use IM instead of any APIs (e.g. post)
86
110
 
87
- ### ratio=<timeline>:<friends>
111
+ ### ratio=<timeline>:<dm>[:<mentions>]
112
+
113
+ "121:6:20" by default.
114
+
115
+ /me ratios
116
+
117
+ Ratio | Timeline | DM | Mentions |
118
+ ---------+----------+-------+----------|
119
+ 1 | 24s | N/A | N/A |
120
+ 141:6 | 26s | 10m OR N/A |
121
+ 135:12 | 27s | 5m OR N/A |
122
+ 135:6:6 | 27s | 10m | 10m |
123
+ ---------+----------+-------+----------|
124
+ 121:6:20 | 30s | 10m | 3m |
125
+ ---------+----------+-------+----------|
126
+ 4:1 | 31s | 2m1s | N/A |
127
+ 50:5:12 | 49s | 8m12s | 3m25s |
128
+ 20:5:6 | 57s | 3m48s | 3m10s |
129
+ 30:5:12 | 58s | 5m45s | 2m24s |
130
+ 1:1:1 | 1m13s | 1m13s | 1m13s |
131
+ ---------------------------------------+
132
+ (Hourly limit: 150)
133
+
134
+ ### dm[=<ratio>]
135
+
136
+ ### mentions[=<ratio>]
137
+
138
+ ### maxlimit=<hourly_limit>
139
+
140
+ ### clientspoofing
141
+
142
+ ### httpproxy=[<user>[:<password>]@]<address>[:<port>]
143
+
144
+ ### main_channel=<channel:#twitter>
145
+
146
+ ### api_source=<source>
88
147
 
89
- ### replies[=<ratio>]
148
+ ### max_params_count=<number:3>
90
149
 
91
- ### maxlimit=<hourly limit>
150
+ ### check_friends_interval=<seconds:3600>
92
151
 
93
- ### checkrls=<interval seconds>
152
+ ### check_updates_interval=<seconds:86400>
94
153
 
95
- ### secure
154
+ Set 0 to disable checking.
96
155
 
97
- Force SSL for API.
156
+ ### old_style_reply
157
+
158
+ ### tmap_size=<number:10404>
159
+
160
+ ### strftime=<format:%m-%d %H:%M>
161
+
162
+ ### untiny_whole_urls
163
+
164
+ ### bitlify=<username>:<apikey>:<minlength:20>
165
+
166
+ ### unuify
167
+
168
+ ### shuffled_tmap
98
169
 
99
170
  ## Extended commands through the CTCP ACTION
100
171
 
101
172
  ### list (ls)
102
173
 
103
- /me list NICK_or_screen_name
174
+ /me list NICK [NUMBER]
104
175
 
105
176
  ### fav (favorite, favourite, unfav, unfavorite, unfavourite)
106
177
 
107
- /me fav ID [ID...]
108
- /me unfav ID [ID...]
178
+ /me fav [ID...]
179
+ /me unfav [ID...]
180
+ /me fav! [ID...]
181
+ /me fav NICK
109
182
 
110
183
  ### link (ln)
111
184
 
@@ -113,27 +186,45 @@ Force SSL for API.
113
186
 
114
187
  ### destroy (del, delete, miss, oops, remove, rm)
115
188
 
116
- /me destroy ID [ID...]
189
+ /me destroy [ID...]
117
190
 
118
191
  ### in (location)
119
192
 
120
193
  /me in Sugamo, Tokyo, Japan
121
194
 
122
- ### reply (re)
195
+ ### reply (re, mention)
123
196
 
124
197
  /me reply ID blah, blah...
125
198
 
126
- ### utf7
199
+ ### retweet (rt)
200
+
201
+ /me retweet ID (blah, blah...)
202
+
203
+ ### utf7 (utf-7)
127
204
 
128
- /me utf7
205
+ /me utf7
129
206
 
130
207
  ### name
131
208
 
132
- /me name My Name
209
+ /me name My Name
133
210
 
134
211
  ### description (desc)
135
212
 
136
- /me description blah, blah...
213
+ /me description blah, blah...
214
+
215
+ ### spoof
216
+
217
+ /me spoof
218
+ /me spoo[o...]f
219
+ /me spoof tigrb twitterircgateway twitt web mobileweb
220
+
221
+ ### bot (drone)
222
+
223
+ /me bot NICK [NICK...]
224
+
225
+ ## Feed
226
+
227
+ <http://coderepos.org/share/log/lang/ruby/net-irc/trunk/examples/tig.rb?limit=100&mode=stop_on_copy&format=rss>
137
228
 
138
229
  ## License
139
230
 
@@ -141,24 +232,30 @@ Ruby's by cho45
141
232
 
142
233
  =end
143
234
 
144
- $LOAD_PATH << "lib"
145
- $LOAD_PATH << "../lib"
146
-
147
- $KCODE = "u" # json use this
235
+ if File.directory? "lib"
236
+ $LOAD_PATH << "lib"
237
+ elsif File.directory? File.expand_path("lib", "..")
238
+ $LOAD_PATH << File.expand_path("lib", "..")
239
+ end
148
240
 
149
241
  require "rubygems"
150
242
  require "net/irc"
151
243
  require "net/https"
152
244
  require "uri"
153
- require "json"
154
- require "socket"
155
245
  require "time"
156
246
  require "logger"
157
247
  require "yaml"
158
248
  require "pathname"
159
- require "cgi"
249
+ require "ostruct"
250
+ require "json"
160
251
 
161
- Net::HTTP.version_1_2
252
+ begin
253
+ require "iconv"
254
+ require "punycode"
255
+ rescue LoadError
256
+ end
257
+
258
+ module Net::IRC::Constants; RPL_WHOISBOT = "335"; RPL_CREATEONTIME = "329"; end
162
259
 
163
260
  class TwitterIrcGateway < Net::IRC::Server::Session
164
261
  def server_name
@@ -166,19 +263,29 @@ class TwitterIrcGateway < Net::IRC::Server::Session
166
263
  end
167
264
 
168
265
  def server_version
169
- "0.0.0"
266
+ rev = %q$Revision: 34381 $.split[1]
267
+ rev &&= "+r#{rev}"
268
+ "0.0.0#{rev}"
269
+ end
270
+
271
+ def available_user_modes
272
+ "o"
273
+ end
274
+
275
+ def available_channel_modes
276
+ "mnti"
170
277
  end
171
278
 
172
279
  def main_channel
173
- "#twitter"
280
+ @opts.main_channel || "#twitter"
174
281
  end
175
282
 
176
- def api_base
177
- URI("http://twitter.com/")
283
+ def api_base(secure = true)
284
+ URI("http#{"s" if secure}://twitter.com/")
178
285
  end
179
286
 
180
287
  def api_source
181
- "tigrb"
288
+ "#{@opts.api_source || "tigrb"}"
182
289
  end
183
290
 
184
291
  def jabber_bot_id
@@ -186,37 +293,83 @@ class TwitterIrcGateway < Net::IRC::Server::Session
186
293
  end
187
294
 
188
295
  def hourly_limit
189
- 60
296
+ 150
190
297
  end
191
298
 
192
- class ApiFailed < StandardError; end
299
+ class APIFailed < StandardError; end
193
300
 
194
301
  def initialize(*args)
195
302
  super
196
- @groups = {}
197
- @channels = [] # joined channels (groups)
198
- @nicknames = {}
199
- @user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)})"
200
- @config = Pathname.new(ENV["HOME"]) + ".tig"
201
- @map = nil
303
+ @groups = {}
304
+ @channels = [] # joined channels (groups)
305
+ @nicknames = {}
306
+ @drones = []
307
+ @config = Pathname.new(ENV["HOME"]) + ".tig"
308
+ @etags = {}
309
+ @consums = []
310
+ @limit = hourly_limit
311
+ @friends =
312
+ @sources =
313
+ @rsuffix_regex =
314
+ @im =
315
+ @im_thread =
316
+ @utf7 =
317
+ @httpproxy = nil
202
318
  load_config
203
319
  end
204
320
 
205
321
  def on_user(m)
206
322
  super
207
- post @prefix, JOIN, main_channel
208
- post server_name, MODE, main_channel, "+o", @prefix.nick
209
323
 
210
- @real, *@opts = @opts.name || @real.split(/\s+/)
211
- @opts = @opts.inject({}) {|r,i|
212
- key, value = i.split("=")
213
- r.update(key => value)
214
- }
215
- @tmap = TypableMap.new
324
+ @real, *@opts = (@opts.name || @real).split(" ")
325
+ @opts = @opts.inject({}) do |r, i|
326
+ key, value = i.split("=", 2)
327
+ key = "mentions" if key == "replies" # backcompat
328
+ r.update key => case value
329
+ when nil then true
330
+ when /\A\d+\z/ then value.to_i
331
+ when /\A(?:\d+\.\d*|\.\d+)\z/ then value.to_f
332
+ else value
333
+ end
334
+ end
335
+ @opts = OpenStruct.new(@opts)
336
+ @opts.httpproxy.sub!(/\A(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?\z/) do
337
+ @httpproxy = OpenStruct.new({
338
+ :user => $1, :password => $2, :address => $3, :port => $4.to_i,
339
+ })
340
+ $&.sub(/[^:@]+(?=@)/, "********")
341
+ end if @opts.httpproxy
342
+
343
+ retry_count = 0
344
+ begin
345
+ @me = api("account/update_profile") #api("account/verify_credentials")
346
+ rescue APIFailed => e
347
+ @log.error e.inspect
348
+ sleep 1
349
+ retry_count += 1
350
+ retry if retry_count < 3
351
+ log "Failed to access API 3 times." <<
352
+ " Please check your username/email and password combination, " <<
353
+ " Twitter Status <http://status.twitter.com/> and try again later."
354
+ finish
355
+ end
356
+
357
+ @prefix = prefix(@me)
358
+ @user = @prefix.user
359
+ @host = @prefix.host
216
360
 
217
- if @opts["jabber"]
218
- jid, pass = @opts["jabber"].split(":", 2)
219
- @opts["jabber"].replace("jabber=#{jid}:********")
361
+ #post NICK, @me.screen_name if @nick != @me.screen_name
362
+ post server_name, MODE, @nick, "+o"
363
+ post @prefix, JOIN, main_channel
364
+ post server_name, MODE, main_channel, "+mto", @nick
365
+ if @me.status
366
+ @me.status.user = @me
367
+ post @prefix, TOPIC, main_channel, generate_status_message(@me.status.text)
368
+ end
369
+
370
+ if @opts.jabber
371
+ jid, pass = @opts.jabber.split(":", 2)
372
+ @opts.jabber.replace("jabber=#{jid}:********")
220
373
  if jabber_bot_id
221
374
  begin
222
375
  require "xmpp4r-simple"
@@ -227,21 +380,36 @@ class TwitterIrcGateway < Net::IRC::Server::Session
227
380
  finish
228
381
  end
229
382
  else
230
- @opts.delete("jabber")
383
+ @opts.delete_field :jabber
231
384
  log "This gateway does not support Jabber bot."
232
385
  end
233
386
  end
234
387
 
235
- log "Client Options: #{@opts.inspect}"
236
- @log.info "Client Options: #{@opts.inspect}"
388
+ log "Client options: #{@opts.marshal_dump.inspect}"
389
+ @log.info "Client options: #{@opts.inspect}"
390
+
391
+ @opts.tid = begin
392
+ c = @opts.tid # expect: 0..15, true, "0,1"
393
+ b = nil
394
+ c, b = c.split(",", 2).map {|i| i.to_i } if c.respond_to? :split
395
+ c = 10 unless (0 .. 15).include? c # 10: teal
396
+ if (0 .. 15).include?(b)
397
+ "\003%.2d,%.2d[%%s]\017" % [c, b]
398
+ else
399
+ "\003%.2d[%%s]\017" % c
400
+ end
401
+ end if @opts.tid
237
402
 
238
- @hourly_limit = hourly_limit
403
+ @ratio = (@opts.ratio || "121").split(":")
404
+ @ratio = Struct.new(:timeline, :dm, :mentions).new(*@ratio)
405
+ @ratio.dm ||= @opts.dm == true ? @opts.mentions ? 6 : 26 : @opts.dm
406
+ @ratio.mentions ||= @opts.mentions == true ? @opts.dm ? 20 : 26 : @opts.mentions
239
407
 
240
- @check_rate_limit_thread = Thread.start do
408
+ @check_friends_thread = Thread.start do
241
409
  loop do
242
410
  begin
243
- check_rate_limit
244
- rescue ApiFailed => e
411
+ check_friends
412
+ rescue APIFailed => e
245
413
  @log.error e.inspect
246
414
  rescue Exception => e
247
415
  @log.error e.inspect
@@ -249,44 +417,41 @@ class TwitterIrcGateway < Net::IRC::Server::Session
249
417
  @log.error "\t#{l}"
250
418
  end
251
419
  end
252
- sleep @opts["checkrls"] || 3600 # 1 hour
420
+ sleep @opts.check_friends_interval || 3600
253
421
  end
254
422
  end
255
- sleep 5
256
423
 
257
- @ratio = Struct.new(:timeline, :friends, :replies).new(*(@opts["ratio"] || "10:3").split(":").map {|ratio| ratio.to_f })
258
- @ratio[:replies] = @opts.key?("replies") ? (@opts["replies"] || 5).to_f : 0.0
424
+ return if @opts.jabber
259
425
 
260
- footing = @ratio.inject {|sum, ratio| sum + ratio }
426
+ @timeline = TypableMap.new(@opts.tmap_size || 10_404,
427
+ @opts.shuffled_tmap || false)
428
+ @sources = @opts.clientspoofing ? fetch_sources : [[api_source, "tig.rb"]]
261
429
 
262
- @ratio.each_pair {|m, v| @ratio[m] = v / footing }
430
+ update_redundant_suffix
431
+ @check_updates_thread = Thread.start do
432
+ sleep @opts.check_updates_interval || 86400
263
433
 
264
- @timeline = []
265
- @check_friends_thread = Thread.start do
266
434
  loop do
267
435
  begin
268
- check_friends
269
- rescue ApiFailed => e
270
- @log.error e.inspect
436
+ check_updates
271
437
  rescue Exception => e
272
438
  @log.error e.inspect
273
439
  e.backtrace.each do |l|
274
440
  @log.error "\t#{l}"
275
441
  end
276
442
  end
277
- sleep freq(@ratio[:friends])
443
+ sleep 0.01 * (90 + rand(21)) *
444
+ (@opts.check_updates_interval || 86400) # 0.9 ... 1.1 day
278
445
  end
279
- end
446
+ end if @opts.check_updates_interval != 0
280
447
 
281
- return if @opts["jabber"]
282
-
283
- sleep 3
284
448
  @check_timeline_thread = Thread.start do
449
+ sleep 2 * (@me.friends_count / 100.0).ceil
450
+
285
451
  loop do
286
452
  begin
287
453
  check_timeline
288
- # check_direct_messages
289
- rescue ApiFailed => e
454
+ rescue APIFailed => e
290
455
  @log.error e.inspect
291
456
  rescue Exception => e
292
457
  @log.error e.inspect
@@ -294,18 +459,33 @@ class TwitterIrcGateway < Net::IRC::Server::Session
294
459
  @log.error "\t#{l}"
295
460
  end
296
461
  end
297
- sleep freq(@ratio[:timeline])
462
+ sleep interval(@ratio.timeline)
298
463
  end
299
464
  end
300
465
 
301
- return unless @opts.key?("replies")
466
+ @check_dms_thread = Thread.start do
467
+ loop do
468
+ begin
469
+ check_direct_messages
470
+ rescue APIFailed => e
471
+ @log.error e.inspect
472
+ rescue Exception => e
473
+ @log.error e.inspect
474
+ e.backtrace.each do |l|
475
+ @log.error "\t#{l}"
476
+ end
477
+ end
478
+ sleep interval(@ratio.dm)
479
+ end
480
+ end if @opts.dm
481
+
482
+ @check_mentions_thread = Thread.start do
483
+ sleep interval(@ratio.timeline) / 2
302
484
 
303
- sleep 10
304
- @check_replies_thread = Thread.start do
305
485
  loop do
306
486
  begin
307
- check_replies
308
- rescue ApiFailed => e
487
+ check_mentions
488
+ rescue APIFailed => e
309
489
  @log.error e.inspect
310
490
  rescue Exception => e
311
491
  @log.error e.inspect
@@ -313,406 +493,821 @@ class TwitterIrcGateway < Net::IRC::Server::Session
313
493
  @log.error "\t#{l}"
314
494
  end
315
495
  end
316
- sleep freq(@ratio[:replies])
496
+ sleep interval(@ratio.mentions)
317
497
  end
318
- end
498
+ end if @opts.mentions
319
499
  end
320
500
 
321
501
  def on_disconnected
322
- @check_friends_thread.kill rescue nil
323
- @check_replies_thread.kill rescue nil
324
- @check_timeline_thread.kill rescue nil
325
- @check_rate_limit_thread.kill rescue nil
326
- @im_thread.kill rescue nil
327
- @im.disconnect rescue nil
502
+ @check_friends_thread.kill rescue nil
503
+ @check_timeline_thread.kill rescue nil
504
+ @check_mentions_thread.kill rescue nil
505
+ @check_dms_thread.kill rescue nil
506
+ @check_updates_thread.kill rescue nil
507
+ @im_thread.kill rescue nil
508
+ @im.disconnect rescue nil
328
509
  end
329
510
 
330
511
  def on_privmsg(m)
331
- return on_ctcp(m[0], ctcp_decoding(m[1])) if m.ctcp?
512
+ target, mesg = *m.params
513
+
514
+ m.ctcps.each {|ctcp| on_ctcp target, ctcp } if m.ctcp?
515
+
516
+ return if mesg.empty?
517
+ return on_ctcp_action(target, mesg) if mesg.sub!(/\A +/, "") #and @opts.direct_action
518
+
519
+ command, params = mesg.split(" ", 2)
520
+ case command.downcase # TODO: escape recursive
521
+ when "d", "dm"
522
+ screen_name, mesg = params.split(" ", 2)
523
+ unless screen_name or mesg
524
+ log 'Send "d NICK message" to send a direct (private) message.' <<
525
+ " You may reply to a direct message the same way."
526
+ return
527
+ end
528
+ m.params[0] = screen_name.sub(/\A@/, "")
529
+ m.params[1] = mesg #.rstrip
530
+ return on_privmsg(m)
531
+ # TODO
532
+ #when "f", "follow"
533
+ #when "on"
534
+ #when "off" # BUG if no args
535
+ #when "g", "get"
536
+ #when "w", "whois"
537
+ #when "n", "nudge" # BUG if no args
538
+ #when "*", "fav"
539
+ #when "delete"
540
+ #when "stats" # no args
541
+ #when "leave"
542
+ #when "invite"
543
+ end unless command.nil?
544
+
545
+ mesg = escape_http_urls(mesg)
546
+ mesg = @opts.unuify ? unuify(mesg) : bitlify(mesg)
547
+ mesg = Iconv.iconv("UTF-7", "UTF-8", mesg).join.encoding!("ASCII-8BIT") if @utf7
548
+
549
+ ret = nil
332
550
  retry_count = 3
333
- ret = nil
334
- target, message = *m.params
335
- message = Iconv.iconv("UTF-7", "UTF-8", message).join.force_encoding("ASCII-8BIT") if @utf7
336
551
  begin
337
- if target =~ /^#/
338
- if @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post
339
- ret = @im.deliver(jabber_bot_id, message)
340
- post "#{nick}!#{nick}@#{api_base.host}", TOPIC, main_channel, untinyurl(message)
552
+ case
553
+ when target.ch?
554
+ if @opts.alwaysim and @im and @im.connected? # in Jabber mode, using Jabber post
555
+ ret = @im.deliver(jabber_bot_id, mesg)
556
+ post @prefix, TOPIC, main_channel, mesg
341
557
  else
342
- ret = api("statuses/update", { :status => message })
558
+ previous = @me.status
559
+ if previous and
560
+ ((Time.now - Time.parse(previous.created_at)).to_i < 60 rescue true) and
561
+ mesg.strip == previous.text
562
+ log "You can't submit the same status twice in a row."
563
+ return
564
+ end
565
+
566
+ in_reply_to = nil
567
+ 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
577
+ end
578
+ end
579
+
580
+ q = { :status => mesg, :source => source }
581
+ q.update(:in_reply_to_status_id => in_reply_to) if in_reply_to
582
+ ret = api("statuses/update", q)
583
+ log oops(ret) if ret.truncated
584
+ ret.user.status = ret
585
+ @me = ret.user
586
+ log "Status updated"
343
587
  end
588
+ when target.nick? # Direct message
589
+ ret = api("direct_messages/new", { :screen_name => target, :text => mesg })
590
+ post server_name, NOTICE, @nick, "Your direct message has been sent to #{target}."
344
591
  else
345
- # direct message
346
- ret = api("direct_messages/new", { :user => target, :text => message })
592
+ post server_name, ERR_NOSUCHNICK, target, "No such nick/channel"
347
593
  end
348
- raise ApiFailed, "API failed" unless ret
349
- log "Status Updated"
350
594
  rescue => e
351
595
  @log.error [retry_count, e.inspect].inspect
352
596
  if retry_count > 0
353
597
  retry_count -= 1
354
598
  @log.debug "Retry to setting status..."
355
599
  retry
356
- else
357
- log "Some Error Happened on Sending #{message}. #{e}"
358
600
  end
601
+ log "Some Error Happened on Sending #{mesg}. #{e}"
359
602
  end
360
603
  end
361
604
 
362
- def on_ctcp(target, message)
363
- _, command, *args = message.split(/\s+/)
364
- case command
365
- when "call" # /me call <twitter-id> as <nickname>
366
- twitter_id = args[0]
367
- nickname = args[2] || args[1] # allow omitting 'as'
368
- if nickname == "is"
369
- @nicknames.delete(twitter_id)
370
- post server_name, NOTICE, main_channel, "Removed nickname for #{twitter_id}"
371
- else
372
- @nicknames[twitter_id] = nickname
373
- post server_name, NOTICE, main_channel, "Call #{twitter_id} as #{nickname}"
374
- end
375
- when "utf7"
376
- begin
377
- require "iconv"
378
- @utf7 = !@utf7
379
- log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}"
380
- rescue LoadError => e
381
- log "Can't load iconv."
382
- end
383
- when "list", "ls"
384
- nick = args.first
385
- unless (1..200).include?(count = args[1].to_i)
386
- count = 20
387
- end
388
- @log.debug([nick, message])
389
- to = nick == @nick ? server_name : nick
390
- res = api("statuses/user_timeline", { :id => nick, :count => "#{count}" }).reverse_each do |s|
391
- @log.debug(s)
392
- post to, NOTICE, main_channel, "#{generate_status_message(s)}"
393
- end
394
- unless res
605
+ def on_whois(m)
606
+ nick = m.params[0]
607
+ unless nick.nick?
608
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
609
+ return
610
+ end
611
+
612
+ unless user = user(nick)
613
+ if api("users/username_available", { :username => nick }).valid
614
+ # TODO: 404 suspended
395
615
  post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
616
+ return
396
617
  end
397
- when /^(un)?fav(?:ou?rite)?$/
398
- method, pfx = $1.nil? ? ["create", "F"] : ["destroy", "Unf"]
399
- args.each_with_index do |tid, i|
400
- st = @tmap[tid]
401
- if st
402
- sleep 1 if i > 0
403
- res = api("favorites/#{method}/#{st["id"]}")
404
- post server_name, NOTICE, main_channel, "#{pfx}av: #{res["user"]["screen_name"]}: #{res["text"]}"
405
- else
406
- post server_name, NOTICE, main_channel, "No such ID #{tid}"
407
- end
408
- end
409
- when "link", "ln"
410
- args.each do |tid|
411
- st = @tmap[tid]
412
- if st
413
- post server_name, NOTICE, main_channel, "#{api_base + st["user"]["screen_name"]}/statuses/#{st["id"]}"
414
- else
415
- post server_name, NOTICE, main_channel, "No such ID #{tid}"
416
- end
417
- end
418
- # when /^ratios?$/
419
- # if args[1].nil? ||
420
- # @opts.key?("replies") && args[2].nil?
421
- # return post server_name, NOTICE, main_channel, "/me ratios <timeline> <frends>[ <replies>]"
422
- # end
423
- # ratios = args.map {|ratio| ratio.to_f }
424
- # if ratios.any? {|ratio| ratio <= 0.0 }
425
- # return post server_name, NOTICE, main_channel, "Ratios must be greater than 0."
426
- # end
427
- # footing = ratios.inject {|sum, ratio| sum + ratio }
428
- # @ratio[:timeline] = ratios[0]
429
- # @ratio[:friends] = ratios[1]
430
- # @ratio[:replies] = ratios[2] || 0.0
431
- # @ratio.each_pair {|m, v| @ratio[m] = v / footing }
432
- # intervals = @ratio.map {|ratio| freq ratio }
433
- # post server_name, NOTICE, main_channel, "Intervals: #{intervals.join(", ")}"
434
- when /^(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))$/ # destroy, delete, del, remove, rm, miss, oops
435
- args.each_with_index do |tid, i|
436
- st = @tmap[tid]
437
- if st
438
- sleep 1 if i > 0
439
- res = api("statuses/destroy/#{st["id"]}")
440
- post server_name, NOTICE, main_channel, "Destroyed: #{res["text"]}"
441
- else
442
- post server_name, NOTICE, main_channel, "No such ID #{tid}"
443
- end
444
- end
445
- when "name"
446
- name = message.split(/\s+/, 3)[2]
447
- unless name.nil?
448
- api("account/update_profile", { :name => name })
449
- post server_name, NOTICE, main_channel, "You are named #{name}."
450
- end
451
- when "email"
452
- # FIXME
453
- email = args.first
454
- unless email.nil?
455
- api("account/update_profile", { :email => email })
456
- end
457
- when "url"
458
- # FIXME
459
- url = args.first || ""
460
- api("account/update_profile", { :url => url })
461
- when "in", "location"
462
- location = message.split(/\s+/, 3)[2] || ""
463
- api("account/update_profile", { :location => location })
464
- location = location.empty? ? "nowhere" : "in #{location}"
465
- post server_name, NOTICE, main_channel, "You are #{location} now."
466
- when /^desc(?:ription)?$/
467
- # FIXME
468
- description = message.split(/\s+/, 3)[2] || ""
469
- api("account/update_profile", { :description => description })
470
- when /^colou?rs?$/
471
- # FIXME
472
- # bg, text, link, fill and border
473
- when "image", "img"
474
- # FIXME
475
- url = args.first
476
- # TODO: DCC SEND
477
- when "follow"
478
- # FIXME
479
- when "leave"
480
- # FIXME
481
- when /^re(?:ply)?$/
482
- tid = args.first
483
- st = @tmap[tid]
484
- if st
485
- msg = message.split(/\s+/, 4)[3]
486
- ret = api("statuses/update", { :status => msg, :in_reply_to_status_id => "#{st["id"]}" })
487
- if ret
488
- log "Status updated (In reply to \x03#{@opts["tid"] || 10}[#{tid}]\x0f <#{api_base + st["user"]["screen_name"]}/statuses/#{st["id"]}>)"
489
- end
490
- end
618
+ user = api("users/show/#{nick}", {}, { :authenticate => false })
491
619
  end
492
- rescue ApiFailed => e
493
- log e.inspect
494
- end
495
620
 
496
- def on_whois(m)
497
- nick = m.params[0]
498
- f = (@friends || []).find {|i| i["screen_name"] == nick }
499
- if f
500
- post server_name, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}"
501
- post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s
502
- post server_name, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle"
503
- post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list"
504
- else
505
- post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
621
+ prefix = prefix(user)
622
+ desc = user.name
623
+ desc = "#{desc} / #{user.description}".gsub(/\s+/, " ") if user.description and not user.description.empty?
624
+ signon_at = Time.parse(user.created_at).to_i rescue 0
625
+ idle_sec = (Time.now - (user.status ? Time.parse(user.status.created_at) : signon_at)).to_i rescue 0
626
+ location = user.location
627
+ location = "SoMa neighborhood of San Francisco, CA" if location.nil? or location.empty?
628
+ post server_name, RPL_WHOISUSER, @nick, nick, prefix.user, prefix.host, "*", desc
629
+ post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, location
630
+ post server_name, RPL_WHOISIDLE, @nick, nick, "#{idle_sec}", "#{signon_at}", "seconds idle, signon time"
631
+ post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list"
632
+ if @drones.include?(user.id)
633
+ post server_name, RPL_WHOISBOT, @nick, nick, "is a \002Bot\002 on #{server_name}"
506
634
  end
507
635
  end
508
636
 
509
637
  def on_who(m)
510
638
  channel = m.params[0]
511
639
  case
512
- when channel == main_channel
513
- # "<channel> <user> <host> <server> <nick>
514
- # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
515
- # :<hopcount> <real name>"
516
- @friends.each do |f|
517
- user = nick = f["screen_name"]
518
- host = serv = api_base.host
519
- real = f["name"]
520
- post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
521
- end
640
+ when channel.casecmp(main_channel).zero?
641
+ users = [@me]
642
+ users.concat @friends.reverse if @friends
643
+ users.each {|friend| whoreply channel, friend }
522
644
  post server_name, RPL_ENDOFWHO, @nick, channel
523
- when @groups.key?(channel)
524
- @groups[channel].each do |name|
525
- f = @friends.find {|i| i["screen_name"] == name }
526
- user = nick = f["screen_name"]
527
- host = serv = api_base.host
528
- real = f["name"]
529
- post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
645
+ when (@groups.key?(channel) and @friends)
646
+ @groups[channel].each do |nick|
647
+ whoreply channel, friend(nick)
530
648
  end
531
649
  post server_name, RPL_ENDOFWHO, @nick, channel
532
650
  else
533
- post server_name, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel"
651
+ post server_name, ERR_NOSUCHNICK, @nick, "No such nick/channel"
534
652
  end
535
653
  end
536
654
 
537
655
  def on_join(m)
538
- channels = m.params[0].split(/\s*,\s*/)
656
+ channels = m.params[0].split(/ *, */)
539
657
  channels.each do |channel|
540
- next if channel == main_channel
658
+ channel = channel.split(" ", 2).first
659
+ next if channel.casecmp(main_channel).zero?
541
660
 
542
661
  @channels << channel
543
662
  @channels.uniq!
544
- post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
545
- post server_name, MODE, channel, "+o", @nick
663
+ post @prefix, JOIN, channel
664
+ post server_name, MODE, channel, "+mtio", @nick
546
665
  save_config
547
666
  end
548
667
  end
549
668
 
550
669
  def on_part(m)
551
670
  channel = m.params[0]
552
- return if channel == main_channel
671
+ return if channel.casecmp(main_channel).zero?
553
672
 
554
673
  @channels.delete(channel)
555
- post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet."
674
+ post @prefix, PART, channel, "Ignore group #{channel}, but setting is alive yet."
556
675
  end
557
676
 
558
677
  def on_invite(m)
559
678
  nick, channel = *m.params
560
- return if channel == main_channel
679
+ if not nick.nick? or @nick.casecmp(nick).zero?
680
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" # or yourself
681
+ return
682
+ end
561
683
 
562
- if (@friends || []).find {|i| i["screen_name"] == nick }
563
- ((@groups[channel] ||= []) << nick).uniq!
564
- post "#{nick}!#{nick}@#{api_base.host}", JOIN, channel
565
- post server_name, MODE, channel, "+o", nick
684
+ friend = friend(nick)
685
+
686
+ case
687
+ when channel.casecmp(main_channel).zero?
688
+ case
689
+ when friend #TODO
690
+ when api("users/username_available", { :username => nick }).valid
691
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
692
+ else
693
+ user = api("friendships/create/#{nick}")
694
+ join main_channel, [user]
695
+ @friends << user if @friends
696
+ @me.friends_count += 1
697
+ end
698
+ when friend
699
+ ((@groups[channel] ||= []) << friend.screen_name).uniq!
700
+ join channel, [friend]
566
701
  save_config
567
702
  else
568
- post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
703
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
569
704
  end
570
705
  end
571
706
 
572
707
  def on_kick(m)
573
- channel, nick, mes = *m.params
574
- return if channel == main_channel
575
-
576
- if (@friends || []).find {|i| i["screen_name"] == nick }
577
- (@groups[channel] ||= []).delete(nick)
578
- post nick, PART, channel
579
- save_config
708
+ channel, nick, msg = *m.params
709
+
710
+ if channel.casecmp(main_channel).zero?
711
+ @friends.delete_if do |friend|
712
+ if friend.screen_name.casecmp(nick).zero?
713
+ user = api("friendships/destroy/#{friend.id}")
714
+ if user.is_a? User
715
+ post prefix(user), PART, main_channel, "Removed: #{msg}"
716
+ @me.friends_count -= 1
717
+ end
718
+ end
719
+ end if @friends
580
720
  else
581
- post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
721
+ friend = friend(nick)
722
+ if friend
723
+ (@groups[channel] ||= []).delete(friend.screen_name)
724
+ post prefix(friend), PART, channel, "Removed: #{msg}"
725
+ save_config
726
+ else
727
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
728
+ end
582
729
  end
583
730
  end
584
731
 
585
- private
586
- def check_timeline
587
- q = { :count => "117" }
588
- q[:since_id] = @timeline.last.to_s if @timeline.last
589
- api("statuses/friends_timeline", q).reverse_each do |s|
590
- id = s["id"]
591
- next if id.nil? || @timeline.include?(id)
732
+ #def on_nick(m)
733
+ # @nicknames[@nick] = m.params[0]
734
+ #end
592
735
 
593
- @timeline << id
594
- nick = s["user"]["screen_name"]
595
- mesg = generate_status_message(s)
596
- tid = @tmap.push(s)
597
-
598
- if @opts.key?("tid")
599
- mesg = "%s \x03%s[%s]" % [mesg, @opts["tid"] || 10, tid]
600
- end
736
+ def on_topic(m)
737
+ channel = m.params[0]
738
+ return if not channel.casecmp(main_channel).zero? or @me.status.nil?
601
739
 
602
- @log.debug [id, nick, mesg]
603
- if nick == @nick # 自分のときは TOPIC に
604
- post "#{nick}!#{nick}@#{api_base.host}", TOPIC, main_channel, untinyurl(mesg)
740
+ begin
741
+ require "levenshtein"
742
+ topic = m.params[1]
743
+ previous = @me.status
744
+ return unless previous
745
+
746
+ distance = Levenshtein.normalized_distance(previous.text, topic)
747
+ return if distance.zero?
748
+
749
+ status = api("statuses/update", { :status => topic, :source => source })
750
+ log oops(ret) if status.truncated
751
+ status.user.status = status
752
+ @me = status.user
753
+
754
+ if distance < 0.5
755
+ deleted = api("statuses/destroy/#{previous.id}")
756
+ @timeline.delete_if {|tid, s| s.id == deleted.id }
757
+ log "Fixed: #{status.text}"
605
758
  else
606
- message(nick, main_channel, mesg)
607
- end
608
- @groups.each do |channel, members|
609
- next unless members.include?(nick)
610
- message(nick, channel, mesg)
759
+ log "Status updated"
611
760
  end
761
+ rescue LoadError
612
762
  end
613
- @log.debug "@timeline.size = #{@timeline.size}"
614
- @timeline = @timeline.last(200)
615
763
  end
616
764
 
617
- def generate_status_message(status)
618
- s = status
619
- mesg = s["text"]
620
- @log.debug(mesg)
765
+ def on_mode(m)
766
+ channel = m.params[0]
621
767
 
622
- begin
623
- require "iconv"
624
- mesg = mesg.sub(/^.+ > |^.+/) {|str| Iconv.iconv("UTF-8", "UTF-7", str).join }
625
- mesg = "[utf7]: #{mesg}" if body =~ /[^a-z0-9\s]/i
626
- rescue LoadError
627
- rescue Iconv::IllegalSequence
768
+ unless m.params[1]
769
+ if channel.ch?
770
+ mode = "+mt"
771
+ mode += "i" unless channel.casecmp(main_channel).zero?
772
+ post server_name, RPL_CHANNELMODEIS, @nick, channel, mode
773
+ #post server_name, RPL_CREATEONTIME, @nick, channel, 0
774
+ elsif channel.casecmp(@nick).zero?
775
+ post server_name, RPL_UMODEIS, @nick, @nick, "+o"
776
+ end
628
777
  end
629
-
630
- # time = Time.parse(s["created_at"]) rescue Time.now
631
- m = { "&quot;" => "\"", "&lt;" => "<", "&gt;" => ">", "&amp;" => "&", "\n" => " " }
632
- mesg.gsub!(/#{m.keys.join("|")}/) { m[$&] }
633
- mesg
634
778
  end
635
779
 
636
- def check_replies
637
- time = @prev_time_r || Time.now
638
- @prev_time_r = Time.now
639
- api("statuses/replies").reverse_each do |s|
640
- id = s["id"]
641
- next if id.nil? || @timeline.include?(id)
780
+ private
781
+ def on_ctcp(target, mesg)
782
+ type, mesg = mesg.split(" ", 2)
783
+ method = "on_ctcp_#{type.downcase}".to_sym
784
+ send(method, target, mesg) if respond_to? method, true
785
+ end
642
786
 
643
- created_at = Time.parse(s["created_at"]) rescue next
644
- next if created_at < time
787
+ def on_ctcp_action(target, mesg)
788
+ #return unless main_channel.casecmp(target).zero?
789
+ 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
795
+ 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}}
801
+ else
802
+ @nicknames[screen_name] = nickname
803
+ log "Call #{screen_name} as #{nickname}"
804
+ end
805
+ #save_config
806
+ when /\Autf-?7\z/
807
+ unless defined? ::Iconv
808
+ log "Can't load iconv."
809
+ return
810
+ 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
817
+ 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
823
+ 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
830
+ end
831
+ unless (1..200).include?(count = args[1].to_i)
832
+ count = 20
833
+ 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."
839
+ return
840
+ end
841
+ res.reverse_each do |s|
842
+ message(s, target, nil, nil, NOTICE)
843
+ 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]
870
+ 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
+ else
878
+ # PRIVMSG: fav nick
879
+ log "No such ID/NICK #{@opts.tid % tid_or_nick}"
880
+ 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
+ else
903
+ log "No such ID #{@opts.tid % tid}"
904
+ end
905
+ 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}"
948
+ end
949
+ end
950
+ 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 })
994
+ @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 })
1001
+ @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/
1027
+ 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
1038
+ 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
1053
+ end
1054
+ save_config
1055
+ when "home", "h"
1056
+ if args.empty?
1057
+ log "/me home <NICK>"
1058
+ return
1059
+ 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
1065
+ 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
1083
+ end
1084
+ end unless command.nil?
1085
+ rescue APIFailed => e
1086
+ log e.inspect
1087
+ end
645
1088
 
646
- nick = s["user"]["screen_name"]
647
- mesg = generate_status_message(s)
648
- tid = @tmap.push(s)
1089
+ def on_ctcp_clientinfo(target, msg)
1090
+ if user = user(target)
1091
+ post prefix(user), NOTICE, @nick, ctcp_encode("CLIENTINFO :CLIENTINFO USERINFO VERSION TIME")
1092
+ end
1093
+ end
649
1094
 
650
- if @opts.key?("tid")
651
- mesg = "%s \x03%s[%s]" % [mesg, @opts["tid"] || 10, tid]
652
- end
1095
+ def on_ctcp_userinfo(target, msg)
1096
+ user = user(target)
1097
+ if user and not user.description.empty?
1098
+ post prefix(user), NOTICE, @nick, ctcp_encode("USERINFO :#{user.description}")
1099
+ end
1100
+ end
653
1101
 
654
- @log.debug [id, nick, mesg]
655
- message nick, main_channel, mesg
1102
+ def on_ctcp_version(target, msg)
1103
+ user = user(target)
1104
+ if user and user.status
1105
+ source = user.status.source
1106
+ version = source.gsub(/<[^>]*>/, "").strip
1107
+ version << " <#{$1}>" if / href="([^"]+)/ === source
1108
+ post prefix(user), NOTICE, @nick, ctcp_encode("VERSION :#{version}")
656
1109
  end
657
1110
  end
658
1111
 
659
- def check_direct_messages
660
- time = @prev_time_d || Time.now
661
- @prev_time_d = Time.now
662
- api("direct_messages", { :since => time.httpdate }).reverse_each do |s|
663
- nick = s["sender_screen_name"]
664
- mesg = s["text"]
665
- time = Time.parse(s["created_at"])
666
- @log.debug [nick, mesg, time].inspect
667
- message(nick, @nick, mesg)
1112
+ def on_ctcp_time(target, msg)
1113
+ if user = user(target)
1114
+ offset = user.utc_offset
1115
+ post prefix(user), NOTICE, @nick, ctcp_encode("TIME :%s%s (%s)" % [
1116
+ (Time.now + offset).utc.iso8601[0, 19],
1117
+ "%+.2d:%.2d" % (offset/60).divmod(60),
1118
+ user.time_zone,
1119
+ ])
668
1120
  end
669
1121
  end
670
1122
 
671
1123
  def check_friends
672
- first = true unless @friends
673
- @friends ||= []
674
- friends = api("statuses/friends")
675
- if first && !@opts.key?("athack")
676
- @friends = friends
677
- post server_name, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ")
678
- post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
1124
+ if @friends.nil?
1125
+ @friends = page("statuses/friends/#{@me.id}", @me.friends_count)
1126
+ if @opts.athack
1127
+ join main_channel, @friends
1128
+ else
1129
+ rest = @friends.map do |i|
1130
+ prefix = "+" #@drones.include?(i.id) ? "%" : "+" # FIXME ~&%
1131
+ "#{prefix}#{i.screen_name}"
1132
+ end.reverse.inject("@#{@nick}") do |r, nick|
1133
+ if r.size < 400
1134
+ r << " " << nick
1135
+ else
1136
+ post server_name, RPL_NAMREPLY, @nick, "=", main_channel, r
1137
+ nick
1138
+ end
1139
+ end
1140
+ post server_name, RPL_NAMREPLY, @nick, "=", main_channel, rest
1141
+ post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
1142
+ end
679
1143
  else
680
- prv_friends = @friends.map {|i| i["screen_name"] }
681
- now_friends = friends.map {|i| i["screen_name"] }
1144
+ new_ids = page("friends/ids/#{@me.id}", @me.friends_count)
1145
+ friend_ids = @friends.reverse.map {|friend| friend.id }
1146
+
1147
+ (friend_ids - new_ids).each do |id|
1148
+ @friends.delete_if do |friend|
1149
+ if friend.id == id
1150
+ post prefix(friend), PART, main_channel, ""
1151
+ @me.friends_count -= 1
1152
+ end
1153
+ end
1154
+ end
1155
+
1156
+ new_ids -= friend_ids
1157
+ unless new_ids.empty?
1158
+ new_friends = page("statuses/friends/#{@me.id}", new_ids.size)
1159
+ join main_channel, new_friends.delete_if {|friend|
1160
+ @friends.any? {|i| i.id == friend.id }
1161
+ }.reverse
1162
+ @friends.concat new_friends
1163
+ @me.friends_count += new_friends.size
1164
+ end
1165
+ end
1166
+ end
1167
+
1168
+ def check_timeline
1169
+ cmd = PRIVMSG
1170
+ q = { :count => 200 }
1171
+ if @latest_id ||= nil
1172
+ q.update(:since_id => @latest_id)
1173
+ elsif not @me.statuses_count.zero? and not @me.friends_count.zero?
1174
+ cmd = NOTICE
1175
+ end
1176
+
1177
+ api("statuses/friends_timeline", q).reverse_each do |status|
1178
+ id = @latest_id = status.id
1179
+ next if @timeline.any? {|tid, s| s.id == id }
1180
+
1181
+ status.user.status = status
1182
+ user = status.user
1183
+ tid = @timeline.push(status)
1184
+ tid = nil unless @opts.tid
682
1185
 
683
- # Twitter API bug?
684
- return if !first && (now_friends.length - prv_friends.length).abs > 10
1186
+ @log.debug [id, user.screen_name, status.text].inspect
1187
+
1188
+ if user.id == @me.id
1189
+ mesg = generate_status_message(status.text)
1190
+ mesg << " " << @opts.tid % tid if tid
1191
+ post @prefix, TOPIC, main_channel, mesg
1192
+
1193
+ @me = user
1194
+ else
1195
+ if @friends
1196
+ b = false
1197
+ @friends.each_with_index do |friend, i|
1198
+ if b = friend.id == user.id
1199
+ @friends[i] = user
1200
+ break
1201
+ end
1202
+ end
1203
+ unless b
1204
+ join main_channel, [user]
1205
+ @friends << user
1206
+ @me.friends_count += 1
1207
+ end
1208
+ end
685
1209
 
686
- (now_friends - prv_friends).each do |join|
687
- join = "@#{join}" if @opts.key?("athack")
688
- post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
1210
+ message(status, main_channel, tid, nil, cmd)
689
1211
  end
690
- (prv_friends - now_friends).each do |part|
691
- part = "@#{part}" if @opts.key?("athack")
692
- post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
1212
+ @groups.each do |channel, members|
1213
+ next unless members.include?(user.screen_name)
1214
+ message(status, channel, tid, nil, cmd)
1215
+ end
1216
+ end
1217
+ end
1218
+
1219
+ def check_direct_messages
1220
+ @prev_dm_id ||= nil
1221
+ q = @prev_dm_id ? { :count => 200, :since_id => @prev_dm_id } \
1222
+ : { :count => 1 }
1223
+ api("direct_messages", q).reverse_each do |mesg|
1224
+ unless @prev_dm_id &&= mesg.id
1225
+ @prev_dm_id = mesg.id
1226
+ next
693
1227
  end
694
- @friends = friends
1228
+
1229
+ id = mesg.id
1230
+ user = mesg.sender
1231
+ tid = nil
1232
+ text = mesg.text
1233
+ @log.debug [id, user.screen_name, text].inspect
1234
+ message(user, @nick, tid, text)
695
1235
  end
696
1236
  end
697
1237
 
698
- def check_rate_limit
699
- @log.debug rate_limit = api("account/rate_limit_status")
700
- if rate_limit.key?("hourly_limit") && @hourly_limit != rate_limit["hourly_limit"]
701
- msg = "Rate limit was changed: #{@hourly_limit} to #{rate_limit["hourly_limit"]}"
702
- log msg
703
- @log.info msg
704
- @hourly_limit = rate_limit["hourly_limit"]
1238
+ def check_mentions
1239
+ return if @timeline.empty?
1240
+ @prev_mention_id ||= @timeline.last.id
1241
+ api("statuses/mentions", {
1242
+ :count => 200,
1243
+ :since_id => @prev_mention_id
1244
+ }).reverse_each do |mention|
1245
+ id = @prev_mention_id = mention.id
1246
+ next if @timeline.any? {|tid, s| s.id == id }
1247
+
1248
+ mention.user.status = mention
1249
+ user = mention.user
1250
+ tid = @timeline.push(mention)
1251
+ tid = nil unless @opts.tid
1252
+
1253
+ @log.debug [id, user.screen_name, mention.text].inspect
1254
+ message(mention, main_channel, tid)
1255
+
1256
+ @friends.each_with_index do |friend, i|
1257
+ if friend.id == user.id
1258
+ @friends[i] = user
1259
+ break
1260
+ end
1261
+ end if @friends
705
1262
  end
706
- # rate_limit["remaining_hits"] < 1
707
- # rate_limit["reset_time_in_seconds"] - Time.now.to_i
708
1263
  end
709
1264
 
710
- def freq(ratio)
711
- max = (@opts["maxlimit"] || 100).to_i
712
- limit = @hourly_limit < max ? @hourly_limit : max
713
- f = 3600 / (limit * ratio).round
714
- @log.debug "Frequency: #{f}"
715
- f
1265
+ def check_updates
1266
+ update_redundant_suffix
1267
+
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")
1271
+ @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}>"
1279
+ rescue Errno::ECONNREFUSED, Timeout::Error => e
1280
+ @log.error "Failed to get the latest revision of tig.rb from #{uri.host}: #{e.inspect}"
1281
+ end
1282
+
1283
+ def interval(ratio)
1284
+ now = Time.now
1285
+ max = @opts.maxlimit
1286
+ limit = 0.98 * @limit # 98% of the rate limit
1287
+ i = 3600.0 # an hour in seconds
1288
+ i *= @ratio.inject {|sum, r| sum.to_f + r.to_f } +
1289
+ @consums.delete_if {|t| t < now }.size
1290
+ i /= ratio.to_f
1291
+ i /= (max and 0 < max and max < limit) ? max : limit
1292
+ rescue => e
1293
+ @log.error e.inspect
1294
+ 100
1295
+ end
1296
+
1297
+ def join(channel, users)
1298
+ max_params_count = @opts.max_params_count || 3
1299
+ params = []
1300
+ users.each do |user|
1301
+ prefix = prefix(user)
1302
+ post prefix, JOIN, channel
1303
+ params << prefix.nick
1304
+ next if params.size < max_params_count
1305
+
1306
+ post server_name, MODE, channel, "+#{"v" * params.size}", *params
1307
+ params = []
1308
+ end
1309
+ post server_name, MODE, channel, "+#{"v" * params.size}", *params unless params.empty?
1310
+ users
716
1311
  end
717
1312
 
718
1313
  def start_jabber(jid, pass)
@@ -720,26 +1315,24 @@ class TwitterIrcGateway < Net::IRC::Server::Session
720
1315
  @im = Jabber::Simple.new(jid, pass)
721
1316
  @im.add(jabber_bot_id)
722
1317
  @im_thread = Thread.start do
1318
+ require "cgi"
1319
+
723
1320
  loop do
724
1321
  begin
725
1322
  @im.received_messages.each do |msg|
726
- @log.debug [msg.from, msg.body]
1323
+ @log.debug [msg.from, msg.body].inspect
727
1324
  if msg.from.strip == jabber_bot_id
728
1325
  # Twitter -> 'id: msg'
729
- body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "")
730
-
731
- begin
732
- require "iconv"
733
- body = body.sub(/^.+ > |^.+/) {|str| Iconv.iconv("UTF-8", "UTF-7", str).join }
734
- body = "[utf7]: #{body}" if body =~ /[^a-z0-9\s]/i
735
- rescue LoadError
736
- rescue Iconv::IllegalSequence
737
- end
1326
+ body = msg.body.sub(/\A(.+?)(?:\(([^()]+)\))?: /, "")
1327
+ body = decode_utf7(body)
738
1328
 
739
1329
  if Regexp.last_match
740
1330
  nick, id = Regexp.last_match.captures
741
- body = CGI.unescapeHTML(body)
742
- message(id || nick, main_channel, body)
1331
+ body = untinyurl(CGI.unescapeHTML(body))
1332
+ user = nick
1333
+ nick = id || nick
1334
+ nick = @nicknames[nick] || nick
1335
+ post "#{nick}!#{user}@#{api_base.host}", PRIVMSG, main_channel, body
743
1336
  end
744
1337
  end
745
1338
  end
@@ -754,151 +1347,593 @@ class TwitterIrcGateway < Net::IRC::Server::Session
754
1347
  end
755
1348
  end
756
1349
 
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
+
757
1366
  def save_config
758
1367
  config = {
759
- :channels => @channels,
760
- :groups => @groups,
1368
+ :groups => @groups,
1369
+ :channels => @channels,
1370
+ #:nicknames => @nicknames,
1371
+ :drones => @drones,
761
1372
  }
762
- @config.open("w") do |f|
763
- YAML.dump(config, f)
764
- end
1373
+ @config.open("w") {|f| YAML.dump(config, f) }
765
1374
  end
766
1375
 
767
1376
  def load_config
768
1377
  @config.open do |f|
769
- config = YAML.load(f)
770
- @channels = config[:channels]
771
- @groups = config[:groups]
1378
+ config = YAML.load(f)
1379
+ @groups = config[:groups] || {}
1380
+ @channels = config[:channels] || []
1381
+ #@nicknames = config[:nicknames] || {}
1382
+ @drones = config[:drones] || []
772
1383
  end
773
1384
  rescue Errno::ENOENT
774
1385
  end
775
1386
 
776
1387
  def require_post?(path)
777
1388
  %r{
778
- ^
779
- (?: status(?:es)?/update $
780
- | direct_messages/new $
1389
+ \A
1390
+ (?: status(?:es)?/update \z
1391
+ | direct_messages/new \z
781
1392
  | friendships/create/
782
- | account/ (?: end_session $
783
- | update_ )
784
- | favou?ri(?:ing|tes)/create/
1393
+ | account/(?: end_session \z | update_ )
1394
+ | favou?ri(?: ing | tes )/create/
785
1395
  | notifications/
786
1396
  | blocks/create/ )
787
1397
  }x === path
788
1398
  end
789
1399
 
790
- def require_delete?(path)
791
- #%r{
792
- # ^
793
- # (?: status(?:es)?
794
- # | direct_messages
795
- # | friendships
796
- # | favou?ri(?:ing|tes) )
797
- # | blocks
798
- # /destroy/
799
- #}x === path
800
- path.include? "/destroy/"
801
- end
1400
+ def api(path, query = {}, opts = {})
1401
+ path.sub!(%r{\A/+}, "")
1402
+ query = query.to_query_str
802
1403
 
803
- def api(path, q = {}, opt = {})
804
- ret = {}
805
- headers = { "User-Agent" => @user_agent }
806
- headers["If-Modified-Since"] = q["since"] if q.key?("since")
1404
+ authenticate = opts.fetch(:authenticate, true)
807
1405
 
808
- q["source"] ||= api_source
1406
+ uri = api_base(authenticate)
1407
+ uri.path += path
1408
+ uri.path += ".json" if path != "users/username_available"
1409
+ uri.query = query unless query.empty?
1410
+ @log.debug uri.inspect
809
1411
 
810
- path = path.sub(%r{^/+}, "")
811
- uri = api_base.dup
812
- if @opts.key?("secure")
813
- uri.scheme = "https"
814
- uri.port = 443
815
- end
816
- uri.path += "#{path}.json"
817
- uri.query = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^-.!~*'()\w]/n)}" : r }.join("&")
818
- case
819
- when require_post?(path)
820
- req = Net::HTTP::Post.new(uri.path, headers)
821
- req.body = uri.query
822
- when require_delete?(path)
823
- req = Net::HTTP::Delete.new(uri.path, headers)
824
- req.body = uri.query
825
- else
826
- req = Net::HTTP::Get.new(uri.request_uri, headers)
1412
+ header = {}
1413
+ credentials = authenticate ? [@real, @pass] : nil
1414
+ req = case
1415
+ when path.include?("/destroy/")
1416
+ http_req :delete, uri, header, credentials
1417
+ when require_post?(path)
1418
+ http_req :post, uri, header, credentials
1419
+ else
1420
+ http_req :get, uri, header, credentials
827
1421
  end
828
- req.basic_auth(@real, @pass)
829
- @log.debug uri.inspect
830
1422
 
831
- http = Net::HTTP.new(uri.host, uri.port)
832
- if uri.scheme == "https"
833
- http.use_ssl = true
834
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME
1423
+ ret = http(uri, 30, 30).request req
1424
+
1425
+ #@etags[uri.to_s] = ret["ETag"]
1426
+
1427
+ if authenticate
1428
+ hourly_limit = ret["X-RateLimit-Limit"].to_i
1429
+ unless hourly_limit.zero?
1430
+ if @limit != hourly_limit
1431
+ msg = "The rate limit per hour was changed: #{@limit} to #{hourly_limit}"
1432
+ log msg
1433
+ @log.info msg
1434
+ @limit = hourly_limit
1435
+ end
1436
+
1437
+ #if req.is_a?(Net::HTTP::Get) and not %w{
1438
+ if not %w{
1439
+ statuses/friends_timeline
1440
+ direct_messages
1441
+ statuses/mentions
1442
+ }.include?(path) and not ret.is_a?(Net::HTTPServerError)
1443
+ expired_on = Time.parse(ret["Date"]) rescue Time.now
1444
+ expired_on += 3636 # 1.01 hours in seconds later
1445
+ @consums << expired_on
1446
+ end
1447
+ end
1448
+ elsif ret["X-RateLimit-Remaining"]
1449
+ @limit_remaining_for_ip = ret["X-RateLimit-Remaining"].to_i
1450
+ @log.debug "IP based limit: #{@limit_remaining_for_ip}"
835
1451
  end
836
- case ret = http.request(req)
1452
+
1453
+ case ret
837
1454
  when Net::HTTPOK # 200
838
- ret = JSON.parse(ret.body.gsub(/'(y(?:es)?|no?|true|false|null)'/, '"\1"'))
839
- if ret.kind_of?(Hash) && !opt[:suppress_errors] && ret["error"]
840
- raise ApiFailed, "Server Returned Error: #{ret["error"]}"
1455
+ # Avoid Twitter's invalid JSON
1456
+ json = ret.body.strip.sub(/\A(?:false|true)\z/, "[\\&]")
1457
+
1458
+ res = JSON.parse json
1459
+ if res.is_a?(Hash) and res["error"] # and not res["response"]
1460
+ if @error != res["error"]
1461
+ @error = res["error"]
1462
+ log @error
1463
+ end
1464
+ raise APIFailed, res["error"]
841
1465
  end
842
- ret
843
- when Net::HTTPNotModified # 304
1466
+ res.to_tig_struct
1467
+ when Net::HTTPNoContent, # 204
1468
+ Net::HTTPNotModified # 304
844
1469
  []
845
- when Net::HTTPBadRequest # 400
846
- # exceeded the rate limitation
847
- raise ApiFailed, "#{ret.code}: #{ret.message}"
1470
+ when Net::HTTPBadRequest # 400: exceeded the rate limitation
1471
+ if ret.key?("X-RateLimit-Reset")
1472
+ 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
1475
+ end
1476
+ raise APIFailed, "#{ret.code}: #{ret.message}"
1477
+ when Net::HTTPUnauthorized # 401
1478
+ raise APIFailed, "#{ret.code}: #{ret.message}"
848
1479
  else
849
- raise ApiFailed, "Server Returned #{ret.code} #{ret.message}"
1480
+ raise APIFailed, "Server Returned #{ret.code} #{ret.message}"
850
1481
  end
851
1482
  rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
852
- raise ApiFailed, e.inspect
1483
+ raise APIFailed, e.inspect
1484
+ end
1485
+
1486
+ def page(path, max_count, authenticate = false)
1487
+ @limit_remaining_for_ip ||= 52
1488
+ limit = 0.98 * @limit_remaining_for_ip # 98% of IP based rate limit
1489
+ r = []
1490
+ cpp = nil # counts per page
1491
+ 1.upto(limit) do |num|
1492
+ ret = api(path, { :page => num }, { :authenticate => authenticate })
1493
+ cpp ||= ret.size
1494
+ r.concat ret
1495
+ break if ret.empty? or num >= max_count / cpp.to_f or
1496
+ ret.size != cpp or r.size >= max_count
1497
+ end
1498
+ r
1499
+ end
1500
+
1501
+ def generate_status_message(mesg)
1502
+ mesg = decode_utf7(mesg)
1503
+ mesg.delete!("\000\001")
1504
+ mesg.gsub!("&gt;", ">")
1505
+ 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]/, " ")
1508
+ mesg = untinyurl(mesg)
1509
+ mesg.sub!(@rsuffix_regex, "") if @rsuffix_regex
1510
+ mesg.strip
1511
+ end
1512
+
1513
+ def friend(id)
1514
+ return nil unless @friends
1515
+ if id.is_a? String
1516
+ @friends.find {|i| i.screen_name.casecmp(id).zero? }
1517
+ else
1518
+ @friends.find {|i| i.id == id }
1519
+ end
1520
+ end
1521
+
1522
+ def user(id)
1523
+ if id.is_a? String
1524
+ @nick.casecmp(id).zero? ? @me : friend(id)
1525
+ else
1526
+ @me.id == id ? @me : friend(id)
1527
+ end
853
1528
  end
854
1529
 
855
- def message(sender, target, str)
856
- # str.gsub!(/&#(x)?([0-9a-f]+);/i) do
857
- # [$1 ? $2.hex : $2.to_i].pack("U")
858
- # end
859
- str = untinyurl(str)
860
- sender = @nicknames[sender] || sender
861
- sender = "#{sender}!#{sender}@#{api_base.host}"
862
- post sender, PRIVMSG, target, str
1530
+ def prefix(u)
1531
+ nick = u.screen_name
1532
+ nick = "@#{nick}" if @opts.athack
1533
+ user = "id=%.9d" % u.id
1534
+ host = api_base.host
1535
+ host += "/protected" if u.protected
1536
+ host += "/bot" if @drones.include?(u.id)
1537
+
1538
+ Prefix.new("#{nick}!#{user}@#{host}")
1539
+ end
1540
+
1541
+ def message(struct, target, tid = nil, str = nil, command = PRIVMSG)
1542
+ unless str
1543
+ status = struct.is_a?(Status) ? struct : struct.status
1544
+ str = status.text
1545
+ if command != PRIVMSG
1546
+ time = Time.parse(status.created_at) rescue Time.now
1547
+ str = "#{time.strftime(@opts.strftime || "%m-%d %H:%M")} #{str}" # TODO: color
1548
+ end
1549
+ end
1550
+ user = (struct.is_a?(User) ? struct : struct.user).dup
1551
+ screen_name = user.screen_name
1552
+
1553
+ user.screen_name = @nicknames[screen_name] || screen_name
1554
+ prefix = prefix(user)
1555
+ str = generate_status_message(str)
1556
+ str = "#{str} #{@opts.tid % tid}" if tid
1557
+
1558
+ post prefix, command, target, str
863
1559
  end
864
1560
 
865
1561
  def log(str)
866
- str.gsub!(/\r\n|[\r\n]/, " ")
867
- post server_name, NOTICE, main_channel, str
1562
+ post server_name, NOTICE, main_channel, str.gsub(/\r\n|[\r\n]/, " ")
1563
+ end
1564
+
1565
+ def decode_utf7(str)
1566
+ return str unless defined?(::Iconv) and str.include?("+")
1567
+
1568
+ str.sub!(/\A(?:.+ > |.+\z)/) { Iconv.iconv("UTF-8", "UTF-7", $&).join }
1569
+ #FIXME str = "[utf7]: #{str}" if str =~ /[^a-z0-9\s]/i
1570
+ str
1571
+ rescue Iconv::IllegalSequence
1572
+ str
1573
+ rescue => e
1574
+ @log.error e
1575
+ str
868
1576
  end
869
1577
 
870
1578
  def untinyurl(text)
871
- text.gsub(%r"http://(?:(preview\.)?tin|rub)yurl\.com/[0-9a-z=]+"i) {|m|
872
- uri = URI(m)
873
- uri.host = uri.host.sub($1, "") if $1
874
- Net::HTTP.start(uri.host, uri.port) {|http|
875
- http.open_timeout = 3
1579
+ text.gsub(@opts.untiny_whole_urls ? URI.regexp(%w[http https]) : %r{
1580
+ http:// (?:
1581
+ (?: bit\.ly | (?: tin | rub) yurl\.com
1582
+ | is\.gd | cli\.gs | tr\.im | u\.nu | airme\.us
1583
+ | ff\.im | twurl.nl | bkite\.com | tumblr\.com
1584
+ | pic\.gd | sn\.im | digg\.com )
1585
+ / [0-9a-z=-]+ |
1586
+ blip\.fm/~ (?> [0-9a-z]+) (?! /) |
1587
+ flic\.kr/[a-z0-9/]+
1588
+ )
1589
+ }ix) {|url| "#{resolve_http_redirect(URI(url)) || url}" }
1590
+ end
1591
+
1592
+ def bitlify(text)
1593
+ login, key, len = @opts.bitlify.split(":", 3) if @opts.bitlify
1594
+ len = (len || 20).to_i
1595
+ longurls = URI.extract(text, %w[http https]).uniq.map do |url|
1596
+ URI.rstrip_unpaired_paren(url)
1597
+ end.reject {|url| url.size < len }
1598
+ return text if longurls.empty?
1599
+
1600
+ bitly = URI("http://api.bit.ly/shorten")
1601
+ if login and key
1602
+ bitly.path = "/shorten"
1603
+ bitly.query = {
1604
+ :version => "2.0.1", :format => "json", :longUrl => longurls,
1605
+ }.to_query_str(";")
1606
+ @log.debug bitly
1607
+ req = http_req(:get, bitly, {}, [login, key])
1608
+ res = http(bitly, 5, 10).request(req)
1609
+ res = JSON.parse(res.body)
1610
+ res = res["results"]
1611
+
1612
+ longurls.each do |longurl|
1613
+ text.gsub!(longurl) do
1614
+ res[$&] && res[$&]["shortUrl"] || $&
1615
+ end
1616
+ end
1617
+ else
1618
+ bitly.path = "/api"
1619
+ longurls.each do |longurl|
1620
+ bitly.query = { :url => longurl }.to_query_str
1621
+ @log.debug bitly
1622
+ req = http_req(:get, bitly)
1623
+ res = http(bitly, 5, 5).request(req)
1624
+ text.gsub!(longurl, res.body)
1625
+ end
1626
+ end
1627
+
1628
+ text
1629
+ rescue => e
1630
+ @log.error e
1631
+ text
1632
+ end
1633
+
1634
+ def unuify(text)
1635
+ unu_url = "http://u.nu/"
1636
+ unu = URI("#{unu_url}unu-api-simple")
1637
+ size = unu_url.size
1638
+
1639
+ text.gsub(URI.regexp(%w[http https])) do |url|
1640
+ url = URI.rstrip_unpaired_paren(url)
1641
+ if url.size < size + 5 or url[0, size] == unu_url
1642
+ return url
1643
+ end
1644
+
1645
+ unu.query = { :url => url }.to_query_str
1646
+ @log.debug unu
1647
+
1648
+ res = http(unu, 5, 5).request(http_req(:get, unu)).body
1649
+
1650
+ if res[0, 12] == unu_url
1651
+ res
1652
+ else
1653
+ raise res.split("|")
1654
+ end
1655
+ end
1656
+ rescue => e
1657
+ @log.error e
1658
+ text
1659
+ end
1660
+
1661
+ def escape_http_urls(text)
1662
+ original_text = text.encoding!("UTF-8").dup
1663
+
1664
+ if defined? ::Punycode
1665
+ # TODO: Nameprep
1666
+ text.gsub!(%r{(https?://)([^\x00-\x2C\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+)}) do
1667
+ domain = $2
1668
+ # Dots:
1669
+ # * U+002E (full stop) * U+3002 (ideographic full stop)
1670
+ # * U+FF0E (fullwidth full stop) * U+FF61 (halfwidth ideographic full stop)
1671
+ # => /[.\u3002\uFF0E\uFF61] # Ruby 1.9 /x
1672
+ $1 + domain.split(/\.|\343\200\202|\357\274\216|\357\275\241/).map do |label|
1673
+ break [domain] if /\A-|[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]|-\z/ === label
1674
+ next label unless /[^-A-Za-z0-9]/ === label
1675
+ punycode = Punycode.encode(label)
1676
+ break [domain] if punycode.size > 59
1677
+ "xn--#{punycode}"
1678
+ end.join(".")
1679
+ end
1680
+ if text != original_text
1681
+ log "Punycode encoded: #{text}"
1682
+ original_text = text.dup
1683
+ end
1684
+ end
1685
+
1686
+ urls = []
1687
+ (text.split(/[\s<>]+/) + [text]).each do |str|
1688
+ next if /%[0-9A-Fa-f]{2}/ === str
1689
+ # URI::UNSAFE + "#"
1690
+ escaped_str = URI.escape(str, %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]#]})
1691
+ URI.extract(escaped_str, %w[http https]).each do |url|
1692
+ uri = URI(URI.rstrip_unpaired_paren(url))
1693
+ if not urls.include?(uri.to_s) and exist_uri?(uri)
1694
+ urls << uri.to_s
1695
+ end
1696
+ end if escaped_str != str
1697
+ end
1698
+ urls.each do |url|
1699
+ unescaped_url = URI.unescape(url).encoding!("UTF-8")
1700
+ text.gsub!(unescaped_url, url)
1701
+ end
1702
+ log "Percent encoded: #{text}" if text != original_text
1703
+
1704
+ text.encoding!("ASCII-8BIT")
1705
+ rescue => e
1706
+ @log.error e
1707
+ text
1708
+ end
1709
+
1710
+ def exist_uri?(uri, limit = 1)
1711
+ ret = nil
1712
+ #raise "Not supported." unless uri.is_a?(URI::HTTP)
1713
+ return ret if limit.zero? or uri.nil? or not uri.is_a?(URI::HTTP)
1714
+ @log.debug uri.inspect
1715
+
1716
+ req = http_req :head, uri
1717
+ http(uri, 3, 2).request(req) do |res|
1718
+ ret = case res
1719
+ when Net::HTTPSuccess
1720
+ true
1721
+ when Net::HTTPRedirection
1722
+ uri = resolve_http_redirect(uri)
1723
+ exist_uri?(uri, limit - 1)
1724
+ when Net::HTTPClientError
1725
+ false
1726
+ #when Net::HTTPServerError
1727
+ # nil
1728
+ else
1729
+ nil
1730
+ end
1731
+ end
1732
+
1733
+ ret
1734
+ rescue => e
1735
+ @log.error e.inspect
1736
+ ret
1737
+ end
1738
+
1739
+ def resolve_http_redirect(uri, limit = 3)
1740
+ return uri if limit.zero? or uri.nil?
1741
+ @log.debug uri.inspect
1742
+
1743
+ req = http_req :head, uri
1744
+ http(uri, 3, 2).request(req) do |res|
1745
+ break if not res.is_a?(Net::HTTPRedirection) or
1746
+ not res.key?("Location")
1747
+ begin
1748
+ location = URI(res["Location"])
1749
+ rescue URI::InvalidURIError
1750
+ end
1751
+ unless location.is_a? URI::HTTP
876
1752
  begin
877
- http.head(uri.request_uri, { "User-Agent" => @user_agent })["Location"] || m
878
- rescue Timeout::Error
879
- m
1753
+ location = URI.join(uri.to_s, res["Location"])
1754
+ rescue URI::InvalidURIError, URI::BadURIError
1755
+ # FIXME
880
1756
  end
881
- }
882
- }
1757
+ end
1758
+ uri = resolve_http_redirect(location, limit - 1)
1759
+ end
1760
+
1761
+ uri
1762
+ rescue => e
1763
+ @log.error e.inspect
1764
+ uri
883
1765
  end
884
1766
 
885
- class TypableMap < Hash
886
- Roman = %w|k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q|.unshift("").map {|consonant|
887
- case
888
- when consonant.size > 1, consonant == "y"
889
- %w|a u o|
890
- when consonant == "q"
891
- %w|a i e o|
1767
+ def fetch_sources(n = nil)
1768
+ n = n.to_i
1769
+ uri = URI("http://wedata.net/databases/TwitterSources/items.json")
1770
+ @log.debug uri.inspect
1771
+ json = http(uri).request(http_req(:get, uri)).body
1772
+ 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
1778
+ rescue => e
1779
+ @log.error e.inspect
1780
+ log "An error occured while loading #{uri.host}."
1781
+ @sources || [[api_source, "tig.rb"]]
1782
+ end
1783
+
1784
+ def update_redundant_suffix
1785
+ uri = URI("http://svn.coderepos.org/share/platform/twitterircgateway/suffixesblacklist.txt")
1786
+ @log.debug uri.inspect
1787
+ res = http(uri).request(http_req(:get, uri))
1788
+ @etags[uri.to_s] = res["ETag"]
1789
+ return if res.is_a? Net::HTTPNotModified
1790
+ source = res.body
1791
+ source.encoding!("UTF-8") if source.respond_to?(:encoding) and source.encoding == Encoding::BINARY
1792
+ @rsuffix_regex = /#{Regexp.union(*source.split)}\z/
1793
+ rescue Errno::ECONNREFUSED, Timeout::Error => e
1794
+ @log.error "Failed to get the redundant suffix blacklist from #{uri.host}: #{e.inspect}"
1795
+ end
1796
+
1797
+ def http(uri, open_timeout = nil, read_timeout = 60)
1798
+ http = case
1799
+ when @httpproxy
1800
+ Net::HTTP.new(uri.host, uri.port, @httpproxy.address, @httpproxy.port,
1801
+ @httpproxy.user, @httpproxy.password)
1802
+ when ENV["HTTP_PROXY"], ENV["http_proxy"]
1803
+ proxy = URI(ENV["HTTP_PROXY"] || ENV["http_proxy"])
1804
+ Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port,
1805
+ proxy.user, proxy.password)
892
1806
  else
893
- %w|a i u e o|
894
- end.map {|vowel| "#{consonant}#{vowel}" }
895
- }.flatten
1807
+ Net::HTTP.new(uri.host, uri.port)
1808
+ end
1809
+ http.open_timeout = open_timeout if open_timeout # nil by default
1810
+ http.read_timeout = read_timeout if read_timeout # 60 by default
1811
+ if uri.is_a? URI::HTTPS
1812
+ http.use_ssl = true
1813
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
1814
+ end
1815
+ http
1816
+ rescue => e
1817
+ @log.error e
1818
+ end
896
1819
 
897
- def initialize(size = 1)
898
- @seq = Roman
899
- @map = {}
1820
+ def http_req(method, uri, header = {}, credentials = nil)
1821
+ accepts = ["*/*;q=0.1"]
1822
+ #require "mime/types"; accepts.unshift MIME::Types.of(uri.path).first.simplified
1823
+ types = { "json" => "application/json", "txt" => "text/plain" }
1824
+ ext = uri.path[/[^.]+\z/]
1825
+ accepts.unshift types[ext] if types.key?(ext)
1826
+ user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)}; net-irc) Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
1827
+
1828
+ header["User-Agent"] ||= user_agent
1829
+ header["Accept"] ||= accepts.join(",")
1830
+ header["Accept-Charset"] ||= "UTF-8,*;q=0.0" if ext != "json"
1831
+ #header["Accept-Language"] ||= @opts.lang # "en-us,en;q=0.9,ja;q=0.5"
1832
+ header["If-None-Match"] ||= @etags[uri.to_s] if @etags[uri.to_s]
1833
+
1834
+ req = case method.to_s.downcase.to_sym
1835
+ when :get
1836
+ Net::HTTP::Get.new uri.request_uri, header
1837
+ when :head
1838
+ Net::HTTP::Head.new uri.request_uri, header
1839
+ when :post
1840
+ Net::HTTP::Post.new uri.path, header
1841
+ when :put
1842
+ Net::HTTP::Put.new uri.path, header
1843
+ when :delete
1844
+ Net::HTTP::Delete.new uri.request_uri, header
1845
+ else # raise ""
1846
+ end
1847
+ if req.request_body_permitted?
1848
+ req["Content-Type"] ||= "application/x-www-form-urlencoded"
1849
+ req.body = uri.query
1850
+ end
1851
+ req.basic_auth(*credentials) if credentials
1852
+ req
1853
+ rescue => e
1854
+ @log.error e
1855
+ end
1856
+
1857
+ def oops(status)
1858
+ "Oops! Your update was over 140 characters. We sent the short version" <<
1859
+ " to your friends (they can view the entire update on the Web <" <<
1860
+ permalink(status) << ">)."
1861
+ end
1862
+
1863
+ def permalink(struct)
1864
+ path = struct.is_a?(Status) ? "#{struct.user.screen_name}/statuses/#{struct.id}" \
1865
+ : struct.screen_name
1866
+ "http://twitter.com/#{path}"
1867
+ end
1868
+
1869
+ def source
1870
+ @sources[rand(@sources.size)].first
1871
+ end
1872
+
1873
+ def initial_message
1874
+ super
1875
+ post server_name, RPL_ISUPPORT, @nick,
1876
+ "NETWORK=Twitter",
1877
+ "CHANTYPES=#", "CHANNELLEN=50", "CHANMODES=#{available_channel_modes}",
1878
+ "NICKLEN=15", "TOPICLEN=420", "PREFIX=(hov)%@+",
1879
+ "are supported by this server"
1880
+ end
1881
+
1882
+ User = Struct.new(:id, :name, :screen_name, :location, :description, :url,
1883
+ :following, :notifications, :protected, :time_zone,
1884
+ :utc_offset, :created_at, :friends_count, :followers_count,
1885
+ :statuses_count, :favourites_count, :verified,
1886
+ :verified_profile,
1887
+ :profile_image_url, :profile_background_color, :profile_text_color,
1888
+ :profile_link_color, :profile_sidebar_fill_color,
1889
+ :profile_sidebar_border_color, :profile_background_image_url,
1890
+ :profile_background_tile, :status)
1891
+ Status = Struct.new(:id, :text, :source, :created_at, :truncated, :favorited,
1892
+ :in_reply_to_status_id, :in_reply_to_user_id,
1893
+ :in_reply_to_screen_name, :user)
1894
+ DM = Struct.new(:id, :text, :created_at,
1895
+ :sender_id, :sender_screen_name, :sender,
1896
+ :recipient_id, :recipient_screen_name, :recipient)
1897
+
1898
+ class TypableMap < Hash
1899
+ #Roman = %w[
1900
+ # k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q
1901
+ #].unshift("").map do |consonant|
1902
+ # case consonant
1903
+ # when "h", "q" then %w|a i e o|
1904
+ # when /[hy]$/ then %w|a u o|
1905
+ # else %w|a i u e o|
1906
+ # end.map {|vowel| "#{consonant}#{vowel}" }
1907
+ #end.flatten
1908
+ Roman = %w[
1909
+ a i u e o ka ki ku ke ko sa shi su se so
1910
+ ta chi tsu te to na ni nu ne no ha hi fu he ho
1911
+ ma mi mu me mo ya yu yo ra ri ru re ro
1912
+ wa wo n
1913
+ ga gi gu ge go za ji zu ze zo da de do
1914
+ ba bi bu be bo pa pi pu pe po
1915
+ kya kyu kyo sha shu sho cha chu cho
1916
+ nya nyu nyo hya hyu hyo mya myu myo
1917
+ rya ryu ryo
1918
+ gya gyu gyo ja ju jo bya byu byo
1919
+ pya pyu pyo
1920
+ ].freeze
1921
+
1922
+ def initialize(size = nil, shuffle = false)
1923
+ if shuffle
1924
+ @seq = Roman.dup
1925
+ if @seq.respond_to?(:shuffle!)
1926
+ @seq.shuffle!
1927
+ else
1928
+ seq = @seq.dup
1929
+ @seq.map! { seq.slice!(rand(seq.size)) }
1930
+ end
1931
+ @seq.freeze
1932
+ else
1933
+ @seq = Roman
1934
+ end
900
1935
  @n = 0
901
- @size = size
1936
+ @size = size || @seq.size
902
1937
  end
903
1938
 
904
1939
  def generate(n)
@@ -907,23 +1942,39 @@ class TwitterIrcGateway < Net::IRC::Server::Session
907
1942
  n, r = n.divmod(@seq.size)
908
1943
  ret << @seq[r]
909
1944
  end while n > 0
910
- ret.reverse.join
1945
+ ret.reverse.join #.gsub(/n(?=[bmp])/, "m")
911
1946
  end
912
1947
 
913
1948
  def push(obj)
914
1949
  id = generate(@n)
915
1950
  self[id] = obj
916
1951
  @n += 1
917
- @n = @n % (@seq.size ** @size)
1952
+ @n %= @size
918
1953
  id
919
1954
  end
920
- alias << push
1955
+ alias :<< :push
921
1956
 
922
1957
  def clear
923
1958
  @n = 0
924
1959
  super
925
1960
  end
926
1961
 
1962
+ def first
1963
+ @size.times do |i|
1964
+ id = generate((@n + i) % @size)
1965
+ return self[id] if key? id
1966
+ end unless empty?
1967
+ nil
1968
+ end
1969
+
1970
+ def last
1971
+ @size.times do |i|
1972
+ id = generate((@n - 1 - i) % @size)
1973
+ return self[id] if key? id
1974
+ end unless empty?
1975
+ nil
1976
+ end
1977
+
927
1978
  private :[]=
928
1979
  undef update, merge, merge!, replace
929
1980
  end
@@ -931,6 +1982,102 @@ class TwitterIrcGateway < Net::IRC::Server::Session
931
1982
 
932
1983
  end
933
1984
 
1985
+ class Array
1986
+ def to_tig_struct
1987
+ map do |v|
1988
+ v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v
1989
+ end
1990
+ end
1991
+ end
1992
+
1993
+ class Hash
1994
+ def to_tig_struct
1995
+ if empty?
1996
+ #warn "" if $VERBOSE
1997
+ #raise Error
1998
+ return nil
1999
+ end
2000
+
2001
+ struct = case
2002
+ when struct_of?(TwitterIrcGateway::User)
2003
+ TwitterIrcGateway::User.new
2004
+ when struct_of?(TwitterIrcGateway::Status)
2005
+ TwitterIrcGateway::Status.new
2006
+ when struct_of?(TwitterIrcGateway::DM)
2007
+ TwitterIrcGateway::DM.new
2008
+ else
2009
+ members = keys
2010
+ members.concat TwitterIrcGateway::User.members
2011
+ members.concat TwitterIrcGateway::Status.members
2012
+ members.concat TwitterIrcGateway::DM.members
2013
+ members.map! {|m| m.to_sym }
2014
+ members.uniq!
2015
+ Struct.new(*members).new
2016
+ end
2017
+ each do |k, v|
2018
+ struct[k.to_sym] = v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v
2019
+ end
2020
+ struct
2021
+ end
2022
+
2023
+ # { :f => nil } #=> "f"
2024
+ # { "f" => "" } #=> "f="
2025
+ # { "f" => "v" } #=> "f=v"
2026
+ # { "f" => [1, 2] } #=> "f=1&f=2"
2027
+ def to_query_str separator = "&"
2028
+ inject([]) do |r, (k, v)|
2029
+ k = URI.encode_component k.to_s
2030
+ (v.is_a?(Array) ? v : [v]).each do |i|
2031
+ if i.nil?
2032
+ r << k
2033
+ else
2034
+ r << "#{k}=#{URI.encode_component i.to_s}"
2035
+ end
2036
+ end
2037
+ r
2038
+ end.join separator
2039
+ end
2040
+
2041
+ private
2042
+ def struct_of? struct
2043
+ (keys - struct.members.map {|m| m.to_s }).size.zero?
2044
+ end
2045
+ end
2046
+
2047
+ class String
2048
+ def ch?
2049
+ /\A[&#+!][^ \007,]{1,50}\z/ === self
2050
+ end
2051
+
2052
+ def nick? # Twitter screen_name (username)
2053
+ /\A[A-Za-z0-9_]{1,15}\z/ === self
2054
+ end
2055
+
2056
+ def encoding! enc
2057
+ return self unless respond_to? :force_encoding
2058
+ force_encoding enc
2059
+ end
2060
+ end
2061
+
2062
+ 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
+ alias :_orig_escape :escape
2067
+ def escape str, unsafe = %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]]}
2068
+ _orig_escape(str, unsafe)
2069
+ end
2070
+ alias :encode :escape
2071
+
2072
+ def encode_component str, unsafe = /[^-_.!~*'()a-zA-Z0-9 ]/
2073
+ _orig_escape(str, unsafe).tr(" ", "+")
2074
+ end
2075
+
2076
+ def rstrip_unpaired_paren str
2077
+ str.sub(%r{(/[^/()]*(?:\([^/()]*\)[^/()]*)*)\)[^/()]*\z}, "\\1")
2078
+ end
2079
+ end
2080
+
934
2081
  if __FILE__ == $0
935
2082
  require "optparse"
936
2083
 
@@ -985,23 +2132,23 @@ if __FILE__ == $0
985
2132
  opts[:logger] = Logger.new(opts[:log], "daily")
986
2133
  opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
987
2134
 
988
- # def daemonize(foreground = false)
989
- # [:INT, :TERM, :HUP].each do |sig|
990
- # Signal.trap sig, "EXIT"
991
- # end
992
- # return yield if $DEBUG || foreground
993
- # Process.fork do
994
- # Process.setsid
995
- # Dir.chdir "/"
996
- # STDIN.reopen "/dev/null"
997
- # STDOUT.reopen "/dev/null", "a"
998
- # STDERR.reopen STDOUT
999
- # yield
1000
- # end
1001
- # exit! 0
1002
- # end
1003
-
1004
- # daemonize(opts[:debug] || opts[:foreground]) do
2135
+ #def daemonize(foreground = false)
2136
+ # [:INT, :TERM, :HUP].each do |sig|
2137
+ # Signal.trap sig, "EXIT"
2138
+ # end
2139
+ # return yield if $DEBUG or foreground
2140
+ # Process.fork do
2141
+ # Process.setsid
2142
+ # Dir.chdir "/"
2143
+ # STDIN.reopen "/dev/null"
2144
+ # STDOUT.reopen "/dev/null", "a"
2145
+ # STDERR.reopen STDOUT
2146
+ # yield
2147
+ # end
2148
+ # exit! 0
2149
+ #end
2150
+
2151
+ #daemonize(opts[:debug] || opts[:foreground]) do
1005
2152
  Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start
1006
- # end
2153
+ #end
1007
2154
  end