root/lang/ruby/OppaiSan/trunk/oppaisanbot.nb @ 48

Revision 48, 33.9 kB (checked in by kan, 6 years ago)

add ignore_user config.

Line 
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#
8require 'iconv'
9require 'resolv'
10require 'date'
11require 'csv'
12require 'shellwords'
13require 'enumerator'
14require 'net/http'
15require 'uri'
16require 'set'
17require 'digest/md5'
18require 'digest/sha1'
19require 'thread'
20require 'stringio'
21require 'zlib'
22require 'gdbm'
23
24require 'rubygems'
25require 'hatena/api/graph'
26
27module OppaiSanBotModule
28  WordInfo = Struct.new(:active, :inactive)
29  RegInfo = Struct.new(:time, :prefix, :reg)
30end
31
32class 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 
1234end
Note: See TracBrowser for help on using the browser.