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
|