]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/markov.rb
markov: Only work with unreplied messages.
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / markov.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Markov plugin
5 #
6 # Author:: Tom Gilbert <tom@linuxbrit.co.uk>
7 # Copyright:: (C) 2005 Tom Gilbert
8 #
9 # Contribute to chat with random phrases built from word sequences learned
10 # by listening to chat
11
12 class MarkovPlugin < Plugin
13   Config.register Config::BooleanValue.new('markov.enabled',
14     :default => false,
15     :desc => "Enable and disable the plugin")
16   Config.register Config::IntegerValue.new('markov.probability',
17     :default => 25,
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',
21     :default => [],
22     :desc => "Hostmasks and channel names markov should NOT learn from (e.g. idiot*!*@*, #privchan).")
23   Config.register Config::IntegerValue.new('markov.max_words',
24     :default => 50,
25     :validate => Proc.new { |v| (0..100).include? v },
26     :desc => "Maximum number of words the bot should put in a sentence")
27
28   def initialize
29     super
30     @registry.set_default([])
31     if @registry.has_key?('enabled')
32       @bot.config['markov.enabled'] = @registry['enabled']
33       @registry.delete('enabled')
34     end
35     if @registry.has_key?('probability')
36       @bot.config['markov.probability'] = @registry['probability']
37       @registry.delete('probability')
38     end
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)
43     end
44     @learning_queue = Queue.new
45     @learning_thread = Thread.new do
46       while s = @learning_queue.pop
47         learn_line s
48         sleep 0.5
49       end
50     end
51     @learning_thread.priority = -1
52   end
53
54   def cleanup
55     debug 'closing learning thread'
56     @learning_queue.push nil
57     @learning_thread.join
58     debug 'learning thread closed'
59   end
60
61   def generate_string(word1, word2)
62     # limit to max of markov.max_words words
63     if word2
64       output = "#{word1} #{word2}"
65     else
66       output = word1.to_s
67     end
68
69     if @registry.key? output
70       wordlist = @registry[output]
71       wordlist.delete(:nonword)
72     else
73       output.downcase!
74       keys = []
75       @registry.each_key(output) do |key|
76         if key.downcase.include? output
77           keys << key
78         else
79           break
80         end
81       end
82       if keys.empty?
83         keys = @registry.keys.select { |k| k.downcase.include? output }
84       end
85       return nil if keys.empty?
86       while key = keys.delete_one
87         wordlist = @registry[key]
88         wordlist.delete(:nonword)
89         unless wordlist.empty?
90           output = key
91           word1, word2 = output.split
92           break
93         end
94       end
95     end
96     return nil if wordlist.empty?
97
98     word3 = wordlist.pick_one
99     output << " #{word3}"
100     word1, word2 = word2, word3
101
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
109     end
110     return output
111   end
112
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)"
115   end
116
117   def clean_str(s)
118     str = s.dup
119     str.gsub!(/^\S+[:,;]/, "")
120     str.gsub!(/\s{2,}/, ' ') # fix for two or more spaces
121     return str.strip
122   end
123
124   def probability?
125     return @bot.config['markov.probability']
126   end
127
128   def status(m,params)
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
133     else
134       reply = _("markov is currently disabled")
135     end
136     m.reply reply
137   end
138
139   def ignore?(m=nil)
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)
145     end
146     return false
147   end
148
149   def ignore(m, params)
150     action = params[:action]
151     user = params[:option]
152     case action
153     when 'remove':
154       if @bot.config['markov.ignore'].include? user
155         s = @bot.config['markov.ignore']
156         s.delete user
157         @bot.config['ignore'] = s
158         m.reply "#{user} removed"
159       else
160         m.reply "not found in list"
161       end
162     when 'add':
163       if user
164         if @bot.config['markov.ignore'].include?(user)
165           m.reply "#{user} already in list"
166         else
167           @bot.config['markov.ignore'] = @bot.config['markov.ignore'].push user
168           m.reply "#{user} added to markov ignore list"
169         end
170       else
171         m.reply "give the name of a person or channel to ignore"
172       end
173     when 'list':
174       m.reply "I'm ignoring #{@bot.config['markov.ignore'].join(", ")}"
175     else
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"
177     end
178   end
179
180   def enable(m, params)
181     @bot.config['markov.enabled'] = true
182     m.okay
183   end
184
185   def probability(m, params)
186     if params[:probability]
187       @bot.config['markov.probability'] = params[:probability].to_i
188       m.okay
189     else
190       m.reply _("markov has a %{prob}% chance of chipping in") % { :prob => probability? }
191     end
192   end
193
194   def disable(m, params)
195     @bot.config['markov.enabled'] = false
196     m.okay
197   end
198
199   def should_talk
200     return false unless @bot.config['markov.enabled']
201     prob = probability?
202     return true if prob > rand(100)
203     return false
204   end
205
206   def delay
207     1 + rand(5)
208   end
209
210   def random_markov(m, message)
211     return unless should_talk
212
213     word1, word2 = message.split(/\s+/)
214     return unless word1 and word2
215     line = generate_string(word1, word2)
216     return unless line
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
222     }
223   end
224
225   def chat(m, params)
226     line = generate_string(params[:seed1], params[:seed2])
227     if line and line != [params[:seed1], params[:seed2]].compact.join(" ")
228       m.reply line
229     else
230       m.reply "I can't :("
231     end
232   end
233
234   def rand_chat(m, params)
235     # pick a random pair from the db and go from there
236     word1, word2 = :nonword, :nonword
237     output = Array.new
238     50.times do
239       wordlist = @registry["#{word1} #{word2}"]
240       break if wordlist.empty?
241       word3 = wordlist[rand(wordlist.length)]
242       break if word3 == :nonword
243       output << word3
244       word1, word2 = word2, word3
245     end
246     if output.length > 1
247       m.reply output.join(" ")
248     else
249       m.reply "I can't :("
250     end
251   end
252
253   def learn(*lines)
254     lines.each { |l| @learning_queue.push l }
255   end
256
257   def unreplied(m)
258     return if ignore? m
259
260     # in channel message, the kind we are interested in
261     message = clean_str m.plainmessage
262
263     if m.action?
264       message = "#{m.sourcenick} #{message}"
265     end
266
267     learn message
268     random_markov(m, message) unless m.replied?
269   end
270
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
280     end
281     k = "#{word1} #{word2}"
282     @registry[k] = @registry[k].push(:nonword)
283   end
284 end
285
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+%?$/}
297
298 plugin.default_auth('ignore', false)
299 plugin.default_auth('probability', false)
300