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.
- data/ChangeLog +6 -0
- data/README +14 -15
- data/Rakefile +1 -1
- data/examples/client.rb +1 -1
- data/examples/echo_bot.rb +1 -1
- data/examples/gmail.rb +2 -1
- data/examples/hatena-star-stream.rb +56 -11
- data/examples/hig.rb +54 -22
- data/examples/iig.rb +63 -86
- data/examples/ircd.rb +358 -0
- data/examples/mixi.rb +2 -1
- data/examples/sig.rb +2 -1
- data/examples/tig.rb +1671 -524
- data/examples/wig.rb +27 -27
- data/lib/net/irc.rb +36 -21
- data/lib/net/irc/client.rb +8 -2
- data/lib/net/irc/client/channel_manager.rb +13 -13
- data/lib/net/irc/constants.rb +2 -2
- data/lib/net/irc/message.rb +17 -10
- data/lib/net/irc/message/modeparser.rb +5 -5
- data/lib/net/irc/message/serverconfig.rb +3 -3
- data/lib/net/irc/pattern.rb +2 -2
- data/lib/net/irc/server.rb +8 -5
- data/spec/net-irc_spec.rb +33 -8
- metadata +16 -18
- data/examples/lig.rb +0 -551
- data/examples/lingr.rb +0 -327
- data/examples/nig.rb +0 -154
data/examples/ircd.rb
ADDED
@@ -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
|
+
|
data/examples/mixi.rb
CHANGED
@@ -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"
|
data/examples/sig.rb
CHANGED
@@ -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"
|
data/examples/tig.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# vim:fileencoding=
|
3
|
-
#
|
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
|
-
|
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
|
23
|
+
Configuration example for Tiarra <http://coderepos.org/share/wiki/Tiarra>.
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
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
|
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
|
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>:<
|
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
|
-
###
|
148
|
+
### max_params_count=<number:3>
|
90
149
|
|
91
|
-
###
|
150
|
+
### check_friends_interval=<seconds:3600>
|
92
151
|
|
93
|
-
###
|
152
|
+
### check_updates_interval=<seconds:86400>
|
94
153
|
|
95
|
-
|
154
|
+
Set 0 to disable checking.
|
96
155
|
|
97
|
-
|
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
|
174
|
+
/me list NICK [NUMBER]
|
104
175
|
|
105
176
|
### fav (favorite, favourite, unfav, unfavorite, unfavourite)
|
106
177
|
|
107
|
-
/me fav
|
108
|
-
/me unfav
|
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
|
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
|
-
###
|
199
|
+
### retweet (rt)
|
200
|
+
|
201
|
+
/me retweet ID (blah, blah...)
|
202
|
+
|
203
|
+
### utf7 (utf-7)
|
127
204
|
|
128
|
-
|
205
|
+
/me utf7
|
129
206
|
|
130
207
|
### name
|
131
208
|
|
132
|
-
|
209
|
+
/me name My Name
|
133
210
|
|
134
211
|
### description (desc)
|
135
212
|
|
136
|
-
|
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
|
-
|
145
|
-
$LOAD_PATH << "
|
146
|
-
|
147
|
-
$
|
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 "
|
249
|
+
require "ostruct"
|
250
|
+
require "json"
|
160
251
|
|
161
|
-
|
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
|
-
|
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
|
-
|
296
|
+
150
|
190
297
|
end
|
191
298
|
|
192
|
-
class
|
299
|
+
class APIFailed < StandardError; end
|
193
300
|
|
194
301
|
def initialize(*args)
|
195
302
|
super
|
196
|
-
@groups
|
197
|
-
@channels
|
198
|
-
@nicknames
|
199
|
-
@
|
200
|
-
@config
|
201
|
-
@
|
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(
|
211
|
-
@opts = @opts.inject({})
|
212
|
-
key, value = i.split("=")
|
213
|
-
|
214
|
-
|
215
|
-
|
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 @
|
218
|
-
|
219
|
-
|
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.
|
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
|
236
|
-
@log.info "Client
|
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
|
-
@
|
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
|
-
@
|
408
|
+
@check_friends_thread = Thread.start do
|
241
409
|
loop do
|
242
410
|
begin
|
243
|
-
|
244
|
-
rescue
|
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
|
420
|
+
sleep @opts.check_friends_interval || 3600
|
253
421
|
end
|
254
422
|
end
|
255
|
-
sleep 5
|
256
423
|
|
257
|
-
|
258
|
-
@ratio[:replies] = @opts.key?("replies") ? (@opts["replies"] || 5).to_f : 0.0
|
424
|
+
return if @opts.jabber
|
259
425
|
|
260
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
462
|
+
sleep interval(@ratio.timeline)
|
298
463
|
end
|
299
464
|
end
|
300
465
|
|
301
|
-
|
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
|
-
|
308
|
-
rescue
|
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
|
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
|
323
|
-
@
|
324
|
-
@
|
325
|
-
@
|
326
|
-
@
|
327
|
-
@
|
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
|
-
|
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
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
-
|
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
|
-
|
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
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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
|
-
|
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
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
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
|
513
|
-
|
514
|
-
|
515
|
-
|
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 |
|
525
|
-
|
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,
|
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(
|
656
|
+
channels = m.params[0].split(/ *, */)
|
539
657
|
channels.each do |channel|
|
540
|
-
|
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
|
545
|
-
post server_name, MODE, channel, "+
|
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
|
671
|
+
return if channel.casecmp(main_channel).zero?
|
553
672
|
|
554
673
|
@channels.delete(channel)
|
555
|
-
post @
|
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
|
-
|
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
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
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
|
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,
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
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
|
-
|
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
|
-
|
586
|
-
|
587
|
-
|
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
|
-
|
594
|
-
|
595
|
-
|
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
|
-
|
603
|
-
|
604
|
-
|
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
|
-
|
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
|
618
|
-
|
619
|
-
mesg = s["text"]
|
620
|
-
@log.debug(mesg)
|
765
|
+
def on_mode(m)
|
766
|
+
channel = m.params[0]
|
621
767
|
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
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 = { """ => "\"", "<" => "<", ">" => ">", "&" => "&", "\n" => " " }
|
632
|
-
mesg.gsub!(/#{m.keys.join("|")}/) { m[$&] }
|
633
|
-
mesg
|
634
778
|
end
|
635
779
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
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
|
-
|
644
|
-
|
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
|
-
|
647
|
-
|
648
|
-
|
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
|
-
|
651
|
-
|
652
|
-
|
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
|
-
|
655
|
-
|
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
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
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
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
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
|
-
|
681
|
-
|
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
|
-
|
684
|
-
|
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
|
-
|
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
|
-
|
691
|
-
|
692
|
-
|
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
|
-
|
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
|
699
|
-
@
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
@
|
704
|
-
|
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
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
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
|
-
|
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
|
-
:
|
760
|
-
:
|
1368
|
+
:groups => @groups,
|
1369
|
+
:channels => @channels,
|
1370
|
+
#:nicknames => @nicknames,
|
1371
|
+
:drones => @drones,
|
761
1372
|
}
|
762
|
-
@config.open("w")
|
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
|
770
|
-
@
|
771
|
-
@
|
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/
|
783
|
-
|
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
|
791
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
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
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
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
|
-
|
1452
|
+
|
1453
|
+
case ret
|
837
1454
|
when Net::HTTPOK # 200
|
838
|
-
|
839
|
-
|
840
|
-
|
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
|
-
|
843
|
-
when Net::
|
1466
|
+
res.to_tig_struct
|
1467
|
+
when Net::HTTPNoContent, # 204
|
1468
|
+
Net::HTTPNotModified # 304
|
844
1469
|
[]
|
845
|
-
when Net::HTTPBadRequest # 400
|
846
|
-
|
847
|
-
|
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
|
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
|
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!(">", ">")
|
1505
|
+
mesg.gsub!("<", "<")
|
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
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
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
|
867
|
-
|
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(%
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
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
|
-
|
878
|
-
rescue
|
879
|
-
|
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
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
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
|
-
|
894
|
-
|
895
|
-
|
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
|
-
|
898
|
-
|
899
|
-
|
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
|
1952
|
+
@n %= @size
|
918
1953
|
id
|
919
1954
|
end
|
920
|
-
alias
|
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
|
-
#
|
989
|
-
#
|
990
|
-
#
|
991
|
-
#
|
992
|
-
#
|
993
|
-
#
|
994
|
-
#
|
995
|
-
#
|
996
|
-
#
|
997
|
-
#
|
998
|
-
#
|
999
|
-
#
|
1000
|
-
#
|
1001
|
-
#
|
1002
|
-
#
|
1003
|
-
|
1004
|
-
#
|
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
|
-
#
|
2153
|
+
#end
|
1007
2154
|
end
|