4 # :title: Quotes plugin
8 require 'rexml/document'
11 define_structure :Quote, :num, :date, :source, :quote
13 class QuotePlugin < Plugin
14 Config.register Config::StringValue.new('quotes.html_path',
15 :default => '/var/www/html/quotes',
16 :desc => "Where to dump our output, the user the bot runs as needs write access here")
18 Config.register Config::StringValue.new('quotes.header_template',
19 :default => 'templates/header',
20 :desc => "HTML 'header' file that will be used to build HTML output. You can use the following variables: %%channel%% - channel name")
22 Config.register Config::StringValue.new('quotes.body_template',
23 :default => 'templates/body',
24 :desc => "HTML 'body' file that will be used to build HTML output. This will be used to generate HTML for each quote in the channel. You can use the following variables: %%timestamp%% - when the quote was recorded ; %%id%% - the quote's id number (for getquote) ; %%author%% - nick of the person who added the quote ; %%channel%% - channel name ; %%quote%% - the actual quote")
26 Config.register Config::StringValue.new('quotes.footer_template',
27 :default => 'templates/footer',
28 :desc => "HTML 'footer' file that will be used to build HTML output. You can use the following variables: %%channel%% - channel name")
38 Dir[datafile '*'].each {|f|
39 next if File.directory?(f)
40 channel = File.basename(f)
41 @lists[channel] = Array.new if(!@lists.has_key?(channel))
43 if(line =~ /^(\d+) \| ([^|]+) \| (\S+) \| (.*)$/)
45 @lists[channel][num] = Quote.new(num, $2, $3, $4)
48 @changed[channel] = false
53 Dir.mkdir(datafile) unless FileTest.directory? datafile
54 @lists.each {|channel, quotes|
57 debug "Writing new quotefile for channel #{channel} ..."
58 Utils.safe_save(datafile channel) {|file|
59 quotes.compact.each {|q|
60 file.puts "#{q.num} | #{q.date} | #{q.source} | #{q.quote}"
63 @changed[channel] = false
65 debug "Not writing quotefile for channel #{channel} (unchanged)"
68 error "failed to write quotefile for channel #{channel}!\n#{$!}"
69 error "#{e.class}: #{e}"
70 error e.backtrace.join("\n")
75 def cmd_dumptoxml(m, p)
76 destDir = @bot.config['quotes.html_path']
78 Dir.mkdir(destDir) unless FileTest.directory?(destDir)
79 @lists.each { |channel, quotes|
81 doc = REXML::Document.new
83 doc.add_element("channel", {"name" => channel})
84 quoteList = REXML::Element.new("quotes")
86 quotes.compact.each{ |q|
87 node = REXML::Element.new("quote")
88 node.add_attributes({"date" => q.date,
90 "author" => q.source[0,q.source.index("!")]})
91 node.add_text(q.quote.gsub(/[\x00-\x1f]/, ''))
92 quoteList.add_element(node)
95 doc.root.add_element(quoteList)
97 filePath = "#{destDir}/#{channel.delete("#")}.xml"
98 outF = File.new(filePath, "w")
102 error "Failed to dump quotes for channel #{channel}!\n#{$!}"
103 error "#{e.class}: #{e}"
104 error e.backtrace.join("\n")
105 m.reply("Hrm, that didn't work for #{channel}. Check the logs")
111 def cmd_dumptohtml(m, p)
112 destDir = @bot.config['quotes.html_path']
115 headerF = File.new(datafile(@bot.config['quotes.header_template']))
116 headerLines = headerF.readlines
119 bodyF = File.new(datafile(@bot.config['quotes.body_template']))
120 bodyLines = bodyF.readlines
123 footerF = File.new(datafile(@bot.config['quotes.footer_template']))
124 footerLines = footerF.readlines
127 error "Had problems with templates!"
128 error "#{e.class}: #{e}"
129 error e.backtrace.join("\n")
130 m.reply("Had problems with the templates. Check the logs")
133 Dir.mkdir(destDir) unless FileTest.directory?(destDir)
134 @lists.each { |channel, quotes|
136 filePath = "#{destDir}/#{channel.delete("#")}.html"
137 outF = File.new(filePath, "w")
139 headerSubs = { "%%channel%%" => channel }
141 headerLines.each { |line|
142 headerSubs.each { |pattern, value| line = line.gsub(pattern, value) }
146 quotes.compact.each{ |q|
147 bodySubs = { "%%timestamp%%" => q.date.to_s,
148 "%%id%%" => CGI.escapeHTML(q.num.to_s),
149 "%%author%%" => CGI.escapeHTML(q.source[0, q.source.index("!")]),
150 "%%channel%%" => CGI.escapeHTML(channel),
151 "%%quote%%" => CGI.escapeHTML(q.quote.gsub(/[\x00-\x1f]/, '')),
154 bodyLines.each { |line|
155 bodySubs.each { |pattern, value| line = line.gsub(pattern, value) }
160 footerSubs = { "%%channel%%" => channel }
161 footerLines.each { |line|
162 footerSubs.each { |pattern, value| line = line.gsub(pattern, value) }
168 error "Failed to dump quotes for channel #{channel}!\n#{$!}"
169 error "#{e.class}: #{e}"
170 error e.backtrace.join("\n")
171 m.reply("Hrm, that didn't work for #{channel}. Check the logs")
183 def lastquote(channel)
184 @lists[channel].length-1
187 def addquote(source, channel, quote)
188 @lists[channel] = Array.new if(!@lists.has_key?(channel))
189 num = @lists[channel].length
190 @lists[channel][num] = Quote.new(num, Time.new, source.fullform, quote)
191 @changed[channel] = true
195 def getquote(source, channel, num=nil)
196 return nil unless(@lists.has_key?(channel))
197 return nil unless(@lists[channel].length > 0)
199 if(@lists[channel][num])
200 return @lists[channel][num], @lists[channel].length - 1
204 return @lists[channel].compact[rand(@lists[channel].nitems)],
205 @lists[channel].length - 1
209 def delquote(channel, num)
210 return false unless(@lists.has_key?(channel))
211 return false unless(@lists[channel].length > 0)
212 if(@lists[channel][num])
213 @lists[channel][num] = nil
214 @lists[channel].pop if num == @lists[channel].length - 1
215 @changed[channel] = true
221 def countquote(source, channel=nil, regexp=nil)
224 @lists.each_value {|l|
225 total += l.compact.length
229 return 0 unless(@lists.has_key?(channel))
230 return 0 unless(@lists[channel].length > 0)
232 matches = @lists[channel].compact.find_all {|a| a.quote =~ /#{regexp}/i }
234 matches = @lists[channel].compact
236 return matches.length
239 def searchquote(source, channel, regexp)
240 return nil unless(@lists.has_key?(channel))
241 return nil unless(@lists[channel].length > 0)
242 matches = @lists[channel].compact.find_all {|a| a.quote =~ /#{regexp}/i }
243 if(matches.length > 0)
244 return matches[rand(matches.length)], @lists[channel].length - 1
250 def listquotes(source, channel, regexp)
251 return nil unless(@lists.has_key?(channel))
252 return nil unless(@lists[channel].length > 0)
253 matches = @lists[channel].compact.find_all {|a| a.quote =~ /#{regexp}/i }
254 if matches.length > 0
261 def help(plugin, topic="")
264 _("addquote [<channel>] <quote> => Add quote <quote> for channel <channel>. You only need to supply <channel> if you are addressing %{nick} privately.") % { :nick => @bot.nick }
266 _("delquote [<channel>] <num> => delete quote from <channel> with number <num>. You only need to supply <channel> if you are addressing %{nick} privately.") % { :nick => @bot.nick }
268 _("getquote [<channel>] [<num>] => get quote from <channel> with number <num>. You only need to supply <channel> if you are addressing %{nick} privately. Without <num>, a random quote will be returned.") % { :nick => @bot.nick }
270 _("searchquote [<channel>] <regexp> => search for quote from <channel> that matches <regexp>. You only need to supply <channel> if you are addressing %{nick} privately.") % { :nick => @bot.nick }
272 _("listquotes [<channel>] <regexp> => list the quotes from <channel> that match <regexp>. You only need to supply <channel> if you are addressing %{nick} privately.") % { :nick => @bot.nick }
274 _("topicquote [<channel>] [<num>] => set topic to quote from <channel> with number <num>. You only need to supply <channel> if you are addressing %{nick} privately. Without <num>, a random quote will be set.") % { :nick => @bot.nick }
276 _("countquote [<channel>] <regexp> => count quotes from <channel> that match <regexp>. You only need to supply <channel> if you are addressing %{nick} privately.") % { :nick => @bot.nick }
278 _("whoquote [<channel>] <num> => show who added quote <num>. You only need to supply <channel> if you are addressing %{nick} privately") % { :nick => @bot.nick }
280 _("whenquote [<channel>] <num> => show when quote <num> was added. You only need to supply <channel> if you are addressing %{nick} privately") % { :nick => @bot.nick }
282 _("lastquote [<channel>] => show the last quote in a given channel. You only need to supply <channel> if you are addressing %{nick} privately") % { :nick => @bot.nick }
284 _("Quote module (Quote storage and retrieval) topics: addquote, delquote, getquote, searchquote, listquotes, topicquote, countquote, whoquote, whenquote, lastquote") % { :nick => @bot.nick }
288 def cmd_addquote(m, p)
289 channel = p[:channel] || m.channel.to_s
290 quote = p[:quote].to_s
291 num = addquote(m.source, channel, quote)
292 m.reply _("added the quote (#%{num})") % { :num => num }
295 def cmd_delquote(m, p)
296 channel = p[:channel] || m.channel.to_s
298 if delquote(channel, num)
301 m.reply _("quote not found!")
305 def cmd_getquote(m, p)
306 channel = p[:channel] || m.channel.to_s
307 num = p[:num] ? p[:num].to_i : nil
308 quote, total = getquote(m.source, channel, num)
310 m.reply _("[%{num}] %{quote}") % {
312 :quote => quote.quote
315 m.reply _("quote not found!")
319 def cmd_whoquote(m, p)
320 channel = p[:channel] || m.channel.to_s
321 num = p[:num] ? p[:num].to_i : nil
322 quote, total = getquote(m.source, channel, num)
324 m.reply _("quote %{num} added by %{source}") % {
326 :source => quote.source
329 m.reply _("quote not found!")
333 def cmd_whenquote(m, p)
334 channel = p[:channel] || m.channel.to_s
335 num = p[:num] ? p[:num].to_i : nil
336 quote, total = getquote(m.source, channel, num)
338 m.reply _("quote %{num} added on %{date}") % {
343 m.reply _("quote not found!")
347 def cmd_searchquote(m, p)
348 channel = p[:channel] || m.channel.to_s
350 quote, total = searchquote(m.source, channel, reg)
352 m.reply _("[%{num}] %{quote}") % {
354 :quote => quote.quote
357 m.reply _("quote not found!")
361 def cmd_listquotes(m, p)
362 channel = p[:channel] || m.channel.to_s
364 if quotes = listquotes(m.source, channel, reg)
365 m.reply _("%{total} quotes matching %{reg} found: %{list}") % {
366 :total => quotes.size,
368 :list => quotes.map {|q| q.num }.join(', ')
371 m.reply _("quote not found!")
375 def cmd_countquote(m, p)
376 channel = p[:channel] || m.channel.to_s
377 reg = p[:reg] ? p[:reg].to_s : nil
378 total = countquote(m.source, channel, reg)
380 m.reply _("%{total} quotes matching %{reg}") % {
385 m.reply _("%{total} quotes") % { :total => total }
389 def cmd_topicquote(m, p)
390 channel = p[:channel] || m.channel.to_s
391 num = p[:num] ? p[:num].to_i : nil
392 quote, total = getquote(m.source, channel, num)
394 @bot.topic channel, _("[%{num}] %{quote}") % {
396 :quote => quote.quote
399 m.reply _("quote not found!")
403 def cmd_lastquote(m, p)
404 channel = p[:channel] || m.channel.to_s
405 quote, total = getquote(m.source, channel, lastquote(channel))
407 m.reply _("[%{num}] %{quote}") % {
409 :quote => quote.quote
412 m.reply _("quote not found!")
417 plugin = QuotePlugin.new
418 plugin.register("quotes")
420 plugin.default_auth('other::edit', false) # Prevent random people from editing other channels quote lists by default
421 plugin.default_auth('other::view', true) # But allow them to view them
423 plugin.map "addquote :channel *quote", :action => :cmd_addquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN }, :auth_path => '!quote::other::edit::add!'
424 plugin.map "delquote :channel :num", :action => :cmd_delquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN, :num => /^\d+$/ }, :auth_path => '!quote::other::edit::del!'
425 plugin.map "getquote :channel [:num]", :action => :cmd_getquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN, :num => /^\d+$/ }, :auth_path => '!quote::other::view::get!'
426 plugin.map "whoquote :channel :num", :action => :cmd_whoquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN, :num => /^\d+$/ }, :auth_path => '!quote::other::view::who!'
427 plugin.map "whenquote :channel :num", :action => :cmd_whenquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN, :num => /^\d+$/ }, :auth_path => '!quote::other::view::when!'
428 plugin.map "searchquote :channel *reg", :action => :cmd_searchquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN }, :auth_path => '!quote::other::view::search!'
429 plugin.map "listquotes :channel *reg", :action => :cmd_listquotes, :requirements => { :channel => Regexp::Irc::GEN_CHAN }, :auth_path => '!quote::other::view::list!'
430 plugin.map "countquote :channel [*reg]", :action => :cmd_countquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN }, :auth_path => '!quote::other::view::count!'
431 plugin.map "topicquote :channel [:num]", :action => :cmd_topicquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN, :num => /^\d+$/ }, :auth_path => '!quote::other::topic!'
432 plugin.map "lastquote :channel", :action => :cmd_lastquote, :requirements => { :channel => Regexp::Irc::GEN_CHAN }, :auth_path => '!quote::other::view::last!'
434 plugin.default_auth('edit', false) # Prevent random people from removing quotes
435 plugin.default_auth('edit::add', true) # But allow them to add them
437 plugin.map "addquote *quote", :action => :cmd_addquote, :private => false, :auth_path => '!quote::edit::add!'
438 plugin.map "delquote :num", :action => :cmd_delquote, :private => false, :requirements => { :num => /^\d+$/ }, :auth_path => '!quote::edit::del!'
439 plugin.map "getquote [:num]", :action => :cmd_getquote, :private => false, :requirements => { :num => /^\d+$/ }, :auth_path => '!quote::view::get!'
440 plugin.map "whoquote :num", :action => :cmd_whoquote, :private => false, :requirements => { :num => /^\d+$/ }, :auth_path => '!quote::view::who!'
441 plugin.map "whenquote :num", :action => :cmd_whenquote, :private => false, :requirements => { :num => /^\d+$/ }, :auth_path => '!quote::view::when!'
442 plugin.map "searchquote *reg", :action => :cmd_searchquote, :private => false, :auth_path => '!quote::view::search!'
443 plugin.map "listquotes *reg", :action => :cmd_listquotes, :private => false, :auth_path => '!quote::view::list!'
444 plugin.map "countquote [*reg]", :action => :cmd_countquote, :private => false, :auth_path => '!quote::view::count!'
445 plugin.map "topicquote [:num]", :action => :cmd_topicquote, :private => false, :requirements => { :num => /^\d+$/ }, :auth_path => '!quote::topic!'
446 plugin.map "lastquote", :action => :cmd_lastquote, :private => false, :auth_path => '!quote::view::last!'
448 plugin.default_auth('dump', false) # Prevent random people from dumping the database
449 plugin.map "dumpxml", :action => :cmd_dumptoxml, :auth_path => '!quote::dump!'
450 plugin.map "dumphtml", :action => :cmd_dumptohtml, :auth_path => '!quote::dump!'