ircbot 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +71 -0
- data/README +72 -3
- data/bin/ircbot +3 -0
- data/config/samples/postgres.yml +19 -0
- data/config/{sama-zu.yml → samples/sama-zu.yml} +1 -1
- data/config/{yml.erb → samples/yml.erb} +0 -0
- data/ircbot.gemspec +13 -0
- data/lib/ircbot.rb +3 -1
- data/lib/ircbot/client.rb +6 -0
- data/lib/ircbot/client/config.rb +9 -0
- data/lib/ircbot/client/plugins.rb +14 -1
- data/lib/ircbot/core_ext/message.rb +4 -1
- data/lib/ircbot/plugin.rb +17 -0
- data/lib/ircbot/plugins.rb +68 -13
- data/lib/ircbot/utils/html_parser.rb +26 -0
- data/lib/ircbot/utils/watcher.rb +36 -0
- data/lib/ircbot/version.rb +1 -1
- data/old/plugins/summary.cpi +267 -0
- data/plugins/plugins.rb +1 -1
- data/plugins/reminder.rb +79 -175
- data/plugins/summary/ch2.rb +272 -0
- data/plugins/summary/engines.rb +30 -0
- data/plugins/summary/engines/base.rb +105 -0
- data/plugins/summary/engines/ch2.rb +14 -0
- data/plugins/summary/engines/https.rb +6 -0
- data/plugins/summary/engines/none.rb +10 -0
- data/plugins/summary/engines/twitter.rb +16 -0
- data/plugins/summary/spec/ch2_spec.rb +64 -0
- data/plugins/summary/spec/spec_helper.rb +19 -0
- data/plugins/summary/spec/summarizers_none_spec.rb +15 -0
- data/plugins/summary/spec/summarizers_spec.rb +23 -0
- data/plugins/summary/summary.rb +58 -0
- data/plugins/watchdog/db.rb +80 -0
- data/plugins/watchdog/exceptions.rb +4 -0
- data/plugins/watchdog/updater.rb +21 -0
- data/plugins/watchdog/watchdog.rb +82 -0
- data/spec/plugin_spec.rb +11 -0
- data/spec/plugins_spec.rb +35 -1
- data/spec/utils/html_parser_spec.rb +30 -0
- data/spec/utils/spec_helper.rb +1 -0
- metadata +190 -13
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            require 'cgi'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Ircbot
         | 
| 4 | 
            +
              module Utils
         | 
| 5 | 
            +
                module HtmlParser
         | 
| 6 | 
            +
                  def get_title(html)
         | 
| 7 | 
            +
                    title = $1.strip if %r{<title>(.*?)</title>}mi =~ html
         | 
| 8 | 
            +
                    title ? trim_tags(title) : ""
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def trim_tags(html)
         | 
| 12 | 
            +
                    html.gsub!(%r{<head.*?>.*?</head>}mi, '')
         | 
| 13 | 
            +
                    html.gsub!(%r{<script.*?>.*?</script>}mi, '')
         | 
| 14 | 
            +
                    html.gsub!(%r{<style.*?>.*?</style>}mi, '')
         | 
| 15 | 
            +
                    html.gsub!(%r{<noscript.*?>.*?</noscript>}mi, '')
         | 
| 16 | 
            +
                    html.gsub!(%r{</?.*?>}, '')
         | 
| 17 | 
            +
                    html.gsub!(%r{<\!--.*?-->}mi, '')
         | 
| 18 | 
            +
                    html.gsub!(%r{<\!\w.*?>}mi, '')
         | 
| 19 | 
            +
                    html.gsub!(/\s+/m, ' ')
         | 
| 20 | 
            +
                    html.strip!
         | 
| 21 | 
            +
                    html = CGI.unescapeHTML(html)
         | 
| 22 | 
            +
                    return html
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            module Ircbot
         | 
| 2 | 
            +
              module Utils
         | 
| 3 | 
            +
                class Watcher
         | 
| 4 | 
            +
                  dsl_accessor :interval, 60, :instance=>true
         | 
| 5 | 
            +
                  dsl_accessor :callback, proc{|e| puts e}, :instance=>true
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def initialize(options = {})
         | 
| 8 | 
            +
                    interval options[:interval] || self.class.interval
         | 
| 9 | 
            +
                    callback options[:callback] || self.class.callback
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def srcs
         | 
| 13 | 
            +
                    return []
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def process(src)
         | 
| 17 | 
            +
                    return true
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def run
         | 
| 21 | 
            +
                    loop do
         | 
| 22 | 
            +
                      srcs.each do |src|
         | 
| 23 | 
            +
                        if process(src)
         | 
| 24 | 
            +
                          callback.call(src)
         | 
| 25 | 
            +
                        end
         | 
| 26 | 
            +
                      end
         | 
| 27 | 
            +
                      sleep interval
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def start
         | 
| 32 | 
            +
                    Thread.new{ run }
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
    
        data/lib/ircbot/version.rb
    CHANGED
    
    
| @@ -0,0 +1,267 @@ | |
| 1 | 
            +
            #!/usr/bin/ruby -Ke
         | 
| 2 | 
            +
            load 'html.rb'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'ggcl/net/http'
         | 
| 5 | 
            +
            require 'cgi'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            class SummaryAgent
         | 
| 8 | 
            +
              WAIT_SEC = 3
         | 
| 9 | 
            +
              OUT_SIZE = 90		# ɽ��ʸ����
         | 
| 10 | 
            +
              CONTENT_LENGTH_LIMIT = 3 * 1024 * 1024		# within 3Mbytes
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              class IRCFloodError		< StandardError; end
         | 
| 13 | 
            +
              class AlreadyOpenedError	< StandardError; end
         | 
| 14 | 
            +
              class NonTextError		< StandardError; end
         | 
| 15 | 
            +
              class SizeExceededError	< StandardError; end
         | 
| 16 | 
            +
              class MissingUrlError		< StandardError; end
         | 
| 17 | 
            +
              class NoSummaryError		< StandardError; end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              PARENTHESIS = Hash[*%w|
         | 
| 20 | 
            +
                ( ) [ ] { } �� �� �� ��
         | 
| 21 | 
            +
              |]
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              class <<self
         | 
| 24 | 
            +
                def adjust_parenthesis (url, prefix)
         | 
| 25 | 
            +
                  # xxx(http://.../) �ʾ�硢�����γ�̤�Ĵ�����롣
         | 
| 26 | 
            +
                  keys = PARENTHESIS .keys
         | 
| 27 | 
            +
                  vals = PARENTHESIS .values
         | 
| 28 | 
            +
                  regexp1 = '(' + keys .collect{|i| Regexp.escape(i)} .join('|') + ')'
         | 
| 29 | 
            +
                  regexp2 = '(' + vals .collect{|i| Regexp.escape(i)} .join('|') + ')'
         | 
| 30 | 
            +
                  if /(#{regexp1}+)$/ === prefix .to_s
         | 
| 31 | 
            +
            	url .gsub!(/#{regexp2}+$/, '')
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                  url
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                PATTERN_EUC = '[\xa1-\xfe][\xa1-\xfe]'
         | 
| 37 | 
            +
                REGEXP_EUC  = Regexp .new(PATTERN_EUC, 'n')
         | 
| 38 | 
            +
                def adjust_2bytes_code (url)
         | 
| 39 | 
            +
                  if REGEXP_EUC === url
         | 
| 40 | 
            +
            	url = $`
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                  return url
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def adjust_url(url, prefix)
         | 
| 46 | 
            +
                  url = adjust_2bytes_code(url)
         | 
| 47 | 
            +
                  adjust_parenthesis(url, prefix)
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def extract_url (string)
         | 
| 51 | 
            +
                  case string .to_s
         | 
| 52 | 
            +
                  when /http:\/\/([^:\/]+)(:(\d+))?(\/[^#\s��]*)(#(\S+))?/oi
         | 
| 53 | 
            +
            	return adjust_url($&, $`)
         | 
| 54 | 
            +
                  when /https:\/\/([^:\/]+)(:(\d+))?(\/[^#\s��]*)(#(\S+))?/oi
         | 
| 55 | 
            +
            	return adjust_url($&, $`)
         | 
| 56 | 
            +
                  else
         | 
| 57 | 
            +
            	raise MissingUrlError
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
              def initialize (*)
         | 
| 63 | 
            +
                @url_history = []
         | 
| 64 | 
            +
                @previous_times = {}	# �ܵ�ǽ�κǽ����ѳ��ϻ���(�ƥ桼����)
         | 
| 65 | 
            +
                @options = {}		# �Ƽ索�ץ����
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                title(true)
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              def title (setter = nil)
         | 
| 71 | 
            +
                if setter
         | 
| 72 | 
            +
                  return @options[:title] = setter
         | 
| 73 | 
            +
                else
         | 
| 74 | 
            +
                  return @options[:title]
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
              def out_size
         | 
| 79 | 
            +
                @out_size || OUT_SIZE
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
              def wait_sec
         | 
| 83 | 
            +
                @wait_sec || WAIT_SEC
         | 
| 84 | 
            +
              end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
              def do_help (msg)
         | 
| 87 | 
            +
                nick = msg[:client].config[:nick]
         | 
| 88 | 
            +
                <<HELP
         | 
| 89 | 
            +
            [Web�ڡ���������](#{wait_sec}���Ե���#{out_size}ʸ��ɽ��)
         | 
| 90 | 
            +
            ���ޥ��: #{nick}.(summary|����).{wait=,size=,title=(on/off)}
         | 
| 91 | 
            +
            ������ˡ: URL �� #{nick}.(summary|����)
         | 
| 92 | 
            +
            HELP
         | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
              def check_type (url)
         | 
| 96 | 
            +
                response = GGCL::Net::HTTP .new(url) .head
         | 
| 97 | 
            +
                response .content_type
         | 
| 98 | 
            +
              end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
              def text? (url)
         | 
| 101 | 
            +
                /text/i === check_type(url)
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
              def response (url)
         | 
| 105 | 
            +
                GGCL::Net::HTTP .new(url) .head
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              def check_header (url)
         | 
| 109 | 
            +
                res = response(url)
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                # Content-Type ��Ĵ�٤�
         | 
| 112 | 
            +
                /text/i === res .content_type or
         | 
| 113 | 
            +
                  raise NonTextError
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                # Content-Length ��Ĵ�٤�
         | 
| 116 | 
            +
                size = res['content-length'] .to_i
         | 
| 117 | 
            +
                size <= CONTENT_LENGTH_LIMIT or
         | 
| 118 | 
            +
                  raise SizeExceededError .new("#{size}bytes for #{CONTENT_LENGTH_LIMIT}")
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
              def maybe_flood? (from)
         | 
| 122 | 
            +
                (Time .now .to_i - @previous_times[from] .to_i) < wait_sec
         | 
| 123 | 
            +
              end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
              def check_flood (from)
         | 
| 126 | 
            +
                maybe_flood? and raise IRCFloodError
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
              def summary (url, size = nil)
         | 
| 130 | 
            +
                html  = HTML.get(url)
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                if html .frame?
         | 
| 133 | 
            +
                  frame = html .frames .sort .first
         | 
| 134 | 
            +
                  if frame .src
         | 
| 135 | 
            +
            	url = HTML::compose_path(url, frame .src)
         | 
| 136 | 
            +
            	return summary(url, size)
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                value = html .summary(size || out_size)
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                if title
         | 
| 143 | 
            +
            #      lead  = html .title_or_nil
         | 
| 144 | 
            +
                  lead = ''
         | 
| 145 | 
            +
                  if /<TITLE(.*?)>(.*?)<\/TITLE>/im =~ html.html then
         | 
| 146 | 
            +
                   lead = $2
         | 
| 147 | 
            +
                  end	
         | 
| 148 | 
            +
                  if lead
         | 
| 149 | 
            +
            	lead .gsub!(/\s+/, '')
         | 
| 150 | 
            +
            	lead = "[#{lead}]"
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
                  return "#{lead}#{value}"
         | 
| 153 | 
            +
                else
         | 
| 154 | 
            +
                  return value
         | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
              end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
              def already_opened? (url)
         | 
| 159 | 
            +
                bool = @url_history .include? url
         | 
| 160 | 
            +
                @url_history << url
         | 
| 161 | 
            +
                return bool
         | 
| 162 | 
            +
              end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
              def check_duplicate_url (url)
         | 
| 165 | 
            +
                already_opened?(url) and
         | 
| 166 | 
            +
                  raise AlreadyOpenedError
         | 
| 167 | 
            +
              end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
              def do_summary (msg, wait = nil)
         | 
| 170 | 
            +
                begin
         | 
| 171 | 
            +
                  from = msg[:from]
         | 
| 172 | 
            +
                  return nil if from =~ /sama-zu/
         | 
| 173 | 
            +
                  maybe_flood?(from) if wait
         | 
| 174 | 
            +
                  url = self.class.extract_url(msg[:str])
         | 
| 175 | 
            +
                  return nil if url =~ /onamae/
         | 
| 176 | 
            +
                  return nil if url =~ /dzz.jp\/up\/download/
         | 
| 177 | 
            +
                  check_duplicate_url(url)
         | 
| 178 | 
            +
                  check_header(url)
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  @previous_times[from] = Time .now
         | 
| 181 | 
            +
                  buffer = summary(url) or raise NoSummaryError
         | 
| 182 | 
            +
                  buffer = CGI::unescapeHTML(buffer)
         | 
| 183 | 
            +
                  return ">> #{buffer}"
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                rescue IRCFloodError
         | 
| 186 | 
            +
                rescue AlreadyOpenedError
         | 
| 187 | 
            +
                rescue NonTextError
         | 
| 188 | 
            +
                rescue SizeExceededError	; return "error: size exceeded. #{$!}"
         | 
| 189 | 
            +
                rescue MissingUrlError
         | 
| 190 | 
            +
                rescue NoSummaryError
         | 
| 191 | 
            +
                rescue Exception		; return "error: #{$!}"
         | 
| 192 | 
            +
                end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                return nil
         | 
| 195 | 
            +
              end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
              def do_command (msg)
         | 
| 198 | 
            +
                case msg[:command] .to_s
         | 
| 199 | 
            +
                when 'summary', '����'
         | 
| 200 | 
            +
                  return do_summary(msg)
         | 
| 201 | 
            +
                when /^(summary|����).title=/io
         | 
| 202 | 
            +
                  case $' .to_s
         | 
| 203 | 
            +
                  when /off/i, /0/, ''
         | 
| 204 | 
            +
            	title(nil)
         | 
| 205 | 
            +
            	return "���Ф���OFF�ˤ����ˤ㡼��"
         | 
| 206 | 
            +
                  else
         | 
| 207 | 
            +
            	title(true)
         | 
| 208 | 
            +
            	return "���Ф���ON�ˤ����ˤ㡼��"
         | 
| 209 | 
            +
                  end
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                when /^(summary|����).size=/io
         | 
| 212 | 
            +
                  range = (5..1000)
         | 
| 213 | 
            +
                  if range === (size = $'.to_i)
         | 
| 214 | 
            +
            	@out_size = size
         | 
| 215 | 
            +
            	return "ɽ��ʸ������#{out_size}bytes���ѹ������ˤ㡼��"
         | 
| 216 | 
            +
                  else
         | 
| 217 | 
            +
            	return "error: ɽ��������(#{range})��ȿ�Ǥ���"
         | 
| 218 | 
            +
                  end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                when /^(summary|����).wait=/io
         | 
| 221 | 
            +
                  if (sec = $'.to_i) >= WAIT_SEC
         | 
| 222 | 
            +
            	@wait_sec = sec
         | 
| 223 | 
            +
            	return "�Ե����֤�#{wait_sec}�ä��ѹ������ˤ㡼��"
         | 
| 224 | 
            +
                  else
         | 
| 225 | 
            +
            	return "error: �Ե����֤β��¤�#{WAIT_SEC}�ä����ꤵ��Ƥ��ޤ���"
         | 
| 226 | 
            +
                  end
         | 
| 227 | 
            +
                end
         | 
| 228 | 
            +
                return nil
         | 
| 229 | 
            +
              end
         | 
| 230 | 
            +
             | 
| 231 | 
            +
              def do_reply (msg)
         | 
| 232 | 
            +
                return do_summary(msg, :wait)
         | 
| 233 | 
            +
              end
         | 
| 234 | 
            +
            end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
            if $0 == __FILE__
         | 
| 237 | 
            +
             | 
| 238 | 
            +
            #   require 'nkf'
         | 
| 239 | 
            +
            #   str = NKF::nkf('-e', ARGF.read)
         | 
| 240 | 
            +
            #   str = 'aho'
         | 
| 241 | 
            +
            #   html = HTML .new(str)
         | 
| 242 | 
            +
            #   p html .frame?
         | 
| 243 | 
            +
            #   frames = html .frames
         | 
| 244 | 
            +
            #   best = frames .sort .first
         | 
| 245 | 
            +
            #   p best
         | 
| 246 | 
            +
            #   exit
         | 
| 247 | 
            +
             | 
| 248 | 
            +
              agent = SummaryAgent .new
         | 
| 249 | 
            +
              string = 'http://www.asahi.com/'
         | 
| 250 | 
            +
              sum = SummaryAgent.new.summary(string,100)
         | 
| 251 | 
            +
              puts sum	
         | 
| 252 | 
            +
             | 
| 253 | 
            +
             | 
| 254 | 
            +
            #  string = '���㥢������ http://www.zdnet.co.jp/news/bursts/0111/09/zaku.html �������������������ˤ㡼����������'
         | 
| 255 | 
            +
            #  p SummaryAgent::extract_url(string)
         | 
| 256 | 
            +
             | 
| 257 | 
            +
            #  string = 'http://kazu.nori.org/'
         | 
| 258 | 
            +
            #  string = "http://pucca.astron.s.u-tokyo.ac.jp/image/ogiyahagi1.mpg"
         | 
| 259 | 
            +
            #  string = 'http://kazu.nori.org/1.png'
         | 
| 260 | 
            +
            #  agent .check_type(string)
         | 
| 261 | 
            +
             | 
| 262 | 
            +
            #  string = 'http://www.asahi.com/'
         | 
| 263 | 
            +
             | 
| 264 | 
            +
            #  p agent .do_reply({:from, :AnnaChan, :str, string})
         | 
| 265 | 
            +
            end
         | 
| 266 | 
            +
             | 
| 267 | 
            +
            SummaryAgent .new
         | 
    
        data/plugins/plugins.rb
    CHANGED
    
    
    
        data/plugins/reminder.rb
    CHANGED
    
    | @@ -1,15 +1,8 @@ | |
| 1 1 | 
             
            #!/usr/bin/env ruby -Ku
         | 
| 2 2 | 
             
            # -*- coding: utf-8 -*-
         | 
| 3 3 |  | 
| 4 | 
            -
            ######################################################################
         | 
| 5 | 
            -
            # [Install]
         | 
| 6 | 
            -
            #
         | 
| 7 | 
            -
            # gem install chawan night-time dm-core dm-migrations dm-timestamps do_sqlite3 data_objects dm-sqlite-adapter -V
         | 
| 8 | 
            -
            #
         | 
| 9 | 
            -
             | 
| 10 4 | 
             
            require 'rubygems'
         | 
| 11 5 | 
             
            require 'ircbot'
         | 
| 12 | 
            -
            require 'chawan'
         | 
| 13 6 | 
             
            require 'night-time'
         | 
| 14 7 |  | 
| 15 8 | 
             
            require 'dm-core'
         | 
| @@ -17,28 +10,24 @@ require 'dm-migrations' | |
| 17 10 | 
             
            require 'dm-timestamps'
         | 
| 18 11 |  | 
| 19 12 | 
             
            module Reminder
         | 
| 20 | 
            -
               | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
                   path = Pathname(path).expand_path
         | 
| 26 | 
            -
                   path.parent.mkpath
         | 
| 27 | 
            -
                   DataMapper.setup(:default, "sqlite3://#{path}")
         | 
| 28 | 
            -
                   Reminder::Event.auto_upgrade!
         | 
| 29 | 
            -
                   )
         | 
| 13 | 
            +
              REPOSITORY_NAME = :reminder
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              def self.connect(uri)
         | 
| 16 | 
            +
                DataMapper.setup(REPOSITORY_NAME, uri)
         | 
| 17 | 
            +
                Reminder::Event.auto_upgrade!
         | 
| 30 18 | 
             
              end
         | 
| 31 19 |  | 
| 32 20 | 
             
              ######################################################################
         | 
| 33 21 | 
             
              ### Exceptions
         | 
| 34 22 |  | 
| 35 23 | 
             
              class EventNotFound < RuntimeError; end
         | 
| 36 | 
            -
              class  | 
| 24 | 
            +
              class EventFound    < RuntimeError
         | 
| 37 25 | 
             
                attr_accessor :event
         | 
| 38 26 | 
             
                def initialize(event)
         | 
| 39 27 | 
             
                  @event = event
         | 
| 40 28 | 
             
                end
         | 
| 41 29 | 
             
              end
         | 
| 30 | 
            +
              class EventNotSaved < EventFound   ; end
         | 
| 42 31 | 
             
              class EventHasDone  < EventNotSaved; end
         | 
| 43 32 | 
             
              class StartNotFound < EventNotSaved; end
         | 
| 44 33 |  | 
| @@ -46,14 +35,18 @@ module Reminder | |
| 46 35 | 
             
              ### Event
         | 
| 47 36 |  | 
| 48 37 | 
             
              class Event
         | 
| 38 | 
            +
                def self.default_repository_name; REPOSITORY_NAME; end
         | 
| 39 | 
            +
                def self.default_storage_name   ; "event"; end
         | 
| 40 | 
            +
             | 
| 49 41 | 
             
                include DataMapper::Resource
         | 
| 50 42 |  | 
| 51 43 | 
             
                property :id       , Serial
         | 
| 52 44 | 
             
                property :st       , DateTime                   # 開始日時
         | 
| 53 45 | 
             
                property :en       , DateTime                   # 終了日時
         | 
| 54 | 
            -
                property :title    , String | 
| 55 | 
            -
                property :desc     ,  | 
| 56 | 
            -
                property :where    , String | 
| 46 | 
            +
                property :title    , String, :length=>255       # 件名
         | 
| 47 | 
            +
                property :desc     , Text                       # 詳細
         | 
| 48 | 
            +
                property :where    , String, :length=>255       # 場所
         | 
| 49 | 
            +
                property :source   , String, :length=>255       # 情報ソース
         | 
| 57 50 | 
             
                property :allday   , Boolean , :default=>false  # 終日フラグ
         | 
| 58 51 | 
             
                property :alerted  , Boolean , :default=>false  # お知らせ済
         | 
| 59 52 | 
             
                property :alert_at , DateTime                   # お知らせ日時
         | 
| @@ -62,12 +55,12 @@ module Reminder | |
| 62 55 | 
             
                ### Class methods
         | 
| 63 56 |  | 
| 64 57 | 
             
                class << self
         | 
| 65 | 
            -
                  def  | 
| 66 | 
            -
                     | 
| 58 | 
            +
                  def alerts
         | 
| 59 | 
            +
                    all(:alerted=>false, :alert_at.lt=>Time.now, :order=>[:alert_at])
         | 
| 67 60 | 
             
                  end
         | 
| 68 61 |  | 
| 69 | 
            -
                  def  | 
| 70 | 
            -
                    all(:alerted=>false, :alert_at. | 
| 62 | 
            +
                  def future
         | 
| 63 | 
            +
                    all(:alerted=>false, :alert_at.gt=>Time.now, :order=>[:alert_at])
         | 
| 71 64 | 
             
                  end
         | 
| 72 65 | 
             
                end
         | 
| 73 66 |  | 
| @@ -84,33 +77,35 @@ module Reminder | |
| 84 77 | 
             
                end
         | 
| 85 78 | 
             
              end
         | 
| 86 79 |  | 
| 87 | 
            -
               | 
| 88 | 
            -
                 | 
| 80 | 
            +
              def self.parse!(text)
         | 
| 81 | 
            +
                case text
         | 
| 82 | 
            +
                when /^\s*>>/
         | 
| 83 | 
            +
                  # 引用は無視
         | 
| 84 | 
            +
                  raise EventNotFound
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                else
         | 
| 87 | 
            +
                  # 先頭40bytesに時刻ぽい文字列があれば登録とみなす
         | 
| 88 | 
            +
                  jst    = NightTime::Jst.new(text[0,40])
         | 
| 89 | 
            +
                  array  = jst.parse
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  # 日付なし
         | 
| 92 | 
            +
                  array[1] && array[2] or raise EventNotFound
         | 
| 93 | 
            +
             | 
| 89 94 | 
             
                  event = Event.new
         | 
| 90 95 | 
             
                  event.desc   = text
         | 
| 91 96 | 
             
                  event.title  = text.sub(%r{^[\s\d:-]+}, '')
         | 
| 92 | 
            -
                  event.allday =  | 
| 93 | 
            -
             | 
| 94 | 
            -
                  t = Date._parse(text)
         | 
| 95 | 
            -
                  # => {:zone=>"-14:55", :year=>2010, :hour=>13, :min=>30, :mday=>4, :offset=>-53700, :mon=>1}
         | 
| 96 | 
            -
             | 
| 97 | 
            -
                  if t[:year] && t[:mon] && t[:mday] && t[:hour]
         | 
| 98 | 
            -
                    event.st = Time.mktime(t[:year], t[:mon], t[:mday], t[:hour], t[:min], t[:sec])
         | 
| 99 | 
            -
                    if t[:zone].to_s =~ /^-?(\d+):(\d+)(:(\d+))?$/
         | 
| 100 | 
            -
                      event.en = Time.mktime(t[:year], t[:mon], t[:mday], $1, $2, $4)
         | 
| 101 | 
            -
                    end
         | 
| 102 | 
            -
                  else
         | 
| 103 | 
            -
                    event.allday = true
         | 
| 104 | 
            -
                    event.st     = Time.mktime(t[:year], t[:mon], t[:mday]) rescue nil
         | 
| 105 | 
            -
                  end
         | 
| 97 | 
            +
                  event.allday = array[3].nil?
         | 
| 98 | 
            +
                  event.st     = jst.time
         | 
| 106 99 |  | 
| 107 100 | 
             
                  return event
         | 
| 108 101 | 
             
                end
         | 
| 102 | 
            +
              end
         | 
| 109 103 |  | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 104 | 
            +
              module Registable
         | 
| 105 | 
            +
                def register(event)
         | 
| 112 106 | 
             
                  event.st or raise StartNotFound, event
         | 
| 113 107 | 
             
                  if event.st.to_time > Time.now
         | 
| 108 | 
            +
                    event.source   = "irc/reminder"
         | 
| 114 109 | 
             
                    event.alert_at = Time.at(event.st.to_time.to_i - 30*60)
         | 
| 115 110 | 
             
                    event.save or raise EventNotSaved, event
         | 
| 116 111 | 
             
                    return event
         | 
| @@ -120,160 +115,69 @@ module Reminder | |
| 120 115 | 
             
                end
         | 
| 121 116 | 
             
              end
         | 
| 122 117 |  | 
| 123 | 
            -
              extend  | 
| 118 | 
            +
              extend Registable
         | 
| 124 119 | 
             
            end
         | 
| 125 120 |  | 
| 126 121 |  | 
| 127 122 | 
             
            class ReminderPlugin < Ircbot::Plugin
         | 
| 128 | 
            -
              class EventWatcher
         | 
| 129 | 
            -
                 | 
| 130 | 
            -
             | 
| 123 | 
            +
              class EventWatcher < Ircbot::Utils::Watcher
         | 
| 124 | 
            +
                def srcs
         | 
| 125 | 
            +
                  Reminder::Event.alerts
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
              def help
         | 
| 130 | 
            +
                ["list -> show future reminders",
         | 
| 131 | 
            +
                 "YYYY-mm-dd or YYYY-mm-dd HH:MM text -> register the text"].join("\n")
         | 
| 132 | 
            +
              end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
              def setup
         | 
| 135 | 
            +
                return if @watcher
         | 
| 136 | 
            +
                bot = self.bot
         | 
| 131 137 |  | 
| 132 | 
            -
                 | 
| 133 | 
            -
             | 
| 134 | 
            -
                   | 
| 138 | 
            +
                uri = self[:db]
         | 
| 139 | 
            +
                unless uri
         | 
| 140 | 
            +
                  path = Ircbot.root + "db" + "#{config.nick}-reminder.db"
         | 
| 141 | 
            +
                  uri  = "sqlite3://#{path}"
         | 
| 142 | 
            +
                  path.parent.mkpath
         | 
| 135 143 | 
             
                end
         | 
| 136 144 |  | 
| 137 | 
            -
                 | 
| 138 | 
            -
             | 
| 139 | 
            -
             | 
| 140 | 
            -
             | 
| 141 | 
            -
             | 
| 142 | 
            -
             | 
| 143 | 
            -
             | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
             | 
| 147 | 
            -
             | 
| 148 | 
            -
                   | 
| 145 | 
            +
                Reminder.connect(uri)
         | 
| 146 | 
            +
                callback = proc{|event| bot.broadcast event.to_s; event.done!}
         | 
| 147 | 
            +
                reminder = EventWatcher.new(:interval=>60, :callback=>callback)
         | 
| 148 | 
            +
                @watcher = Thread.new { reminder.start }
         | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
              def list
         | 
| 152 | 
            +
                events = Reminder::Event.future
         | 
| 153 | 
            +
                if events.size == 0
         | 
| 154 | 
            +
                  return "no reminders"
         | 
| 155 | 
            +
                else
         | 
| 156 | 
            +
                  lead = "#{events.size} reminder(s)"
         | 
| 157 | 
            +
                  body = events.map(&:to_s)[0,5]
         | 
| 158 | 
            +
                  return ([lead] + body).join("\n")
         | 
| 149 159 | 
             
                end
         | 
| 150 160 | 
             
              end
         | 
| 151 161 |  | 
| 152 162 | 
             
              def reply(text)
         | 
| 153 | 
            -
                start_reminder
         | 
| 154 | 
            -
             | 
| 155 163 | 
             
                # strip noise
         | 
| 156 164 | 
             
                text = text.sub(/^<.*?>/,'').strip
         | 
| 157 | 
            -
             | 
| 158 | 
            -
                 | 
| 159 | 
            -
                 | 
| 160 | 
            -
             | 
| 161 | 
            -
             | 
| 162 | 
            -
                  return text
         | 
| 163 | 
            -
                end
         | 
| 164 | 
            -
                return nil
         | 
| 165 | 
            +
               
         | 
| 166 | 
            +
                event = Reminder.parse!(text)
         | 
| 167 | 
            +
                Reminder.register(event)
         | 
| 168 | 
            +
                text  = "Remind you again at %s" % event.alert_at.strftime("%Y-%m-%d %H:%M")
         | 
| 169 | 
            +
                return text
         | 
| 165 170 |  | 
| 166 171 | 
             
              rescue Reminder::EventNotFound
         | 
| 167 172 | 
             
                return nil
         | 
| 168 173 |  | 
| 169 174 | 
             
              rescue Reminder::StartNotFound => e
         | 
| 170 | 
            -
                return "Reminder cannot detect start: #{e.event.st}"
         | 
| 175 | 
            +
                # return "Reminder cannot detect start: #{e.event.st}"
         | 
| 176 | 
            +
                return nil
         | 
| 171 177 |  | 
| 172 178 | 
             
              rescue Reminder::EventHasDone => e
         | 
| 173 179 | 
             
                puts "Reminder ignores past event: #{e.event.st}"
         | 
| 174 180 | 
             
                return nil
         | 
| 175 181 | 
             
              end
         | 
| 176 | 
            -
             | 
| 177 | 
            -
              private
         | 
| 178 | 
            -
                def start_reminder(&callback)
         | 
| 179 | 
            -
                  bot = self.bot
         | 
| 180 | 
            -
                  callback ||= proc{|event| bot.broadcast event.to_s}
         | 
| 181 | 
            -
                  @event_watcher_thread ||=
         | 
| 182 | 
            -
                    (connect
         | 
| 183 | 
            -
                     reminder = EventWatcher.new(:interval=>60, :callback=>callback)
         | 
| 184 | 
            -
                     Thread.new { reminder.start })
         | 
| 185 | 
            -
                end
         | 
| 186 | 
            -
             | 
| 187 | 
            -
                def reminder_db_path
         | 
| 188 | 
            -
                  Ircbot.root + "db" + "reminder-#{config.nick}.db"
         | 
| 189 | 
            -
                end
         | 
| 190 | 
            -
             | 
| 191 | 
            -
                def connect
         | 
| 192 | 
            -
                  @connect ||= Reminder.connect(reminder_db_path)
         | 
| 193 | 
            -
                end
         | 
| 194 | 
            -
            end
         | 
| 195 | 
            -
             | 
| 196 | 
            -
             | 
| 197 | 
            -
            ######################################################################
         | 
| 198 | 
            -
            ### Spec in file:
         | 
| 199 | 
            -
            ###   ruby plugins/reminder.rb
         | 
| 200 | 
            -
             | 
| 201 | 
            -
            if $0 == __FILE__
         | 
| 202 | 
            -
             | 
| 203 | 
            -
              def spec(src, buffer, &block)
         | 
| 204 | 
            -
                buffer = "require '#{Pathname(src).expand_path}'\n" + buffer
         | 
| 205 | 
            -
                tmp = Tempfile.new("dynamic-spec")
         | 
| 206 | 
            -
                tmp.print(buffer)
         | 
| 207 | 
            -
                tmp.close
         | 
| 208 | 
            -
                block.call(tmp)
         | 
| 209 | 
            -
              ensure
         | 
| 210 | 
            -
                tmp.close(true)
         | 
| 211 | 
            -
              end
         | 
| 212 | 
            -
             | 
| 213 | 
            -
              spec($0, DATA.read{}) do |tmp|
         | 
| 214 | 
            -
                system("rspec -cfs #{tmp.path}")
         | 
| 215 | 
            -
              end
         | 
| 216 182 | 
             
            end
         | 
| 217 183 |  | 
| 218 | 
            -
            __END__
         | 
| 219 | 
            -
             | 
| 220 | 
            -
            require 'rspec'
         | 
| 221 | 
            -
            require 'ostruct'
         | 
| 222 | 
            -
             | 
| 223 | 
            -
            module RSpec
         | 
| 224 | 
            -
              module Core
         | 
| 225 | 
            -
                module SharedExampleGroup
         | 
| 226 | 
            -
                  def parse(text, &block)
         | 
| 227 | 
            -
                    describe "(#{text})" do
         | 
| 228 | 
            -
                      subject {
         | 
| 229 | 
            -
                        event = Reminder.parse(text)
         | 
| 230 | 
            -
                        hash  = {
         | 
| 231 | 
            -
                          :st     => (event.st.strftime("%Y-%m-%d %H:%M:%S") rescue nil),
         | 
| 232 | 
            -
                          :en     => (event.en.strftime("%Y-%m-%d %H:%M:%S") rescue nil),
         | 
| 233 | 
            -
                          :title  => event.title.to_s,
         | 
| 234 | 
            -
                          :desc   => event.title.to_s,
         | 
| 235 | 
            -
                          :allday => event.allday,
         | 
| 236 | 
            -
                        }
         | 
| 237 | 
            -
                        OpenStruct.new(hash)
         | 
| 238 | 
            -
                      }
         | 
| 239 | 
            -
                      instance_eval(&block)
         | 
| 240 | 
            -
                    end
         | 
| 241 | 
            -
                  end
         | 
| 242 | 
            -
                end
         | 
| 243 | 
            -
              end
         | 
| 244 | 
            -
            end
         | 
| 245 | 
            -
             | 
| 246 | 
            -
            describe "Reminder#parse" do
         | 
| 247 | 
            -
             | 
| 248 | 
            -
              parse '' do
         | 
| 249 | 
            -
                its(:st)     { should == nil }
         | 
| 250 | 
            -
              end
         | 
| 251 | 
            -
             | 
| 252 | 
            -
              parse '2010-01-04 CX' do
         | 
| 253 | 
            -
                its(:st)     { should == "2010-01-04 00:00:00" }
         | 
| 254 | 
            -
                its(:en)     { should == nil }
         | 
| 255 | 
            -
                its(:title)  { should == "CX" }
         | 
| 256 | 
            -
                its(:allday) { should == true }
         | 
| 257 | 
            -
              end
         | 
| 258 | 
            -
             | 
| 259 | 
            -
              parse '2010-01-04 13:30 CX' do
         | 
| 260 | 
            -
                its(:st)     { should == "2010-01-04 13:30:00" }
         | 
| 261 | 
            -
                its(:en)     { should == nil }
         | 
| 262 | 
            -
                its(:title)  { should == "CX" }
         | 
| 263 | 
            -
                its(:allday) { should == false }
         | 
| 264 | 
            -
              end
         | 
| 265 | 
            -
             | 
| 266 | 
            -
              parse '2010-01-04 13:30-14:55 CX' do
         | 
| 267 | 
            -
                its(:st)     { should == "2010-01-04 13:30:00" }
         | 
| 268 | 
            -
                its(:en)     { should == "2010-01-04 14:55:00" }
         | 
| 269 | 
            -
                its(:title)  { should == "CX" }
         | 
| 270 | 
            -
                its(:allday) { should == false }
         | 
| 271 | 
            -
              end
         | 
| 272 | 
            -
             | 
| 273 | 
            -
              parse '2010-01-18 27:15-27:45 TX' do
         | 
| 274 | 
            -
                its(:st)     { should == "2010-01-19 03:15:00" }
         | 
| 275 | 
            -
                its(:en)     { should == "2010-01-19 03:45:00" }
         | 
| 276 | 
            -
                its(:title)  { should == "TX" }
         | 
| 277 | 
            -
                its(:allday) { should == false }
         | 
| 278 | 
            -
              end
         | 
| 279 | 
            -
            end
         |