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