rbot 0.9.9 → 0.9.10

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.
Files changed (72) hide show
  1. data/AUTHORS +8 -0
  2. data/ChangeLog +51 -0
  3. data/INSTALL +4 -0
  4. data/README +1 -0
  5. data/REQUIREMENTS +11 -0
  6. data/TODO +2 -0
  7. data/bin/rbot +21 -2
  8. data/data/rbot/languages/german.lang +4 -1
  9. data/data/rbot/languages/russian.lang +75 -0
  10. data/data/rbot/plugins/autoop.rb +42 -51
  11. data/data/rbot/plugins/bans.rb +205 -0
  12. data/data/rbot/plugins/bash.rb +56 -0
  13. data/data/rbot/plugins/chucknorris.rb +74 -0
  14. data/data/rbot/plugins/chucknorris.yml.gz +0 -0
  15. data/data/rbot/plugins/deepthoughts.rb +95 -0
  16. data/data/rbot/plugins/demauro.rb +95 -0
  17. data/data/rbot/plugins/digg.rb +51 -0
  18. data/data/rbot/plugins/figlet.rb +24 -0
  19. data/data/rbot/plugins/forecast.rb +133 -0
  20. data/data/rbot/plugins/freshmeat.rb +13 -7
  21. data/data/rbot/plugins/google.rb +2 -0
  22. data/data/rbot/plugins/grouphug.rb +36 -0
  23. data/data/rbot/plugins/imdb.rb +92 -0
  24. data/data/rbot/plugins/insult.rb +8 -1
  25. data/data/rbot/plugins/iplookup.rb +227 -0
  26. data/data/rbot/plugins/karma.rb +2 -2
  27. data/data/rbot/plugins/keywords.rb +470 -0
  28. data/data/rbot/plugins/lart.rb +132 -146
  29. data/data/rbot/plugins/lastfm.rb +25 -0
  30. data/data/rbot/plugins/markov.rb +204 -0
  31. data/data/rbot/plugins/math.rb +5 -1
  32. data/data/rbot/plugins/nickserv.rb +71 -11
  33. data/data/rbot/plugins/opme.rb +19 -19
  34. data/data/rbot/plugins/quakeauth.rb +2 -2
  35. data/data/rbot/plugins/quotes.rb +40 -25
  36. data/data/rbot/plugins/remind.rb +1 -1
  37. data/data/rbot/plugins/rot13.rb +2 -2
  38. data/data/rbot/plugins/roulette.rb +49 -15
  39. data/data/rbot/plugins/rss.rb +585 -0
  40. data/data/rbot/plugins/rubyurl.rb +39 -0
  41. data/data/rbot/plugins/seen.rb +2 -1
  42. data/data/rbot/plugins/slashdot.rb +5 -5
  43. data/data/rbot/plugins/spell.rb +5 -0
  44. data/data/rbot/plugins/theyfightcrime.rb +121 -0
  45. data/data/rbot/plugins/threat.rb +55 -0
  46. data/data/rbot/plugins/tinyurl.rb +39 -0
  47. data/data/rbot/plugins/topic.rb +204 -0
  48. data/data/rbot/plugins/urban.rb +71 -0
  49. data/data/rbot/plugins/url.rb +399 -4
  50. data/data/rbot/plugins/wow.rb +123 -0
  51. data/data/rbot/plugins/wserver.rb +1 -1
  52. data/data/rbot/templates/levels.rbot +2 -0
  53. data/lib/rbot/auth.rb +207 -96
  54. data/lib/rbot/channel.rb +5 -5
  55. data/lib/rbot/config.rb +125 -24
  56. data/lib/rbot/dbhash.rb +87 -21
  57. data/lib/rbot/httputil.rb +181 -13
  58. data/lib/rbot/ircbot.rb +525 -179
  59. data/lib/rbot/ircsocket.rb +330 -54
  60. data/lib/rbot/message.rb +66 -23
  61. data/lib/rbot/messagemapper.rb +25 -17
  62. data/lib/rbot/plugins.rb +244 -115
  63. data/lib/rbot/post-clean.rb +1 -0
  64. data/lib/rbot/{post-install.rb → post-config.rb} +1 -1
  65. data/lib/rbot/rbotconfig.rb +29 -14
  66. data/lib/rbot/registry.rb +111 -72
  67. data/lib/rbot/rfc2812.rb +208 -197
  68. data/lib/rbot/timer.rb +4 -0
  69. data/lib/rbot/utils.rb +2 -2
  70. metadata +127 -104
  71. data/data/rbot/plugins/rss.rb.disabled +0 -414
  72. data/lib/rbot/keywords.rb +0 -433
@@ -58,16 +58,22 @@ class FreshmeatPlugin < Plugin
58
58
  def freshmeat(m, params)
59
59
  max = params[:limit].to_i
60
60
  max = 8 if max > 8
61
- xml = @bot.httputil.get(URI.parse("http://images.feedstermedia.com/feedcache/ostg/freshmeat/fm-releases-global.xml"))
62
- unless xml
63
- m.reply "freshmeat news parse failed"
64
- return
65
- end
66
- doc = Document.new xml
67
- unless doc
61
+ begin
62
+ xml = @bot.httputil.get(URI.parse("http://images.feedstermedia.com/feedcache/ostg/freshmeat/fm-releases-global.xml"))
63
+ unless xml
64
+ m.reply "freshmeat news parse failed"
65
+ return
66
+ end
67
+ doc = Document.new xml
68
+ unless doc
69
+ m.reply "freshmeat news parse failed"
70
+ return
71
+ end
72
+ rescue
68
73
  m.reply "freshmeat news parse failed"
69
74
  return
70
75
  end
76
+
71
77
  matches = Array.new
72
78
  max_width = 60
73
79
  title_width = 0
@@ -49,3 +49,5 @@ class GooglePlugin < Plugin
49
49
  end
50
50
  plugin = GooglePlugin.new
51
51
  plugin.register("search")
52
+ plugin.register("google")
53
+
@@ -0,0 +1,36 @@
1
+ # Plugin for the Ruby IRC bot (http://linuxbrit.co.uk/rbot/)
2
+ # (c) 2005 Mark Kretschmann <markey@web.de>
3
+ # Licensed under GPL V2.
4
+
5
+ require "net/http"
6
+
7
+
8
+ class GrouphugPlugin < Plugin
9
+ def help( plugin, topic="" )
10
+ "Grouphug plugin. Confess! Usage: 'confess' for random confession, 'confess <number>' for specific one."
11
+ end
12
+
13
+ def privmsg( m )
14
+ path = "/random"
15
+ path = "/confessions/#{m.params()}" if m.params()
16
+ begin
17
+ data = bot.httputil.get(URI.parse("http://grouphug.us/#{path}"))
18
+
19
+ reg = Regexp.new( '(<td class="conf-text")(.*?)(<p>)(.*?)(</p>)', Regexp::MULTILINE )
20
+ confession = reg.match( data )[4]
21
+ confession.gsub!( /<.*?>/, "" ) # Remove html tags
22
+ confession.gsub!( "\t", "" ) # Remove tab characters
23
+
24
+ @bot.say(m.replyto, confession)
25
+ rescue
26
+ m.reply "failed to connect to grouphug.us"
27
+ end
28
+ end
29
+ end
30
+
31
+
32
+ plugin = GrouphugPlugin.new
33
+
34
+ plugin.register("grouphug")
35
+ plugin.register("confess")
36
+
@@ -0,0 +1,92 @@
1
+ # IMDB plugin for RubyBot
2
+ # (c) 2005 Arnaud Cornet <arnaud.cornet@gmail.com>
3
+ # Licensed under MIT License.
4
+
5
+ require 'net/http'
6
+ require 'cgi'
7
+ require 'uri/common'
8
+
9
+ class Imdb
10
+ def initialize(bot)
11
+ @bot = bot
12
+ end
13
+
14
+ def search(rawstr)
15
+ str = URI.escape(rawstr)
16
+ @http = @bot.httputil.get_proxy(URI.parse("http://us.imdb.com/find?q=#{str}"))
17
+ @http.start
18
+ begin
19
+ resp, data = @http.get("/find?q=#{str}", "User-Agent" => "Mozilla/5.0")
20
+ rescue Net::ProtoRetriableError => detail
21
+ head = detail.data
22
+ if head.code == "301" or head.code == "302"
23
+ return head['location'].gsub(/http:\/\/us.imdb.com/, "").gsub(/\?.*/, "")
24
+ end
25
+ end
26
+ if resp.code == "200"
27
+ m = /<a href="(\/title\/tt[0-9]+\/?)[^"]*"(:?[^>]*)>([^<]*)<\/a>/.match(resp.body)
28
+ if m
29
+ url = m[1]
30
+ title = m[2]
31
+ return url
32
+ end
33
+ elsif resp.code == "302"
34
+ return resp['location'].gsub(/http:\/\/us.imdb.com/, "").gsub(/\?.*/, "")
35
+ end
36
+ return nil
37
+ end
38
+
39
+ def info(rawstr)
40
+ sr = search(rawstr)
41
+ if !sr
42
+ debug "IMDB: search returned NIL"
43
+ return nil
44
+ end
45
+ resp, data = @http.get(sr, "User-Agent" =>
46
+ "Mozilla/5.0 (compatible; Konqueror/3.1; Linux)")
47
+ if resp.code == "200"
48
+ m = /<title>([^<]*)<\/title>/.match(resp.body)
49
+ return nil if !m
50
+ title = CGI.unescapeHTML(m[1])
51
+
52
+ m = /<b>([0-9.]+)\/10<\/b> \(([0-9,]+) votes?\)/.match(resp.body)
53
+ return nil if !m
54
+ score = m[1]
55
+ votes = m[2]
56
+
57
+ genre = Array.new
58
+ resp.body.scan(/<a href="\/Sections\/Genres\/[^\/]+\/">([^<]+)<\/a>/) do |gnr|
59
+ genre << gnr
60
+ end
61
+ return ["http://us.imdb.com" + sr, title, score, votes,
62
+ genre]
63
+ end
64
+ return nil
65
+ end
66
+ end
67
+
68
+ class ImdbPlugin < Plugin
69
+ def help(plugin, topic="")
70
+ "imdb <string> => search http://www.imdb.org for <string>"
71
+ end
72
+
73
+ def privmsg(m)
74
+ unless(m.params && m.params.length > 0)
75
+ m.reply "incorrect usage: " + help(m.plugin)
76
+ return
77
+ end
78
+
79
+ i = Imdb.new(@bot)
80
+ info = i.info(m.params)
81
+ if !info
82
+ m.reply "Nothing found for #{m.params}"
83
+ return nil
84
+ end
85
+ m.reply "#{info[1]} : #{info[0]}"
86
+ m.reply "Ratings: #{info[2]}/10 (#{info[3]} voters). Genre: #{info[4].join('/')}"
87
+ end
88
+ end
89
+
90
+ plugin = ImdbPlugin.new
91
+ plugin.register("imdb")
92
+
@@ -216,6 +216,9 @@ class InsultPlugin < Plugin
216
216
  return "insult module topics: msginsult, insult"
217
217
  end
218
218
  end
219
+ def name
220
+ "insult"
221
+ end
219
222
  def privmsg(m)
220
223
  suffix=""
221
224
  unless(m.params)
@@ -233,7 +236,11 @@ class InsultPlugin < Plugin
233
236
  elsif(m.params =~ /^me$/)
234
237
  prefix = "you are "
235
238
  else
236
- prefix = "#{m.params} is "
239
+ who = m.params
240
+ if (who == @bot.nick)
241
+ who = m.sourcenick
242
+ end
243
+ prefix = "#{who} is "
237
244
  end
238
245
  insult = generate_insult
239
246
  @bot.say msgto, prefix + insult + suffix
@@ -0,0 +1,227 @@
1
+ #################################################################
2
+ # IP Lookup Plugin
3
+ # ----------------------------
4
+ # by Chris Gahan (chris@ill-logic.com)
5
+ #
6
+ # Purpose:
7
+ # ------------------
8
+ # Lets you lookup the owner and their address for any IP address
9
+ # or IRC user.
10
+ #
11
+ #################################################################
12
+
13
+ require 'socket'
14
+ require 'resolv'
15
+
16
+ #################################################################
17
+ ## ARIN Whois module...
18
+ ##
19
+
20
+ module ArinWhois
21
+
22
+ class Chunk < Hash
23
+ def customer?
24
+ keys.grep(/^(City|Address|StateProv|(Org|Cust)Name)$/).any?
25
+ end
26
+
27
+ def network?
28
+ keys.grep(/^(CIDR|NetHandle|Parent)$/).any?
29
+ end
30
+
31
+ def contact?
32
+ keys.grep(/^(R|Org)(Tech|Abuse)(Handle|Name|Phone|Email)$/).any?
33
+ end
34
+
35
+ def valid?
36
+ customer? or network? or contact?
37
+ end
38
+
39
+ def owner
40
+ self[keys.grep(/^(Org|Cust)Name$/).first]
41
+ end
42
+
43
+ def location
44
+ [ self['City'], self['StateProv'], self['Country'] ].compact.join(', ')
45
+ end
46
+
47
+ def address
48
+ [ self['Address'], location, self['PostalCode'] ].compact.join(', ')
49
+ end
50
+
51
+ end
52
+
53
+ class ArinWhoisParser
54
+
55
+ def initialize(data)
56
+ @data = data
57
+ end
58
+
59
+ def split_array_at(a, &block)
60
+ return a unless a.any?
61
+ a = a.to_a
62
+
63
+ results = []
64
+ last_cutpoint = 0
65
+
66
+ a.each_with_index do |el,i|
67
+ if block.call(el)
68
+ unless i == 0
69
+ results << a[last_cutpoint...i]
70
+ last_cutpoint = i
71
+ end
72
+ end
73
+ end
74
+
75
+ if last_cutpoint < a.size or last_cutpoint == 0
76
+ results << a[last_cutpoint..-1]
77
+ end
78
+
79
+ results
80
+ end
81
+
82
+ # Whois output format
83
+ # ------------------------
84
+ # Owner info block:
85
+ # {Org,Cust}Name
86
+ # Address
87
+ # City
88
+ # StateProv
89
+ # PostalCode
90
+ # Country (2-digit)
91
+ #
92
+ # Network Information:
93
+ # CIDR (69.195.25.0/25)
94
+ # NetHandle (NET-72-14-192-0-1)
95
+ # Parent (NET-72-0-0-0-0)
96
+ #
97
+ # Contacts:
98
+ # ({R,Org}{Tech,Abuse}{Handle,Name,Phone,Email})*
99
+
100
+ def parse_chunks
101
+ return if @data =~ /^No match found /
102
+ chunks = @data.gsub(/^# ARIN WHOIS database, last updated.+/m, '').scan(/(([^\n]+\n)+\n)/m)
103
+ chunks.map do |chunk|
104
+ result = Chunk.new
105
+
106
+ chunk[0].scan(/([A-Za-z]+?):(.*)/).each do |tuple|
107
+ tuple[1].strip!
108
+ result[tuple[0]] = tuple[1].empty? ? nil : tuple[1]
109
+ end
110
+
111
+ result
112
+ end
113
+ end
114
+
115
+
116
+ def get_parsed_data
117
+ return unless chunks = parse_chunks
118
+
119
+ results = split_array_at(parse_chunks) {|chunk|chunk.customer?}
120
+ results.map do |chunks|
121
+ {
122
+ :customer => chunks.select{|x|x.customer?}[0],
123
+ :net => chunks.select{|x|x.network?}[0],
124
+ :contacts => chunks.select{|x|x.contact?}
125
+ }
126
+ end
127
+ end
128
+
129
+ # Return a hash with :customer, :net, and :contacts info filled in.
130
+ def get_most_specific_owner
131
+ return unless datas = get_parsed_data
132
+
133
+ datas_with_bitmasks = datas.map do |data|
134
+ bitmask = data[:net]['CIDR'].split('/')[1].to_i
135
+ [bitmask, data]
136
+ end
137
+ #datas_with_bitmasks.sort.each{|x|puts x[0]}
138
+ winner = datas_with_bitmasks.sort[-1][1]
139
+ end
140
+
141
+ end # of class ArinWhoisParser
142
+
143
+ module_function
144
+
145
+ def raw_whois(query_string, host)
146
+ s = TCPsocket.open(host, 43)
147
+ s.write(query_string+"\n")
148
+ ret = s.read
149
+ s.close
150
+ return ret
151
+ end
152
+
153
+ def lookup(ip)
154
+ data = raw_whois("+#{ip}", 'whois.arin.net')
155
+ arin = ArinWhoisParser.new data
156
+ arin.get_most_specific_owner
157
+ end
158
+
159
+ def lookup_location(ip)
160
+ result = lookup(ip)
161
+ result[:customer].location
162
+ end
163
+
164
+ def lookup_address(ip)
165
+ result = lookup(ip)
166
+ result[:customer].address
167
+ end
168
+
169
+ def lookup_info(ip)
170
+ if result = lookup(ip)
171
+ "#{result[:net]['CIDR']} => #{result[:customer].owner} (#{result[:customer].address})"
172
+ else
173
+ "Address not found."
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+
180
+
181
+ #################################################################
182
+ ## The Plugin
183
+ ##
184
+
185
+ class IPLookupPlugin < Plugin
186
+ def help(plugin, topic="")
187
+ "iplookup [ip address / domain name] => lookup info about the owner of the IP address from the ARIN whois database"
188
+ end
189
+
190
+ def iplookup(m, params)
191
+ reply = ""
192
+ if params[:domain]
193
+ begin
194
+ ip = Resolv.getaddress(params[:domain])
195
+ reply += "#{params[:domain]} | "
196
+ rescue => e
197
+ m.reply "#{e.message}"
198
+ return
199
+ end
200
+ else
201
+ ip = params[:ip]
202
+ end
203
+
204
+ reply += ArinWhois.lookup_info(ip)
205
+ m.reply reply
206
+ end
207
+
208
+ def userip(m, params)
209
+ #users = @channels[m.channel].users
210
+ #m.reply "users = #{users.inspect}"
211
+ #m.reply @bot.sendq("WHO #{params[:user]}")
212
+ end
213
+
214
+ end
215
+
216
+ plugin = IPLookupPlugin.new
217
+ plugin.map 'iplookup :ip', :action => 'iplookup', :requirements => {:ip => /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/}
218
+ plugin.map 'iplookup :domain', :action => 'iplookup', :requirements => {:domain => /^[a-z0-9\.\-]{4,255}$/i}
219
+ plugin.map 'userip :user', :action => 'userip', :requirements => {:user => /\w+/}
220
+
221
+
222
+ if __FILE__ == $0
223
+ include ArinWhois
224
+ data = open('whoistest.txt').read
225
+ c = ArinWhoisParser.new data
226
+ puts c.get_parsed_data.inspect
227
+ end
@@ -15,7 +15,7 @@ class KarmaPlugin < Plugin
15
15
 
16
16
  # import if old file format found
17
17
  if(File.exist?("#{@bot.botclass}/karma.rbot"))
18
- puts "importing old karma data"
18
+ log "importing old karma data"
19
19
  IO.foreach("#{@bot.botclass}/karma.rbot") do |line|
20
20
  if(line =~ /^(\S+)<=>([\d-]+)$/)
21
21
  item = $1
@@ -52,7 +52,7 @@ class KarmaPlugin < Plugin
52
52
 
53
53
 
54
54
  def help(plugin, topic="")
55
- "karma module: <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>, karmastats => show stats. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own."
55
+ "karma module: Listens to everyone's chat. <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>, karmastats => show stats. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own."
56
56
  end
57
57
  def listen(m)
58
58
  return unless m.kind_of?(PrivMessage) && m.public?
@@ -0,0 +1,470 @@
1
+ require 'pp'
2
+
3
+ # Keyword class
4
+ #
5
+ # Encapsulates a keyword ("foo is bar" is a keyword called foo, with type
6
+ # is, and has a single value of bar).
7
+ # Keywords can have multiple values, to_s() will choose one at random
8
+ class Keyword
9
+
10
+ # type of keyword (e.g. "is" or "are")
11
+ attr_reader :type
12
+
13
+ # type:: type of keyword (e.g "is" or "are")
14
+ # values:: array of values
15
+ #
16
+ # create a keyword of type +type+ with values +values+
17
+ def initialize(type, values)
18
+ @type = type.downcase
19
+ @values = values
20
+ end
21
+
22
+ # pick a random value for this keyword and return it
23
+ def to_s
24
+ if(@values.length > 1)
25
+ Keyword.unescape(@values[rand(@values.length)])
26
+ else
27
+ Keyword.unescape(@values[0])
28
+ end
29
+ end
30
+
31
+ # describe the keyword (show all values without interpolation)
32
+ def desc
33
+ @values.join(" | ")
34
+ end
35
+
36
+ # return the keyword in a stringified form ready for storage
37
+ def dump
38
+ @type + "/" + Keyword.unescape(@values.join("<=or=>"))
39
+ end
40
+
41
+ # deserialize the stringified form to an object
42
+ def Keyword.restore(str)
43
+ if str =~ /^(\S+?)\/(.*)$/
44
+ type = $1
45
+ vals = $2.split("<=or=>")
46
+ return Keyword.new(type, vals)
47
+ end
48
+ return nil
49
+ end
50
+
51
+ # values:: array of values to add
52
+ # add values to a keyword
53
+ def <<(values)
54
+ if(@values.length > 1 || values.length > 1)
55
+ values.each {|v|
56
+ @values << v
57
+ }
58
+ else
59
+ @values[0] += " or " + values[0]
60
+ end
61
+ end
62
+
63
+ # unescape special words/characters in a keyword
64
+ def Keyword.unescape(str)
65
+ str.gsub(/\\\|/, "|").gsub(/ \\is /, " is ").gsub(/ \\are /, " are ").gsub(/\\\?(\s*)$/, "?\1")
66
+ end
67
+
68
+ # escape special words/characters in a keyword
69
+ def Keyword.escape(str)
70
+ str.gsub(/\|/, "\\|").gsub(/ is /, " \\is ").gsub(/ are /, " \\are ").gsub(/\?(\s*)$/, "\\?\1")
71
+ end
72
+ end
73
+
74
+ # keywords class.
75
+ #
76
+ # Handles all that stuff like "bot: foo is bar", "bot: foo?"
77
+ #
78
+ # Fallback after core and auth have had a look at a message and refused to
79
+ # handle it, checks for a keyword command or lookup, otherwise the message
80
+ # is delegated to plugins
81
+ class Keywords < Plugin
82
+ BotConfig.register BotConfigBooleanValue.new('keyword.listen',
83
+ :default => false,
84
+ :desc => "Should the bot listen to all chat and attempt to automatically detect keywords? (e.g. by spotting someone say 'foo is bar')")
85
+ BotConfig.register BotConfigBooleanValue.new('keyword.address',
86
+ :default => true,
87
+ :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")
88
+
89
+ # create a new Keywords instance, associated to bot +bot+
90
+ def initialize
91
+ super
92
+
93
+ @statickeywords = Hash.new
94
+ @keywords = @registry.sub_registry('keywords') # DBTree.new bot, "keyword"
95
+ upgrade_data
96
+
97
+ scan
98
+
99
+ # import old format keywords into DBHash
100
+ if(File.exist?("#{@bot.botclass}/keywords.rbot"))
101
+ log "auto importing old keywords.rbot"
102
+ IO.foreach("#{@bot.botclass}/keywords.rbot") do |line|
103
+ if(line =~ /^(.*?)\s*<=(is|are)?=?>\s*(.*)$/)
104
+ lhs = $1
105
+ mhs = $2
106
+ rhs = $3
107
+ mhs = "is" unless mhs
108
+ rhs = Keyword.escape rhs
109
+ values = rhs.split("<=or=>")
110
+ @keywords[lhs] = Keyword.new(mhs, values).dump
111
+ end
112
+ end
113
+ File.rename("#{@bot.botclass}/keywords.rbot", "#{@bot.botclass}/keywords.rbot.old")
114
+ end
115
+ end
116
+
117
+ # drop static keywords and reload them from files, picking up any new
118
+ # keyword files that have been added
119
+ def rescan
120
+ @statickeywords = Hash.new
121
+ scan
122
+ end
123
+
124
+ # load static keywords from files, picking up any new keyword files that
125
+ # have been added
126
+ def scan
127
+ # first scan for old DBHash files, and convert them
128
+ Dir["#{@bot.botclass}/keywords/*"].each {|f|
129
+ next unless f =~ /\.db$/
130
+ log "upgrading keyword db #{f} (rbot 0.9.5 or prior) database format"
131
+ newname = f.gsub(/\.db$/, ".kdb")
132
+ old = BDB::Hash.open f, nil,
133
+ "r+", 0600
134
+ new = BDB::CIBtree.open(newname, nil,
135
+ BDB::CREATE | BDB::EXCL,
136
+ 0600)
137
+ old.each {|k,v|
138
+ new[k] = v
139
+ }
140
+ old.close
141
+ new.close
142
+ File.delete(f)
143
+ }
144
+
145
+ # then scan for current DBTree files, and load them
146
+ Dir["#{@bot.botclass}/keywords/*"].each {|f|
147
+ next unless f =~ /\.kdb$/
148
+ hsh = DBTree.new @bot, f, true
149
+ key = File.basename(f).gsub(/\.kdb$/, "")
150
+ debug "keywords module: loading DBTree file #{f}, key #{key}"
151
+ @statickeywords[key] = hsh
152
+ }
153
+
154
+ # then scan for non DB files, and convert/import them and delete
155
+ Dir["#{@bot.botclass}/keywords/*"].each {|f|
156
+ next if f =~ /\.kdb$/
157
+ next if f =~ /CVS$/
158
+ log "auto converting keywords from #{f}"
159
+ key = File.basename(f)
160
+ unless @statickeywords.has_key?(key)
161
+ @statickeywords[key] = DBHash.new @bot, "#{f}.db", true
162
+ end
163
+ IO.foreach(f) {|line|
164
+ if(line =~ /^(.*?)\s*<?=(is|are)?=?>\s*(.*)$/)
165
+ lhs = $1
166
+ mhs = $2
167
+ rhs = $3
168
+ # support infobot style factfiles, by fixing them up here
169
+ rhs.gsub!(/\$who/, "<who>")
170
+ mhs = "is" unless mhs
171
+ rhs = Keyword.escape rhs
172
+ values = rhs.split("<=or=>")
173
+ @statickeywords[key][lhs] = Keyword.new(mhs, values).dump
174
+ end
175
+ }
176
+ File.delete(f)
177
+ @statickeywords[key].flush
178
+ }
179
+ end
180
+
181
+ # upgrade data files found in old rbot formats to current
182
+ def upgrade_data
183
+ if File.exist?("#{@bot.botclass}/keywords.db")
184
+ log "upgrading old keywords (rbot 0.9.5 or prior) database format"
185
+ old = BDB::Hash.open "#{@bot.botclass}/keywords.db", nil,
186
+ "r+", 0600
187
+ old.each {|k,v|
188
+ @keywords[k] = v
189
+ }
190
+ old.close
191
+ @keywords.flush
192
+ File.rename("#{@bot.botclass}/keywords.db", "#{@bot.botclass}/keywords.db.old")
193
+ end
194
+
195
+ if File.exist?("#{@bot.botclass}/keyword.db")
196
+ log "upgrading old keywords (rbot 0.9.9 or prior) database format"
197
+ old = BDB::CIBtree.open "#{@bot.botclass}/keyword.db", nil,
198
+ "r+", 0600
199
+ old.each {|k,v|
200
+ @keywords[k] = v
201
+ }
202
+ old.close
203
+ @keywords.flush
204
+ File.rename("#{@bot.botclass}/keyword.db", "#{@bot.botclass}/keyword.db.old")
205
+ end
206
+ end
207
+
208
+ # save dynamic keywords to file
209
+ def save
210
+ @keywords.flush
211
+ end
212
+ def oldsave
213
+ File.open("#{@bot.botclass}/keywords.rbot", "w") do |file|
214
+ @keywords.each do |key, value|
215
+ file.puts "#{key}<=#{value.type}=>#{value.dump}"
216
+ end
217
+ end
218
+ end
219
+
220
+ # lookup keyword +key+, return it or nil
221
+ def [](key)
222
+ return nil if key.nil?
223
+ debug "keywords module: looking up key #{key}"
224
+ if(@keywords.has_key?(key))
225
+ return Keyword.restore(@keywords[key])
226
+ else
227
+ # key name order for the lookup through these
228
+ @statickeywords.keys.sort.each {|k|
229
+ v = @statickeywords[k]
230
+ if v.has_key?(key)
231
+ return Keyword.restore(v[key])
232
+ end
233
+ }
234
+ end
235
+ return nil
236
+ end
237
+
238
+ # does +key+ exist as a keyword?
239
+ def has_key?(key)
240
+ if @keywords.has_key?(key) && Keyword.restore(@keywords[key]) != nil
241
+ return true
242
+ end
243
+ @statickeywords.each {|k,v|
244
+ if v.has_key?(key) && Keyword.restore(v[key]) != nil
245
+ return true
246
+ end
247
+ }
248
+ return false
249
+ end
250
+
251
+ # m:: PrivMessage containing message info
252
+ # key:: key being queried
253
+ # dunno:: optional, if true, reply "dunno" if +key+ not found
254
+ #
255
+ # handle a message asking about a keyword
256
+ def keyword(m, key, dunno=true)
257
+ return if key.nil?
258
+ unless(kw = self[key])
259
+ m.reply @bot.lang.get("dunno") if (dunno)
260
+ return
261
+ end
262
+ response = kw.to_s
263
+ response.gsub!(/<who>/, m.sourcenick)
264
+ if(response =~ /^<reply>\s*(.*)/)
265
+ m.reply "#$1"
266
+ elsif(response =~ /^<action>\s*(.*)/)
267
+ @bot.action m.replyto, "#$1"
268
+ elsif(m.public? && response =~ /^<topic>\s*(.*)/)
269
+ topic = $1
270
+ @bot.topic m.target, topic
271
+ else
272
+ m.reply "#{key} #{kw.type} #{response}"
273
+ end
274
+ end
275
+
276
+
277
+ # handle a message which alters a keyword
278
+ # like "foo is bar", or "no, foo is baz", or "foo is also qux"
279
+ def keyword_command(sourcenick, target, lhs, mhs, rhs, quiet=false)
280
+ debug "got keyword command #{lhs}, #{mhs}, #{rhs}"
281
+ overwrite = false
282
+ overwrite = true if(lhs.gsub!(/^no,\s*/, ""))
283
+ also = true if(rhs.gsub!(/^also\s+/, ""))
284
+ values = rhs.split(/\s+\|\s+/)
285
+ lhs = Keyword.unescape lhs
286
+ if(overwrite || also || !has_key?(lhs))
287
+ if(also && has_key?(lhs))
288
+ kw = self[lhs]
289
+ kw << values
290
+ @keywords[lhs] = kw.dump
291
+ else
292
+ @keywords[lhs] = Keyword.new(mhs, values).dump
293
+ end
294
+ @bot.okay target if !quiet
295
+ elsif(has_key?(lhs))
296
+ kw = self[lhs]
297
+ @bot.say target, "but #{lhs} #{kw.type} #{kw.desc}" if kw && !quiet
298
+ end
299
+ end
300
+
301
+ # return help string for Keywords with option topic +topic+
302
+ def help(plugin, topic="")
303
+ case topic
304
+ when "overview"
305
+ return "set: <keyword> is <definition>, overide: no, <keyword> is <definition>, add to definition: <keyword> is also <definition>, random responses: <keyword> is <definition> | <definition> [| ...], plurals: <keyword> are <definition>, escaping: \\is, \\are, \\|, specials: <reply>, <action>, <who>"
306
+ when "set"
307
+ return "set => <keyword> is <definition>"
308
+ when "plurals"
309
+ return "plurals => <keywords> are <definition>"
310
+ when "override"
311
+ return "overide => no, <keyword> is <definition>"
312
+ when "also"
313
+ return "also => <keyword> is also <definition>"
314
+ when "random"
315
+ return "random responses => <keyword> is <definition> | <definition> [| ...]"
316
+ when "get"
317
+ return "asking for keywords => (with addressing) \"<keyword>?\", (without addressing) \"'<keyword>\""
318
+ when "tell"
319
+ return "tell <nick> about <keyword> => if <keyword> is known, tell <nick>, via /msg, its definition"
320
+ when "forget"
321
+ return "forget <keyword> => forget fact <keyword>"
322
+ when "keywords"
323
+ return "keywords => show current keyword counts"
324
+ when "<reply>"
325
+ return "<reply> => normal response is \"<keyword> is <definition>\", but if <definition> begins with <reply>, the response will be \"<definition>\""
326
+ when "<action>"
327
+ return "<action> => makes keyword respnse \"/me <definition>\""
328
+ when "<who>"
329
+ return "<who> => replaced with questioner in reply"
330
+ when "<topic>"
331
+ return "<topic> => respond by setting the topic to the rest of the definition"
332
+ when "search"
333
+ return "keywords search [--all] [--full] <regexp> => search keywords for <regexp>. If --all is set, search static keywords too, if --full is set, search definitions too."
334
+ else
335
+ return "Keyword module (Fact learning and regurgitation) topics: overview, set, plurals, override, also, random, get, tell, forget, keywords, keywords search, <reply>, <action>, <who>, <topic>"
336
+ end
337
+ end
338
+
339
+ # handle a message asking the bot to tell someone about a keyword
340
+ def keyword_tell(m, param)
341
+ target = param[:target]
342
+ key = nil
343
+
344
+ # extract the keyword from the message, because unfortunately
345
+ # the message mapper doesn't preserve whtiespace
346
+ if m.message =~ /about\s+(.+)$/
347
+ key = $1
348
+ end
349
+
350
+ unless(kw = self[key])
351
+ m.reply @bot.lang.get("dunno_about_X") % key
352
+ return
353
+ end
354
+
355
+ response = kw.to_s
356
+ response.gsub!(/<who>/, m.sourcenick)
357
+ if(response =~ /^<reply>\s*(.*)/)
358
+ @bot.say target, "#{m.sourcenick} wanted me to tell you: (#{key}) #$1"
359
+ m.reply "okay, I told #{target}: (#{key}) #$1"
360
+ elsif(response =~ /^<action>\s*(.*)/)
361
+ @bot.action target, "#$1 (#{m.sourcenick} wanted me to tell you)"
362
+ m.reply "okay, I told #{target}: * #$1"
363
+ else
364
+ @bot.say target, "#{m.sourcenick} wanted me to tell you that #{key} #{kw.type} #{response}"
365
+ m.reply "okay, I told #{target} that #{key} #{kw.type} #{response}"
366
+ end
367
+ end
368
+
369
+ # return the number of known keywords
370
+ def keyword_stats(m, param)
371
+ length = 0
372
+ @statickeywords.each {|k,v|
373
+ length += v.length
374
+ }
375
+ m.reply "There are currently #{@keywords.length} keywords, #{length} static facts defined."
376
+ end
377
+
378
+ # search for keywords, optionally also the definition and the static keywords
379
+ def keyword_search(m, param)
380
+ str = param[:pattern]
381
+ all = (param[:all] == '--all')
382
+ full = (param[:full] == '--full')
383
+
384
+ begin
385
+ re = Regexp.new(str, Regexp::IGNORECASE)
386
+ if(@bot.auth.allow?("keyword", m.source, m.replyto))
387
+ matches = Array.new
388
+ @keywords.each {|k,v|
389
+ kw = Keyword.restore(v)
390
+ if re.match(k) || (full && re.match(kw.desc))
391
+ matches << [k,kw]
392
+ end
393
+ }
394
+ if all
395
+ @statickeywords.each {|k,v|
396
+ v.each {|kk,vv|
397
+ kw = Keyword.restore(vv)
398
+ if re.match(kk) || (full && re.match(kw.desc))
399
+ matches << [kk,kw]
400
+ end
401
+ }
402
+ }
403
+ end
404
+ if matches.length == 1
405
+ rkw = matches[0]
406
+ m.reply "#{rkw[0]} #{rkw[1].type} #{rkw[1].desc}"
407
+ elsif matches.length > 0
408
+ i = 0
409
+ matches.each {|rkw|
410
+ m.reply "[#{i+1}/#{matches.length}] #{rkw[0]} #{rkw[1].type} #{rkw[1].desc}"
411
+ i += 1
412
+ break if i == 3
413
+ }
414
+ else
415
+ m.reply "no keywords match #{str}"
416
+ end
417
+ end
418
+ rescue RegexpError => e
419
+ m.reply "no keywords match #{str}: #{e}"
420
+ rescue
421
+ debug e.inspect
422
+ m.reply "no keywords match #{str}: an error occurred"
423
+ end
424
+ end
425
+
426
+ # forget one of the dynamic keywords
427
+ def keyword_forget(m, param)
428
+ key = param[:key]
429
+ if(@keywords.has_key?(key))
430
+ @keywords.delete(key)
431
+ @bot.okay m.replyto
432
+ end
433
+ end
434
+
435
+ # privmsg handler
436
+ def listen(m)
437
+ return if m.replied?
438
+ if(m.address?)
439
+ if(!(m.message =~ /\\\?\s*$/) && m.message =~ /^(.*\S)\s*\?\s*$/)
440
+ keyword m, $1 if(@bot.auth.allow?("keyword", m.source, m.replyto))
441
+ elsif(m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/)
442
+ keyword_command(m.sourcenick, m.replyto, $1, $2, $3) if(@bot.auth.allow?("keycmd", m.source, m.replyto))
443
+ end
444
+ else
445
+ # in channel message, not to me
446
+ # TODO option to do if(m.message =~ /^(.*)$/, ie try any line as a
447
+ # keyword lookup.
448
+ if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.address"] && m.message =~ /^(.*\S)\s*\?\s*$/))
449
+ keyword m, $1, false if(@bot.auth.allow?("keyword", m.source))
450
+ elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/))
451
+ # TODO MUCH more selective on what's allowed here
452
+ keyword_command(m.sourcenick, m.replyto, $1, $2, $3, true) if(@bot.auth.allow?("keycmd", m.source))
453
+ end
454
+ end
455
+ end
456
+ end
457
+
458
+ plugin = Keywords.new
459
+
460
+ plugin.map 'keyword stats', :action => 'keyword_stats'
461
+
462
+ plugin.map 'keyword search :all :full :pattern', :action => 'keyword_search',
463
+ :defaults => {:all => '', :full => ''},
464
+ :requirements => {:all => '--all', :full => '--full'}
465
+
466
+ plugin.map 'keyword forget :key', :action => 'keyword_forget'
467
+ plugin.map 'forget :key', :action => 'keyword_forget', :auth => 'keycmd'
468
+
469
+ plugin.map 'keyword tell :target about *keyword', :action => 'keyword_tell'
470
+ plugin.map 'tell :target about *keyword', :action => 'keyword_tell', :auth => 'keyword'