| 1 | #!/usr/bin/env ruby |
|---|
| 2 | # vim:fileencoding=UTF-8: |
|---|
| 3 | $KCODE = "u" unless defined? ::Encoding # json use this |
|---|
| 4 | =begin |
|---|
| 5 | |
|---|
| 6 | # tig.rb |
|---|
| 7 | |
|---|
| 8 | Ruby version of TwitterIrcGateway |
|---|
| 9 | <http://www.misuzilla.org/dist/net/twitterircgateway/> |
|---|
| 10 | |
|---|
| 11 | ## Launch |
|---|
| 12 | |
|---|
| 13 | $ ruby tig.rb |
|---|
| 14 | |
|---|
| 15 | If you want to help: |
|---|
| 16 | |
|---|
| 17 | $ ruby tig.rb --help |
|---|
| 18 | |
|---|
| 19 | ## Configuration |
|---|
| 20 | |
|---|
| 21 | Options specified by after IRC realname. |
|---|
| 22 | |
|---|
| 23 | Configuration example for Tiarra <http://coderepos.org/share/wiki/Tiarra>. |
|---|
| 24 | |
|---|
| 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 |
|---|
| 38 | password: password on Twitter |
|---|
| 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 |
|---|
| 57 | } |
|---|
| 58 | |
|---|
| 59 | ### athack |
|---|
| 60 | |
|---|
| 61 | If `athack` client option specified, |
|---|
| 62 | all nick in join message is leading with @. |
|---|
| 63 | |
|---|
| 64 | So if you complemente nicks (e.g. Irssi), |
|---|
| 65 | it's good for Twitter like reply command (@nick). |
|---|
| 66 | |
|---|
| 67 | In this case, you will see torrent of join messages after connected, |
|---|
| 68 | because NAMES list can't send @ leading nick (it interpreted op.) |
|---|
| 69 | |
|---|
| 70 | ### tid[=<color:10>[,<bgcolor>]] |
|---|
| 71 | |
|---|
| 72 | Apply ID to each message for make favorites by CTCP ACTION. |
|---|
| 73 | |
|---|
| 74 | /me fav [ID...] |
|---|
| 75 | |
|---|
| 76 | <color> and <bgcolor> can be |
|---|
| 77 | |
|---|
| 78 | 0 => white |
|---|
| 79 | 1 => black |
|---|
| 80 | 2 => blue navy |
|---|
| 81 | 3 => green |
|---|
| 82 | 4 => red |
|---|
| 83 | 5 => brown maroon |
|---|
| 84 | 6 => purple |
|---|
| 85 | 7 => orange olive |
|---|
| 86 | 8 => yellow |
|---|
| 87 | 9 => lightgreen lime |
|---|
| 88 | 10 => teal |
|---|
| 89 | 11 => lightcyan cyan aqua |
|---|
| 90 | 12 => lightblue royal |
|---|
| 91 | 13 => pink lightpurple fuchsia |
|---|
| 92 | 14 => grey |
|---|
| 93 | 15 => lightgrey silver |
|---|
| 94 | |
|---|
| 95 | ### jabber=<jid>:<pass> |
|---|
| 96 | |
|---|
| 97 | If `jabber=<jid>:<pass>` option specified, |
|---|
| 98 | use Jabber to get friends timeline. |
|---|
| 99 | |
|---|
| 100 | You must setup im notifing settings in the site and |
|---|
| 101 | install "xmpp4r-simple" gem. |
|---|
| 102 | |
|---|
| 103 | $ sudo gem install xmpp4r-simple |
|---|
| 104 | |
|---|
| 105 | Be careful for managing password. |
|---|
| 106 | |
|---|
| 107 | ### alwaysim |
|---|
| 108 | |
|---|
| 109 | Use IM instead of any APIs (e.g. post) |
|---|
| 110 | |
|---|
| 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> |
|---|
| 147 | |
|---|
| 148 | ### check_friends_interval=<seconds:3600> |
|---|
| 149 | |
|---|
| 150 | ### check_updates_interval=<seconds:86400> |
|---|
| 151 | |
|---|
| 152 | Set 0 to disable checking. |
|---|
| 153 | |
|---|
| 154 | ### old_style_reply |
|---|
| 155 | |
|---|
| 156 | ### tmap_size=<number:10404> |
|---|
| 157 | |
|---|
| 158 | ### strftime=<format:%m-%d %H:%M> |
|---|
| 159 | |
|---|
| 160 | ### untiny_whole_urls |
|---|
| 161 | |
|---|
| 162 | ### bitlify=<username>:<apikey>:<minlength:20> |
|---|
| 163 | |
|---|
| 164 | ### unuify |
|---|
| 165 | |
|---|
| 166 | ### shuffled_tmap |
|---|
| 167 | |
|---|
| 168 | ### ll=<lat>,<long> |
|---|
| 169 | |
|---|
| 170 | ### with_retweets |
|---|
| 171 | |
|---|
| 172 | ## Extended commands through the CTCP ACTION |
|---|
| 173 | |
|---|
| 174 | ### list (ls) |
|---|
| 175 | |
|---|
| 176 | /me list NICK [NUMBER] |
|---|
| 177 | |
|---|
| 178 | ### fav (favorite, favourite, unfav, unfavorite, unfavourite) |
|---|
| 179 | |
|---|
| 180 | /me fav [ID...] |
|---|
| 181 | /me unfav [ID...] |
|---|
| 182 | /me fav! [ID...] |
|---|
| 183 | /me fav NICK |
|---|
| 184 | |
|---|
| 185 | ### link (ln, url, u) |
|---|
| 186 | |
|---|
| 187 | /me link ID [ID...] |
|---|
| 188 | |
|---|
| 189 | ### destroy (del, delete, miss, oops, remove, rm) |
|---|
| 190 | |
|---|
| 191 | /me destroy [ID...] |
|---|
| 192 | |
|---|
| 193 | ### in (location) |
|---|
| 194 | |
|---|
| 195 | /me in Sugamo, Tokyo, Japan |
|---|
| 196 | |
|---|
| 197 | ### reply (re, mention) |
|---|
| 198 | |
|---|
| 199 | /me reply ID blah, blah... |
|---|
| 200 | |
|---|
| 201 | ### retweet (rt) |
|---|
| 202 | |
|---|
| 203 | /me retweet ID (blah, blah...) |
|---|
| 204 | |
|---|
| 205 | ### utf7 (utf-7) |
|---|
| 206 | |
|---|
| 207 | /me utf7 |
|---|
| 208 | |
|---|
| 209 | ### name |
|---|
| 210 | |
|---|
| 211 | /me name My Name |
|---|
| 212 | |
|---|
| 213 | ### description (desc) |
|---|
| 214 | |
|---|
| 215 | /me description blah, blah... |
|---|
| 216 | |
|---|
| 217 | ### spoof |
|---|
| 218 | |
|---|
| 219 | /me spoof |
|---|
| 220 | /me spoo[o...]f |
|---|
| 221 | /me spoof tigrb twitterircgateway twitt web mobileweb |
|---|
| 222 | |
|---|
| 223 | ### bot (drone) |
|---|
| 224 | |
|---|
| 225 | /me bot NICK [NICK...] |
|---|
| 226 | |
|---|
| 227 | ## Feed |
|---|
| 228 | |
|---|
| 229 | <http://coderepos.org/share/log/lang/ruby/net-irc/trunk/examples/tig.rb?limit=100&mode=stop_on_copy&format=rss> |
|---|
| 230 | |
|---|
| 231 | ## License |
|---|
| 232 | |
|---|
| 233 | Ruby's by cho45 |
|---|
| 234 | |
|---|
| 235 | =end |
|---|
| 236 | |
|---|
| 237 | case |
|---|
| 238 | when File.directory?("lib") |
|---|
| 239 | $LOAD_PATH << "lib" |
|---|
| 240 | when File.directory?(File.expand_path("lib", "..")) |
|---|
| 241 | $LOAD_PATH << File.expand_path("lib", "..") |
|---|
| 242 | end |
|---|
| 243 | |
|---|
| 244 | require "rubygems" |
|---|
| 245 | require "net/irc" |
|---|
| 246 | require "net/https" |
|---|
| 247 | require "uri" |
|---|
| 248 | require "time" |
|---|
| 249 | require "logger" |
|---|
| 250 | require "yaml" |
|---|
| 251 | require "pathname" |
|---|
| 252 | require "ostruct" |
|---|
| 253 | require "json" |
|---|
| 254 | |
|---|
| 255 | begin |
|---|
| 256 | require "iconv" |
|---|
| 257 | require "punycode" |
|---|
| 258 | rescue LoadError |
|---|
| 259 | end |
|---|
| 260 | |
|---|
| 261 | module Net::IRC::Constants; RPL_WHOISBOT = "335"; RPL_CREATEONTIME = "329"; end |
|---|
| 262 | |
|---|
| 263 | class TwitterIrcGateway < Net::IRC::Server::Session |
|---|
| 264 | @@ctcp_action_commands = [] |
|---|
| 265 | |
|---|
| 266 | class << self |
|---|
| 267 | def ctcp_action(*commands, &block) |
|---|
| 268 | name = "+ctcp_action_#{commands.inspect}" |
|---|
| 269 | define_method(name, block) |
|---|
| 270 | commands.each do |command| |
|---|
| 271 | @@ctcp_action_commands << [command, name] |
|---|
| 272 | end |
|---|
| 273 | end |
|---|
| 274 | end |
|---|
| 275 | |
|---|
| 276 | def server_name |
|---|
| 277 | "twittergw" |
|---|
| 278 | end |
|---|
| 279 | |
|---|
| 280 | def server_version |
|---|
| 281 | rev = %q$Revision$.split[1] |
|---|
| 282 | rev &&= "+r#{rev}" |
|---|
| 283 | "0.0.0#{rev}" |
|---|
| 284 | end |
|---|
| 285 | |
|---|
| 286 | def available_user_modes |
|---|
| 287 | "o" |
|---|
| 288 | end |
|---|
| 289 | |
|---|
| 290 | def available_channel_modes |
|---|
| 291 | "mnti" |
|---|
| 292 | end |
|---|
| 293 | |
|---|
| 294 | def main_channel |
|---|
| 295 | @opts.main_channel || "#twitter" |
|---|
| 296 | end |
|---|
| 297 | |
|---|
| 298 | def api_base(secure = true) |
|---|
| 299 | URI("http#{"s" if secure}://twitter.com/") |
|---|
| 300 | end |
|---|
| 301 | |
|---|
| 302 | def api_source |
|---|
| 303 | "#{@opts.api_source || "tigrb"}" |
|---|
| 304 | end |
|---|
| 305 | |
|---|
| 306 | def jabber_bot_id |
|---|
| 307 | "twitter@twitter.com" |
|---|
| 308 | end |
|---|
| 309 | |
|---|
| 310 | def hourly_limit |
|---|
| 311 | 150 |
|---|
| 312 | end |
|---|
| 313 | |
|---|
| 314 | class APIFailed < StandardError; end |
|---|
| 315 | |
|---|
| 316 | MAX_MODE_PARAMS = 3 |
|---|
| 317 | WSP_REGEX = Regexp.new("\\r\\n|[\\r\\n\\t#{"\\u00A0\\u1680\\u180E\\u2002-\\u200D\\u202F\\u205F\\u2060\\uFEFF" if "\u0000" == "\000"}]") |
|---|
| 318 | |
|---|
| 319 | def initialize(*args) |
|---|
| 320 | super |
|---|
| 321 | @groups = {} |
|---|
| 322 | @channels = [] # joined channels (groups) |
|---|
| 323 | @nicknames = {} |
|---|
| 324 | @drones = [] |
|---|
| 325 | @config = Pathname.new(ENV["HOME"]) + ".tig" ### TODO マルチユーザに対応してない |
|---|
| 326 | @etags = {} |
|---|
| 327 | @consums = [] |
|---|
| 328 | @limit = hourly_limit |
|---|
| 329 | @friends = |
|---|
| 330 | @sources = |
|---|
| 331 | @rsuffix_regex = |
|---|
| 332 | @im = |
|---|
| 333 | @im_thread = |
|---|
| 334 | @utf7 = |
|---|
| 335 | @httpproxy = nil |
|---|
| 336 | load_config |
|---|
| 337 | end |
|---|
| 338 | |
|---|
| 339 | def on_user(m) |
|---|
| 340 | super |
|---|
| 341 | |
|---|
| 342 | @real, *@opts = (@opts.name || @real).split(" ") |
|---|
| 343 | @opts = @opts.inject({}) do |r, i| |
|---|
| 344 | key, value = i.split("=", 2) |
|---|
| 345 | key = "mentions" if key == "replies" # backcompat |
|---|
| 346 | r.update key => case value |
|---|
| 347 | when nil then true |
|---|
| 348 | when /\A\d+\z/ then value.to_i |
|---|
| 349 | when /\A(?:\d+\.\d*|\.\d+)\z/ then value.to_f |
|---|
| 350 | else value |
|---|
| 351 | end |
|---|
| 352 | end |
|---|
| 353 | @opts = OpenStruct.new(@opts) |
|---|
| 354 | @opts.httpproxy.sub!(/\A(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?\z/) do |
|---|
| 355 | @httpproxy = OpenStruct.new({ |
|---|
| 356 | :user => $1, :password => $2, :address => $3, :port => $4.to_i, |
|---|
| 357 | }) |
|---|
| 358 | $&.sub(/[^:@]+(?=@)/, "********") |
|---|
| 359 | end if @opts.httpproxy |
|---|
| 360 | |
|---|
| 361 | retry_count = 0 |
|---|
| 362 | begin |
|---|
| 363 | @me = api("account/update_profile") #api("account/verify_credentials") |
|---|
| 364 | rescue APIFailed => e |
|---|
| 365 | @log.error e.inspect |
|---|
| 366 | sleep 1 |
|---|
| 367 | retry_count += 1 |
|---|
| 368 | retry if retry_count < 3 |
|---|
| 369 | log "Failed to access API 3 times." << |
|---|
| 370 | " Please check your username/email and password combination, " << |
|---|
| 371 | " Twitter Status <http://status.twitter.com/> and try again later." |
|---|
| 372 | finish |
|---|
| 373 | end |
|---|
| 374 | |
|---|
| 375 | @prefix = prefix(@me) |
|---|
| 376 | @user = @prefix.user |
|---|
| 377 | @host = @prefix.host |
|---|
| 378 | |
|---|
| 379 | #post NICK, @me.screen_name if @nick != @me.screen_name |
|---|
| 380 | post server_name, MODE, @nick, "+o" |
|---|
| 381 | post @prefix, JOIN, main_channel |
|---|
| 382 | post server_name, MODE, main_channel, "+mto", @nick |
|---|
| 383 | post server_name, MODE, main_channel, "+q", @nick |
|---|
| 384 | if @me.status |
|---|
| 385 | @me.status.user = @me |
|---|
| 386 | post @prefix, TOPIC, main_channel, generate_status_message(@me.status.text) |
|---|
| 387 | end |
|---|
| 388 | |
|---|
| 389 | if @opts.jabber |
|---|
| 390 | jid, pass = @opts.jabber.split(":", 2) |
|---|
| 391 | @opts.jabber.replace("jabber=#{jid}:********") |
|---|
| 392 | if jabber_bot_id |
|---|
| 393 | begin |
|---|
| 394 | require "xmpp4r-simple" |
|---|
| 395 | start_jabber(jid, pass) |
|---|
| 396 | rescue LoadError |
|---|
| 397 | log "Failed to start Jabber." |
|---|
| 398 | log 'Installl "xmpp4r-simple" gem or check your ID/pass.' |
|---|
| 399 | finish |
|---|
| 400 | end |
|---|
| 401 | else |
|---|
| 402 | @opts.delete_field :jabber |
|---|
| 403 | log "This gateway does not support Jabber bot." |
|---|
| 404 | end |
|---|
| 405 | end |
|---|
| 406 | |
|---|
| 407 | log "Client options: #{@opts.marshal_dump.inspect}" |
|---|
| 408 | @log.info "Client options: #{@opts.inspect}" |
|---|
| 409 | |
|---|
| 410 | @opts.tid = begin |
|---|
| 411 | c = @opts.tid # expect: 0..15, true, "0,1" |
|---|
| 412 | b = nil |
|---|
| 413 | c, b = c.split(",", 2).map {|i| i.to_i } if c.respond_to? :split |
|---|
| 414 | c = 10 unless (0 .. 15).include? c # 10: teal |
|---|
| 415 | if (0 .. 15).include?(b) |
|---|
| 416 | "\003%.2d,%.2d[%%s]\017" % [c, b] |
|---|
| 417 | else |
|---|
| 418 | "\003%.2d[%%s]\017" % c |
|---|
| 419 | end |
|---|
| 420 | end if @opts.tid |
|---|
| 421 | |
|---|
| 422 | @ratio = (@opts.ratio || "121").split(":") |
|---|
| 423 | @ratio = Struct.new(:timeline, :dm, :mentions).new(*@ratio) |
|---|
| 424 | @ratio.dm ||= @opts.dm == true ? @opts.mentions ? 6 : 26 : @opts.dm |
|---|
| 425 | @ratio.mentions ||= @opts.mentions == true ? @opts.dm ? 20 : 26 : @opts.mentions |
|---|
| 426 | |
|---|
| 427 | @check_friends_thread = Thread.start do |
|---|
| 428 | loop do |
|---|
| 429 | begin |
|---|
| 430 | check_friends |
|---|
| 431 | rescue APIFailed => e |
|---|
| 432 | @log.error e.inspect |
|---|
| 433 | rescue Exception => e |
|---|
| 434 | @log.error e.inspect |
|---|
| 435 | e.backtrace.each do |l| |
|---|
| 436 | @log.error "\t#{l}" |
|---|
| 437 | end |
|---|
| 438 | end |
|---|
| 439 | sleep @opts.check_friends_interval || 3600 |
|---|
| 440 | end |
|---|
| 441 | end |
|---|
| 442 | |
|---|
| 443 | return if @opts.jabber |
|---|
| 444 | |
|---|
| 445 | @timeline = TypableMap.new(@opts.tmap_size || 10_404, |
|---|
| 446 | @opts.shuffled_tmap || false) |
|---|
| 447 | |
|---|
| 448 | if @opts.clientspoofing |
|---|
| 449 | update_sources |
|---|
| 450 | else |
|---|
| 451 | @sources = [api_source] |
|---|
| 452 | end |
|---|
| 453 | |
|---|
| 454 | update_redundant_suffix |
|---|
| 455 | @check_updates_thread = Thread.start do |
|---|
| 456 | sleep 30 |
|---|
| 457 | |
|---|
| 458 | loop do |
|---|
| 459 | begin |
|---|
| 460 | check_updates |
|---|
| 461 | rescue Exception => e |
|---|
| 462 | @log.error e.inspect |
|---|
| 463 | e.backtrace.each do |l| |
|---|
| 464 | @log.error "\t#{l}" |
|---|
| 465 | end |
|---|
| 466 | end |
|---|
| 467 | sleep 0.01 * (90 + rand(21)) * |
|---|
| 468 | (@opts.check_updates_interval || 86400) # 0.9 ... 1.1 day |
|---|
| 469 | end |
|---|
| 470 | |
|---|
| 471 | sleep @opts.check_updates_interval || 86400 |
|---|
| 472 | end |
|---|
| 473 | |
|---|
| 474 | @check_timeline_thread = Thread.start do |
|---|
| 475 | sleep 2 * (@me.friends_count / 100.0).ceil |
|---|
| 476 | |
|---|
| 477 | loop do |
|---|
| 478 | begin |
|---|
| 479 | check_timeline |
|---|
| 480 | rescue APIFailed => e |
|---|
| 481 | @log.error e.inspect |
|---|
| 482 | rescue Exception => e |
|---|
| 483 | @log.error e.inspect |
|---|
| 484 | e.backtrace.each do |l| |
|---|
| 485 | @log.error "\t#{l}" |
|---|
| 486 | end |
|---|
| 487 | end |
|---|
| 488 | sleep interval(@ratio.timeline) |
|---|
| 489 | end |
|---|
| 490 | end |
|---|
| 491 | |
|---|
| 492 | @check_dms_thread = Thread.start do |
|---|
| 493 | loop do |
|---|
| 494 | begin |
|---|
| 495 | check_direct_messages |
|---|
| 496 | rescue APIFailed => e |
|---|
| 497 | @log.error e.inspect |
|---|
| 498 | rescue Exception => e |
|---|
| 499 | @log.error e.inspect |
|---|
| 500 | e.backtrace.each do |l| |
|---|
| 501 | @log.error "\t#{l}" |
|---|
| 502 | end |
|---|
| 503 | end |
|---|
| 504 | sleep interval(@ratio.dm) |
|---|
| 505 | end |
|---|
| 506 | end if @opts.dm |
|---|
| 507 | |
|---|
| 508 | @check_mentions_thread = Thread.start do |
|---|
| 509 | sleep interval(@ratio.timeline) / 2 |
|---|
| 510 | |
|---|
| 511 | loop do |
|---|
| 512 | begin |
|---|
| 513 | check_mentions |
|---|
| 514 | rescue APIFailed => e |
|---|
| 515 | @log.error e.inspect |
|---|
| 516 | rescue Exception => e |
|---|
| 517 | @log.error e.inspect |
|---|
| 518 | e.backtrace.each do |l| |
|---|
| 519 | @log.error "\t#{l}" |
|---|
| 520 | end |
|---|
| 521 | end |
|---|
| 522 | sleep interval(@ratio.mentions) |
|---|
| 523 | end |
|---|
| 524 | end if @opts.mentions |
|---|
| 525 | end |
|---|
| 526 | |
|---|
| 527 | def on_disconnected |
|---|
| 528 | @check_friends_thread.kill rescue nil |
|---|
| 529 | @check_timeline_thread.kill rescue nil |
|---|
| 530 | @check_mentions_thread.kill rescue nil |
|---|
| 531 | @check_dms_thread.kill rescue nil |
|---|
| 532 | @check_updates_thread.kill rescue nil |
|---|
| 533 | @im_thread.kill rescue nil |
|---|
| 534 | @im.disconnect rescue nil |
|---|
| 535 | end |
|---|
| 536 | |
|---|
| 537 | def on_privmsg(m) |
|---|
| 538 | target, mesg = *m.params |
|---|
| 539 | |
|---|
| 540 | m.ctcps.each {|ctcp| on_ctcp target, ctcp } if m.ctcp? |
|---|
| 541 | |
|---|
| 542 | return if mesg.empty? |
|---|
| 543 | return on_ctcp_action(target, mesg) if mesg.sub!(/\A +/, "") #and @opts.direct_action |
|---|
| 544 | |
|---|
| 545 | command, params = mesg.split(" ", 2) |
|---|
| 546 | case command.downcase # TODO: escape recursive |
|---|
| 547 | when "d", "dm" |
|---|
| 548 | screen_name, mesg = params.split(" ", 2) |
|---|
| 549 | unless screen_name or mesg |
|---|
| 550 | log 'Send "d NICK message" to send a direct (private) message.' << |
|---|
| 551 | " You may reply to a direct message the same way." |
|---|
| 552 | return |
|---|
| 553 | end |
|---|
| 554 | m.params[0] = screen_name.sub(/\A@/, "") |
|---|
| 555 | m.params[1] = mesg #.rstrip |
|---|
| 556 | return on_privmsg(m) |
|---|
| 557 | # TODO |
|---|
| 558 | #when "f", "follow" |
|---|
| 559 | #when "on" |
|---|
| 560 | #when "off" # BUG if no args |
|---|
| 561 | #when "g", "get" |
|---|
| 562 | #when "w", "whois" |
|---|
| 563 | #when "n", "nudge" # BUG if no args |
|---|
| 564 | #when "*", "fav" |
|---|
| 565 | #when "delete" |
|---|
| 566 | #when "stats" # no args |
|---|
| 567 | #when "leave" |
|---|
| 568 | #when "invite" |
|---|
| 569 | end unless command.nil? |
|---|
| 570 | |
|---|
| 571 | mesg = escape_http_urls(mesg) |
|---|
| 572 | mesg = @opts.unuify ? unuify(mesg) : bitlify(mesg) |
|---|
| 573 | mesg = Iconv.iconv("UTF-7", "UTF-8", mesg).join.encoding!("ASCII-8BIT") if @utf7 |
|---|
| 574 | |
|---|
| 575 | ret = nil |
|---|
| 576 | retry_count = 3 |
|---|
| 577 | begin |
|---|
| 578 | case |
|---|
| 579 | when target.ch? |
|---|
| 580 | if @opts.alwaysim and @im and @im.connected? # in Jabber mode, using Jabber post |
|---|
| 581 | ret = @im.deliver(jabber_bot_id, mesg) |
|---|
| 582 | post @prefix, TOPIC, main_channel, mesg |
|---|
| 583 | else |
|---|
| 584 | previous = @me.status |
|---|
| 585 | if previous and |
|---|
| 586 | ((Time.now - Time.parse(previous.created_at)).to_i < 60 rescue true) and |
|---|
| 587 | mesg.strip == previous.text |
|---|
| 588 | log "You can't submit the same status twice in a row." |
|---|
| 589 | return |
|---|
| 590 | end |
|---|
| 591 | |
|---|
| 592 | q = { :status => mesg, :source => source } |
|---|
| 593 | |
|---|
| 594 | if @opts.old_style_reply and mesg[/\A@(?>([A-Za-z0-9_]{1,15}))[^A-Za-z0-9_]/] |
|---|
| 595 | if user = friend($1) || api("users/show/#{$1}") |
|---|
| 596 | unless user.status |
|---|
| 597 | user = api("users/show/#{user.id}", {}, |
|---|
| 598 | { :authenticate => user.protected }) |
|---|
| 599 | end |
|---|
| 600 | if user.status |
|---|
| 601 | q.update :in_reply_to_status_id => user.status.id |
|---|
| 602 | end |
|---|
| 603 | end |
|---|
| 604 | end |
|---|
| 605 | if @opts.ll |
|---|
| 606 | lat, long = @opts.ll.split(",", 2) |
|---|
| 607 | q.update :lat => lat.to_f |
|---|
| 608 | q.update :long => long.to_f |
|---|
| 609 | end |
|---|
| 610 | |
|---|
| 611 | ret = api("statuses/update", q) |
|---|
| 612 | log oops(ret) if ret.truncated |
|---|
| 613 | ret.user.status = ret |
|---|
| 614 | @me = ret.user |
|---|
| 615 | log "Status updated" |
|---|
| 616 | end |
|---|
| 617 | when target.screen_name? # Direct message |
|---|
| 618 | ret = api("direct_messages/new", { :screen_name => target, :text => mesg }) |
|---|
| 619 | post server_name, NOTICE, @nick, "Your direct message has been sent to #{target}." |
|---|
| 620 | else |
|---|
| 621 | post server_name, ERR_NOSUCHNICK, target, "No such nick/channel" |
|---|
| 622 | end |
|---|
| 623 | rescue => e |
|---|
| 624 | @log.error [retry_count, e.inspect].inspect |
|---|
| 625 | if retry_count > 0 |
|---|
| 626 | retry_count -= 1 |
|---|
| 627 | @log.debug "Retry to setting status..." |
|---|
| 628 | retry |
|---|
| 629 | end |
|---|
| 630 | log "Some Error Happened on Sending #{mesg}. #{e}" |
|---|
| 631 | end |
|---|
| 632 | end |
|---|
| 633 | |
|---|
| 634 | def on_whois(m) |
|---|
| 635 | nick = m.params[0] |
|---|
| 636 | unless nick.screen_name? |
|---|
| 637 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" |
|---|
| 638 | return |
|---|
| 639 | end |
|---|
| 640 | |
|---|
| 641 | unless user = user(nick) |
|---|
| 642 | if api("users/username_available", { :username => nick }).valid |
|---|
| 643 | # TODO: 404 suspended |
|---|
| 644 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" |
|---|
| 645 | return |
|---|
| 646 | end |
|---|
| 647 | user = api("users/show/#{nick}", {}, { :authenticate => false }) |
|---|
| 648 | end |
|---|
| 649 | |
|---|
| 650 | prefix = prefix(user) |
|---|
| 651 | desc = user.name |
|---|
| 652 | desc = "#{desc} / #{user.description}".gsub(/\s+/, " ") if user.description and not user.description.empty? |
|---|
| 653 | signon_at = Time.parse(user.created_at).to_i rescue 0 |
|---|
| 654 | idle_sec = (Time.now - (user.status ? Time.parse(user.status.created_at) : signon_at)).to_i rescue 0 |
|---|
| 655 | location = user.location |
|---|
| 656 | location = "SoMa neighborhood of San Francisco, CA" if location.nil? or location.empty? |
|---|
| 657 | post server_name, RPL_WHOISUSER, @nick, nick, prefix.user, prefix.host, "*", desc |
|---|
| 658 | post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, location |
|---|
| 659 | post server_name, RPL_WHOISIDLE, @nick, nick, "#{idle_sec}", "#{signon_at}", "seconds idle, signon time" |
|---|
| 660 | post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list" |
|---|
| 661 | if @drones.include?(user.id) |
|---|
| 662 | post server_name, RPL_WHOISBOT, @nick, nick, "is a \002Bot\002 on #{server_name}" |
|---|
| 663 | end |
|---|
| 664 | end |
|---|
| 665 | |
|---|
| 666 | def on_who(m) |
|---|
| 667 | channel = m.params[0] |
|---|
| 668 | whoreply = Proc.new do |ch, user| |
|---|
| 669 | # "<channel> <user> <host> <server> <nick> |
|---|
| 670 | # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] |
|---|
| 671 | # :<hopcount> <real name>" |
|---|
| 672 | prefix = prefix(user) |
|---|
| 673 | server = api_base.host |
|---|
| 674 | mode = case prefix.nick |
|---|
| 675 | when @nick then "~" |
|---|
| 676 | #when @drones.include?(user.id) then "%" # FIXME |
|---|
| 677 | else "+" |
|---|
| 678 | end |
|---|
| 679 | hop = prefix.host.count("/") |
|---|
| 680 | real = user.name |
|---|
| 681 | post server_name, RPL_WHOREPLY, @nick, |
|---|
| 682 | ch, prefix.user, prefix.host, server, prefix.nick, "H*#{mode}", "#{hop} #{real}" |
|---|
| 683 | end |
|---|
| 684 | |
|---|
| 685 | case |
|---|
| 686 | when channel.casecmp(main_channel).zero? |
|---|
| 687 | users = [@me] |
|---|
| 688 | users.concat @friends.reverse if @friends |
|---|
| 689 | users.each {|friend| whoreply.call channel, friend } |
|---|
| 690 | post server_name, RPL_ENDOFWHO, @nick, channel |
|---|
| 691 | when (@groups.key?(channel) and @friends) |
|---|
| 692 | @groups[channel].each do |nick| |
|---|
| 693 | whoreply.call channel, friend(nick) |
|---|
| 694 | end |
|---|
| 695 | post server_name, RPL_ENDOFWHO, @nick, channel |
|---|
| 696 | else |
|---|
| 697 | post server_name, ERR_NOSUCHNICK, @nick, "No such nick/channel" |
|---|
| 698 | end |
|---|
| 699 | end |
|---|
| 700 | |
|---|
| 701 | def on_join(m) |
|---|
| 702 | channels = m.params[0].split(/ *, */) |
|---|
| 703 | channels.each do |channel| |
|---|
| 704 | channel = channel.split(" ", 2).first |
|---|
| 705 | next if channel.casecmp(main_channel).zero? |
|---|
| 706 | |
|---|
| 707 | @channels << channel |
|---|
| 708 | @channels.uniq! |
|---|
| 709 | post @prefix, JOIN, channel |
|---|
| 710 | post server_name, MODE, channel, "+mtio", @nick |
|---|
| 711 | post server_name, MODE, channel, "+q", @nick |
|---|
| 712 | save_config |
|---|
| 713 | end |
|---|
| 714 | end |
|---|
| 715 | |
|---|
| 716 | def on_part(m) |
|---|
| 717 | channel = m.params[0] |
|---|
| 718 | return if channel.casecmp(main_channel).zero? |
|---|
| 719 | |
|---|
| 720 | @channels.delete(channel) |
|---|
| 721 | post @prefix, PART, channel, "Ignore group #{channel}, but setting is alive yet." |
|---|
| 722 | end |
|---|
| 723 | |
|---|
| 724 | def on_invite(m) |
|---|
| 725 | nick, channel = *m.params |
|---|
| 726 | if not nick.screen_name? or @nick.casecmp(nick).zero? |
|---|
| 727 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" # or yourself |
|---|
| 728 | return |
|---|
| 729 | end |
|---|
| 730 | |
|---|
| 731 | friend = friend(nick) |
|---|
| 732 | |
|---|
| 733 | case |
|---|
| 734 | when channel.casecmp(main_channel).zero? |
|---|
| 735 | case |
|---|
| 736 | when friend #TODO |
|---|
| 737 | when api("users/username_available", { :username => nick }).valid |
|---|
| 738 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" |
|---|
| 739 | else |
|---|
| 740 | user = api("friendships/create/#{nick}") |
|---|
| 741 | join main_channel, [user] |
|---|
| 742 | @friends << user if @friends |
|---|
| 743 | @me.friends_count += 1 |
|---|
| 744 | end |
|---|
| 745 | when friend |
|---|
| 746 | ((@groups[channel] ||= []) << friend.screen_name).uniq! |
|---|
| 747 | join channel, [friend] |
|---|
| 748 | save_config |
|---|
| 749 | else |
|---|
| 750 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" |
|---|
| 751 | end |
|---|
| 752 | end |
|---|
| 753 | |
|---|
| 754 | def on_kick(m) |
|---|
| 755 | channel, nick, msg = *m.params |
|---|
| 756 | |
|---|
| 757 | if channel.casecmp(main_channel).zero? |
|---|
| 758 | @friends.delete_if do |friend| |
|---|
| 759 | if friend.screen_name.casecmp(nick).zero? |
|---|
| 760 | user = api("friendships/destroy/#{friend.id}") |
|---|
| 761 | if user.is_a? User |
|---|
| 762 | post prefix(user), PART, main_channel, "Removed: #{msg}" |
|---|
| 763 | @me.friends_count -= 1 |
|---|
| 764 | end |
|---|
| 765 | end |
|---|
| 766 | end if @friends |
|---|
| 767 | else |
|---|
| 768 | friend = friend(nick) |
|---|
| 769 | if friend |
|---|
| 770 | (@groups[channel] ||= []).delete(friend.screen_name) |
|---|
| 771 | post prefix(friend), PART, channel, "Removed: #{msg}" |
|---|
| 772 | save_config |
|---|
| 773 | else |
|---|
| 774 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" |
|---|
| 775 | end |
|---|
| 776 | end |
|---|
| 777 | end |
|---|
| 778 | |
|---|
| 779 | #def on_nick(m) |
|---|
| 780 | # @nicknames[@nick] = m.params[0] |
|---|
| 781 | #end |
|---|
| 782 | |
|---|
| 783 | def on_topic(m) |
|---|
| 784 | channel = m.params[0] |
|---|
| 785 | return if not channel.casecmp(main_channel).zero? or @me.status.nil? |
|---|
| 786 | |
|---|
| 787 | return if not @opts.mesautofix |
|---|
| 788 | begin |
|---|
| 789 | require "levenshtein" |
|---|
| 790 | topic = m.params[1] |
|---|
| 791 | previous = @me.status |
|---|
| 792 | return unless previous |
|---|
| 793 | |
|---|
| 794 | distance = Levenshtein.normalized_distance(previous.text, topic) |
|---|
| 795 | return if distance.zero? |
|---|
| 796 | |
|---|
| 797 | status = api("statuses/update", { :status => topic, :source => source }) |
|---|
| 798 | log oops(ret) if status.truncated |
|---|
| 799 | status.user.status = status |
|---|
| 800 | @me = status.user |
|---|
| 801 | |
|---|
| 802 | if distance < 0.5 |
|---|
| 803 | deleted = api("statuses/destroy/#{previous.id}") |
|---|
| 804 | @timeline.delete_if {|tid, s| s.id == deleted.id } |
|---|
| 805 | log "Similar update in previous. Conclude that it has error." |
|---|
| 806 | log "And overwrite previous as new status: #{status.text}" |
|---|
| 807 | else |
|---|
| 808 | log "Status updated" |
|---|
| 809 | end |
|---|
| 810 | rescue LoadError |
|---|
| 811 | end |
|---|
| 812 | end |
|---|
| 813 | |
|---|
| 814 | def on_mode(m) |
|---|
| 815 | channel = m.params[0] |
|---|
| 816 | |
|---|
| 817 | unless m.params[1] |
|---|
| 818 | case |
|---|
| 819 | when channel.ch? |
|---|
| 820 | mode = "+mt" |
|---|
| 821 | mode += "i" unless channel.casecmp(main_channel).zero? |
|---|
| 822 | post server_name, RPL_CHANNELMODEIS, @nick, channel, mode |
|---|
| 823 | #post server_name, RPL_CREATEONTIME, @nick, channel, 0 |
|---|
| 824 | when channel.casecmp(@nick).zero? |
|---|
| 825 | post server_name, RPL_UMODEIS, @nick, @nick, "+o" |
|---|
| 826 | end |
|---|
| 827 | end |
|---|
| 828 | end |
|---|
| 829 | |
|---|
| 830 | private |
|---|
| 831 | def on_ctcp(target, mesg) |
|---|
| 832 | type, mesg = mesg.split(" ", 2) |
|---|
| 833 | method = "on_ctcp_#{type.downcase}".to_sym |
|---|
| 834 | send(method, target, mesg) if respond_to? method, true |
|---|
| 835 | end |
|---|
| 836 | |
|---|
| 837 | def on_ctcp_action(target, mesg) |
|---|
| 838 | #return unless main_channel.casecmp(target).zero? |
|---|
| 839 | command, *args = mesg.split(" ") |
|---|
| 840 | if command |
|---|
| 841 | command.downcase! |
|---|
| 842 | |
|---|
| 843 | @@ctcp_action_commands.each do |define, name| |
|---|
| 844 | if define === command |
|---|
| 845 | send(name, target, mesg, Regexp.last_match || command, args) |
|---|
| 846 | break |
|---|
| 847 | end |
|---|
| 848 | end |
|---|
| 849 | else |
|---|
| 850 | commands = @@ctcp_action_commands.map {|define, name| |
|---|
| 851 | define |
|---|
| 852 | }.select {|define| |
|---|
| 853 | define.is_a? String |
|---|
| 854 | } |
|---|
| 855 | |
|---|
| 856 | log "[tig.rb] CTCP ACTION COMMANDS:" |
|---|
| 857 | commands.each_slice(5) do |c| |
|---|
| 858 | log c.join(" ") |
|---|
| 859 | end |
|---|
| 860 | end |
|---|
| 861 | |
|---|
| 862 | rescue APIFailed => e |
|---|
| 863 | log e.inspect |
|---|
| 864 | rescue Exception => e |
|---|
| 865 | log e.inspect |
|---|
| 866 | e.backtrace.each do |l| |
|---|
| 867 | @log.error "\t#{l}" |
|---|
| 868 | end |
|---|
| 869 | end |
|---|
| 870 | |
|---|
| 871 | ctcp_action "call" do |target, mesg, command, args| |
|---|
| 872 | if args.size < 2 |
|---|
| 873 | log "/me call <Twitter_screen_name> as <IRC_nickname>" |
|---|
| 874 | return |
|---|
| 875 | end |
|---|
| 876 | screen_name = args[0] |
|---|
| 877 | nickname = args[2] || args[1] # allow omitting "as" |
|---|
| 878 | if nickname == "is" and |
|---|
| 879 | deleted_nick = @nicknames.delete(screen_name) |
|---|
| 880 | log %Q{Removed the nickname "#{deleted_nick}" for #{screen_name}} |
|---|
| 881 | else |
|---|
| 882 | @nicknames[screen_name] = nickname |
|---|
| 883 | log "Call #{screen_name} as #{nickname}" |
|---|
| 884 | end |
|---|
| 885 | #save_config |
|---|
| 886 | end |
|---|
| 887 | |
|---|
| 888 | ctcp_action "debug" do |target, mesg, command, args| |
|---|
| 889 | code = args.join(" ") |
|---|
| 890 | begin |
|---|
| 891 | log instance_eval(code).inspect |
|---|
| 892 | rescue Exception => e |
|---|
| 893 | log e.inspect |
|---|
| 894 | end |
|---|
| 895 | end |
|---|
| 896 | |
|---|
| 897 | ctcp_action "utf-7", "utf7" do |target, mesg, command, args| |
|---|
| 898 | unless defined? ::Iconv |
|---|
| 899 | log "Can't load iconv." |
|---|
| 900 | return |
|---|
| 901 | end |
|---|
| 902 | @utf7 = !@utf7 |
|---|
| 903 | log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}" |
|---|
| 904 | end |
|---|
| 905 | |
|---|
| 906 | ctcp_action "list", "ls" do |target, mesg, command, args| |
|---|
| 907 | if args.empty? |
|---|
| 908 | log "/me list <NICK> [<NUM>]" |
|---|
| 909 | return |
|---|
| 910 | end |
|---|
| 911 | nick = args.first |
|---|
| 912 | if not nick.screen_name? or |
|---|
| 913 | api("users/username_available", { :username => nick }).valid |
|---|
| 914 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" |
|---|
| 915 | return |
|---|
| 916 | end |
|---|
| 917 | id = nick |
|---|
| 918 | authenticate = false |
|---|
| 919 | if user = friend(nick) |
|---|
| 920 | id = user.id |
|---|
| 921 | nick = user.screen_name |
|---|
| 922 | authenticate = user.protected |
|---|
| 923 | end |
|---|
| 924 | unless (1..200).include?(count = args[1].to_i) |
|---|
| 925 | count = 20 |
|---|
| 926 | end |
|---|
| 927 | begin |
|---|
| 928 | res = api("statuses/user_timeline/#{id}", |
|---|
| 929 | { :count => count }, { :authenticate => authenticate }) |
|---|
| 930 | rescue APIFailed |
|---|
| 931 | #log "#{nick} has protected their updates." |
|---|
| 932 | return |
|---|
| 933 | end |
|---|
| 934 | res.reverse_each do |s| |
|---|
| 935 | message(s, target, nil, nil, NOTICE) |
|---|
| 936 | end |
|---|
| 937 | end |
|---|
| 938 | |
|---|
| 939 | ctcp_action %r/\A(un)?fav(?:ou?rite)?(!)?\z/ do |target, mesg, command, args| |
|---|
| 940 | # fav, unfav, favorite, unfavorite, favourite, unfavourite |
|---|
| 941 | method = command[1].nil? ? "create" : "destroy" |
|---|
| 942 | force = !!command[2] |
|---|
| 943 | entered = command[0].capitalize |
|---|
| 944 | statuses = [] |
|---|
| 945 | if args.empty? |
|---|
| 946 | if method == "create" |
|---|
| 947 | if status = @timeline.last |
|---|
| 948 | statuses << status |
|---|
| 949 | else |
|---|
| 950 | #log "" |
|---|
| 951 | return |
|---|
| 952 | end |
|---|
| 953 | else |
|---|
| 954 | @favorites ||= api("favorites").reverse |
|---|
| 955 | if @favorites.empty? |
|---|
| 956 | log "You've never favorite yet. No favorites to unfavorite." |
|---|
| 957 | return |
|---|
| 958 | end |
|---|
| 959 | statuses.push @favorites.last |
|---|
| 960 | end |
|---|
| 961 | else |
|---|
| 962 | args.each do |tid_or_nick| |
|---|
| 963 | case |
|---|
| 964 | when status = @timeline[tid = tid_or_nick] |
|---|
| 965 | statuses.push status |
|---|
| 966 | when friend = friend(nick = tid_or_nick) |
|---|
| 967 | if friend.status |
|---|
| 968 | statuses.push friend.status |
|---|
| 969 | else |
|---|
| 970 | log "#{tid_or_nick} has no status." |
|---|
| 971 | end |
|---|
| 972 | else |
|---|
| 973 | # PRIVMSG: fav nick |
|---|
| 974 | log "No such ID/NICK #{@opts.tid % tid_or_nick}" |
|---|
| 975 | end |
|---|
| 976 | end |
|---|
| 977 | end |
|---|
| 978 | @favorites ||= [] |
|---|
| 979 | statuses.each do |s| |
|---|
| 980 | if not force and method == "create" and |
|---|
| 981 | @favorites.find {|i| i.id == s.id } |
|---|
| 982 | log "The status is already favorited! <#{permalink(s)}>" |
|---|
| 983 | next |
|---|
| 984 | end |
|---|
| 985 | res = api("favorites/#{method}/#{s.id}") |
|---|
| 986 | log "#{entered}: #{res.user.screen_name}: #{generate_status_message(res.text)}" |
|---|
| 987 | if method == "create" |
|---|
| 988 | @favorites.push res |
|---|
| 989 | else |
|---|
| 990 | @favorites.delete_if {|i| i.id == res.id } |
|---|
| 991 | end |
|---|
| 992 | end |
|---|
| 993 | end |
|---|
| 994 | |
|---|
| 995 | ctcp_action "link", "ln", /\Au(?:rl)?\z/ do |target, mesg, command, args| |
|---|
| 996 | args.each do |tid| |
|---|
| 997 | if status = @timeline[tid] |
|---|
| 998 | log "#{@opts.tid % tid}: #{permalink(status)}" |
|---|
| 999 | else |
|---|
| 1000 | log "No such ID #{@opts.tid % tid}" |
|---|
| 1001 | end |
|---|
| 1002 | end |
|---|
| 1003 | end |
|---|
| 1004 | |
|---|
| 1005 | ctcp_action "ratio", "ratios" do |target, mesg, command, args| |
|---|
| 1006 | unless args.empty? |
|---|
| 1007 | args = args.first.split(":") if args.size == 1 |
|---|
| 1008 | case |
|---|
| 1009 | when @opts.dm && @opts.mentions && args.size < 3 |
|---|
| 1010 | log "/me ratios <timeline> <dm> <mentions>" |
|---|
| 1011 | return |
|---|
| 1012 | when @opts.dm && args.size < 2 |
|---|
| 1013 | log "/me ratios <timeline> <dm>" |
|---|
| 1014 | return |
|---|
| 1015 | when @opts.mentions && args.size < 2 |
|---|
| 1016 | log "/me ratios <timeline> <mentions>" |
|---|
| 1017 | return |
|---|
| 1018 | end |
|---|
| 1019 | ratios = args.map {|ratio| ratio.to_f } |
|---|
| 1020 | if ratios.any? {|ratio| ratio <= 0.0 } |
|---|
| 1021 | log "Ratios must be greater than 0.0 and fractional values are permitted." |
|---|
| 1022 | return |
|---|
| 1023 | end |
|---|
| 1024 | @ratio.timeline = ratios[0] |
|---|
| 1025 | |
|---|
| 1026 | case |
|---|
| 1027 | when @opts.dm |
|---|
| 1028 | @ratio.dm = ratios[1] |
|---|
| 1029 | @ratio.mentions = ratios[2] if @opts.mentions |
|---|
| 1030 | when @opts.mentions |
|---|
| 1031 | @ratio.mentions = ratios[1] |
|---|
| 1032 | end |
|---|
| 1033 | end |
|---|
| 1034 | log "Intervals: " + @ratio.zip([:timeline, :dm, :mentions]).map {|ratio, name| [name, "#{interval(ratio).round}sec"] }.inspect |
|---|
| 1035 | end |
|---|
| 1036 | |
|---|
| 1037 | ctcp_action "rm", %r/\A(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))\z/ do |target, mesg, command, args| |
|---|
| 1038 | # destroy, delete, del, remove, rm, miss, oops |
|---|
| 1039 | statuses = [] |
|---|
| 1040 | if args.empty? and @me.status |
|---|
| 1041 | statuses.push @me.status |
|---|
| 1042 | else |
|---|
| 1043 | args.each do |tid| |
|---|
| 1044 | if status = @timeline[tid] |
|---|
| 1045 | if status.user.id == @me.id |
|---|
| 1046 | statuses.push status |
|---|
| 1047 | else |
|---|
| 1048 | log "The status you specified by the ID #{@opts.tid % tid} is not yours." |
|---|
| 1049 | end |
|---|
| 1050 | else |
|---|
| 1051 | log "No such ID #{@opts.tid % tid}" |
|---|
| 1052 | end |
|---|
| 1053 | end |
|---|
| 1054 | end |
|---|
| 1055 | b = false |
|---|
| 1056 | statuses.each do |st| |
|---|
| 1057 | res = api("statuses/destroy/#{st.id}") |
|---|
| 1058 | @timeline.delete_if {|tid, s| s.id == res.id } |
|---|
| 1059 | b = @me.status && @me.status.id == res.id |
|---|
| 1060 | log "Destroyed: #{res.text}" |
|---|
| 1061 | end |
|---|
| 1062 | Thread.start do |
|---|
| 1063 | sleep 2 |
|---|
| 1064 | @me = api("account/update_profile") #api("account/verify_credentials") |
|---|
| 1065 | if @me.status |
|---|
| 1066 | @me.status.user = @me |
|---|
| 1067 | msg = generate_status_message(@me.status.text) |
|---|
| 1068 | @timeline.any? do |tid, s| |
|---|
| 1069 | if s.id == @me.status.id |
|---|
| 1070 | msg << " " << @opts.tid % tid |
|---|
| 1071 | end |
|---|
| 1072 | end |
|---|
| 1073 | post @prefix, TOPIC, main_channel, msg |
|---|
| 1074 | end |
|---|
| 1075 | end if b |
|---|
| 1076 | end |
|---|
| 1077 | |
|---|
| 1078 | ctcp_action "name" do |target, mesg, command, args| |
|---|
| 1079 | name = mesg.split(" ", 2)[1] |
|---|
| 1080 | unless name.nil? |
|---|
| 1081 | @me = api("account/update_profile", { :name => name }) |
|---|
| 1082 | @me.status.user = @me if @me.status |
|---|
| 1083 | log "You are named #{@me.name}." |
|---|
| 1084 | end |
|---|
| 1085 | end |
|---|
| 1086 | |
|---|
| 1087 | ctcp_action "email" do |target, mesg, command, args| |
|---|
| 1088 | # FIXME |
|---|
| 1089 | email = args.first |
|---|
| 1090 | unless email.nil? |
|---|
| 1091 | @me = api("account/update_profile", { :email => email }) |
|---|
| 1092 | @me.status.user = @me if @me.status |
|---|
| 1093 | end |
|---|
| 1094 | end |
|---|
| 1095 | |
|---|
| 1096 | ctcp_action "url" do |target, mesg, command, args| |
|---|
| 1097 | # FIXME |
|---|
| 1098 | url = args.first || "" |
|---|
| 1099 | @me = api("account/update_profile", { :url => url }) |
|---|
| 1100 | @me.status.user = @me if @me.status |
|---|
| 1101 | end |
|---|
| 1102 | |
|---|
| 1103 | ctcp_action "in", "location" do |target, mesg, command, args| |
|---|
| 1104 | location = mesg.split(" ", 2)[1] || "" |
|---|
| 1105 | @me = api("account/update_profile", { :location => location }) |
|---|
| 1106 | @me.status.user = @me if @me.status |
|---|
| 1107 | location = (@me.location and @me.location.empty?) ? "nowhere" : "in #{@me.location}" |
|---|
| 1108 | log "You are #{location} now." |
|---|
| 1109 | end |
|---|
| 1110 | |
|---|
| 1111 | ctcp_action %r/\Adesc(?:ription)?\z/ do |target, mesg, command, args| |
|---|
| 1112 | # FIXME |
|---|
| 1113 | description = mesg.split(" ", 2)[1] || "" |
|---|
| 1114 | @me = api("account/update_profile", { :description => description }) |
|---|
| 1115 | @me.status.user = @me if @me.status |
|---|
| 1116 | end |
|---|
| 1117 | |
|---|
| 1118 | ctcp_action %r/\A(?:mention|re(?:ply)?)\z/ do |target, mesg, command, args| |
|---|
| 1119 | # reply, re, mention |
|---|
| 1120 | tid = args.first |
|---|
| 1121 | if status = @timeline[tid] |
|---|
| 1122 | text = mesg.split(" ", 3)[2] |
|---|
| 1123 | screen_name = "@#{status.user.screen_name}" |
|---|
| 1124 | if text.nil? or not text.include?(screen_name) |
|---|
| 1125 | text = "#{screen_name} #{text}" |
|---|
| 1126 | end |
|---|
| 1127 | ret = api("statuses/update", { :status => text, :source => source, |
|---|
| 1128 | :in_reply_to_status_id => status.id }) |
|---|
| 1129 | log oops(ret) if ret.truncated |
|---|
| 1130 | msg = generate_status_message(status.text) |
|---|
| 1131 | url = permalink(status) |
|---|
| 1132 | log "Status updated (In reply to #{@opts.tid % tid}: #{msg} <#{url}>)" |
|---|
| 1133 | ret.user.status = ret |
|---|
| 1134 | @me = ret.user |
|---|
| 1135 | end |
|---|
| 1136 | end |
|---|
| 1137 | |
|---|
| 1138 | ctcp_action %r/\Aspoo(o+)?f\z/ do |target, mesg, command, args| |
|---|
| 1139 | if args.empty? |
|---|
| 1140 | Thread.start do |
|---|
| 1141 | update_sources(command[1].nil?? 0 : command[1].size) |
|---|
| 1142 | end |
|---|
| 1143 | return |
|---|
| 1144 | end |
|---|
| 1145 | names = [] |
|---|
| 1146 | @sources = args.map do |arg| |
|---|
| 1147 | names << "=#{arg}" |
|---|
| 1148 | case arg.upcase |
|---|
| 1149 | when "WEB" then "" |
|---|
| 1150 | when "API" then nil |
|---|
| 1151 | else arg |
|---|
| 1152 | end |
|---|
| 1153 | end |
|---|
| 1154 | log(names.inject([]) do |r, name| |
|---|
| 1155 | s = r.join(", ") |
|---|
| 1156 | if s.size < 400 |
|---|
| 1157 | r << name |
|---|
| 1158 | else |
|---|
| 1159 | log s |
|---|
| 1160 | [name] |
|---|
| 1161 | end |
|---|
| 1162 | end.join(", ")) |
|---|
| 1163 | end |
|---|
| 1164 | |
|---|
| 1165 | ctcp_action "bot", "drone" do |target, mesg, command, args| |
|---|
| 1166 | if args.empty? |
|---|
| 1167 | log "/me bot <NICK> [<NICK>...]" |
|---|
| 1168 | return |
|---|
| 1169 | end |
|---|
| 1170 | args.each do |bot| |
|---|
| 1171 | user = friend(bot) |
|---|
| 1172 | unless user |
|---|
| 1173 | post server_name, ERR_NOSUCHNICK, bot, "No such nick/channel" |
|---|
| 1174 | next |
|---|
| 1175 | end |
|---|
| 1176 | if @drones.delete(user.id) |
|---|
| 1177 | mode = "-#{mode}" |
|---|
| 1178 | log "#{bot} is no longer a bot." |
|---|
| 1179 | else |
|---|
| 1180 | @drones << user.id |
|---|
| 1181 | mode = "+#{mode}" |
|---|
| 1182 | log "Marks #{bot} as a bot." |
|---|
| 1183 | end |
|---|
| 1184 | end |
|---|
| 1185 | save_config |
|---|
| 1186 | |
|---|
| 1187 | end |
|---|
| 1188 | |
|---|
| 1189 | ctcp_action "home", "h" do |target, mesg, command, args| |
|---|
| 1190 | if args.empty? |
|---|
| 1191 | log "/me home <NICK>" |
|---|
| 1192 | return |
|---|
| 1193 | end |
|---|
| 1194 | nick = args.first |
|---|
| 1195 | if not nick.screen_name? or |
|---|
| 1196 | api("users/username_available", { :username => nick }).valid |
|---|
| 1197 | post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" |
|---|
| 1198 | return |
|---|
| 1199 | end |
|---|
| 1200 | log "http://twitter.com/#{nick}" |
|---|
| 1201 | end |
|---|
| 1202 | |
|---|
| 1203 | ctcp_action "retweet", "rt" do |target, mesg, command, args| |
|---|
| 1204 | if args.empty? |
|---|
| 1205 | log "/me #{command} <ID> blah blah" |
|---|
| 1206 | return |
|---|
| 1207 | end |
|---|
| 1208 | tid = args.first |
|---|
| 1209 | if status = @timeline[tid] |
|---|
| 1210 | if args.size >= 2 |
|---|
| 1211 | comment = mesg.split(" ", 3)[2] + " " |
|---|
| 1212 | else |
|---|
| 1213 | comment = "" |
|---|
| 1214 | end |
|---|
| 1215 | screen_name = "@#{status.user.screen_name}" |
|---|
| 1216 | rt_message = generate_status_message(status.text) |
|---|
| 1217 | text = "#{comment}RT #{screen_name}: #{rt_message}" |
|---|
| 1218 | ret = api("statuses/update", { :status => text, :source => source }) |
|---|
| 1219 | log oops(ret) if ret.truncated |
|---|
| 1220 | log "Status updated (RT to #{@opts.tid % tid}: #{text})" |
|---|
| 1221 | ret.user.status = ret |
|---|
| 1222 | @me = ret.user |
|---|
| 1223 | end |
|---|
| 1224 | end |
|---|
| 1225 | |
|---|
| 1226 | def on_ctcp_clientinfo(target, msg) |
|---|
| 1227 | if user = user(target) |
|---|
| 1228 | post prefix(user), NOTICE, @nick, ctcp_encode("CLIENTINFO :CLIENTINFO USERINFO VERSION TIME") |
|---|
| 1229 | end |
|---|
| 1230 | end |
|---|
| 1231 | |
|---|
| 1232 | def on_ctcp_userinfo(target, msg) |
|---|
| 1233 | user = user(target) |
|---|
| 1234 | if user and not user.description.empty? |
|---|
| 1235 | post prefix(user), NOTICE, @nick, ctcp_encode("USERINFO :#{user.description}") |
|---|
| 1236 | end |
|---|
| 1237 | end |
|---|
| 1238 | |
|---|
| 1239 | def on_ctcp_version(target, msg) |
|---|
| 1240 | user = user(target) |
|---|
| 1241 | if user and user.status |
|---|
| 1242 | source = user.status.source |
|---|
| 1243 | version = source.gsub(/<[^>]*>/, "").strip |
|---|
| 1244 | version << " <#{$1}>" if / href="([^"]+)/ === source |
|---|
| 1245 | post prefix(user), NOTICE, @nick, ctcp_encode("VERSION :#{version}") |
|---|
| 1246 | end |
|---|
| 1247 | end |
|---|
| 1248 | |
|---|
| 1249 | def on_ctcp_time(target, msg) |
|---|
| 1250 | if user = user(target) |
|---|
| 1251 | offset = user.utc_offset |
|---|
| 1252 | post prefix(user), NOTICE, @nick, ctcp_encode("TIME :%s%s (%s)" % [ |
|---|
| 1253 | (Time.now + offset).utc.iso8601[0, 19], |
|---|
| 1254 | "%+.2d:%.2d" % (offset/60).divmod(60), |
|---|
| 1255 | user.time_zone, |
|---|
| 1256 | ]) |
|---|
| 1257 | end |
|---|
| 1258 | end |
|---|
| 1259 | |
|---|
| 1260 | def check_friends |
|---|
| 1261 | if @friends.nil? |
|---|
| 1262 | @friends = page("statuses/friends/#{@me.id}", @me.friends_count) |
|---|
| 1263 | if @opts.athack |
|---|
| 1264 | join main_channel, @friends |
|---|
| 1265 | else |
|---|
| 1266 | rest = @friends.map do |i| |
|---|
| 1267 | prefix = "+" #@drones.include?(i.id) ? "%" : "+" # FIXME ~&% |
|---|
| 1268 | "#{prefix}#{i.screen_name}" |
|---|
| 1269 | end.reverse.inject("~#{@nick}") do |r, nick| |
|---|
| 1270 | if r.size < 400 |
|---|
| 1271 | r << " " << nick |
|---|
| 1272 | else |
|---|
| 1273 | post server_name, RPL_NAMREPLY, @nick, "=", main_channel, r |
|---|
| 1274 | nick |
|---|
| 1275 | end |
|---|
| 1276 | end |
|---|
| 1277 | post server_name, RPL_NAMREPLY, @nick, "=", main_channel, rest |
|---|
| 1278 | post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list" |
|---|
| 1279 | end |
|---|
| 1280 | else |
|---|
| 1281 | new_ids = page("friends/ids/#{@me.id}", @me.friends_count) |
|---|
| 1282 | friend_ids = @friends.reverse.map {|friend| friend.id } |
|---|
| 1283 | |
|---|
| 1284 | (friend_ids - new_ids).each do |id| |
|---|
| 1285 | @friends.delete_if do |friend| |
|---|
| 1286 | if friend.id == id |
|---|
| 1287 | post prefix(friend), PART, main_channel, "" |
|---|
| 1288 | @me.friends_count -= 1 |
|---|
| 1289 | end |
|---|
| 1290 | end |
|---|
| 1291 | end |
|---|
| 1292 | |
|---|
| 1293 | new_ids -= friend_ids |
|---|
| 1294 | unless new_ids.empty? |
|---|
| 1295 | new_friends = page("statuses/friends/#{@me.id}", new_ids.size) |
|---|
| 1296 | join main_channel, new_friends.delete_if {|friend| |
|---|
| 1297 | @friends.any? {|i| i.id == friend.id } |
|---|
| 1298 | }.reverse |
|---|
| 1299 | @friends.concat new_friends |
|---|
| 1300 | @me.friends_count += new_friends.size |
|---|
| 1301 | end |
|---|
| 1302 | end |
|---|
| 1303 | end |
|---|
| 1304 | |
|---|
| 1305 | def check_timeline |
|---|
| 1306 | cmd = PRIVMSG |
|---|
| 1307 | path = "statuses/#{@opts.with_retweets ? "home" : "friends"}_timeline" |
|---|
| 1308 | q = { :count => 200 } |
|---|
| 1309 | @latest_id ||= nil |
|---|
| 1310 | |
|---|
| 1311 | case |
|---|
| 1312 | when @latest_id |
|---|
| 1313 | q.update(:since_id => @latest_id) |
|---|
| 1314 | when is_first_retrieve = !@me.statuses_count.zero? && !@me.friends_count.zero? |
|---|
| 1315 | # cmd = NOTICE # デバッグするときめんどくさいので |
|---|
| 1316 | q.update(:count => 20) |
|---|
| 1317 | end |
|---|
| 1318 | |
|---|
| 1319 | api(path, q).reverse_each do |status| |
|---|
| 1320 | id = @latest_id = status.id |
|---|
| 1321 | next if @timeline.any? {|tid, s| s.id == id } |
|---|
| 1322 | |
|---|
| 1323 | status.user.status = status |
|---|
| 1324 | user = status.user |
|---|
| 1325 | tid = @timeline.push(status) |
|---|
| 1326 | tid = nil unless @opts.tid |
|---|
| 1327 | |
|---|
| 1328 | @log.debug [id, user.screen_name, status.text].inspect |
|---|
| 1329 | |
|---|
| 1330 | if user.id == @me.id |
|---|
| 1331 | mesg = generate_status_message(status.text) |
|---|
| 1332 | mesg << " " << @opts.tid % tid if tid |
|---|
| 1333 | post @prefix, TOPIC, main_channel, mesg |
|---|
| 1334 | |
|---|
| 1335 | @me = user |
|---|
| 1336 | else |
|---|
| 1337 | if @friends |
|---|
| 1338 | b = false |
|---|
| 1339 | @friends.each_with_index do |friend, i| |
|---|
| 1340 | if b = friend.id == user.id |
|---|
| 1341 | if friend.screen_name != user.screen_name |
|---|
| 1342 | post prefix(friend), NICK, user.screen_name |
|---|
| 1343 | end |
|---|
| 1344 | @friends[i] = user |
|---|
| 1345 | break |
|---|
| 1346 | end |
|---|
| 1347 | end |
|---|
| 1348 | unless b |
|---|
| 1349 | join main_channel, [user] |
|---|
| 1350 | @friends << user |
|---|
| 1351 | @me.friends_count += 1 |
|---|
| 1352 | end |
|---|
| 1353 | end |
|---|
| 1354 | |
|---|
| 1355 | message(status, main_channel, tid, nil, cmd) |
|---|
| 1356 | end |
|---|
| 1357 | @groups.each do |channel, members| |
|---|
| 1358 | next unless members.include?(user.screen_name) |
|---|
| 1359 | message(status, channel, tid, nil, cmd) |
|---|
| 1360 | end |
|---|
| 1361 | end |
|---|
| 1362 | end |
|---|
| 1363 | |
|---|
| 1364 | def check_direct_messages |
|---|
| 1365 | @prev_dm_id ||= nil |
|---|
| 1366 | q = @prev_dm_id ? { :count => 200, :since_id => @prev_dm_id } \ |
|---|
| 1367 | : { :count => 1 } |
|---|
| 1368 | api("direct_messages", q).reverse_each do |mesg| |
|---|
| 1369 | unless @prev_dm_id &&= mesg.id |
|---|
| 1370 | @prev_dm_id = mesg.id |
|---|
| 1371 | next |
|---|
| 1372 | end |
|---|
| 1373 | |
|---|
| 1374 | id = mesg.id |
|---|
| 1375 | user = mesg.sender |
|---|
| 1376 | tid = nil |
|---|
| 1377 | text = mesg.text |
|---|
| 1378 | @log.debug [id, user.screen_name, text].inspect |
|---|
| 1379 | message(user, @nick, tid, text) |
|---|
| 1380 | end |
|---|
| 1381 | end |
|---|
| 1382 | |
|---|
| 1383 | def check_mentions |
|---|
| 1384 | return if @timeline.empty? |
|---|
| 1385 | @prev_mention_id ||= @timeline.last.id |
|---|
| 1386 | api("statuses/mentions", { |
|---|
| 1387 | :count => 200, |
|---|
| 1388 | :since_id => @prev_mention_id |
|---|
| 1389 | }).reverse_each do |mention| |
|---|
| 1390 | id = @prev_mention_id = mention.id |
|---|
| 1391 | next if @timeline.any? {|tid, s| s.id == id } |
|---|
| 1392 | |
|---|
| 1393 | mention.user.status = mention |
|---|
| 1394 | user = mention.user |
|---|
| 1395 | tid = @timeline.push(mention) |
|---|
| 1396 | tid = nil unless @opts.tid |
|---|
| 1397 | |
|---|
| 1398 | @log.debug [id, user.screen_name, mention.text].inspect |
|---|
| 1399 | message(mention, main_channel, tid) |
|---|
| 1400 | |
|---|
| 1401 | @friends.each_with_index do |friend, i| |
|---|
| 1402 | if friend.id == user.id |
|---|
| 1403 | @friends[i] = user |
|---|
| 1404 | break |
|---|
| 1405 | end |
|---|
| 1406 | end if @friends |
|---|
| 1407 | end |
|---|
| 1408 | end |
|---|
| 1409 | |
|---|
| 1410 | def check_updates |
|---|
| 1411 | update_redundant_suffix |
|---|
| 1412 | |
|---|
| 1413 | return unless /\+r(\d+)\z/ === server_version |
|---|
| 1414 | rev = $1.to_i |
|---|
| 1415 | uri = URI("http://svn.coderepos.org/share/lang/ruby/net-irc/trunk/examples/tig.rb") |
|---|
| 1416 | @log.debug uri.inspect |
|---|
| 1417 | res = http(uri).request(http_req(:head, uri)) |
|---|
| 1418 | @etags[uri.to_s] = res["ETag"] |
|---|
| 1419 | return unless not res.is_a?(Net::HTTPNotModified) and |
|---|
| 1420 | /\A"(\d+)/ === res["ETag"] and rev < $1.to_i |
|---|
| 1421 | uri = URI("http://coderepos.org/share/log/lang/ruby/net-irc/trunk/examples/tig.rb") |
|---|
| 1422 | uri.query = { :rev => $1, :stop_rev => rev, :verbose => "on" }.to_query_str(";") |
|---|
| 1423 | log "\002New version is available.\017 <#{uri}>" |
|---|
| 1424 | rescue Errno::ECONNREFUSED, Timeout::Error => e |
|---|
| 1425 | @log.error "Failed to get the latest revision of tig.rb from #{uri.host}: #{e.inspect}" |
|---|
| 1426 | end |
|---|
| 1427 | |
|---|
| 1428 | def interval(ratio) |
|---|
| 1429 | now = Time.now |
|---|
| 1430 | max = @opts.maxlimit || 0 |
|---|
| 1431 | limit = 0.98 * @limit # 98% of the rate limit |
|---|
| 1432 | i = 3600.0 # an hour in seconds |
|---|
| 1433 | i *= @ratio.inject {|sum, r| sum.to_f + r.to_f } + |
|---|
| 1434 | @consums.delete_if {|t| t < now }.size |
|---|
| 1435 | i /= ratio.to_f |
|---|
| 1436 | i /= (0 < max && max < limit) ? max : limit |
|---|
| 1437 | i = 60 * 30 if i > 60 * 30 # 30分以上止まらないように。 |
|---|
| 1438 | i |
|---|
| 1439 | rescue => e |
|---|
| 1440 | @log.error e.inspect |
|---|
| 1441 | 100 |
|---|
| 1442 | end |
|---|
| 1443 | |
|---|
| 1444 | def join(channel, users) |
|---|
| 1445 | params = [] |
|---|
| 1446 | users.each do |user| |
|---|
| 1447 | prefix = prefix(user) |
|---|
| 1448 | post prefix, JOIN, channel |
|---|
| 1449 | params << prefix.nick if user.protected |
|---|
| 1450 | next if params.size < MAX_MODE_PARAMS |
|---|
| 1451 | |
|---|
| 1452 | post server_name, MODE, channel, "+#{"v" * params.size}", *params |
|---|
| 1453 | params = [] |
|---|
| 1454 | end |
|---|
| 1455 | post server_name, MODE, channel, "+#{"v" * params.size}", *params unless params.empty? |
|---|
| 1456 | users |
|---|
| 1457 | end |
|---|
| 1458 | |
|---|
| 1459 | def start_jabber(jid, pass) |
|---|
| 1460 | @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}" |
|---|
| 1461 | @im = Jabber::Simple.new(jid, pass) |
|---|
| 1462 | @im.add(jabber_bot_id) |
|---|
| 1463 | @im_thread = Thread.start do |
|---|
| 1464 | require "cgi" |
|---|
| 1465 | |
|---|
| 1466 | loop do |
|---|
| 1467 | begin |
|---|
| 1468 | @im.received_messages.each do |msg| |
|---|
| 1469 | @log.debug [msg.from, msg.body].inspect |
|---|
| 1470 | if msg.from.strip == jabber_bot_id |
|---|
| 1471 | # Twitter -> 'id: msg' |
|---|
| 1472 | body = msg.body.sub(/\A(.+?)(?:\(([^()]+)\))?: /, "") |
|---|
| 1473 | body = decode_utf7(body) |
|---|
| 1474 | |
|---|
| 1475 | if Regexp.last_match |
|---|
| 1476 | nick, id = Regexp.last_match.captures |
|---|
| 1477 | body = untinyurl(CGI.unescapeHTML(body)) |
|---|
| 1478 | user = nick |
|---|
| 1479 | nick = id || nick |
|---|
| 1480 | nick = @nicknames[nick] || nick |
|---|
| 1481 | post "#{nick}!#{user}@#{api_base.host}", PRIVMSG, main_channel, body |
|---|
| 1482 | end |
|---|
| 1483 | end |
|---|
| 1484 | end |
|---|
| 1485 | rescue Exception => e |
|---|
| 1486 | @log.error "Error on Jabber loop: #{e.inspect}" |
|---|
| 1487 | e.backtrace.each do |l| |
|---|
| 1488 | @log.error "\t#{l}" |
|---|
| 1489 | end |
|---|
| 1490 | end |
|---|
| 1491 | sleep 1 |
|---|
| 1492 | end |
|---|
| 1493 | end |
|---|
| 1494 | end |
|---|
| 1495 | |
|---|
| 1496 | def save_config |
|---|
| 1497 | config = { |
|---|
| 1498 | :groups => @groups, |
|---|
| 1499 | :channels => @channels, |
|---|
| 1500 | #:nicknames => @nicknames, |
|---|
| 1501 | :drones => @drones, |
|---|
| 1502 | } |
|---|
| 1503 | @config.open("w") {|f| YAML.dump(config, f) } |
|---|
| 1504 | end |
|---|
| 1505 | |
|---|
| 1506 | def load_config |
|---|
| 1507 | @config.open do |f| |
|---|
| 1508 | config = YAML.load(f) |
|---|
| 1509 | @groups = config[:groups] || {} |
|---|
| 1510 | @channels = config[:channels] || [] |
|---|
| 1511 | #@nicknames = config[:nicknames] || {} |
|---|
| 1512 | @drones = config[:drones] || [] |
|---|
| 1513 | end |
|---|
| 1514 | rescue Errno::ENOENT |
|---|
| 1515 | end |
|---|
| 1516 | |
|---|
| 1517 | def require_post?(path) |
|---|
| 1518 | %r{ |
|---|
| 1519 | \A |
|---|
| 1520 | (?: status(?:es)?/update \z |
|---|
| 1521 | | direct_messages/new \z |
|---|
| 1522 | | friendships/create/ |
|---|
| 1523 | | account/(?: end_session \z | update_ ) |
|---|
| 1524 | | favou?ri(?: ing | tes )/create/ |
|---|
| 1525 | | notifications/ |
|---|
| 1526 | | blocks/create/ ) |
|---|
| 1527 | }x === path |
|---|
| 1528 | end |
|---|
| 1529 | |
|---|
| 1530 | #def require_put?(path) |
|---|
| 1531 | # %r{ \A status(?:es)?/retweet (?:/|\z) }x === path |
|---|
| 1532 | #end |
|---|
| 1533 | |
|---|
| 1534 | def api(path, query = {}, opts = {}) |
|---|
| 1535 | path.sub!(%r{\A/+}, "") |
|---|
| 1536 | query = query.to_query_str |
|---|
| 1537 | |
|---|
| 1538 | authenticate = opts.fetch(:authenticate, true) |
|---|
| 1539 | |
|---|
| 1540 | uri = api_base(authenticate) |
|---|
| 1541 | uri.path += path |
|---|
| 1542 | uri.path += ".json" if path != "users/username_available" |
|---|
| 1543 | uri.query = query unless query.empty? |
|---|
| 1544 | |
|---|
| 1545 | header = {} |
|---|
| 1546 | credentials = authenticate ? [@real, @pass] : nil |
|---|
| 1547 | req = case |
|---|
| 1548 | when path.include?("/destroy/") |
|---|
| 1549 | http_req :delete, uri, header, credentials |
|---|
| 1550 | when require_post?(path) |
|---|
| 1551 | http_req :post, uri, header, credentials |
|---|
| 1552 | #when require_put?(path) |
|---|
| 1553 | # http_req :put, uri, header, credentials |
|---|
| 1554 | else |
|---|
| 1555 | http_req :get, uri, header, credentials |
|---|
| 1556 | end |
|---|
| 1557 | |
|---|
| 1558 | @log.debug [req.method, uri.to_s] |
|---|
| 1559 | ret = http(uri, 30, 30).request req |
|---|
| 1560 | |
|---|
| 1561 | #@etags[uri.to_s] = ret["ETag"] |
|---|
| 1562 | |
|---|
| 1563 | case |
|---|
| 1564 | when authenticate |
|---|
| 1565 | hourly_limit = ret["X-RateLimit-Limit"].to_i |
|---|
| 1566 | unless hourly_limit.zero? |
|---|
| 1567 | if @limit != hourly_limit |
|---|
| 1568 | msg = "The rate limit per hour was changed: #{@limit} to #{hourly_limit}" |
|---|
| 1569 | log msg |
|---|
| 1570 | @log.info msg |
|---|
| 1571 | @limit = hourly_limit |
|---|
| 1572 | end |
|---|
| 1573 | |
|---|
| 1574 | #if req.is_a?(Net::HTTP::Get) and not %w{ |
|---|
| 1575 | if not %w{ |
|---|
| 1576 | statuses/friends_timeline |
|---|
| 1577 | direct_messages |
|---|
| 1578 | statuses/mentions |
|---|
| 1579 | }.include?(path) and not ret.is_a?(Net::HTTPServerError) |
|---|
| 1580 | expired_on = Time.parse(ret["Date"]) rescue Time.now |
|---|
| 1581 | expired_on += 3636 # 1.01 hours in seconds later |
|---|
| 1582 | @consums << expired_on |
|---|
| 1583 | end |
|---|
| 1584 | end |
|---|
| 1585 | when ret["X-RateLimit-Remaining"] |
|---|
| 1586 | @limit_remaining_for_ip = ret["X-RateLimit-Remaining"].to_i |
|---|
| 1587 | @log.debug "IP based limit: #{@limit_remaining_for_ip}" |
|---|
| 1588 | end |
|---|
| 1589 | |
|---|
| 1590 | case ret |
|---|
| 1591 | when Net::HTTPOK # 200 |
|---|
| 1592 | # Avoid Twitter's invalid JSON |
|---|
| 1593 | json = ret.body.strip.sub(/\A(?:false|true)\z/, "[\\&]") |
|---|
| 1594 | |
|---|
| 1595 | res = JSON.parse json |
|---|
| 1596 | if res.is_a?(Hash) and res["error"] # and not res["response"] |
|---|
| 1597 | if @error != res["error"] |
|---|
| 1598 | @error = res["error"] |
|---|
| 1599 | log @error |
|---|
| 1600 | end |
|---|
| 1601 | raise APIFailed, res["error"] |
|---|
| 1602 | end |
|---|
| 1603 | res.to_tig_struct |
|---|
| 1604 | when Net::HTTPNoContent, # 204 |
|---|
| 1605 | Net::HTTPNotModified # 304 |
|---|
| 1606 | [] |
|---|
| 1607 | when Net::HTTPBadRequest # 400: exceeded the rate limitation |
|---|
| 1608 | if ret.key?("X-RateLimit-Reset") |
|---|
| 1609 | s = ret["X-RateLimit-Reset"].to_i - Time.now.to_i |
|---|
| 1610 | if s > 0 |
|---|
| 1611 | log "RateLimit: #{(s / 60.0).ceil} min remaining to get timeline" |
|---|
| 1612 | sleep (s > 60 * 10) ? 60 * 10 : s # 10 分に一回はとってくるように |
|---|
| 1613 | end |
|---|
| 1614 | end |
|---|
| 1615 | raise APIFailed, "#{ret.code}: #{ret.message}" |
|---|
| 1616 | when Net::HTTPUnauthorized # 401 |
|---|
| 1617 | raise APIFailed, "#{ret.code}: #{ret.message}" |
|---|
| 1618 | else |
|---|
| 1619 | raise APIFailed, "Server Returned #{ret.code} #{ret.message}" |
|---|
| 1620 | end |
|---|
| 1621 | rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e |
|---|
| 1622 | raise APIFailed, e.inspect |
|---|
| 1623 | end |
|---|
| 1624 | |
|---|
| 1625 | def page(path, max_count, authenticate = false) |
|---|
| 1626 | @limit_remaining_for_ip ||= 52 |
|---|
| 1627 | limit = 0.98 * @limit_remaining_for_ip # 98% of IP based rate limit |
|---|
| 1628 | r = [] |
|---|
| 1629 | cpp = nil # counts per page |
|---|
| 1630 | 1.upto(limit) do |num| |
|---|
| 1631 | ret = api(path, { :page => num }, { :authenticate => authenticate }) |
|---|
| 1632 | cpp ||= ret.size |
|---|
| 1633 | r.concat ret |
|---|
| 1634 | break if ret.empty? or num >= max_count / cpp.to_f or |
|---|
| 1635 | ret.size != cpp or r.size >= max_count |
|---|
| 1636 | end |
|---|
| 1637 | r |
|---|
| 1638 | end |
|---|
| 1639 | |
|---|
| 1640 | def generate_status_message(mesg) |
|---|
| 1641 | mesg = decode_utf7(mesg) |
|---|
| 1642 | mesg.delete!("\000\001") |
|---|
| 1643 | mesg.gsub!(">", ">") |
|---|
| 1644 | mesg.gsub!("<", "<") |
|---|
| 1645 | mesg.gsub!(WSP_REGEX, " ") |
|---|
| 1646 | mesg = untinyurl(mesg) |
|---|
| 1647 | mesg.sub!(@rsuffix_regex, "") if @rsuffix_regex |
|---|
| 1648 | mesg.strip |
|---|
| 1649 | end |
|---|
| 1650 | |
|---|
| 1651 | def friend(id) |
|---|
| 1652 | return nil unless @friends |
|---|
| 1653 | if id.is_a? String |
|---|
| 1654 | @friends.find {|i| i.screen_name.casecmp(id).zero? } |
|---|
| 1655 | else |
|---|
| 1656 | @friends.find {|i| i.id == id } |
|---|
| 1657 | end |
|---|
| 1658 | end |
|---|
| 1659 | |
|---|
| 1660 | def user(id) |
|---|
| 1661 | if id.is_a? String |
|---|
| 1662 | @nick.casecmp(id).zero? ? @me : friend(id) |
|---|
| 1663 | else |
|---|
| 1664 | @me.id == id ? @me : friend(id) |
|---|
| 1665 | end |
|---|
| 1666 | end |
|---|
| 1667 | |
|---|
| 1668 | def prefix(u) |
|---|
| 1669 | nick = u.screen_name |
|---|
| 1670 | nick = "@#{nick}" if @opts.athack |
|---|
| 1671 | user = "id=%.9d" % u.id |
|---|
| 1672 | host = api_base.host |
|---|
| 1673 | host += "/protected" if u.protected |
|---|
| 1674 | host += "/bot" if @drones.include?(u.id) |
|---|
| 1675 | |
|---|
| 1676 | Prefix.new("#{nick}!#{user}@#{host}") |
|---|
| 1677 | end |
|---|
| 1678 | |
|---|
| 1679 | def message(struct, target, tid = nil, str = nil, command = PRIVMSG) |
|---|
| 1680 | unless str |
|---|
| 1681 | status = struct.is_a?(Status) ? struct : struct.status |
|---|
| 1682 | str = status.text |
|---|
| 1683 | if command != PRIVMSG |
|---|
| 1684 | time = Time.parse(status.created_at) rescue Time.now |
|---|
| 1685 | str = "#{time.strftime(@opts.strftime || "%m-%d %H:%M")} #{str}" # TODO: color |
|---|
| 1686 | end |
|---|
| 1687 | end |
|---|
| 1688 | user = (struct.is_a?(User) ? struct : struct.user).dup |
|---|
| 1689 | screen_name = user.screen_name |
|---|
| 1690 | |
|---|
| 1691 | user.screen_name = @nicknames[screen_name] || screen_name |
|---|
| 1692 | prefix = prefix(user) |
|---|
| 1693 | str = generate_status_message(str) |
|---|
| 1694 | str = "#{str} #{@opts.tid % tid}" if tid |
|---|
| 1695 | |
|---|
| 1696 | post prefix, command, target, str |
|---|
| 1697 | end |
|---|
| 1698 | |
|---|
| 1699 | def log(str) |
|---|
| 1700 | post server_name, NOTICE, main_channel, str.gsub(/\r\n|[\r\n]/, " ") |
|---|
| 1701 | end |
|---|
| 1702 | |
|---|
| 1703 | def decode_utf7(str) |
|---|
| 1704 | return str unless defined? ::Iconv and str.include?("+") |
|---|
| 1705 | |
|---|
| 1706 | str.sub!(/\A(?:.+ > |.+\z)/) { Iconv.iconv("UTF-8", "UTF-7", $&).join } |
|---|
| 1707 | #FIXME str = "[utf7]: #{str}" if str =~ /[^a-z0-9\s]/i |
|---|
| 1708 | str |
|---|
| 1709 | rescue Iconv::IllegalSequence |
|---|
| 1710 | str |
|---|
| 1711 | rescue => e |
|---|
| 1712 | @log.error e |
|---|
| 1713 | str |
|---|
| 1714 | end |
|---|
| 1715 | |
|---|
| 1716 | def untinyurl(text) |
|---|
| 1717 | text.gsub(@opts.untiny_whole_urls ? URI.regexp(%w[http https]) : %r{ |
|---|
| 1718 | http:// (?: |
|---|
| 1719 | (?: bit\.ly | (?: tin | rub) yurl\.com |
|---|
| 1720 | | is\.gd | cli\.gs | tr\.im | u\.nu | airme\.us |
|---|
| 1721 | | ff\.im | twurl.nl | bkite\.com | tumblr\.com |
|---|
| 1722 | | pic\.gd | sn\.im | digg\.com ) |
|---|
| 1723 | / [0-9a-z=-]+ | |
|---|
| 1724 | blip\.fm/~ (?> [0-9a-z]+) (?! /) | |
|---|
| 1725 | flic\.kr/[a-z0-9/]+ |
|---|
| 1726 | ) |
|---|
| 1727 | }ix) {|url| "#{resolve_http_redirect(URI(url)) || url}" } |
|---|
| 1728 | end |
|---|
| 1729 | |
|---|
| 1730 | def bitlify(text) |
|---|
| 1731 | login, key, len = @opts.bitlify.split(":", 3) if @opts.bitlify |
|---|
| 1732 | len = (len || 20).to_i |
|---|
| 1733 | longurls = URI.extract(text, %w[http https]).uniq.map do |url| |
|---|
| 1734 | URI.rstrip url |
|---|
| 1735 | end.reject do |url| |
|---|
| 1736 | url.size < len |
|---|
| 1737 | end |
|---|
| 1738 | return text if longurls.empty? |
|---|
| 1739 | |
|---|
| 1740 | bitly = URI("http://api.bit.ly/shorten") |
|---|
| 1741 | if login and key |
|---|
| 1742 | bitly.path = "/shorten" |
|---|
| 1743 | bitly.query = { |
|---|
| 1744 | :version => "2.0.1", :format => "json", :longUrl => longurls, |
|---|
| 1745 | }.to_query_str(";") |
|---|
| 1746 | @log.debug bitly |
|---|
| 1747 | req = http_req(:get, bitly, {}, [login, key]) |
|---|
| 1748 | res = http(bitly, 5, 10).request(req) |
|---|
| 1749 | res = JSON.parse(res.body) |
|---|
| 1750 | res = res["results"] |
|---|
| 1751 | |
|---|
| 1752 | longurls.each do |longurl| |
|---|
| 1753 | text.gsub!(longurl) do |
|---|
| 1754 | res[$&] && res[$&]["shortUrl"] || $& |
|---|
| 1755 | end |
|---|
| 1756 | end |
|---|
| 1757 | else |
|---|
| 1758 | bitly.path = "/api" |
|---|
| 1759 | longurls.each do |longurl| |
|---|
| 1760 | bitly.query = { :url => longurl }.to_query_str |
|---|
| 1761 | @log.debug bitly |
|---|
| 1762 | req = http_req(:get, bitly) |
|---|
| 1763 | res = http(bitly, 5, 5).request(req) |
|---|
| 1764 | text.gsub!(longurl, res.body) |
|---|
| 1765 | end |
|---|
| 1766 | end |
|---|
| 1767 | |
|---|
| 1768 | text |
|---|
| 1769 | rescue => e |
|---|
| 1770 | @log.error e |
|---|
| 1771 | text |
|---|
| 1772 | end |
|---|
| 1773 | |
|---|
| 1774 | def unuify(text) |
|---|
| 1775 | unu_url = "http://u.nu/" |
|---|
| 1776 | unu = URI("#{unu_url}unu-api-simple") |
|---|
| 1777 | size = unu_url.size |
|---|
| 1778 | |
|---|
| 1779 | text.gsub(URI.regexp(%w[http https])) do |url| |
|---|
| 1780 | url = URI.rstrip url |
|---|
| 1781 | if url.size < size + 5 or url[0, size] == unu_url |
|---|
| 1782 | return url |
|---|
| 1783 | end |
|---|
| 1784 | |
|---|
| 1785 | unu.query = { :url => url }.to_query_str |
|---|
| 1786 | @log.debug unu |
|---|
| 1787 | |
|---|
| 1788 | res = http(unu, 5, 5).request(http_req(:get, unu)).body |
|---|
| 1789 | |
|---|
| 1790 | if res[0, 12] == unu_url |
|---|
| 1791 | res |
|---|
| 1792 | else |
|---|
| 1793 | raise res.split("|") |
|---|
| 1794 | end |
|---|
| 1795 | end |
|---|
| 1796 | rescue => e |
|---|
| 1797 | @log.error e |
|---|
| 1798 | text |
|---|
| 1799 | end |
|---|
| 1800 | |
|---|
| 1801 | def escape_http_urls(text) |
|---|
| 1802 | original_text = text.encoding!("UTF-8").dup |
|---|
| 1803 | |
|---|
| 1804 | if defined? ::Punycode |
|---|
| 1805 | # TODO: Nameprep |
|---|
| 1806 | text.gsub!(%r{(https?://)([^\x00-\x2C\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+)}) do |
|---|
| 1807 | domain = $2 |
|---|
| 1808 | # Dots: |
|---|
| 1809 | # * U+002E (full stop) * U+3002 (ideographic full stop) |
|---|
| 1810 | # * U+FF0E (fullwidth full stop) * U+FF61 (halfwidth ideographic full stop) |
|---|
| 1811 | # => /[.\u3002\uFF0E\uFF61] # Ruby 1.9 /x |
|---|
| 1812 | $1 + domain.split(/\.|\343\200\202|\357\274\216|\357\275\241/).map do |label| |
|---|
| 1813 | break [domain] if /\A-|[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]|-\z/ === label |
|---|
| 1814 | next label unless /[^-A-Za-z0-9]/ === label |
|---|
| 1815 | punycode = Punycode.encode(label) |
|---|
| 1816 | break [domain] if punycode.size > 59 |
|---|
| 1817 | "xn--#{punycode}" |
|---|
| 1818 | end.join(".") |
|---|
| 1819 | end |
|---|
| 1820 | if text != original_text |
|---|
| 1821 | log "Punycode encoded: #{text}" |
|---|
| 1822 | original_text = text.dup |
|---|
| 1823 | end |
|---|
| 1824 | end |
|---|
| 1825 | |
|---|
| 1826 | urls = [] |
|---|
| 1827 | text.split(/[\s<>]+/).each do |str| |
|---|
| 1828 | next if /%[0-9A-Fa-f]{2}/ === str |
|---|
| 1829 | # URI::UNSAFE + "#" |
|---|
| 1830 | escaped_str = URI.escape(str, %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]#]}) |
|---|
| 1831 | URI.extract(escaped_str, %w[http https]).each do |url| |
|---|
| 1832 | uri = URI(URI.rstrip(url)) |
|---|
| 1833 | if not urls.include?(uri.to_s) and exist_uri?(uri) |
|---|
| 1834 | urls << uri.to_s |
|---|
| 1835 | end |
|---|
| 1836 | end if escaped_str != str |
|---|
| 1837 | end |
|---|
| 1838 | urls.each do |url| |
|---|
| 1839 | unescaped_url = URI.unescape(url).encoding!("UTF-8") |
|---|
| 1840 | text.gsub!(unescaped_url, url) |
|---|
| 1841 | end |
|---|
| 1842 | log "Percent encoded: #{text}" if text != original_text |
|---|
| 1843 | |
|---|
| 1844 | text.encoding!("ASCII-8BIT") |
|---|
| 1845 | rescue => e |
|---|
| 1846 | @log.error e |
|---|
| 1847 | text |
|---|
| 1848 | end |
|---|
| 1849 | |
|---|
| 1850 | def exist_uri?(uri, limit = 1) |
|---|
| 1851 | ret = nil |
|---|
| 1852 | #raise "Not supported." unless uri.is_a?(URI::HTTP) |
|---|
| 1853 | return ret if limit.zero? or uri.nil? or not uri.is_a?(URI::HTTP) |
|---|
| 1854 | @log.debug uri.inspect |
|---|
| 1855 | |
|---|
| 1856 | req = http_req :head, uri |
|---|
| 1857 | http(uri, 3, 2).request(req) do |res| |
|---|
| 1858 | ret = case res |
|---|
| 1859 | when Net::HTTPSuccess |
|---|
| 1860 | true |
|---|
| 1861 | when Net::HTTPRedirection |
|---|
| 1862 | uri = resolve_http_redirect(uri) |
|---|
| 1863 | exist_uri?(uri, limit - 1) |
|---|
| 1864 | when Net::HTTPClientError |
|---|
| 1865 | false |
|---|
| 1866 | #when Net::HTTPServerError |
|---|
| 1867 | # nil |
|---|
| 1868 | else |
|---|
| 1869 | nil |
|---|
| 1870 | end |
|---|
| 1871 | end |
|---|
| 1872 | |
|---|
| 1873 | ret |
|---|
| 1874 | rescue => e |
|---|
| 1875 | @log.error e.inspect |
|---|
| 1876 | ret |
|---|
| 1877 | end |
|---|
| 1878 | |
|---|
| 1879 | def resolve_http_redirect(uri, limit = 3) |
|---|
| 1880 | return uri if limit.zero? or uri.nil? |
|---|
| 1881 | @log.debug uri.inspect |
|---|
| 1882 | |
|---|
| 1883 | req = http_req :head, uri |
|---|
| 1884 | http(uri, 3, 2).request(req) do |res| |
|---|
| 1885 | break if not res.is_a?(Net::HTTPRedirection) or |
|---|
| 1886 | not res.key?("Location") |
|---|
| 1887 | begin |
|---|
| 1888 | location = URI(res["Location"]) |
|---|
| 1889 | rescue URI::InvalidURIError |
|---|
| 1890 | end |
|---|
| 1891 | unless location.is_a? URI::HTTP |
|---|
| 1892 | begin |
|---|
| 1893 | location = URI.join(uri.to_s, res["Location"]) |
|---|
| 1894 | rescue URI::InvalidURIError, URI::BadURIError |
|---|
| 1895 | # FIXME |
|---|
| 1896 | end |
|---|
| 1897 | end |
|---|
| 1898 | uri = resolve_http_redirect(location, limit - 1) |
|---|
| 1899 | end |
|---|
| 1900 | |
|---|
| 1901 | uri |
|---|
| 1902 | rescue => e |
|---|
| 1903 | @log.error e.inspect |
|---|
| 1904 | uri |
|---|
| 1905 | end |
|---|
| 1906 | |
|---|
| 1907 | def update_sources(n = 0) |
|---|
| 1908 | if @sources and @sources.size > 1 and n.zero? |
|---|
| 1909 | log "tig.rb" |
|---|
| 1910 | @sources = [api_source] |
|---|
| 1911 | return @sources |
|---|
| 1912 | end |
|---|
| 1913 | |
|---|
| 1914 | uri = URI("http://wedata.net/databases/TwitterSources/items.json") |
|---|
| 1915 | @log.debug uri.inspect |
|---|
| 1916 | json = http(uri).request(http_req(:get, uri)).body |
|---|
| 1917 | sources = JSON.parse json |
|---|
| 1918 | sources.map! {|item| [item["data"]["source"], item["name"]] } |
|---|
| 1919 | sources.push ["", "web"] |
|---|
| 1920 | sources.push [nil, "API"] |
|---|
| 1921 | |
|---|
| 1922 | sources = Array.new(n) do |
|---|
| 1923 | sources.delete_at(rand(sources.size)) |
|---|
| 1924 | end if (1 ... sources.size).include?(n) |
|---|
| 1925 | |
|---|
| 1926 | log(sources.inject([]) do |r, src| |
|---|
| 1927 | s = r.join(", ") |
|---|
| 1928 | if s.size < 400 |
|---|
| 1929 | r << src[1] |
|---|
| 1930 | else |
|---|
| 1931 | log s |
|---|
| 1932 | [src[1]] |
|---|
| 1933 | end |
|---|
| 1934 | end.join(", ")) if @sources |
|---|
| 1935 | |
|---|
| 1936 | @sources = sources.map {|src| src[0] } |
|---|
| 1937 | rescue => e |
|---|
| 1938 | @log.error e.inspect |
|---|
| 1939 | log "An error occured while loading #{uri.host}." |
|---|
| 1940 | @sources ||= [api_source] |
|---|
| 1941 | end |
|---|
| 1942 | |
|---|
| 1943 | def update_redundant_suffix |
|---|
| 1944 | uri = URI("http://svn.coderepos.org/share/platform/twitterircgateway/suffixesblacklist.txt") |
|---|
| 1945 | @log.debug uri.inspect |
|---|
| 1946 | res = http(uri).request(http_req(:get, uri)) |
|---|
| 1947 | @etags[uri.to_s] = res["ETag"] |
|---|
| 1948 | return if res.is_a? Net::HTTPNotModified |
|---|
| 1949 | source = res.body |
|---|
| 1950 | source.encoding!("UTF-8") if source.respond_to?(:encoding) and source.encoding == Encoding::BINARY |
|---|
| 1951 | @rsuffix_regex = /#{Regexp.union(*source.split)}\z/ |
|---|
| 1952 | rescue Errno::ECONNREFUSED, Timeout::Error => e |
|---|
| 1953 | @log.error "Failed to get the redundant suffix blacklist from #{uri.host}: #{e.inspect}" |
|---|
| 1954 | end |
|---|
| 1955 | |
|---|
| 1956 | def http(uri, open_timeout = nil, read_timeout = 60) |
|---|
| 1957 | http = case |
|---|
| 1958 | when @httpproxy |
|---|
| 1959 | Net::HTTP.new(uri.host, uri.port, @httpproxy.address, @httpproxy.port, |
|---|
| 1960 | @httpproxy.user, @httpproxy.password) |
|---|
| 1961 | when ENV["HTTP_PROXY"], ENV["http_proxy"] |
|---|
| 1962 | proxy = URI(ENV["HTTP_PROXY"] || ENV["http_proxy"]) |
|---|
| 1963 | Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, |
|---|
| 1964 | proxy.user, proxy.password) |
|---|
| 1965 | else |
|---|
| 1966 | Net::HTTP.new(uri.host, uri.port) |
|---|
| 1967 | end |
|---|
| 1968 | http.open_timeout = open_timeout if open_timeout # nil by default |
|---|
| 1969 | http.read_timeout = read_timeout if read_timeout # 60 by default |
|---|
| 1970 | if uri.is_a? URI::HTTPS |
|---|
| 1971 | http.use_ssl = true |
|---|
| 1972 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE |
|---|
| 1973 | end |
|---|
| 1974 | http |
|---|
| 1975 | rescue => e |
|---|
| 1976 | @log.error e |
|---|
| 1977 | end |
|---|
| 1978 | |
|---|
| 1979 | def http_req(method, uri, header = {}, credentials = nil) |
|---|
| 1980 | accepts = ["*/*;q=0.1"] |
|---|
| 1981 | #require "mime/types"; accepts.unshift MIME::Types.of(uri.path).first.simplified |
|---|
| 1982 | types = { "json" => "application/json", "txt" => "text/plain" } |
|---|
| 1983 | ext = uri.path[/[^.]+\z/] |
|---|
| 1984 | accepts.unshift types[ext] if types.key?(ext) |
|---|
| 1985 | user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)}; net-irc) Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})" |
|---|
| 1986 | |
|---|
| 1987 | header["User-Agent"] ||= user_agent |
|---|
| 1988 | header["Accept"] ||= accepts.join(",") |
|---|
| 1989 | header["Accept-Charset"] ||= "UTF-8,*;q=0.0" if ext != "json" |
|---|
| 1990 | #header["Accept-Language"] ||= @opts.lang # "en-us,en;q=0.9,ja;q=0.5" |
|---|
| 1991 | header["If-None-Match"] ||= @etags[uri.to_s] if @etags[uri.to_s] |
|---|
| 1992 | |
|---|
| 1993 | req = case method.to_s.downcase.to_sym |
|---|
| 1994 | when :get |
|---|
| 1995 | Net::HTTP::Get.new uri.request_uri, header |
|---|
| 1996 | when :head |
|---|
| 1997 | Net::HTTP::Head.new uri.request_uri, header |
|---|
| 1998 | when :post |
|---|
| 1999 | Net::HTTP::Post.new uri.path, header |
|---|
| 2000 | when :put |
|---|
| 2001 | Net::HTTP::Put.new uri.path, header |
|---|
| 2002 | when :delete |
|---|
| 2003 | Net::HTTP::Delete.new uri.request_uri, header |
|---|
| 2004 | else # raise "" |
|---|
| 2005 | end |
|---|
| 2006 | if req.request_body_permitted? |
|---|
| 2007 | req["Content-Type"] ||= "application/x-www-form-urlencoded" |
|---|
| 2008 | req.body = uri.query |
|---|
| 2009 | end |
|---|
| 2010 | req.basic_auth(*credentials) if credentials |
|---|
| 2011 | req |
|---|
| 2012 | rescue => e |
|---|
| 2013 | @log.error e |
|---|
| 2014 | end |
|---|
| 2015 | |
|---|
| 2016 | def oops(status) |
|---|
| 2017 | "Oops! Your update was over 140 characters. We sent the short version" << |
|---|
| 2018 | " to your friends (they can view the entire update on the Web <" << |
|---|
| 2019 | permalink(status) << ">)." |
|---|
| 2020 | end |
|---|
| 2021 | |
|---|
| 2022 | def permalink(struct) |
|---|
| 2023 | path = struct.is_a?(Status) ? "#{struct.user.screen_name}/statuses/#{struct.id}" \ |
|---|
| 2024 | : struct.screen_name |
|---|
| 2025 | "http://twitter.com/#{path}" |
|---|
| 2026 | end |
|---|
| 2027 | |
|---|
| 2028 | def source |
|---|
| 2029 | @sources[rand(@sources.size)] |
|---|
| 2030 | end |
|---|
| 2031 | |
|---|
| 2032 | def initial_message |
|---|
| 2033 | super |
|---|
| 2034 | post server_name, RPL_ISUPPORT, @nick, |
|---|
| 2035 | "PREFIX=(qov)~@%+", "CHANTYPES=#", "CHANMODES=#{available_channel_modes}", |
|---|
| 2036 | "MODES=#{MAX_MODE_PARAMS}", "NICKLEN=15", "TOPICLEN=420", "CHANNELLEN=50", |
|---|
| 2037 | "NETWORK=Twitter", |
|---|
| 2038 | "are supported by this server" |
|---|
| 2039 | end |
|---|
| 2040 | |
|---|
| 2041 | User = Struct.new(:id, :name, :screen_name, :location, :description, :url, |
|---|
| 2042 | :following, :notifications, :protected, :time_zone, |
|---|
| 2043 | :utc_offset, :created_at, :friends_count, :followers_count, |
|---|
| 2044 | :statuses_count, :favourites_count, :verified, :geo_enabled, |
|---|
| 2045 | :profile_image_url, :profile_background_color, :profile_text_color, |
|---|
| 2046 | :profile_link_color, :profile_sidebar_fill_color, |
|---|
| 2047 | :profile_sidebar_border_color, :profile_background_image_url, |
|---|
| 2048 | :profile_background_tile, :status) |
|---|
| 2049 | Status = Struct.new(:id, :text, :source, :created_at, :truncated, :favorited, :geo, |
|---|
| 2050 | :in_reply_to_status_id, :in_reply_to_user_id, |
|---|
| 2051 | :in_reply_to_screen_name, :user) |
|---|
| 2052 | DM = Struct.new(:id, :text, :created_at, |
|---|
| 2053 | :sender_id, :sender_screen_name, :sender, |
|---|
| 2054 | :recipient_id, :recipient_screen_name, :recipient) |
|---|
| 2055 | Geo = Struct.new(:type, :coordinates, :geometries, :geometry, :properties, :id, |
|---|
| 2056 | :crs, :name, :href, :bbox, :features) |
|---|
| 2057 | |
|---|
| 2058 | class TypableMap < Hash |
|---|
| 2059 | #Roman = %w[ |
|---|
| 2060 | # 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 |
|---|
| 2061 | #].unshift("").map do |consonant| |
|---|
| 2062 | # case consonant |
|---|
| 2063 | # when "h", "q" then %w|a i e o| |
|---|
| 2064 | # when /[hy]$/ then %w|a u o| |
|---|
| 2065 | # else %w|a i u e o| |
|---|
| 2066 | # end.map {|vowel| "#{consonant}#{vowel}" } |
|---|
| 2067 | #end.flatten |
|---|
| 2068 | Roman = %w[ |
|---|
| 2069 | a i u e o ka ki ku ke ko sa shi su se so |
|---|
| 2070 | ta chi tsu te to na ni nu ne no ha hi fu he ho |
|---|
| 2071 | ma mi mu me mo ya yu yo ra ri ru re ro |
|---|
| 2072 | wa wo n |
|---|
| 2073 | ga gi gu ge go za ji zu ze zo da de do |
|---|
| 2074 | ba bi bu be bo pa pi pu pe po |
|---|
| 2075 | kya kyu kyo sha shu sho cha chu cho |
|---|
| 2076 | nya nyu nyo hya hyu hyo mya myu myo |
|---|
| 2077 | rya ryu ryo |
|---|
| 2078 | gya gyu gyo ja ju jo bya byu byo |
|---|
| 2079 | pya pyu pyo |
|---|
| 2080 | ].freeze |
|---|
| 2081 | |
|---|
| 2082 | def initialize(size = nil, shuffle = false) |
|---|
| 2083 | if shuffle |
|---|
| 2084 | @seq = Roman.dup |
|---|
| 2085 | if @seq.respond_to?(:shuffle!) |
|---|
| 2086 | @seq.shuffle! |
|---|
| 2087 | else |
|---|
| 2088 | @seq = Array.new(@seq.size) { @seq.delete_at(rand(@seq.size)) } |
|---|
| 2089 | end |
|---|
| 2090 | @seq.freeze |
|---|
| 2091 | else |
|---|
| 2092 | @seq = Roman |
|---|
| 2093 | end |
|---|
| 2094 | @n = 0 |
|---|
| 2095 | @size = size || @seq.size |
|---|
| 2096 | end |
|---|
| 2097 | |
|---|
| 2098 | def generate(n) |
|---|
| 2099 | ret = [] |
|---|
| 2100 | begin |
|---|
| 2101 | n, r = n.divmod(@seq.size) |
|---|
| 2102 | ret << @seq[r] |
|---|
| 2103 | end while n > 0 |
|---|
| 2104 | ret.reverse.join #.gsub(/n(?=[bmp])/, "m") |
|---|
| 2105 | end |
|---|
| 2106 | |
|---|
| 2107 | def push(obj) |
|---|
| 2108 | id = generate(@n) |
|---|
| 2109 | self[id] = obj |
|---|
| 2110 | @n += 1 |
|---|
| 2111 | @n %= @size |
|---|
| 2112 | id |
|---|
| 2113 | end |
|---|
| 2114 | alias :<< :push |
|---|
| 2115 | |
|---|
| 2116 | def clear |
|---|
| 2117 | @n = 0 |
|---|
| 2118 | super |
|---|
| 2119 | end |
|---|
| 2120 | |
|---|
| 2121 | def first |
|---|
| 2122 | @size.times do |i| |
|---|
| 2123 | id = generate((@n + i) % @size) |
|---|
| 2124 | return self[id] if key? id |
|---|
| 2125 | end unless empty? |
|---|
| 2126 | nil |
|---|
| 2127 | end |
|---|
| 2128 | |
|---|
| 2129 | def last |
|---|
| 2130 | @size.times do |i| |
|---|
| 2131 | id = generate((@n - 1 - i) % @size) |
|---|
| 2132 | return self[id] if key? id |
|---|
| 2133 | end unless empty? |
|---|
| 2134 | nil |
|---|
| 2135 | end |
|---|
| 2136 | |
|---|
| 2137 | private :[]= |
|---|
| 2138 | undef update, merge, merge!, replace |
|---|
| 2139 | end |
|---|
| 2140 | |
|---|
| 2141 | |
|---|
| 2142 | end |
|---|
| 2143 | |
|---|
| 2144 | class Array |
|---|
| 2145 | def to_tig_struct |
|---|
| 2146 | map do |v| |
|---|
| 2147 | v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v |
|---|
| 2148 | end |
|---|
| 2149 | end |
|---|
| 2150 | end |
|---|
| 2151 | |
|---|
| 2152 | class Hash |
|---|
| 2153 | def to_tig_struct |
|---|
| 2154 | if empty? |
|---|
| 2155 | #warn "" if $VERBOSE |
|---|
| 2156 | #raise Error |
|---|
| 2157 | return nil |
|---|
| 2158 | end |
|---|
| 2159 | |
|---|
| 2160 | struct = case |
|---|
| 2161 | when struct_of?(TwitterIrcGateway::User) |
|---|
| 2162 | TwitterIrcGateway::User.new |
|---|
| 2163 | when struct_of?(TwitterIrcGateway::Status) |
|---|
| 2164 | TwitterIrcGateway::Status.new |
|---|
| 2165 | when struct_of?(TwitterIrcGateway::DM) |
|---|
| 2166 | TwitterIrcGateway::DM.new |
|---|
| 2167 | when struct_of?(TwitterIrcGateway::Geo) |
|---|
| 2168 | TwitterIrcGateway::Geo.new |
|---|
| 2169 | else |
|---|
| 2170 | members = keys |
|---|
| 2171 | members.concat TwitterIrcGateway::User.members |
|---|
| 2172 | members.concat TwitterIrcGateway::Status.members |
|---|
| 2173 | members.concat TwitterIrcGateway::DM.members |
|---|
| 2174 | members.concat TwitterIrcGateway::Geo.members |
|---|
| 2175 | members.map! {|m| m.to_sym } |
|---|
| 2176 | members.uniq! |
|---|
| 2177 | Struct.new(*members).new |
|---|
| 2178 | end |
|---|
| 2179 | each do |k, v| |
|---|
| 2180 | struct[k.to_sym] = v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v |
|---|
| 2181 | end |
|---|
| 2182 | struct |
|---|
| 2183 | end |
|---|
| 2184 | |
|---|
| 2185 | # { :f => "v" } #=> "f=v" |
|---|
| 2186 | # { "f" => [1, 2] } #=> "f=1&f=2" |
|---|
| 2187 | # { "f" => "" } #=> "f=" |
|---|
| 2188 | # { "f" => nil } #=> "f" |
|---|
| 2189 | def to_query_str separator = "&" |
|---|
| 2190 | inject([]) do |r, (k, v)| |
|---|
| 2191 | k = URI.encode_component k.to_s |
|---|
| 2192 | (v.is_a?(Array) ? v : [v]).each do |i| |
|---|
| 2193 | if i.nil? |
|---|
| 2194 | r << k |
|---|
| 2195 | else |
|---|
| 2196 | r << "#{k}=#{URI.encode_component i.to_s}" |
|---|
| 2197 | end |
|---|
| 2198 | end |
|---|
| 2199 | r |
|---|
| 2200 | end.join separator |
|---|
| 2201 | end |
|---|
| 2202 | |
|---|
| 2203 | private |
|---|
| 2204 | def struct_of? struct |
|---|
| 2205 | (keys - struct.members.map {|m| m.to_s }).size.zero? |
|---|
| 2206 | end |
|---|
| 2207 | end |
|---|
| 2208 | |
|---|
| 2209 | class String |
|---|
| 2210 | def ch? |
|---|
| 2211 | /\A[&#+!][^ \007,]{1,50}\z/ === self |
|---|
| 2212 | end |
|---|
| 2213 | |
|---|
| 2214 | def screen_name? |
|---|
| 2215 | /\A[A-Za-z0-9_]{1,15}\z/ === self |
|---|
| 2216 | end |
|---|
| 2217 | |
|---|
| 2218 | def encoding! enc |
|---|
| 2219 | return self unless respond_to? :force_encoding |
|---|
| 2220 | force_encoding enc |
|---|
| 2221 | end |
|---|
| 2222 | end |
|---|
| 2223 | |
|---|
| 2224 | module URI::Escape |
|---|
| 2225 | alias :_orig_escape :escape |
|---|
| 2226 | |
|---|
| 2227 | if defined? ::RUBY_REVISION and RUBY_REVISION < 24544 |
|---|
| 2228 | # URI.escape("あ1") #=> "%E3%81%82\xEF\xBC\x91" |
|---|
| 2229 | # URI("file:///4") #=> #<URI::Generic:0x9d09db0 URL:file:/4> |
|---|
| 2230 | # "\\d" -> "[0-9]" for Ruby 1.9 |
|---|
| 2231 | def escape str, unsafe = %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]]} |
|---|
| 2232 | _orig_escape(str, unsafe) |
|---|
| 2233 | end |
|---|
| 2234 | alias :encode :escape |
|---|
| 2235 | end |
|---|
| 2236 | |
|---|
| 2237 | def encode_component str, unsafe = /[^-_.!~*'()a-zA-Z0-9 ]/ |
|---|
| 2238 | _orig_escape(str, unsafe).tr(" ", "+") |
|---|
| 2239 | end |
|---|
| 2240 | |
|---|
| 2241 | def rstrip str |
|---|
| 2242 | str.sub(%r{ |
|---|
| 2243 | (?: ( / [^/?#()]* (?: \( [^/?#()]* \) [^/?#()]* )* ) \) [^/?#()]* |
|---|
| 2244 | | \. |
|---|
| 2245 | ) \z |
|---|
| 2246 | }x, "\\1") |
|---|
| 2247 | end |
|---|
| 2248 | end |
|---|
| 2249 | |
|---|
| 2250 | if __FILE__ == $0 |
|---|
| 2251 | require "optparse" |
|---|
| 2252 | |
|---|
| 2253 | opts = { |
|---|
| 2254 | :port => 16668, |
|---|
| 2255 | :host => "localhost", |
|---|
| 2256 | :log => nil, |
|---|
| 2257 | :debug => false, |
|---|
| 2258 | :foreground => false, |
|---|
| 2259 | } |
|---|
| 2260 | |
|---|
| 2261 | OptionParser.new do |parser| |
|---|
| 2262 | parser.instance_eval do |
|---|
| 2263 | self.banner = <<-EOB.gsub(/^\t+/, "") |
|---|
| 2264 | Usage: #{$0} [opts] |
|---|
| 2265 | |
|---|
| 2266 | EOB |
|---|
| 2267 | |
|---|
| 2268 | separator "" |
|---|
| 2269 | |
|---|
| 2270 | separator "Options:" |
|---|
| 2271 | on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| |
|---|
| 2272 | opts[:port] = port |
|---|
| 2273 | end |
|---|
| 2274 | |
|---|
| 2275 | on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| |
|---|
| 2276 | opts[:host] = host |
|---|
| 2277 | end |
|---|
| 2278 | |
|---|
| 2279 | on("-l", "--log LOG", "log file") do |log| |
|---|
| 2280 | opts[:log] = log |
|---|
| 2281 | end |
|---|
| 2282 | |
|---|
| 2283 | on("--debug", "Enable debug mode") do |debug| |
|---|
| 2284 | opts[:log] = $stdout |
|---|
| 2285 | opts[:debug] = true |
|---|
| 2286 | end |
|---|
| 2287 | |
|---|
| 2288 | on("-f", "--foreground", "run foreground") do |foreground| |
|---|
| 2289 | opts[:log] = $stdout |
|---|
| 2290 | opts[:foreground] = true |
|---|
| 2291 | end |
|---|
| 2292 | |
|---|
| 2293 | on("-n", "--name [user name or email address]") do |name| |
|---|
| 2294 | opts[:name] = name |
|---|
| 2295 | end |
|---|
| 2296 | |
|---|
| 2297 | parse!(ARGV) |
|---|
| 2298 | end |
|---|
| 2299 | end |
|---|
| 2300 | |
|---|
| 2301 | opts[:logger] = Logger.new(opts[:log], "daily") |
|---|
| 2302 | opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO |
|---|
| 2303 | |
|---|
| 2304 | #def daemonize(foreground = false) |
|---|
| 2305 | # [:INT, :TERM, :HUP].each do |sig| |
|---|
| 2306 | # Signal.trap sig, "EXIT" |
|---|
| 2307 | # end |
|---|
| 2308 | # return yield if $DEBUG or foreground |
|---|
| 2309 | # Process.fork do |
|---|
| 2310 | # Process.setsid |
|---|
| 2311 | # Dir.chdir "/" |
|---|
| 2312 | # STDIN.reopen "/dev/null" |
|---|
| 2313 | # STDOUT.reopen "/dev/null", "a" |
|---|
| 2314 | # STDERR.reopen STDOUT |
|---|
| 2315 | # yield |
|---|
| 2316 | # end |
|---|
| 2317 | # exit! 0 |
|---|
| 2318 | #end |
|---|
| 2319 | |
|---|
| 2320 | #daemonize(opts[:debug] || opts[:foreground]) do |
|---|
| 2321 | Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start |
|---|
| 2322 | #end |
|---|
| 2323 | end |
|---|