4 # :title: Markov plugin
6 # Author:: Tom Gilbert <tom@linuxbrit.co.uk>
7 # Copyright:: (C) 2005 Tom Gilbert
9 # Contribute to chat with random phrases built from word sequences learned
10 # by listening to chat
12 class MarkovPlugin < Plugin
13 Config.register Config::BooleanValue.new('markov.enabled',
15 :desc => "Enable and disable the plugin")
16 Config.register Config::IntegerValue.new('markov.probability',
18 :validate => Proc.new { |v| (0..100).include? v },
19 :desc => "Percentage chance of markov plugin chipping in")
20 Config.register Config::ArrayValue.new('markov.ignore',
22 :desc => "Hostmasks and channel names markov should NOT learn from (e.g. idiot*!*@*, #privchan).")
23 Config.register Config::IntegerValue.new('markov.max_words',
25 :validate => Proc.new { |v| (0..100).include? v },
26 :desc => "Maximum number of words the bot should put in a sentence")
30 @registry.set_default([])
31 if @registry.has_key?('enabled')
32 @bot.config['markov.enabled'] = @registry['enabled']
33 @registry.delete('enabled')
35 if @registry.has_key?('probability')
36 @bot.config['markov.probability'] = @registry['probability']
37 @registry.delete('probability')
39 if @bot.config['markov.ignore_users']
40 debug "moving markov.ignore_users to markov.ignore"
41 @bot.config['markov.ignore'] = @bot.config['markov.ignore_users'].dup
42 @bot.config.delete('markov.ignore_users'.to_sym)
44 @learning_queue = Queue.new
45 @learning_thread = Thread.new do
46 while s = @learning_queue.pop
51 @learning_thread.priority = -1
55 debug 'closing learning thread'
56 @learning_queue.push nil
58 debug 'learning thread closed'
61 def generate_string(word1, word2)
62 # limit to max of markov.max_words words
64 output = "#{word1} #{word2}"
69 if @registry.key? output
70 wordlist = @registry[output]
71 wordlist.delete(:nonword)
75 @registry.each_key(output) do |key|
76 if key.downcase.include? output
83 keys = @registry.keys.select { |k| k.downcase.include? output }
85 return nil if keys.empty?
86 while key = keys.delete_one
87 wordlist = @registry[key]
88 wordlist.delete(:nonword)
89 unless wordlist.empty?
91 word1, word2 = output.split
96 return nil if wordlist.empty?
98 word3 = wordlist.pick_one
100 word1, word2 = word2, word3
102 (@bot.config['markov.max_words'] - 1).times do
103 wordlist = @registry["#{word1} #{word2}"]
104 break if wordlist.empty?
105 word3 = wordlist.pick_one
106 break if word3 == :nonword
107 output << " #{word3}"
108 word1, word2 = word2, word3
113 def help(plugin, topic="")
114 "markov plugin: listens to chat to build a markov chain, with which it can (perhaps) attempt to (inanely) contribute to 'discussion'. Sort of.. Will get a *lot* better after listening to a lot of chat. usage: 'markov' to attempt to say something relevant to the last line of chat, if it can. other options to markov: 'ignore' => ignore a hostmask (accept no input), 'status' => show current status, 'probability [<chance>]' => set the % chance of rbot responding to input, or display the current probability, 'chat' => try and say something intelligent, 'chat about <foo> <bar>' => riff on a word pair (if possible)"
119 str.gsub!(/^\S+[:,;]/, "")
120 str.gsub!(/\s{2,}/, ' ') # fix for two or more spaces
125 return @bot.config['markov.probability']
129 if @bot.config['markov.enabled']
130 reply = _("markov is currently enabled, %{p}% chance of chipping in") % { :p => probability? }
131 l = @learning_queue.length
132 reply << (_(", %{l} messages in queue") % {:l => l}) if l > 0
134 reply = _("markov is currently disabled")
140 return false unless m
141 return true if m.address? or m.private?
142 @bot.config['markov.ignore'].each do |mask|
143 return true if m.channel.downcase == mask.downcase
144 return true if m.source.matches?(mask)
149 def ignore(m, params)
150 action = params[:action]
151 user = params[:option]
154 if @bot.config['markov.ignore'].include? user
155 s = @bot.config['markov.ignore']
157 @bot.config['ignore'] = s
158 m.reply "#{user} removed"
160 m.reply "not found in list"
164 if @bot.config['markov.ignore'].include?(user)
165 m.reply "#{user} already in list"
167 @bot.config['markov.ignore'] = @bot.config['markov.ignore'].push user
168 m.reply "#{user} added to markov ignore list"
171 m.reply "give the name of a person or channel to ignore"
174 m.reply "I'm ignoring #{@bot.config['markov.ignore'].join(", ")}"
176 m.reply "have markov ignore the input from a hostmask or a channel. usage: markov ignore add <mask or channel>; markov ignore remove <mask or channel>; markov ignore list"
180 def enable(m, params)
181 @bot.config['markov.enabled'] = true
185 def probability(m, params)
186 if params[:probability]
187 @bot.config['markov.probability'] = params[:probability].to_i
190 m.reply _("markov has a %{prob}% chance of chipping in") % { :prob => probability? }
194 def disable(m, params)
195 @bot.config['markov.enabled'] = false
200 return false unless @bot.config['markov.enabled']
202 return true if prob > rand(100)
210 def random_markov(m, message)
211 return unless should_talk
213 word1, word2 = message.split(/\s+/)
214 return unless word1 and word2
215 line = generate_string(word1, word2)
217 # we do nothing if the line we return is just an initial substring
218 # of the line we received
219 return if message.index(line) == 0
220 @bot.timer.add_once(delay) {
221 m.reply line, :nick => false, :to => :public
226 line = generate_string(params[:seed1], params[:seed2])
227 if line and line != [params[:seed1], params[:seed2]].compact.join(" ")
234 def rand_chat(m, params)
235 # pick a random pair from the db and go from there
236 word1, word2 = :nonword, :nonword
239 wordlist = @registry["#{word1} #{word2}"]
240 break if wordlist.empty?
241 word3 = wordlist[rand(wordlist.length)]
242 break if word3 == :nonword
244 word1, word2 = word2, word3
247 m.reply output.join(" ")
254 lines.each { |l| @learning_queue.push l }
260 # in channel message, the kind we are interested in
261 message = clean_str m.plainmessage
264 message = "#{m.sourcenick} #{message}"
268 random_markov(m, message) unless m.replied?
271 def learn_line(message)
272 # debug "learning #{message}"
273 wordlist = message.split(/\s+/)
274 return unless wordlist.length >= 2
275 word1, word2 = :nonword, :nonword
276 wordlist.each do |word3|
277 k = "#{word1} #{word2}"
278 @registry[k] = @registry[k].push(word3)
279 word1, word2 = word2, word3
281 k = "#{word1} #{word2}"
282 @registry[k] = @registry[k].push(:nonword)
286 plugin = MarkovPlugin.new
287 plugin.map 'markov ignore :action :option', :action => "ignore"
288 plugin.map 'markov ignore :action', :action => "ignore"
289 plugin.map 'markov ignore', :action => "ignore"
290 plugin.map 'markov enable', :action => "enable"
291 plugin.map 'markov disable', :action => "disable"
292 plugin.map 'markov status', :action => "status"
293 plugin.map 'chat about :seed1 [:seed2]', :action => "chat"
294 plugin.map 'chat', :action => "rand_chat"
295 plugin.map 'markov probability [:probability]', :action => "probability",
296 :requirements => {:probability => /^\d+%?$/}
298 plugin.default_auth('ignore', false)
299 plugin.default_auth('probability', false)