X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=data%2Frbot%2Fplugins%2Fkeywords.rb;h=da32078096152ccc33fe71f6064eb48fe0764a83;hb=6cf365c49ce5fbe24c0a4ff0663550390b501fea;hp=3ae760241e778a8d1db90a8b8735fc1bdb98cd76;hpb=6c470685174bdee375beb2d968059172d5689575;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/data/rbot/plugins/keywords.rb b/data/rbot/plugins/keywords.rb index 3ae76024..da320780 100644 --- a/data/rbot/plugins/keywords.rb +++ b/data/rbot/plugins/keywords.rb @@ -9,10 +9,10 @@ class Keyword # type of keyword (e.g. "is" or "are") attr_reader :type - + # type:: type of keyword (e.g "is" or "are") # values:: array of values - # + # # create a keyword of type +type+ with values +values+ def initialize(type, values) @type = type.downcase @@ -28,6 +28,19 @@ class Keyword end end + # return an array of all the possible values + def to_factoids(key) + ar = Array.new + @values.each { |val| + debug "key #{key}, value #{val}" + vals = val.split(" or ") + vals.each { |v| + ar << "%s %s %s" % [key, @type, v] + } + } + return ar + end + # describe the keyword (show all values without interpolation) def desc @values.join(" | ") @@ -71,21 +84,27 @@ class Keyword end end -# keyword plugin class. +# keywords class. # # Handles all that stuff like "bot: foo is bar", "bot: foo?" # # Fallback after core and auth have had a look at a message and refused to # handle it, checks for a keyword command or lookup, otherwise the message # is delegated to plugins -class KeywordPlugin < Plugin - BotConfig.register BotConfigBooleanValue.new('keyword.listen', +class Keywords < Plugin + Config.register Config::BooleanValue.new('keyword.listen', :default => false, :desc => "Should the bot listen to all chat and attempt to automatically detect keywords? (e.g. by spotting someone say 'foo is bar')") - BotConfig.register BotConfigBooleanValue.new('keyword.address', + Config.register Config::BooleanValue.new('keyword.address', :default => true, :desc => "Should the bot require that keyword lookups are addressed to it? If not, the bot will attempt to lookup foo if someone says 'foo?' in channel") - + Config.register Config::IntegerValue.new('keyword.search_results', + :default => 3, + :desc => "How many search results to display at a time") + Config.register Config::ArrayValue.new('keyword.ignore_words', + :default => ["how", "that", "these", "they", "this", "what", "when", "where", "who", "why", "you"], + :desc => "A list of words that the bot should passively ignore.") + # create a new KeywordPlugin instance, associated to bot +bot+ def initialize super @@ -95,11 +114,12 @@ class KeywordPlugin < Plugin upgrade_data scan - + # import old format keywords into DBHash - if(File.exist?("#{@bot.botclass}/keywords.rbot")) + olds = @bot.path 'keywords.rbot' + if File.exist? olds log "auto importing old keywords.rbot" - IO.foreach("#{@bot.botclass}/keywords.rbot") do |line| + IO.foreach(olds) do |line| if(line =~ /^(.*?)\s*<=(is|are)?=?>\s*(.*)$/) lhs = $1 mhs = $2 @@ -110,30 +130,20 @@ class KeywordPlugin < Plugin @keywords[lhs] = Keyword.new(mhs, values).dump end end - File.rename("#{@bot.botclass}/keywords.rbot", "#{@bot.botclass}/keywords.rbot.old") + File.rename(olds, olds + ".old") end end - - # drop static keywords and reload them from files, picking up any new - # keyword files that have been added - def rescan - @statickeywords = Hash.new - scan - end # load static keywords from files, picking up any new keyword files that # have been added def scan # first scan for old DBHash files, and convert them - Dir["#{@bot.botclass}/keywords/*"].each {|f| + Dir[datafile('*')].each {|f| next unless f =~ /\.db$/ log "upgrading keyword db #{f} (rbot 0.9.5 or prior) database format" newname = f.gsub(/\.db$/, ".kdb") - old = BDB::Hash.open f, nil, - "r+", 0600 - new = BDB::CIBtree.open(newname, nil, - BDB::CREATE | BDB::EXCL, - 0600) + old = BDB::Hash.open f, nil, "r+", 0600 + new = BDB::CIBtree.open(newname, nil, BDB::CREATE | BDB::EXCL, 0600) old.each {|k,v| new[k] = v } @@ -141,18 +151,18 @@ class KeywordPlugin < Plugin new.close File.delete(f) } - + # then scan for current DBTree files, and load them - Dir["#{@bot.botclass}/keywords/*"].each {|f| + Dir[@bot.path('keywords', '*')].each {|f| next unless f =~ /\.kdb$/ hsh = DBTree.new @bot, f, true key = File.basename(f).gsub(/\.kdb$/, "") debug "keywords module: loading DBTree file #{f}, key #{key}" @statickeywords[key] = hsh } - + # then scan for non DB files, and convert/import them and delete - Dir["#{@bot.botclass}/keywords/*"].each {|f| + Dir[@bot.path('keywords', '*')].each {|f| next if f =~ /\.kdb$/ next if f =~ /CVS$/ log "auto converting keywords from #{f}" @@ -180,28 +190,28 @@ class KeywordPlugin < Plugin # upgrade data files found in old rbot formats to current def upgrade_data - if File.exist?("#{@bot.botclass}/keywords.db") + olds = @bot.path 'keywords.db' + if File.exist? olds log "upgrading old keywords (rbot 0.9.5 or prior) database format" - old = BDB::Hash.open "#{@bot.botclass}/keywords.db", nil, - "r+", 0600 + old = BDB::Hash.open olds, nil, "r+", 0600 old.each {|k,v| @keywords[k] = v } old.close @keywords.flush - File.rename("#{@bot.botclass}/keywords.db", "#{@bot.botclass}/keywords.db.old") + File.rename(olds, olds + ".old") end - - if File.exist?("#{@bot.botclass}/keyword.db") + + olds.replace(@bot.path('keyword.db')) + if File.exist? olds log "upgrading old keywords (rbot 0.9.9 or prior) database format" - old = BDB::CIBtree.open "#{@bot.botclass}/keyword.db", nil, - "r+", 0600 + old = BDB::CIBtree.open olds, nil, "r+", 0600 old.each {|k,v| @keywords[k] = v } old.close @keywords.flush - File.rename("#{@bot.botclass}/keyword.db", "#{@bot.botclass}/keyword.db.old") + File.rename(olds, olds + ".old") end end @@ -209,14 +219,15 @@ class KeywordPlugin < Plugin def save @keywords.flush end + def oldsave - File.open("#{@bot.botclass}/keywords.rbot", "w") do |file| + File.open(@bot.path("keywords.rbot"), "w") do |file| @keywords.each do |key, value| file.puts "#{key}<=#{value.type}=>#{value.dump}" end end end - + # lookup keyword +key+, return it or nil def [](key) return nil if key.nil? @@ -248,41 +259,52 @@ class KeywordPlugin < Plugin return false end + # is +word+ a passively ignored keyword? + def ignored_word?(word) + @bot.config["keyword.ignore_words"].include?(word) + end + # m:: PrivMessage containing message info # key:: key being queried - # dunno:: optional, if true, reply "dunno" if +key+ not found - # + # quiet:: optional, if false, complain if +key+ is not found + # # handle a message asking about a keyword - def keyword(m, key, dunno=true) + def keyword_lookup(m, key, quiet = false) return if key.nil? - unless(kw = self[key]) - m.reply @bot.lang.get("dunno") if (dunno) - return - end - response = kw.to_s - response.gsub!(//, m.sourcenick) - if(response =~ /^\s*(.*)/) - m.reply "#$1" - elsif(response =~ /^\s*(.*)/) - @bot.action m.replyto, "#$1" - elsif(m.public? && response =~ /^\s*(.*)/) - topic = $1 - @bot.topic m.target, topic - else - m.reply "#{key} #{kw.type} #{response}" - end + unless(kw = self[key]) + m.reply "sorry, I don't know about \"#{key}\"" unless quiet + return + end + + response = kw.to_s + response.gsub!(//, m.sourcenick) + + if(response =~ /^\s*(.*)/) + m.reply $1 + elsif(response =~ /^\s*(.*)/) + m.act $1 + elsif(m.public? && response =~ /^\s*(.*)/) + @bot.topic m.target, $1 + else + m.reply "#{key} #{kw.type} #{response}" + end end - + # handle a message which alters a keyword - # like "foo is bar", or "no, foo is baz", or "foo is also qux" - def keyword_command(sourcenick, target, lhs, mhs, rhs, quiet=false) + # like "foo is bar" or "foo is also qux" + def keyword_command(m, lhs, mhs, rhs, quiet = false) debug "got keyword command #{lhs}, #{mhs}, #{rhs}" + return if lhs.strip.empty? + overwrite = false overwrite = true if(lhs.gsub!(/^no,\s*/, "")) + also = false also = true if(rhs.gsub!(/^also\s+/, "")) + values = rhs.split(/\s+\|\s+/) lhs = Keyword.unescape lhs + if(overwrite || also || !has_key?(lhs)) if(also && has_key?(lhs)) kw = self[lhs] @@ -291,67 +313,71 @@ class KeywordPlugin < Plugin else @keywords[lhs] = Keyword.new(mhs, values).dump end - @bot.okay target if !quiet + m.okay if !quiet elsif(has_key?(lhs)) kw = self[lhs] - @bot.say target, "but #{lhs} #{kw.type} #{kw.desc}" if kw && !quiet + m.reply "but #{lhs} #{kw.type} #{kw.desc}" if kw && !quiet end end # return help string for Keywords with option topic +topic+ - def help(plugin, topic="") - case topic - when "overview" - return "set: is , overide: no, is , add to definition: is also , random responses: is | [| ...], plurals: are , escaping: \\is, \\are, \\|, specials: , , " - when "set" - return "set => is " - when "plurals" - return "plurals => are " - when "override" - return "overide => no, is " - when "also" - return "also => is also " - when "random" - return "random responses => is | [| ...]" - when "get" - return "asking for keywords => (with addressing) \"?\", (without addressing) \"'\"" - when "tell" - return "tell about => if is known, tell , via /msg, its definition" - when "forget" - return "forget => forget fact " - when "keywords" - return "keywords => show current keyword counts" - when "" - return " => normal response is \" is \", but if begins with , the response will be \"\"" - when "" - return " => makes keyword respnse \"/me \"" - when "" - return " => replaced with questioner in reply" - when "" - return " => respond by setting the topic to the rest of the definition" - when "search" - return "keyword search [--all] [--full] => search keywords for . If --all is set, search static keywords too, if --full is set, search definitions too." + def help(plugin, topic = '') + case plugin + when /keyword/ + case topic + when 'export' + 'keyword export => exports definitions to keyword_factoids.rbot' + when 'stats' + 'keyword stats => show statistics about static facts' + when 'wipe' + 'keyword wipe => forgets everything about a keyword' + when 'lookup' + 'keyword [lookup] => look up the definition for a keyword; writing "lookup" is optional' + when 'set' + 'keyword set is/are => define a keyword, definition can contain "|" to separate multiple randomly chosen replies' + when 'forget' + 'keyword forget => forget a keyword' + when 'tell' + 'keyword tell about => tell somebody about a keyword' + when 'search' + 'keyword search [--all] [--full] => search keywords for , which can be a regular expression. If --all is set, search static keywords too, if --full is set, search definitions too.' + when 'listen' + 'when the config option "keyword.listen" is set to false, rbot will try to extract keyword definitions from regular channel messages' + when 'address' + 'when the config option "keyword.address" is set to true, rbot will try to answer channel questions of the form "?"' + when '' + ' => normal response is " is ", but if begins with , the response will be ""' + when '' + ' => makes keyword respond with "/me "' + when '' + ' => replaced with questioner in reply' + when '' + ' => respond by setting the topic to the rest of the definition' else - return "Keyword module (Fact learning and regurgitation) topics: overview, set, plurals, override, also, random, get, tell, forget, keywords, keywords search, , , , " + 'keyword module (fact learning and regurgitation) topics: lookup, set, forget, tell, search, listen, address, stats, export, wipe, , , , ' + end + when "forget" + 'forget => forget a keyword' + when "tell" + 'tell about => tell somebody about a keyword' + when "learn" + 'learn that is/are => define a keyword, definition can contain "|" to separate multiple randomly chosen replies' + else + 'keyword module (fact learning and regurgitation) topics: lookup, set, forget, tell, search, listen, address, , , , ' end end # handle a message asking the bot to tell someone about a keyword - def keyword_tell(m, param) - target = param[:target] - key = nil - - # extract the keyword from the message, because unfortunately - # the message mapper doesn't preserve whtiespace - if m.message =~ /about\s+(.+)$/ - key = $1 - end - + def keyword_tell(m, target, key) unless(kw = self[key]) m.reply @bot.lang.get("dunno_about_X") % key return end - + if target == @bot.nick + m.reply "very funny, trying to make me tell something to myself" + return + end + response = kw.to_s response.gsub!(//, m.sourcenick) if(response =~ /^\s*(.*)/) @@ -367,7 +393,7 @@ class KeywordPlugin < Plugin end # return the number of known keywords - def keyword_stats(m, param) + def keyword_stats(m) length = 0 @statickeywords.each {|k,v| length += v.length @@ -376,95 +402,171 @@ class KeywordPlugin < Plugin end # search for keywords, optionally also the definition and the static keywords - def keyword_search(m, param) - str = param[:pattern] - all = (param[:all] == '--all') - full = (param[:full] == '--full') - + def keyword_search(m, key, full = false, all = false, from = 1) begin - re = Regexp.new(str, Regexp::IGNORECASE) - if(@bot.auth.allow?("keyword", m.source, m.replyto)) - matches = Array.new - @keywords.each {|k,v| - kw = Keyword.restore(v) - if re.match(k) || (full && re.match(kw.desc)) - matches << [k,kw] - end - } - if all - @statickeywords.each {|k,v| - v.each {|kk,vv| - kw = Keyword.restore(vv) - if re.match(kk) || (full && re.match(kw.desc)) - matches << [kk,kw] - end - } - } + if key =~ /^\/(.+)\/$/ + re = Regexp.new($1, Regexp::IGNORECASE) + else + re = Regexp.new(Regexp.escape(key), Regexp::IGNORECASE) + end + + matches = Array.new + @keywords.each {|k,v| + kw = Keyword.restore(v) + if re.match(k) || (full && re.match(kw.desc)) + matches << [k,kw] end - if matches.length == 1 - rkw = matches[0] - m.reply "#{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" - elsif matches.length > 0 - i = 0 - matches.each {|rkw| - m.reply "[#{i+1}/#{matches.length}] #{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" - i += 1 - break if i == 3 + } + if all + @statickeywords.each {|k,v| + v.each {|kk,vv| + kw = Keyword.restore(vv) + if re.match(kk) || (full && re.match(kw.desc)) + matches << [kk,kw] + end } - else - m.reply "no keywords match #{str}" + } + end + + if matches.length == 1 + rkw = matches[0] + m.reply "#{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" + elsif matches.length > 0 + if from > matches.length + m.reply "#{matches.length} found, can't tell you about #{from}" + return end + i = 1 + matches.each {|rkw| + m.reply "[#{i}/#{matches.length}] #{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" if i >= from + i += 1 + break if i == from+@bot.config['keyword.search_results'] + } + else + m.reply "no keywords match #{key}" end rescue RegexpError => e - m.reply "no keywords match #{str}: #{e}" + m.reply "no keywords match #{key}: #{e}" rescue debug e.inspect - m.reply "no keywords match #{str}: an error occurred" + m.reply "no keywords match #{key}: an error occurred" end end # forget one of the dynamic keywords - def keyword_forget(m, param) - key = param[:key] - if(@keywords.has_key?(key)) - @keywords.delete(key) - @bot.okay m.replyto + def keyword_forget(m, key) + if @keywords.delete(key) + m.okay + else + m.reply _("couldn't find keyword %{key}" % { :key => key }) + end + end + + # low-level keyword wipe command for when forget doesn't work + def keyword_wipe(m, key) + reg = @keywords.registry + reg.env.begin(reg) { |t, b| + b.delete_if { |k, v| + (k == key) && (m.reply "wiping keyword #{key} with stored value #{Marshal.restore(v)}") + } + t.commit + } + m.reply "done" + end + + # export keywords to factoids file + def keyword_factoids_export + ar = Array.new + + debug @keywords.keys + + @keywords.each { |k, val| + next unless val + kw = Keyword.restore(val) + ar |= kw.to_factoids(k) + } + + # TODO check factoids config + # also TODO: runtime export + dir = @bot.path 'factoids' + fname = File.join(dir,"keyword_factoids.rbot") + + Dir.mkdir(dir) unless FileTest.directory?(dir) + Utils.safe_save(fname) do |file| + file.puts ar end end # privmsg handler - def listen(m) - return if m.replied? - if(m.address?) - if(!(m.message =~ /\\\?\s*$/) && m.message =~ /^(.*\S)\s*\?\s*$/) - keyword m, $1 if(@bot.auth.allow?("keyword", m.source, m.replyto)) - elsif(m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/) - keyword_command(m.sourcenick, m.replyto, $1, $2, $3) if(@bot.auth.allow?("keycmd", m.source, m.replyto)) + def privmsg(m) + case m.plugin + when "keyword" + case m.params + when /^export$/ + begin + keyword_factoids_export + m.okay + rescue + m.reply _("failed to export keywords as factoids (%{err})" % {:err => $!}) + end + when /^set\s+(.+?)\s+(is|are)\s+(.+)$/ + keyword_command(m, $1, $2, $3) if @bot.auth.allow?('keycmd', m.source, m.replyto) + when /^forget\s+(.+)$/ + keyword_forget(m, $1) if @bot.auth.allow?('keycmd', m.source, m.replyto) + when /^wipe\s(.+)$/ # note that only one space is stripped, allowing removal of space-prefixed keywords + keyword_wipe(m, $1) if @bot.auth.allow?('keycmd', m.source, m.replyto) + when /^lookup\s+(.+)$/ + keyword_lookup(m, $1) if @bot.auth.allow?('keyword', m.source, m.replyto) + when /^stats\s*$/ + keyword_stats(m) if @bot.auth.allow?('keyword', m.source, m.replyto) + when /^search\s+(.+)$/ + key = $1 + full = key.sub!('--full ', '') + all = key.sub!('--all ', '') + if key.sub!(/--from (\d+) /, '') + from = $1.to_i + else + from = 1 + end + from = 1 unless from > 0 + keyword_search(m, key, full, all, from) if @bot.auth.allow?('keyword', m.source, m.replyto) + when /^tell\s+(\S+)\s+about\s+(.+)$/ + keyword_tell(m, $1, $2) if @bot.auth.allow?('keyword', m.source, m.replyto) + else + keyword_lookup(m, m.params) if @bot.auth.allow?('keyword', m.source, m.replyto) end - else - # in channel message, not to me - # TODO option to do if(m.message =~ /^(.*)$/, ie try any line as a - # keyword lookup. - if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.address"] && m.message =~ /^(.*\S)\s*\?\s*$/)) - keyword m, $1, false if(@bot.auth.allow?("keyword", m.source)) - elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/)) - # TODO MUCH more selective on what's allowed here - keyword_command(m.sourcenick, m.replyto, $1, $2, $3, true) if(@bot.auth.allow?("keycmd", m.source)) + when "forget" + keyword_forget(m, m.params) if @bot.auth.allow?('keycmd', m.source, m.replyto) + when "tell" + if m.params =~ /(\S+)\s+about\s+(.+)$/ + keyword_tell(m, $1, $2) if @bot.auth.allow?('keyword', m.source, m.replyto) + else + m.reply "wrong 'tell' syntax" + end + when "learn" + if m.params =~ /^that\s+(.+?)\s+(is|are)\s+(.+)$/ + keyword_command(m, $1, $2, $3) if @bot.auth.allow?('keycmd', m.source, m.replyto) + else + m.reply "wrong 'learn' syntax" end end end -end -plugin = KeywordPlugin.new - -plugin.map 'keyword stats', :action => 'keyword_stats' + def unreplied(m) + # TODO option to do if(m.message =~ /^(.*)$/, ie try any line as a + # keyword lookup. + if m.message =~ /^(.*\S)\s*\?\s*$/ and (m.address? or not @bot.config["keyword.address"]) + keyword_lookup m, $1, true if !ignored_word?($1) && @bot.auth.allow?("keyword", m.source) + elsif @bot.config["keyword.listen"] && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/) + # TODO MUCH more selective on what's allowed here + keyword_command m, $1, $2, $3, true if !ignored_word?($1) && @bot.auth.allow?("keycmd", m.source) + end + end +end -plugin.map 'keyword search :all :full :pattern', :action => 'keyword_search', - :defaults => {:all => '', :full => ''}, - :requirements => {:all => '--all', :full => '--full'} - -plugin.map 'keyword forget :key', :action => 'keyword_forget' -plugin.map 'forget :key', :action => 'keyword_forget', :auth => 'keycmd' +plugin = Keywords.new +plugin.register 'keyword' +plugin.register 'forget' rescue nil +plugin.register 'tell' rescue nil +plugin.register 'learn' rescue nil -plugin.map 'keyword tell :target about *keyword', :action => 'keyword_tell' -plugin.map 'tell :target about *keyword', :action => 'keyword_tell', :auth => 'keyword'