net-irc 0.0.7 → 0.0.8

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