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.
- data/AUTHORS +8 -0
- data/ChangeLog +51 -0
- data/INSTALL +4 -0
- data/README +1 -0
- data/REQUIREMENTS +11 -0
- data/TODO +2 -0
- data/bin/rbot +21 -2
- data/data/rbot/languages/german.lang +4 -1
- data/data/rbot/languages/russian.lang +75 -0
- data/data/rbot/plugins/autoop.rb +42 -51
- data/data/rbot/plugins/bans.rb +205 -0
- data/data/rbot/plugins/bash.rb +56 -0
- data/data/rbot/plugins/chucknorris.rb +74 -0
- data/data/rbot/plugins/chucknorris.yml.gz +0 -0
- data/data/rbot/plugins/deepthoughts.rb +95 -0
- data/data/rbot/plugins/demauro.rb +95 -0
- data/data/rbot/plugins/digg.rb +51 -0
- data/data/rbot/plugins/figlet.rb +24 -0
- data/data/rbot/plugins/forecast.rb +133 -0
- data/data/rbot/plugins/freshmeat.rb +13 -7
- data/data/rbot/plugins/google.rb +2 -0
- data/data/rbot/plugins/grouphug.rb +36 -0
- data/data/rbot/plugins/imdb.rb +92 -0
- data/data/rbot/plugins/insult.rb +8 -1
- data/data/rbot/plugins/iplookup.rb +227 -0
- data/data/rbot/plugins/karma.rb +2 -2
- data/data/rbot/plugins/keywords.rb +470 -0
- data/data/rbot/plugins/lart.rb +132 -146
- data/data/rbot/plugins/lastfm.rb +25 -0
- data/data/rbot/plugins/markov.rb +204 -0
- data/data/rbot/plugins/math.rb +5 -1
- data/data/rbot/plugins/nickserv.rb +71 -11
- data/data/rbot/plugins/opme.rb +19 -19
- data/data/rbot/plugins/quakeauth.rb +2 -2
- data/data/rbot/plugins/quotes.rb +40 -25
- data/data/rbot/plugins/remind.rb +1 -1
- data/data/rbot/plugins/rot13.rb +2 -2
- data/data/rbot/plugins/roulette.rb +49 -15
- data/data/rbot/plugins/rss.rb +585 -0
- data/data/rbot/plugins/rubyurl.rb +39 -0
- data/data/rbot/plugins/seen.rb +2 -1
- data/data/rbot/plugins/slashdot.rb +5 -5
- data/data/rbot/plugins/spell.rb +5 -0
- data/data/rbot/plugins/theyfightcrime.rb +121 -0
- data/data/rbot/plugins/threat.rb +55 -0
- data/data/rbot/plugins/tinyurl.rb +39 -0
- data/data/rbot/plugins/topic.rb +204 -0
- data/data/rbot/plugins/urban.rb +71 -0
- data/data/rbot/plugins/url.rb +399 -4
- data/data/rbot/plugins/wow.rb +123 -0
- data/data/rbot/plugins/wserver.rb +1 -1
- data/data/rbot/templates/levels.rbot +2 -0
- data/lib/rbot/auth.rb +207 -96
- data/lib/rbot/channel.rb +5 -5
- data/lib/rbot/config.rb +125 -24
- data/lib/rbot/dbhash.rb +87 -21
- data/lib/rbot/httputil.rb +181 -13
- data/lib/rbot/ircbot.rb +525 -179
- data/lib/rbot/ircsocket.rb +330 -54
- data/lib/rbot/message.rb +66 -23
- data/lib/rbot/messagemapper.rb +25 -17
- data/lib/rbot/plugins.rb +244 -115
- data/lib/rbot/post-clean.rb +1 -0
- data/lib/rbot/{post-install.rb → post-config.rb} +1 -1
- data/lib/rbot/rbotconfig.rb +29 -14
- data/lib/rbot/registry.rb +111 -72
- data/lib/rbot/rfc2812.rb +208 -197
- data/lib/rbot/timer.rb +4 -0
- data/lib/rbot/utils.rb +2 -2
- metadata +127 -104
- data/data/rbot/plugins/rss.rb.disabled +0 -414
- 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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
data/data/rbot/plugins/google.rb
CHANGED
@@ -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
|
+
|
data/data/rbot/plugins/insult.rb
CHANGED
@@ -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
|
-
|
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
|
data/data/rbot/plugins/karma.rb
CHANGED
@@ -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
|
-
|
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'
|