ircbot 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|