| 1 | # OppaiSan IRCbot for Nadoka by Yappo |
|---|
| 2 | # based linky IRCbot for Nadoka ( http://linky.wikipedia.jp/linky.nb ) |
|---|
| 3 | # |
|---|
| 4 | # Copyright (C) 2004 by Tietew |
|---|
| 5 | # Originally copyright by datura |
|---|
| 6 | # License: GPL |
|---|
| 7 | # |
|---|
| 8 | require 'iconv' |
|---|
| 9 | require 'resolv' |
|---|
| 10 | require 'date' |
|---|
| 11 | require 'csv' |
|---|
| 12 | require 'shellwords' |
|---|
| 13 | require 'enumerator' |
|---|
| 14 | require 'net/http' |
|---|
| 15 | require 'uri' |
|---|
| 16 | require 'set' |
|---|
| 17 | require 'digest/md5' |
|---|
| 18 | require 'digest/sha1' |
|---|
| 19 | require 'thread' |
|---|
| 20 | require 'stringio' |
|---|
| 21 | require 'zlib' |
|---|
| 22 | require 'gdbm' |
|---|
| 23 | |
|---|
| 24 | require 'rubygems' |
|---|
| 25 | require 'hatena/api/graph' |
|---|
| 26 | |
|---|
| 27 | module OppaiSanBotModule |
|---|
| 28 | WordInfo = Struct.new(:active, :inactive) |
|---|
| 29 | RegInfo = Struct.new(:time, :prefix, :reg) |
|---|
| 30 | end |
|---|
| 31 | |
|---|
| 32 | class OppaiSanBot < Nadoka::NDK_Bot |
|---|
| 33 | include OppaiSanBotModule |
|---|
| 34 | |
|---|
| 35 | VERSION = 'OppaiSan (linky IRCbot (Powered by Ruby and Nadoka))' |
|---|
| 36 | |
|---|
| 37 | DEFAULT_LANGUAGE = "ja" |
|---|
| 38 | |
|---|
| 39 | HELP = [ |
|---|
| 40 | "OppaiSan は linky を元にした bot です", |
|---|
| 41 | " >>> OppaiSan is GPL. http://trac.yappo.jp/trac/browser/sandbox/OppaiSan/", |
|---|
| 42 | " >>> linky is GPL. http://cheepedia.tietew.jp/linky.nb", |
|---|
| 43 | "各コマンドの詳細は \"!help コマンド名\" と発言してください。", |
|---|
| 44 | "コマンド一覧:", |
|---|
| 45 | ] |
|---|
| 46 | MARSHAL_VARS = [ |
|---|
| 47 | :@when, :@where, :@who, :@why, :@how, :@what, :@tag, :@dan, :@regexp_user, |
|---|
| 48 | :@charset, :@proxychecker, :@options, |
|---|
| 49 | :@newcomers |
|---|
| 50 | ] |
|---|
| 51 | |
|---|
| 52 | SAVE_INTERVAL = 300 |
|---|
| 53 | |
|---|
| 54 | def bot_initialize |
|---|
| 55 | @starttime = Time.now |
|---|
| 56 | $starttime ||= @starttime |
|---|
| 57 | @threads = [] |
|---|
| 58 | |
|---|
| 59 | #config set |
|---|
| 60 | @path = @bot_config[:path] |
|---|
| 61 | @kick = @bot_config[:kick] || {:nickname => '', :channel => ''} |
|---|
| 62 | @hello = @bot_config[:hello] || {:nickname => '', :channel => ''} |
|---|
| 63 | @admin_re = @bot_config[:admin_re] || /^$/ |
|---|
| 64 | @smile = @bot_config[:smile] || [';-)'] |
|---|
| 65 | @dnbk = @bot_config[:dnbk] || {:re => /^$/, :msg => '', :kick => ''} |
|---|
| 66 | @hiita = @bot_config[:hiita] || {:re => /^$/, :msg => '', :kick => ''} |
|---|
| 67 | @regexp_list = @bot_config[:regexp_list] || [] |
|---|
| 68 | @kick_chains = @bot_config[:kick_chains] || [] |
|---|
| 69 | @nadoka_ch = @bot_config[:nadoka_ch] || /^$/ |
|---|
| 70 | @ban_kick = @bot_config[:ban_kick] || [] |
|---|
| 71 | @nodan = @bot_config[:nodan] || {:default => 300, :sleep => 'sleep', :walk => 'walk'} |
|---|
| 72 | @privmsg = @bot_config[:privmsg] |
|---|
| 73 | @ignore_user = @bot_config[:ignore_user] || [] |
|---|
| 74 | |
|---|
| 75 | @kick_chain_stack = {} |
|---|
| 76 | @kick_chains.each do |kick| |
|---|
| 77 | @kick_chain_stack.store(kick[:name], {}) |
|---|
| 78 | end |
|---|
| 79 | |
|---|
| 80 | @naruto = false |
|---|
| 81 | loaddata |
|---|
| 82 | @when ||= WordInfo.new({}, {}) |
|---|
| 83 | @where ||= WordInfo.new({}, {}) |
|---|
| 84 | @who ||= WordInfo.new({}, {}) |
|---|
| 85 | @why ||= WordInfo.new({}, {}) |
|---|
| 86 | @how ||= WordInfo.new({}, {}) |
|---|
| 87 | @what ||= WordInfo.new({}, {}) |
|---|
| 88 | @tag ||= WordInfo.new({}, {}) |
|---|
| 89 | @dan ||= WordInfo.new({}, {}) |
|---|
| 90 | @regexp_user ||= {} |
|---|
| 91 | @charset ||= {} |
|---|
| 92 | @options ||= {} |
|---|
| 93 | @kick_data ||= { |
|---|
| 94 | :total => 0, |
|---|
| 95 | "#{Time.now.strftime '%Y-%m-%d'}" => 0, |
|---|
| 96 | } |
|---|
| 97 | |
|---|
| 98 | create_file('newcomer.db') |
|---|
| 99 | @newcomer = GDBM.open(path('newcomer.db')) |
|---|
| 100 | @newcomers ||= [] |
|---|
| 101 | |
|---|
| 102 | def @manager.enter_away; end |
|---|
| 103 | def @manager.leave_away; end |
|---|
| 104 | |
|---|
| 105 | @identmutex = Mutex.new |
|---|
| 106 | @newcomermutex = Mutex.new |
|---|
| 107 | |
|---|
| 108 | @cheebot = {} |
|---|
| 109 | @cheelock = Mutex.new |
|---|
| 110 | |
|---|
| 111 | @nexttime = nexttime |
|---|
| 112 | @nodan_thread = {}; |
|---|
| 113 | |
|---|
| 114 | loadconfig |
|---|
| 115 | end |
|---|
| 116 | |
|---|
| 117 | def create_file(name) |
|---|
| 118 | begin |
|---|
| 119 | File.stat(path(name)) |
|---|
| 120 | rescue |
|---|
| 121 | File.open(path(name), 'w') |
|---|
| 122 | end |
|---|
| 123 | end |
|---|
| 124 | |
|---|
| 125 | def bot_destruct |
|---|
| 126 | @newcomer.close rescue nil |
|---|
| 127 | @feeder.each { |th| th.kill } |
|---|
| 128 | savedata rescue nil |
|---|
| 129 | end |
|---|
| 130 | |
|---|
| 131 | def loadconfig |
|---|
| 132 | create_file('entities.csv') |
|---|
| 133 | Thread.exclusive do |
|---|
| 134 | @entity = {} |
|---|
| 135 | @entity_rev = {} |
|---|
| 136 | CSV.open(path('entities.csv'), 'r') do |line| |
|---|
| 137 | name, cp = line.to_a |
|---|
| 138 | @entity[name] = ucstoutf8(cp.to_i) |
|---|
| 139 | @entity_rev[cp.to_i] = name |
|---|
| 140 | end |
|---|
| 141 | end |
|---|
| 142 | GC.start |
|---|
| 143 | end |
|---|
| 144 | |
|---|
| 145 | def loaddata |
|---|
| 146 | begin |
|---|
| 147 | hash = Marshal.load(File.read(path('data'))) |
|---|
| 148 | MARSHAL_VARS.each { |var| instance_variable_set(var, hash[var]) } |
|---|
| 149 | |
|---|
| 150 | @kick_data = Marshal.load(File.read(path('kick_data'))) |
|---|
| 151 | rescue |
|---|
| 152 | # ok |
|---|
| 153 | end |
|---|
| 154 | end |
|---|
| 155 | |
|---|
| 156 | def savedata |
|---|
| 157 | hash = {} |
|---|
| 158 | MARSHAL_VARS.each { |var| hash[var] = instance_variable_get(var) } |
|---|
| 159 | |
|---|
| 160 | data = Marshal.dump(hash) |
|---|
| 161 | open(path('data'), "w") { |f| f.write(data) } |
|---|
| 162 | |
|---|
| 163 | savekickkan |
|---|
| 164 | end |
|---|
| 165 | |
|---|
| 166 | def savekickkan |
|---|
| 167 | open(path('kick_data'), "w") { |f| f.write(Marshal.dump(@kick_data)) } |
|---|
| 168 | end |
|---|
| 169 | |
|---|
| 170 | def hatenakickkan |
|---|
| 171 | return unless @kick[:hatena] |
|---|
| 172 | api = Hatena::API::Graph.new(@kick[:hatena][:username], @kick[:hatena][:password]) |
|---|
| 173 | api.post(@kick[:hatena][:graph], Time.now, @kick_data["#{Time.now.strftime '%Y-%m-%d'}"]) |
|---|
| 174 | end |
|---|
| 175 | |
|---|
| 176 | def path(fname) |
|---|
| 177 | File.expand_path(fname, @path) |
|---|
| 178 | end |
|---|
| 179 | |
|---|
| 180 | def num3(i) |
|---|
| 181 | f = i.to_f |
|---|
| 182 | i = i.to_i |
|---|
| 183 | if f != i |
|---|
| 184 | us = sprintf("%.2f", f - i).sub(/^0/, '') |
|---|
| 185 | end |
|---|
| 186 | i.to_s.reverse.scan(/.{1,3}/).join(',').reverse + us.to_s |
|---|
| 187 | end |
|---|
| 188 | |
|---|
| 189 | def identify |
|---|
| 190 | @identmutex.synchronize do |
|---|
| 191 | return if @lastident && @lastident >= (Time.now - 60) |
|---|
| 192 | password = File.read(path('password')).strip |
|---|
| 193 | nick = @config.config[:nick] |
|---|
| 194 | if @state.nick != nick |
|---|
| 195 | send_privmsg("NickServ", "ghost #{nick} #{password}") |
|---|
| 196 | send_msg(Cmd.nick(nick)) |
|---|
| 197 | end |
|---|
| 198 | send_privmsg("NickServ", "identify #{password}") |
|---|
| 199 | @lastident = Time.now |
|---|
| 200 | end |
|---|
| 201 | end |
|---|
| 202 | |
|---|
| 203 | def bugcheck(ch, e, bt = true) |
|---|
| 204 | mesg = e.message.gsub(/\n/, '/') |
|---|
| 205 | if mesg.empty? |
|---|
| 206 | mesg = (e.class == RuntimeError) ? "unhandled exception" : e.class.to_s |
|---|
| 207 | end |
|---|
| 208 | mesg = "#{e.class}: #{mesg}" |
|---|
| 209 | if bt |
|---|
| 210 | mesg = "[Bug] #{mesg} at " |
|---|
| 211 | mesg << e.backtrace.find { |t| /^#{Regexp.quote(__FILE__)}:/o =~ t } |
|---|
| 212 | end |
|---|
| 213 | notice(ch, mesg) |
|---|
| 214 | end |
|---|
| 215 | |
|---|
| 216 | def get_deflang(ch) |
|---|
| 217 | @config.ch_config(ch, :default_language) || DEFAULT_LANGUAGE |
|---|
| 218 | end |
|---|
| 219 | |
|---|
| 220 | def on_timer(tm) |
|---|
| 221 | if tm >= @nexttime |
|---|
| 222 | savedata |
|---|
| 223 | @nexttime = nexttime |
|---|
| 224 | end |
|---|
| 225 | end |
|---|
| 226 | |
|---|
| 227 | def nexttime |
|---|
| 228 | Time.now + SAVE_INTERVAL |
|---|
| 229 | end |
|---|
| 230 | |
|---|
| 231 | def on_join(prefix, ch) |
|---|
| 232 | if @config.channel_info[ch] && prefix.nick == @state.nick |
|---|
| 233 | identify |
|---|
| 234 | elsif ch == @hello[:channel] && prefix.nick == @hello[:nickname] |
|---|
| 235 | notice(ch, "#{prefix.nick}たん こんにちわー") |
|---|
| 236 | elsif @admin_re =~ prefix.nick |
|---|
| 237 | send_msg(Cmd.mode(ch, '+oo', prefix.nick)) |
|---|
| 238 | end |
|---|
| 239 | |
|---|
| 240 | @ban_kick.each do |conf| |
|---|
| 241 | if conf[:channel] == ch |
|---|
| 242 | if conf[:nickname] =~ prefix.nick |
|---|
| 243 | send_msg(Cmd.kick(ch, prefix.nick, l2c(ch, "miss"))) |
|---|
| 244 | end |
|---|
| 245 | end |
|---|
| 246 | end |
|---|
| 247 | end |
|---|
| 248 | |
|---|
| 249 | def on_rpl_endofnames(prefix, nick, ch, mesg) |
|---|
| 250 | lonelycheck(ch) |
|---|
| 251 | end |
|---|
| 252 | |
|---|
| 253 | def on_part(prefix, ch, reason) |
|---|
| 254 | lonelycheck(ch) if prefix.nick != @state.nick |
|---|
| 255 | if ch == @hello[:channel] && prefix.nick == @hello[:nickname] |
|---|
| 256 | notice(ch, "#{prefix.nick}たん さよなら。。。") |
|---|
| 257 | end |
|---|
| 258 | end |
|---|
| 259 | |
|---|
| 260 | def on_nick(prefix, new) |
|---|
| 261 | if prefix.nick == @kick[:nickname] |
|---|
| 262 | @kick[:nickname] = new |
|---|
| 263 | end |
|---|
| 264 | |
|---|
| 265 | @ban_kick.each do |conf| |
|---|
| 266 | if conf[:nickname] =~ new |
|---|
| 267 | send_msg(Cmd.kick(conf[:channel], new, l2c(conf[:channel], "miss"))) |
|---|
| 268 | end |
|---|
| 269 | end |
|---|
| 270 | end |
|---|
| 271 | |
|---|
| 272 | def on_quit(prefix, reason) |
|---|
| 273 | if prefix.nick != @state.nick |
|---|
| 274 | @state.channels.each { |ch| lonelycheck(ch) } |
|---|
| 275 | end |
|---|
| 276 | end |
|---|
| 277 | |
|---|
| 278 | def lonelycheck(ch) |
|---|
| 279 | if @state.channel_users(ch).size <= 1 && !@config.channel_info[ch] |
|---|
| 280 | send_msg(Cmd.part(ch, "lonely channel...")) |
|---|
| 281 | end |
|---|
| 282 | end |
|---|
| 283 | |
|---|
| 284 | def on_mode(prefix, ch, mode, *users) |
|---|
| 285 | modes = mode.split(//) |
|---|
| 286 | case modes.shift |
|---|
| 287 | when '+' |
|---|
| 288 | nick, mode = users.zip(modes).assoc(@state.nick) |
|---|
| 289 | if mode == 'o' |
|---|
| 290 | @naruto = true |
|---|
| 291 | end |
|---|
| 292 | when '-' |
|---|
| 293 | if mode == 'o' |
|---|
| 294 | @naruto = false |
|---|
| 295 | end |
|---|
| 296 | end |
|---|
| 297 | end |
|---|
| 298 | |
|---|
| 299 | def on_kick(prefix, ch, kicked, reason) |
|---|
| 300 | if kicked == @state.nick && @config.channel_info[ch] |
|---|
| 301 | sleep 1 |
|---|
| 302 | send_msg(Cmd.join(ch)) |
|---|
| 303 | notice(ch, "Oops!") |
|---|
| 304 | end |
|---|
| 305 | end |
|---|
| 306 | |
|---|
| 307 | def on_invite(prefix, nick, ch) |
|---|
| 308 | if nick == @state.nick |
|---|
| 309 | send_msg(Cmd.join(ch)) |
|---|
| 310 | send_notice(ch, "Hello #{prefix.nick}, I'm linky IRCbot.") |
|---|
| 311 | send_notice(ch, "To let me part from this channel, please kick me :)") |
|---|
| 312 | end |
|---|
| 313 | end |
|---|
| 314 | |
|---|
| 315 | def charset(ch) |
|---|
| 316 | @charset[ch] || @config.ch_config(ch, :charset) || 'ISO-2022-JP' |
|---|
| 317 | end |
|---|
| 318 | |
|---|
| 319 | def utf8toucs(str) |
|---|
| 320 | str.unpack('U')[0] |
|---|
| 321 | rescue ArgumentError, RangeError |
|---|
| 322 | nil |
|---|
| 323 | end |
|---|
| 324 | |
|---|
| 325 | def ucstoutf8(chr) |
|---|
| 326 | [ chr ].pack('U*') |
|---|
| 327 | rescue ArgumentError, RangeError |
|---|
| 328 | nil |
|---|
| 329 | end |
|---|
| 330 | |
|---|
| 331 | def safe_iconv(to, from, str, ech = "?") |
|---|
| 332 | if /^ISO-?2022-?JP$/ =~ from |
|---|
| 333 | str = jis2sjis(str) |
|---|
| 334 | from = "cp932" |
|---|
| 335 | end |
|---|
| 336 | if /^ISO-?2022-?JP$/ =~ to |
|---|
| 337 | to = "cp932" |
|---|
| 338 | tojis = true |
|---|
| 339 | end |
|---|
| 340 | iconv = Iconv.new(to, from) |
|---|
| 341 | result = "" |
|---|
| 342 | offset = 0 |
|---|
| 343 | block = block_given? |
|---|
| 344 | |
|---|
| 345 | utf8 = (/^UTF-?8$/i =~ from) |
|---|
| 346 | begin |
|---|
| 347 | result << iconv.iconv(str, offset) |
|---|
| 348 | rescue Iconv::IllegalSequence => e |
|---|
| 349 | return unless ech || block |
|---|
| 350 | str = e.failed |
|---|
| 351 | if utf8 |
|---|
| 352 | if utf8toucs(str) |
|---|
| 353 | offset = /^./u.match(str)[0].size |
|---|
| 354 | else |
|---|
| 355 | offset = 1 |
|---|
| 356 | until utf8toucs(str[offset, 6]) |
|---|
| 357 | offset += 1 |
|---|
| 358 | end |
|---|
| 359 | end |
|---|
| 360 | else |
|---|
| 361 | offset = 1 |
|---|
| 362 | end |
|---|
| 363 | ech = yield(str[0, offset]) if block |
|---|
| 364 | result << e.success << iconv.iconv(ech) |
|---|
| 365 | retry |
|---|
| 366 | rescue Iconv::Failure => e |
|---|
| 367 | return unless ech |
|---|
| 368 | result << e.success |
|---|
| 369 | end |
|---|
| 370 | result << iconv.close |
|---|
| 371 | if tojis |
|---|
| 372 | sjis2jis(result) |
|---|
| 373 | else |
|---|
| 374 | result |
|---|
| 375 | end |
|---|
| 376 | end |
|---|
| 377 | |
|---|
| 378 | def sjis2jis(str) |
|---|
| 379 | result = "" |
|---|
| 380 | str.split(/([\x00-\x0F])/s).each_slice(2) { |s1, s2| |
|---|
| 381 | result << NKF.nkf('-SjX', s1) |
|---|
| 382 | result << s2 if s2 |
|---|
| 383 | } |
|---|
| 384 | result |
|---|
| 385 | end |
|---|
| 386 | |
|---|
| 387 | def jis2sjis(str) |
|---|
| 388 | result = "" |
|---|
| 389 | str.split(/([\x00-\x0F])/n).each_slice(2) { |s1, s2| |
|---|
| 390 | result << NKF.nkf('-JsX', s1) |
|---|
| 391 | result << s2 if s2 |
|---|
| 392 | } |
|---|
| 393 | result |
|---|
| 394 | end |
|---|
| 395 | |
|---|
| 396 | def c2l(ch, str, ech = nil, &block) |
|---|
| 397 | safe_iconv('UTF-8', charset(ch), str, ech, &block) |
|---|
| 398 | end |
|---|
| 399 | |
|---|
| 400 | def l2c(ch, str, ech = nil, &block) |
|---|
| 401 | safe_iconv(charset(ch), 'UTF-8', str, ech, &block) |
|---|
| 402 | end |
|---|
| 403 | |
|---|
| 404 | def notice(ch, mesg, ech = "?", &block) |
|---|
| 405 | return unless ch |
|---|
| 406 | mesg = l2c(ch, mesg, ech, &block) |
|---|
| 407 | if @privmsg |
|---|
| 408 | send_privmsg(ch, mesg) unless mesg.empty? |
|---|
| 409 | else |
|---|
| 410 | send_notice(ch, mesg) unless mesg.empty? |
|---|
| 411 | end |
|---|
| 412 | end |
|---|
| 413 | |
|---|
| 414 | def unescapeHTML(str) |
|---|
| 415 | str.gsub(/&(?:#(?:x([\dA-Fa-f]+)|(\d+))|([A-Za-z\d]+));/u) { |
|---|
| 416 | if $3 |
|---|
| 417 | @entity[$3] || $& |
|---|
| 418 | else |
|---|
| 419 | ucstoutf8($1 ? $1.hex : $2.to_i) || $& |
|---|
| 420 | end |
|---|
| 421 | } |
|---|
| 422 | end |
|---|
| 423 | |
|---|
| 424 | def unescape(str) |
|---|
| 425 | str.gsub(/%(?:u([\dA-Fa-f]{4})|([\dA-Fa-f]{2}))/u) { |
|---|
| 426 | $1 ? ucstoutf8($1.hex) : $2.hex.chr |
|---|
| 427 | } |
|---|
| 428 | end |
|---|
| 429 | |
|---|
| 430 | def escape(str, prefix = '%') |
|---|
| 431 | str = str.gsub(/[^ A-Za-z0-9_.\-:\/]/n) { prefix + ("%02X" % $&[0]) } |
|---|
| 432 | str.tr(' ', '+') |
|---|
| 433 | end |
|---|
| 434 | |
|---|
| 435 | def unescape(str, prefix = '%') |
|---|
| 436 | # str = str.tr('+', ' ') |
|---|
| 437 | str.gsub(/#{Regexp.quote(prefix)}([\dA-Fa-f]{2})/) { $1.hex.chr } |
|---|
| 438 | end |
|---|
| 439 | |
|---|
| 440 | def kick_kan(from, msg, by = '', line = '') |
|---|
| 441 | ch = @kick[:channel] |
|---|
| 442 | nick = @kick[:nickname] |
|---|
| 443 | |
|---|
| 444 | unless @naruto || @state.channel_users(ch).index(nick) |
|---|
| 445 | notice(@kick[:channel], "なると無いし#{nick}さん居ないしkickできない><") |
|---|
| 446 | return |
|---|
| 447 | end |
|---|
| 448 | unless @naruto |
|---|
| 449 | notice(@kick[:channel], "なるとなくて#{nick}さんてkickできない><") |
|---|
| 450 | return |
|---|
| 451 | end |
|---|
| 452 | unless @state.channel_users(ch).index(nick) |
|---|
| 453 | notice(@kick[:channel], "#{nick}さんいなくてkickできない><") |
|---|
| 454 | return |
|---|
| 455 | end |
|---|
| 456 | |
|---|
| 457 | @kick_data[:total] += 1 |
|---|
| 458 | @kick_data["#{Time.now.strftime '%Y-%m-%d'}"] = 0 unless @kick_data["#{Time.now.strftime '%Y-%m-%d'}"] |
|---|
| 459 | @kick_data["#{Time.now.strftime '%Y-%m-%d'}"] += 1 |
|---|
| 460 | if from != ch && line.size > 0 |
|---|
| 461 | notice(ch, "<#{by}@#{from}> #{line}") |
|---|
| 462 | elsif from != ch |
|---|
| 463 | notice(ch, "#{from}でフラグ成立><") |
|---|
| 464 | end |
|---|
| 465 | send_msg(Cmd.kick(ch, nick, l2c(ch, "#{msg} 本日#{@kick_data["#{Time.now.strftime '%Y-%m-%d'}"]}回目 (累計#{@kick_data[:total].to_s}回目)"))) |
|---|
| 466 | send_msg(Cmd.invite(nick, ch)) |
|---|
| 467 | hatenakickkan |
|---|
| 468 | savekickkan |
|---|
| 469 | end |
|---|
| 470 | |
|---|
| 471 | def kick_chain(ch, kick, mesg) |
|---|
| 472 | name = kick[:name] |
|---|
| 473 | @kick_chain_stack[name][ch] = '' unless @kick_chain_stack[name][ch] |
|---|
| 474 | re = Regexp.new("^[#{name}]$") |
|---|
| 475 | if (mesg == name[0].chr || (name[0] == '/' && /\// =~ mesg)) && @kick_chain_stack[name][ch].size == 0 |
|---|
| 476 | @kick_chain_stack[name][ch] = name[0].chr |
|---|
| 477 | elsif re =~ mesg && @kick_chain_stack[name][ch].size > 0 |
|---|
| 478 | @kick_chain_stack[name][ch] += mesg |
|---|
| 479 | re = Regexp.new("^#{@kick_chain_stack[name][ch]}") |
|---|
| 480 | if @kick_chain_stack[name][ch] == name |
|---|
| 481 | kick_kan(ch, kick[:ok]) |
|---|
| 482 | @kick_chain_stack[name][ch] = '' |
|---|
| 483 | elsif ! name.index(@kick_chain_stack[name][ch]) |
|---|
| 484 | notice(ch, kick[:ng]) if kick[:ng].size > 0 |
|---|
| 485 | @kick_chain_stack[name][ch] = '' |
|---|
| 486 | end |
|---|
| 487 | else |
|---|
| 488 | @kick_chain_stack[name][ch] = '' |
|---|
| 489 | end |
|---|
| 490 | end |
|---|
| 491 | |
|---|
| 492 | def on_privmsg(prefix, ch, mesg) |
|---|
| 493 | return if prefix.nick == @state.nick |
|---|
| 494 | |
|---|
| 495 | return if @ignore_user.include?(prefix.nick) |
|---|
| 496 | |
|---|
| 497 | # if fnick = @config.ch_config(ch, :feed_nick) |
|---|
| 498 | # feed_mesg(prefix, ch, mesg) if fnick === prefix.to_s |
|---|
| 499 | # return |
|---|
| 500 | # end |
|---|
| 501 | if snick = @config.ch_config(ch, :status_nick) |
|---|
| 502 | re = @config.ch_config(ch, :status_re) |
|---|
| 503 | if snick === prefix.to_s |
|---|
| 504 | ary = @last_status ||= [] |
|---|
| 505 | re.each_with_index do |re, i| |
|---|
| 506 | if re === mesg |
|---|
| 507 | ary[i] = Time.now.strftime("(%m/%d %H:%M) ") + mesg |
|---|
| 508 | return |
|---|
| 509 | end |
|---|
| 510 | end |
|---|
| 511 | end |
|---|
| 512 | return |
|---|
| 513 | end |
|---|
| 514 | ch = prefix.nick if ch == @state.nick |
|---|
| 515 | |
|---|
| 516 | # assume UTF-8 |
|---|
| 517 | unless /^!charset/ui =~ mesg |
|---|
| 518 | mesg = c2l(ch, mesg) || mesg |
|---|
| 519 | end |
|---|
| 520 | |
|---|
| 521 | mesg_s = mesg.gsub(/[\s ._\-=._‐−=]/u, '') |
|---|
| 522 | |
|---|
| 523 | @kick_chains.each do |kick| |
|---|
| 524 | kick_chain(ch, kick, mesg_s) |
|---|
| 525 | end |
|---|
| 526 | |
|---|
| 527 | #DNBK |
|---|
| 528 | if @dnbk[:re] =~ mesg_s && prefix.nick == @kick[:nickname] |
|---|
| 529 | notice(@kick[:nickname], @dnbk[:msg]) |
|---|
| 530 | kick_kan(ch, @dnbk[:kick], prefix.nick, mesg) |
|---|
| 531 | return |
|---|
| 532 | end |
|---|
| 533 | if @hiita[:re] =~ mesg_s && /^[^!]/ =~ mesg |
|---|
| 534 | kick_kan(ch, @hiita[:kick], prefix.nick, mesg) |
|---|
| 535 | return |
|---|
| 536 | end |
|---|
| 537 | |
|---|
| 538 | if !@nodan_thread[ch] && /^[^!]/ =~ mesg |
|---|
| 539 | @regexp_list.each do |pat| |
|---|
| 540 | if pat[:re] =~ mesg |
|---|
| 541 | if pat[:eval] |
|---|
| 542 | eval pat[:eval] |
|---|
| 543 | else |
|---|
| 544 | notice(ch, pat[:msg]) |
|---|
| 545 | end |
|---|
| 546 | return |
|---|
| 547 | end |
|---|
| 548 | end |
|---|
| 549 | |
|---|
| 550 | @regexp_user.to_a.sort{ |a, b| b[1].source.length <=> a[1].source.length }.each do |data| |
|---|
| 551 | msg, re = data |
|---|
| 552 | if (list = mesg.match(re)) |
|---|
| 553 | send = msg.to_s |
|---|
| 554 | if list.size > 0 && /^[^-]/ =~ mesg |
|---|
| 555 | i = 0 |
|---|
| 556 | list.to_a.each do |word| |
|---|
| 557 | if word |
|---|
| 558 | send = send.gsub("$#{i}", word) |
|---|
| 559 | i += 1 |
|---|
| 560 | end |
|---|
| 561 | end |
|---|
| 562 | end |
|---|
| 563 | notice(ch, send) |
|---|
| 564 | return |
|---|
| 565 | end |
|---|
| 566 | end |
|---|
| 567 | end |
|---|
| 568 | |
|---|
| 569 | case mesg |
|---|
| 570 | when /^(\S+?)[:,]/u |
|---|
| 571 | case $1.downcase |
|---|
| 572 | when @state.nick, @config.config[:user] |
|---|
| 573 | command = $' |
|---|
| 574 | end |
|---|
| 575 | when /^!/u |
|---|
| 576 | command = $' |
|---|
| 577 | end |
|---|
| 578 | if command |
|---|
| 579 | begin |
|---|
| 580 | command, *params = Shellwords.shellwords(command) |
|---|
| 581 | rescue => e |
|---|
| 582 | bugcheck(ch, e, false) |
|---|
| 583 | return |
|---|
| 584 | end |
|---|
| 585 | end |
|---|
| 586 | if command |
|---|
| 587 | command.sub!(/^!/, '') |
|---|
| 588 | meth = command.downcase |
|---|
| 589 | meth = :"command_#{meth}" |
|---|
| 590 | if respond_to?(meth) |
|---|
| 591 | send(meth, prefix, ch, *params) |
|---|
| 592 | return |
|---|
| 593 | end |
|---|
| 594 | if mesg[0] == ?! |
|---|
| 595 | ary = [ "is", "was", "has been", "had been", "will be", |
|---|
| 596 | "gonna be", "wanna be" ] |
|---|
| 597 | name = params[0] || command |
|---|
| 598 | mesg = [ name, ary[rand(ary.size)], smile ].join(' ') |
|---|
| 599 | else |
|---|
| 600 | mesg = ":o" |
|---|
| 601 | end |
|---|
| 602 | notice(ch, mesg) |
|---|
| 603 | return |
|---|
| 604 | end |
|---|
| 605 | |
|---|
| 606 | rescue Iconv::Failure => e |
|---|
| 607 | # ignore |
|---|
| 608 | rescue Exception => e |
|---|
| 609 | bugcheck(ch, e) |
|---|
| 610 | end |
|---|
| 611 | |
|---|
| 612 | def smile |
|---|
| 613 | @smile[rand(@smile.size)] |
|---|
| 614 | end |
|---|
| 615 | |
|---|
| 616 | def command_regexp(prefix, ch, *params) |
|---|
| 617 | if params.empty? |
|---|
| 618 | notice(ch, "regexp: #{@regexp_user.size} の登録があります。") |
|---|
| 619 | elsif params.size < 2 |
|---|
| 620 | msg = params.shift |
|---|
| 621 | if msg.sub!(/^-/, '') |
|---|
| 622 | @regexp_user.delete(msg) |
|---|
| 623 | notice(ch, "regexp: no #{msg}") |
|---|
| 624 | else |
|---|
| 625 | help_regexp(ch) |
|---|
| 626 | end |
|---|
| 627 | elsif params[0] == '-q' |
|---|
| 628 | msg = params[1]; |
|---|
| 629 | notice(ch, "regexp: #{@regexp_user[msg].source}") if @regexp_user[msg] |
|---|
| 630 | |
|---|
| 631 | elsif params[0] == '-qq' |
|---|
| 632 | mesg = params[1]; |
|---|
| 633 | @regexp_user.to_a.sort{ |a, b| b[1].source.length <=> a[1].source.length }.each do |data| |
|---|
| 634 | re_msg, re = data |
|---|
| 635 | if (list = mesg.match(re)) |
|---|
| 636 | send = re_msg.to_s |
|---|
| 637 | if list.size > 0 && /^[^-]/ =~ mesg |
|---|
| 638 | i = 0 |
|---|
| 639 | list.to_a.each do |word| |
|---|
| 640 | if word |
|---|
| 641 | send = send.gsub("$#{i}", word) |
|---|
| 642 | i += 1 |
|---|
| 643 | end |
|---|
| 644 | end |
|---|
| 645 | end |
|---|
| 646 | re_str = re.source |
|---|
| 647 | re_to = re_msg.to_s |
|---|
| 648 | notice(ch, "regexp re : #{re_str}") |
|---|
| 649 | notice(ch, "regexp to : #{re_to}") |
|---|
| 650 | notice(ch, "regexp msg: #{send}") |
|---|
| 651 | sleep 1 |
|---|
| 652 | end |
|---|
| 653 | end |
|---|
| 654 | |
|---|
| 655 | else |
|---|
| 656 | msg = params.shift |
|---|
| 657 | re = params.shift |
|---|
| 658 | |
|---|
| 659 | @regexp_user.store(msg, Regexp.new(re, Regexp::IGNORECASE)) |
|---|
| 660 | notice(ch, "regexp: use #{msg} #{re}") |
|---|
| 661 | savedata |
|---|
| 662 | end |
|---|
| 663 | end |
|---|
| 664 | def help_regexp(ch) |
|---|
| 665 | notice(ch, "usage: !regexp <メッセージ> <正規表現>") |
|---|
| 666 | notice(ch, "正規表現に一致する発言に反応してメッセージを喋ります") |
|---|
| 667 | end |
|---|
| 668 | |
|---|
| 669 | def command_nodan(prefix, ch, *params) |
|---|
| 670 | time = @nodan[:default] |
|---|
| 671 | if ! params.empty? |
|---|
| 672 | if params[0] == '-q' |
|---|
| 673 | notice(ch, "nodan") if @nodan_thread[ch] |
|---|
| 674 | return |
|---|
| 675 | elsif /^\d+$/ =~ params[0] |
|---|
| 676 | time = params[0] |
|---|
| 677 | end |
|---|
| 678 | end |
|---|
| 679 | time = time.to_i |
|---|
| 680 | |
|---|
| 681 | if @nodan_thread[ch] |
|---|
| 682 | @nodan_thread[ch].exit |
|---|
| 683 | @nodan_thread.delete(ch) |
|---|
| 684 | end |
|---|
| 685 | |
|---|
| 686 | @nodan_thread[ch] = Thread.start { |
|---|
| 687 | notice(ch, @nodan[:sleep]) |
|---|
| 688 | sleep time |
|---|
| 689 | notice(ch, @nodan[:walk]) |
|---|
| 690 | @nodan_thread.delete(ch) |
|---|
| 691 | } |
|---|
| 692 | end |
|---|
| 693 | |
|---|
| 694 | def command_time(prefix, ch, *params) |
|---|
| 695 | begin |
|---|
| 696 | now = DateTime.parse(params[0]) |
|---|
| 697 | params.shift |
|---|
| 698 | rescue |
|---|
| 699 | if /(\d+)年(\d+)月(\d+)日\D+(\d+):(\d+)(?::(\d+))?\s*(?:\((\w+)\))?/u =~ params[0] |
|---|
| 700 | now = DateTime.new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, |
|---|
| 701 | Rational(Date.zone_to_diff($7 || "UTC") || 0, 86400)) |
|---|
| 702 | params.shift |
|---|
| 703 | end |
|---|
| 704 | now ||= DateTime.now |
|---|
| 705 | end |
|---|
| 706 | params[0] = 'JST' if params.empty? |
|---|
| 707 | err = [] |
|---|
| 708 | params.each do |zone| |
|---|
| 709 | if offset = Date.zone_to_diff(zone) |
|---|
| 710 | time = now.new_offset(Rational(offset, 86400)) |
|---|
| 711 | notice(ch, "ω・)っ[#{time.strftime('%Y-%m-%d %T %z')}]") |
|---|
| 712 | else |
|---|
| 713 | err << zone |
|---|
| 714 | end |
|---|
| 715 | end |
|---|
| 716 | unless err.empty? |
|---|
| 717 | notice(ch, "Unknown time zone(s): " + err.join(' ')) |
|---|
| 718 | end |
|---|
| 719 | end |
|---|
| 720 | def help_time(ch) |
|---|
| 721 | notice(ch, "usage: !time [時刻] [タイムゾーン]...") |
|---|
| 722 | notice(ch, "時刻が指定された場合その時刻を、省略された場合は現在時刻を表示します。\n") |
|---|
| 723 | notice(ch, "タイムゾーンを指定するとそのタイムゾーンの時刻で表示します。タイムゾーンが省略されるとJSTで表示します。") |
|---|
| 724 | notice(ch, " alias: !時刻, !時間") |
|---|
| 725 | end |
|---|
| 726 | |
|---|
| 727 | def command_kan(prefix, ch, *params) |
|---|
| 728 | if ! params.empty? && @admin_re =~ prefix.nick |
|---|
| 729 | @kick[:nickname] = params.shift |
|---|
| 730 | @kick[:channel] = params.shift unless params.empty? |
|---|
| 731 | end |
|---|
| 732 | notice(ch, "#{@kick[:nickname]}@#{@kick[:channel]}さんの累計kick回数は#{@kick_data[:total].to_s}回です (本日#{@kick_data["#{Time.now.strftime '%Y-%m-%d'}"]}回目)") |
|---|
| 733 | end |
|---|
| 734 | |
|---|
| 735 | def command_uptime(prefix, ch, *params) |
|---|
| 736 | notice(ch, `uptime`.strip) |
|---|
| 737 | end |
|---|
| 738 | def help_uptime(ch) |
|---|
| 739 | notice(ch, "usage: !uptime") |
|---|
| 740 | notice(ch, "OppaiSanの動いているサーバのuptimeを回答します。") |
|---|
| 741 | end |
|---|
| 742 | |
|---|
| 743 | def command_echo(prefix, ch, *params) |
|---|
| 744 | notice(ch, params.join(' ')) |
|---|
| 745 | end |
|---|
| 746 | def help_echo(ch) |
|---|
| 747 | notice(ch, "usage: !echo <文字列>...") |
|---|
| 748 | notice(ch, "文字列をオウム返しします。") |
|---|
| 749 | end |
|---|
| 750 | |
|---|
| 751 | def command_unicode(prefix, ch, *params) |
|---|
| 752 | s = unescapeHTML(unescape(params.join(' '))) |
|---|
| 753 | s = safe_iconv('UCS-4BE', 'UTF-8', s, "\0") |
|---|
| 754 | s = s.unpack('N*').collect { |c| |
|---|
| 755 | c == 0 ? "??" : sprintf(c > 0xFFFF ? "U+%08X" : "U+%04X", c) |
|---|
| 756 | } |
|---|
| 757 | notice(ch, s.join(' ')) |
|---|
| 758 | end |
|---|
| 759 | def help_unicode(ch) |
|---|
| 760 | notice(ch, "usage: !unicode <文字列>...") |
|---|
| 761 | notice(ch, "URIエスケープか実体参照で書かれた文字列をUnicodeのコードポイントに直して返します。") |
|---|
| 762 | end |
|---|
| 763 | |
|---|
| 764 | def command_jis(prefix, ch, *params) |
|---|
| 765 | s = unescapeHTML(unescape(params.join)) |
|---|
| 766 | |
|---|
| 767 | str = [] |
|---|
| 768 | s.scan(/./u) do |c| |
|---|
| 769 | c = safe_iconv('ISO-2022-JP', 'UTF-8', c) |
|---|
| 770 | if c.size == 8 |
|---|
| 771 | str << sprintf("%d区%d点", c[3] - 0x20, c[4] - 0x20) |
|---|
| 772 | else |
|---|
| 773 | str << "?" |
|---|
| 774 | end |
|---|
| 775 | end |
|---|
| 776 | |
|---|
| 777 | notice(ch, str.join(' ')) |
|---|
| 778 | end |
|---|
| 779 | def help_jis(ch) |
|---|
| 780 | notice(ch, "usage: !jis <文字>...") |
|---|
| 781 | notice(ch, "各文字をJISの区点に変換した結果を返します。ISO-2022-JPに変換できない文字は ? になります。") |
|---|
| 782 | end |
|---|
| 783 | |
|---|
| 784 | def command_convert(prefix, ch, *params) |
|---|
| 785 | unless charset = params.shift |
|---|
| 786 | notice(ch, "usage: !convert <charset> <文字列>...") |
|---|
| 787 | return |
|---|
| 788 | end |
|---|
| 789 | begin |
|---|
| 790 | Iconv.new('UTF-8', charset) |
|---|
| 791 | rescue |
|---|
| 792 | notice(ch, "iconv: can't handle #{charset}") |
|---|
| 793 | return |
|---|
| 794 | end |
|---|
| 795 | s = unescapeHTML(unescape(params.join(' '))) |
|---|
| 796 | s = safe_iconv(charset, 'UTF-8', s) { |e| |
|---|
| 797 | if e = utf8toucs(e) |
|---|
| 798 | (r = @entity_rev[e]) ? "&#{r};" : "&\##{e};" |
|---|
| 799 | else |
|---|
| 800 | "?" |
|---|
| 801 | end |
|---|
| 802 | } |
|---|
| 803 | notice(ch, safe_iconv('UTF-8', charset, s)) |
|---|
| 804 | end |
|---|
| 805 | def help_convert(ch) |
|---|
| 806 | notice(ch, "usage: !convert <charset> <文字列>...") |
|---|
| 807 | notice(ch, "文字列をcharsetに変換した結果を返します。変換できない文字は実体参照で表されます。") |
|---|
| 808 | notice(ch, "変換結果は更に現在のチャンネルのcharsetに変換されるので、UTF-8の状態で実行することを推奨します。(コマンド !charset を参照)") |
|---|
| 809 | end |
|---|
| 810 | |
|---|
| 811 | def command_iconv(prefix, ch, *params) |
|---|
| 812 | unless charset = params.shift |
|---|
| 813 | notice(ch, "usage: !iconv <charset> <文字列>...") |
|---|
| 814 | return |
|---|
| 815 | end |
|---|
| 816 | begin |
|---|
| 817 | Iconv.new('UTF-8', charset) |
|---|
| 818 | rescue |
|---|
| 819 | notice(ch, "iconv: can't handle #{charset}") |
|---|
| 820 | return |
|---|
| 821 | end |
|---|
| 822 | r = [] |
|---|
| 823 | s = unescapeHTML(unescape(params.join(' '))) |
|---|
| 824 | s.scan(/./u) do |c| |
|---|
| 825 | c = safe_iconv(charset, 'UTF-8', c, "") |
|---|
| 826 | if c == "" |
|---|
| 827 | r << "?" |
|---|
| 828 | elsif c.size == 1 && c[0] >= ?! && c[0] <= 0x7E |
|---|
| 829 | r << c |
|---|
| 830 | else |
|---|
| 831 | c = c.unpack('C*').collect { |c| sprintf("%02X", c) } |
|---|
| 832 | r << ('(' + c.join + ')') |
|---|
| 833 | end |
|---|
| 834 | end |
|---|
| 835 | notice(ch, r.join(' ')) |
|---|
| 836 | end |
|---|
| 837 | |
|---|
| 838 | def command_calc(prefix, ch, *params) |
|---|
| 839 | q = params.join(' ').sub(/[==]?$/u, '=') |
|---|
| 840 | lang = (/[\x80-\xFF]/n =~ q ? "ja" : "en") |
|---|
| 841 | uri = "/search?hl=" + lang + "&ie=UTF-8&oe=UTF-8&q=" + escape(q) |
|---|
| 842 | Thread.new { |
|---|
| 843 | begin |
|---|
| 844 | resp = Timeout.timeout(10) { |
|---|
| 845 | Net::HTTP.start("www.google.com") { |http| |
|---|
| 846 | http.request_get(uri) |
|---|
| 847 | } |
|---|
| 848 | } |
|---|
| 849 | if resp.code != "200" |
|---|
| 850 | notice(ch, "#{resp.code} #{resp.message}") |
|---|
| 851 | return |
|---|
| 852 | end |
|---|
| 853 | body = resp.body |
|---|
| 854 | if %r[/images/calc_img\.gif] =~ body |
|---|
| 855 | result = $'.split('</td>', 4)[2].to_s |
|---|
| 856 | result.gsub!(/<sup>([^<]+)<\/sup>/, '^(\1)') |
|---|
| 857 | result.gsub!(/<sub>([^<]+)<\/sub>/, '_(\1)') |
|---|
| 858 | result.gsub!(/<[^>]*>/, '') |
|---|
| 859 | result.strip! |
|---|
| 860 | end |
|---|
| 861 | if result && !result.empty? |
|---|
| 862 | notice(ch, "[#{unescapeHTML(result)}]") |
|---|
| 863 | else |
|---|
| 864 | wo = (rand(10) == 0 ? "を、" : "が") |
|---|
| 865 | notice(ch, "答え#{wo}見つかりません。") |
|---|
| 866 | end |
|---|
| 867 | rescue Exception => e |
|---|
| 868 | bugcheck(ch, e, false) |
|---|
| 869 | end |
|---|
| 870 | } |
|---|
| 871 | end |
|---|
| 872 | def help_calc(ch) |
|---|
| 873 | notice(ch, "usage: !calc <式>") |
|---|
| 874 | notice(ch, "式を計算します。どこかで見たような結果が出ます。") |
|---|
| 875 | end |
|---|
| 876 | |
|---|
| 877 | def command_ej(prefix, ch, *params) |
|---|
| 878 | translate(ch, params.join(' '), 'en|ja') |
|---|
| 879 | end |
|---|
| 880 | def help_ej(ch) |
|---|
| 881 | notice(ch, "usage: !ej <英文>") |
|---|
| 882 | notice(ch, "英文を和訳します。どこかで見たような結果が出ます。") |
|---|
| 883 | end |
|---|
| 884 | |
|---|
| 885 | def command_je(prefix, ch, *params) |
|---|
| 886 | translate(ch, params.join(' '), 'ja|en') |
|---|
| 887 | end |
|---|
| 888 | def help_je(ch) |
|---|
| 889 | notice(ch, "usage: !je <和文>") |
|---|
| 890 | notice(ch, "和文を英訳します。どこかで見たような結果が出ます。") |
|---|
| 891 | end |
|---|
| 892 | |
|---|
| 893 | def command_translate(prefix, ch, *params) |
|---|
| 894 | from, to, *params = *params |
|---|
| 895 | case lang = from + "|" + to |
|---|
| 896 | when /^en\|(de|es|fr|it|pt|ko|ja|zh)$/, |
|---|
| 897 | /^(de|es|fr|it|pt|ko|ja|zh)\|en$/, |
|---|
| 898 | 'de|fr', 'fr|de' |
|---|
| 899 | translate(ch, params.join(' '), lang) |
|---|
| 900 | else |
|---|
| 901 | notice(ch, "Payment Required") |
|---|
| 902 | end |
|---|
| 903 | end |
|---|
| 904 | def help_translate(ch) |
|---|
| 905 | notice(ch, "usage: !translate <from> <to> <文>") |
|---|
| 906 | notice(ch, "文を from から to へ翻訳します。どこかで見たような結果が出ます。") |
|---|
| 907 | end |
|---|
| 908 | |
|---|
| 909 | def translate(ch, text, lang) |
|---|
| 910 | uri = "/translate_t?ie=UTF-8&oe=UTF-8&hl=ja&langpair=" + escape(lang) + "&text=" + escape(text) |
|---|
| 911 | Thread.new { |
|---|
| 912 | begin |
|---|
| 913 | resp = Timeout.timeout(30) { |
|---|
| 914 | Net::HTTP.start("translate.google.com") { |http| |
|---|
| 915 | http.request_get(uri) |
|---|
| 916 | } |
|---|
| 917 | } |
|---|
| 918 | if resp.code != "200" |
|---|
| 919 | notice(ch, "#{resp.code} #{resp.message}") |
|---|
| 920 | return |
|---|
| 921 | end |
|---|
| 922 | body = resp.body |
|---|
| 923 | if %r[<textarea.*?>(.*?)</textarea>]m =~ body |
|---|
| 924 | result = $1.gsub(/[\x00-\x20]+/, ' ').strip |
|---|
| 925 | end |
|---|
| 926 | if result && !result.empty? |
|---|
| 927 | notice(ch, "#{unescapeHTML(result)}") |
|---|
| 928 | else |
|---|
| 929 | notice(ch, "見つかりません。") |
|---|
| 930 | end |
|---|
| 931 | rescue Exception => e |
|---|
| 932 | bugcheck(ch, e, false) |
|---|
| 933 | end |
|---|
| 934 | } |
|---|
| 935 | end |
|---|
| 936 | |
|---|
| 937 | def command_google(prefix, ch, *params) |
|---|
| 938 | uri = "http://www.google.com/search?q=" << escape(params.join(' ')) |
|---|
| 939 | notice(ch, uri) |
|---|
| 940 | end |
|---|
| 941 | def help_google(ch) |
|---|
| 942 | notice(ch, "usage: !google <検索語>...") |
|---|
| 943 | notice(ch, "Google検索するリンクを回答します。") |
|---|
| 944 | end |
|---|
| 945 | |
|---|
| 946 | %w[ tag when where who why how what dan ].each do |w| |
|---|
| 947 | module_eval(<<-"END", __FILE__, __LINE__+1) |
|---|
| 948 | def command_#{w}(prefix, ch, *params) |
|---|
| 949 | _5w1h_elt(prefix, ch, '#{w}', @#{w}, params) |
|---|
| 950 | end |
|---|
| 951 | def help_#{w}(ch) |
|---|
| 952 | _5w1h_help(ch, '#{w}') |
|---|
| 953 | end |
|---|
| 954 | END |
|---|
| 955 | end |
|---|
| 956 | |
|---|
| 957 | def _5w1h_elt(prefix, ch, name, var, params) |
|---|
| 958 | |
|---|
| 959 | if params.empty? && name == 'dan' |
|---|
| 960 | notice(ch, "Dan the " + setrand(@dan.active)) |
|---|
| 961 | return |
|---|
| 962 | elsif params.empty? || /^-c$/ =~ params[0] |
|---|
| 963 | notice(ch, "#{name}: #{var.active.size} の登録があります。") |
|---|
| 964 | elsif /^-qq?$/ =~ params[0] |
|---|
| 965 | verbose = (params.shift == "-qq") |
|---|
| 966 | params.each do |p| |
|---|
| 967 | if v = var.active[p] |
|---|
| 968 | reg = "が教えてくれました" |
|---|
| 969 | v = v[0] |
|---|
| 970 | elsif v = var.inactive[p] |
|---|
| 971 | reg = "に忘れさせられました" |
|---|
| 972 | v = v[-1] |
|---|
| 973 | else |
|---|
| 974 | next |
|---|
| 975 | end |
|---|
| 976 | ptime = v.time |
|---|
| 977 | nick = v.prefix |
|---|
| 978 | nick, = nick.scan(/^[^!]+/) unless verbose |
|---|
| 979 | notice(ch, "#{p} は #{ptime.asctime} に #{nick} #{reg}。") |
|---|
| 980 | sleep 1 |
|---|
| 981 | end |
|---|
| 982 | else |
|---|
| 983 | add = [] |
|---|
| 984 | del = [] |
|---|
| 985 | now = Time.now |
|---|
| 986 | nows = now.strftime('%Y-%m-%d %T') |
|---|
| 987 | open(path('5w1h.log'), 'a') do |f| |
|---|
| 988 | params = [ params.join(' ') ] if name == 'dan' |
|---|
| 989 | params.each do |p| |
|---|
| 990 | if p.sub!(/^-/, '') |
|---|
| 991 | if v = var.active.delete(p) |
|---|
| 992 | var.inactive[p] = v |
|---|
| 993 | v.push RegInfo.new(now, prefix.prefix, false) |
|---|
| 994 | end |
|---|
| 995 | del << p |
|---|
| 996 | m = "---" |
|---|
| 997 | else |
|---|
| 998 | if v = var.inactive.delete(p) |
|---|
| 999 | var.active[p] = v |
|---|
| 1000 | else |
|---|
| 1001 | v = var.active[p] ||= [] |
|---|
| 1002 | end |
|---|
| 1003 | v.push RegInfo.new(now, prefix.prefix, true) |
|---|
| 1004 | add << p |
|---|
| 1005 | m = "+++" |
|---|
| 1006 | end |
|---|
| 1007 | f.printf("%s (%s) [%s] %s %s\n", nows, prefix.prefix, name, m, p) |
|---|
| 1008 | end |
|---|
| 1009 | end |
|---|
| 1010 | notice(ch, "#{name}: no KCatch: #{del.join(', ')}") unless del.empty? |
|---|
| 1011 | notice(ch, "#{name}: use KCatch: #{add.join(', ')}") unless add.empty? |
|---|
| 1012 | savedata |
|---|
| 1013 | end |
|---|
| 1014 | end |
|---|
| 1015 | def _5w1h_help(ch, name) |
|---|
| 1016 | notice(ch, "usage: !#{name} [言葉]...") |
|---|
| 1017 | notice(ch, "5W1Hゲームで使う言葉を登録または削除します。言葉を省略すると登録されている言葉の数を発言します。") |
|---|
| 1018 | notice(ch, "言葉の前に - を付けると削除です。") |
|---|
| 1019 | end |
|---|
| 1020 | |
|---|
| 1021 | def setrand(hash) |
|---|
| 1022 | index = rand(hash.size) |
|---|
| 1023 | hash.each_with_index { |(key,), i| return key if i == index } |
|---|
| 1024 | "N/A" |
|---|
| 1025 | end |
|---|
| 1026 | |
|---|
| 1027 | def command_5w1h(prefix, ch, *params) |
|---|
| 1028 | ary = [ @when, @where, @who, @why, @how, @what ] |
|---|
| 1029 | aryq = [ @tag, @when, @where, @who, @why, @how, @what ] |
|---|
| 1030 | aryk = %w[ tag when where who why how what ] |
|---|
| 1031 | case params[0] |
|---|
| 1032 | when "-q" |
|---|
| 1033 | result = ary.inject(1) { |s, e| s * e.active.size } |
|---|
| 1034 | notice(ch, "現在 #{num3(result)} 通りの文章が出現します。(tag除く)") |
|---|
| 1035 | return |
|---|
| 1036 | when "-qq" |
|---|
| 1037 | mesg = aryk.zip(aryq).collect { |name, e| |
|---|
| 1038 | "#{name}: #{num3(e.active.size)}" |
|---|
| 1039 | } |
|---|
| 1040 | notice(ch, mesg.join(' ')) |
|---|
| 1041 | return |
|---|
| 1042 | when "-ratio" |
|---|
| 1043 | re = params[1..-1].uniq.collect { |p| Regexp.quote(p) }.join('|') |
|---|
| 1044 | re = Regexp.compile(re, Regexp::IGNORECASE) |
|---|
| 1045 | te = tr = 0 |
|---|
| 1046 | ratio = 1.0 |
|---|
| 1047 | mesg = aryk.zip(aryq).collect { |name, e| |
|---|
| 1048 | e = e.active |
|---|
| 1049 | r = e.keys.select { |s| re =~ s }.size |
|---|
| 1050 | te += e.size |
|---|
| 1051 | tr += r |
|---|
| 1052 | ratio *= 1.0 - (r.to_f / e.size) |
|---|
| 1053 | sprintf("%s: %d(%.1f%%)", name, r, r * 100.0 / e.size) |
|---|
| 1054 | } |
|---|
| 1055 | mesg << |
|---|
| 1056 | sprintf("(total): %d/%d(%.1f%%) ratio: %.1f%%", |
|---|
| 1057 | tr, te, tr * 100.0 / te, (1.0 - ratio) * 100.0) |
|---|
| 1058 | notice(ch, mesg.join(' ')) |
|---|
| 1059 | return |
|---|
| 1060 | end |
|---|
| 1061 | |
|---|
| 1062 | aryt = [] |
|---|
| 1063 | max = rand(3) |
|---|
| 1064 | max.times { |i| aryt.push setrand(@tag.active) } |
|---|
| 1065 | tag = aryt.uniq.join('][') |
|---|
| 1066 | tag = "[#{tag}] " if tag.size > 0 |
|---|
| 1067 | mesg = ary.collect { |e| setrand(e.active) }.join(' ') |
|---|
| 1068 | score = (rand(21) + rand(11) + rand(11) + 1) / 2 |
|---|
| 1069 | notice(ch, tag+mesg) |
|---|
| 1070 | end |
|---|
| 1071 | def help_5w1h(ch) |
|---|
| 1072 | notice(ch, "usage: !5w1h [-q]") |
|---|
| 1073 | notice(ch, "登録された言葉を元に文章を作って発言します。") |
|---|
| 1074 | notice(ch, " see also: !tag !when !where !who !why !how !what") |
|---|
| 1075 | end |
|---|
| 1076 | |
|---|
| 1077 | def command_charset(prefix, ch, *params) |
|---|
| 1078 | charset = params[0] |
|---|
| 1079 | if charset == "reset" |
|---|
| 1080 | @charset.delete(ch) |
|---|
| 1081 | elsif charset |
|---|
| 1082 | begin |
|---|
| 1083 | iconv = Iconv.new('UTF-8', charset) |
|---|
| 1084 | if Iconv.conv(charset, 'UTF-8', 'Unicode') != "Unicode" |
|---|
| 1085 | notice(ch, "#{charset}: ASCII非互換のキャラクタセットは使えません。") |
|---|
| 1086 | return |
|---|
| 1087 | end |
|---|
| 1088 | @charset[ch] = charset |
|---|
| 1089 | rescue => e |
|---|
| 1090 | notice(ch, "iconv: Can't handle #{charset}") |
|---|
| 1091 | return |
|---|
| 1092 | end |
|---|
| 1093 | end |
|---|
| 1094 | if ch[0] == ?# |
|---|
| 1095 | with = "チャンネル #{ch} では" |
|---|
| 1096 | else |
|---|
| 1097 | with = "#{ch} との対話には" |
|---|
| 1098 | end |
|---|
| 1099 | notice(ch, "#{with} #{charset(ch)} を使用します。") |
|---|
| 1100 | end |
|---|
| 1101 | def help_charset(ch) |
|---|
| 1102 | notice(ch, "usage: !charset [charset]") |
|---|
| 1103 | notice(ch, "現在のチャンネルまたは Nick と会話するときに使う charset を設定または回答します。") |
|---|
| 1104 | notice(ch, "設定されていない場合は ISO-2022-JP を使います。") |
|---|
| 1105 | end |
|---|
| 1106 | |
|---|
| 1107 | def command_nadoka(prefix, ch, *params) |
|---|
| 1108 | return unless @nadoka_ch =~ ch |
|---|
| 1109 | #return if ch[0] == ?# |
|---|
| 1110 | case params.shift |
|---|
| 1111 | when "exit" |
|---|
| 1112 | Thread.new { |
|---|
| 1113 | send_msg(Cmd.quit("rebooting...")) |
|---|
| 1114 | sleep 0.5; |
|---|
| 1115 | Process.kill("TERM", $$) |
|---|
| 1116 | } |
|---|
| 1117 | when "reload" |
|---|
| 1118 | notice(ch, "ACTION is reloading Nadoka...") |
|---|
| 1119 | Thread.new { sleep 0.5; Process.kill("HUP", $$) } |
|---|
| 1120 | when "reload-config" |
|---|
| 1121 | loadconfig |
|---|
| 1122 | notice(ch, "ACTION reloaded configuration.") |
|---|
| 1123 | when "join" |
|---|
| 1124 | send_msg(Cmd.join(*params)) |
|---|
| 1125 | when "part" |
|---|
| 1126 | send_msg(Cmd.part(*params)) |
|---|
| 1127 | when "nick" |
|---|
| 1128 | identify |
|---|
| 1129 | when "debug" |
|---|
| 1130 | $DEBUG = (params[0] == "on") if params[0] |
|---|
| 1131 | notice(ch, "debug is #{$DEBUG ? "on" : "off"}") |
|---|
| 1132 | when "savedata" |
|---|
| 1133 | savedata |
|---|
| 1134 | notice(ch, "Ok") |
|---|
| 1135 | when "gc" |
|---|
| 1136 | GC.start |
|---|
| 1137 | when "echo" |
|---|
| 1138 | ch = params.shift |
|---|
| 1139 | notice(ch, params.join(' ')) |
|---|
| 1140 | # if @state.current_channels.key?(ch) |
|---|
| 1141 | # notice(ch, params.join(' ')) |
|---|
| 1142 | # end |
|---|
| 1143 | when "action" |
|---|
| 1144 | ch = params.shift |
|---|
| 1145 | if @state.current_channels.key?(ch) |
|---|
| 1146 | notice(ch, " ACTION " + params.join(' ') + "") |
|---|
| 1147 | end |
|---|
| 1148 | end |
|---|
| 1149 | end |
|---|
| 1150 | def help_nadoka(ch) |
|---|
| 1151 | notice(ch, "!nadoka は内部コマンドです。") |
|---|
| 1152 | end |
|---|
| 1153 | |
|---|
| 1154 | def command_version(prefix, ch, *params) |
|---|
| 1155 | notice(ch, VERSION + " Ruby/#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]") |
|---|
| 1156 | end |
|---|
| 1157 | def help_version(ch) |
|---|
| 1158 | notice(ch, "usage: !version") |
|---|
| 1159 | notice(ch, "OppaiSan のバージョンを発言します。Nadoka のバージョンは CTCP VERSION を送ると回答します。") |
|---|
| 1160 | end |
|---|
| 1161 | |
|---|
| 1162 | def interval(i) |
|---|
| 1163 | if i.is_a?(Float) |
|---|
| 1164 | f = i |
|---|
| 1165 | usec = f - (i = f.to_i) |
|---|
| 1166 | end |
|---|
| 1167 | i, sec = i.divmod(60) |
|---|
| 1168 | i, min = i.divmod(60) |
|---|
| 1169 | day, hour = i.divmod(24) |
|---|
| 1170 | |
|---|
| 1171 | result = "" |
|---|
| 1172 | result << "#{day}日" if day != 0 |
|---|
| 1173 | result << "" |
|---|
| 1174 | result << sprintf("%02d:", hour) if day != 0 || hour != 0 |
|---|
| 1175 | result << sprintf("%02d:%02d", min, sec) |
|---|
| 1176 | result << sprintf(".%03d", usec * 1000) if usec |
|---|
| 1177 | result << "" |
|---|
| 1178 | end |
|---|
| 1179 | |
|---|
| 1180 | def command_fp(prefix, ch, *params) |
|---|
| 1181 | Thread.new { |
|---|
| 1182 | params.each do |user| |
|---|
| 1183 | if l = @newcomer[user.tr('_', ' ')] |
|---|
| 1184 | notice(ch, "#{user}: " + l) |
|---|
| 1185 | else |
|---|
| 1186 | notice(ch, "#{user}: No such user") |
|---|
| 1187 | end |
|---|
| 1188 | sleep 3 |
|---|
| 1189 | end |
|---|
| 1190 | } |
|---|
| 1191 | end |
|---|
| 1192 | |
|---|
| 1193 | def command_newcomers(prefix, ch, *params) |
|---|
| 1194 | Thread.new { |
|---|
| 1195 | begin |
|---|
| 1196 | nc = @newcomermutex.synchronize { @newcomers.dup } |
|---|
| 1197 | nc.each do |time, user| |
|---|
| 1198 | notice(ch, time.strftime('%Y-%m-%d %T ') + "#{user}") |
|---|
| 1199 | sleep 3 |
|---|
| 1200 | end |
|---|
| 1201 | rescue Exception => e |
|---|
| 1202 | bugcheck(ch, e) |
|---|
| 1203 | end |
|---|
| 1204 | } |
|---|
| 1205 | end |
|---|
| 1206 | def help_newcomers(ch) |
|---|
| 1207 | notice(ch, "usage: !newcomers") |
|---|
| 1208 | notice(ch, "最近の新規ユーザー10人を発言します。") |
|---|
| 1209 | end |
|---|
| 1210 | |
|---|
| 1211 | def command_help(prefix, ch, *params) |
|---|
| 1212 | if command = params[0] |
|---|
| 1213 | command.sub!(/^!/, '') |
|---|
| 1214 | meth = command.downcase |
|---|
| 1215 | meth = :"help_#{meth}" |
|---|
| 1216 | if respond_to?(meth) |
|---|
| 1217 | send(meth, ch) |
|---|
| 1218 | end |
|---|
| 1219 | else |
|---|
| 1220 | HELP.each { |s| notice(ch, s) } |
|---|
| 1221 | methods.grep(/^command_(?!nadoka)/).sort.each_slice(10) do |meths| |
|---|
| 1222 | meths = meths.collect { |m| m.sub(/^command_/, '!') }.join(' ') |
|---|
| 1223 | notice(ch, " " + meths) |
|---|
| 1224 | end |
|---|
| 1225 | end |
|---|
| 1226 | rescue NoMethodError |
|---|
| 1227 | notice(ch, "コマンド #{command} のヘルプがありません。") |
|---|
| 1228 | end |
|---|
| 1229 | def help_help(ch) |
|---|
| 1230 | notice(ch, "usage: !help [コマンド]") |
|---|
| 1231 | notice(ch, "OppaiSan のヘルプを発言します。コマンド名が指定されるとそのコマンドの詳細ヘルプを発言します。") |
|---|
| 1232 | end |
|---|
| 1233 | |
|---|
| 1234 | end |
|---|