]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/markov.rb
remove whitespace
[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 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     output = word1 + " " + word2
64
65     # try to avoid :nonword in the first iteration
66     wordlist = @registry["#{word1} #{word2}"]
67     wordlist.delete(:nonword)
68     if not wordlist.empty?
69       word3 = wordlist[rand(wordlist.length)]
70       output = output + " " + word3
71       word1, word2 = word2, word3
72     end
73
74     (@bot.config['markov.max_words'] - 1).times do
75       wordlist = @registry["#{word1} #{word2}"]
76       break if wordlist.empty?
77       word3 = wordlist[rand(wordlist.length)]
78       break if word3 == :nonword
79       output = output + " " + word3
80       word1, word2 = word2, word3
81     end
82     return output
83   end
84
85   def help(plugin, topic="")
86     "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)"
87   end
88
89   def clean_str(s)
90     str = s.dup
91     str.gsub!(/^\S+[:,;]/, "")
92     str.gsub!(/\s{2,}/, ' ') # fix for two or more spaces
93     return str.strip
94   end
95
96   def probability?
97     return @bot.config['markov.probability']
98   end
99
100   def status(m,params)
101     if @bot.config['markov.enabled']
102       reply = _("markov is currently enabled, %{p}% chance of chipping in") % { :p => probability? }
103       l = @learning_queue.length
104       reply << (_(", %{l} messages in queue") % {:l => l}) if l > 0
105     else
106       reply = _("markov is currently disabled")
107     end
108     m.reply reply
109   end
110
111   def ignore?(m=nil)
112     return false unless m
113     return true if m.address? or m.private?
114     @bot.config['markov.ignore'].each do |mask|
115       return true if m.channel.downcase == mask.downcase
116       return true if m.source.matches?(mask)
117     end
118     return false
119   end
120
121   def ignore(m, params)
122     action = params[:action]
123     user = params[:option]
124     case action
125     when 'remove':
126       if @bot.config['markov.ignore'].include? user
127         s = @bot.config['markov.ignore']
128         s.delete user
129         @bot.config['ignore'] = s
130         m.reply "#{user} removed"
131       else
132         m.reply "not found in list"
133       end
134     when 'add':
135       if user
136         if @bot.config['markov.ignore'].include?(user)
137           m.reply "#{user} already in list"
138         else
139           @bot.config['markov.ignore'] = @bot.config['markov.ignore'].push user
140           m.reply "#{user} added to markov ignore list"
141         end
142       else
143         m.reply "give the name of a person or channel to ignore"
144       end
145     when 'list':
146       m.reply "I'm ignoring #{@bot.config['markov.ignore'].join(", ")}"
147     else
148       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"
149     end
150   end
151
152   def enable(m, params)
153     @bot.config['markov.enabled'] = true
154     m.okay
155   end
156
157   def probability(m, params)
158     if params[:probability]
159       @bot.config['markov.probability'] = params[:probability].to_i
160       m.okay
161     else
162       m.reply _("markov has a %{prob}% chance of chipping in") % { :prob => probability? }
163     end
164   end
165
166   def disable(m, params)
167     @bot.config['markov.enabled'] = false
168     m.okay
169   end
170
171   def should_talk
172     return false unless @bot.config['markov.enabled']
173     prob = probability?
174     return true if prob > rand(100)
175     return false
176   end
177
178   def delay
179     1 + rand(5)
180   end
181
182   def random_markov(m, message)
183     return unless should_talk
184
185     word1, word2 = message.split(/\s+/)
186     return unless word1 and word2
187     line = generate_string(word1, word2)
188     return unless line
189     # we do nothing if the line we return is just an initial substring
190     # of the line we received
191     return if message.index(line) == 0
192     @bot.timer.add_once(delay) {
193       m.reply line, :nick => false, :to => :public
194     }
195   end
196
197   def chat(m, params)
198     line = generate_string(params[:seed1], params[:seed2])
199     if line != "#{params[:seed1]} #{params[:seed2]}"
200       m.reply line
201     else
202       m.reply "I can't :("
203     end
204   end
205
206   def rand_chat(m, params)
207     # pick a random pair from the db and go from there
208     word1, word2 = :nonword, :nonword
209     output = Array.new
210     50.times do
211       wordlist = @registry["#{word1} #{word2}"]
212       break if wordlist.empty?
213       word3 = wordlist[rand(wordlist.length)]
214       break if word3 == :nonword
215       output << word3
216       word1, word2 = word2, word3
217     end
218     if output.length > 1
219       m.reply output.join(" ")
220     else
221       m.reply "I can't :("
222     end
223   end
224
225   def message(m)
226     return if ignore? m
227
228     # in channel message, the kind we are interested in
229     message = clean_str m.plainmessage
230
231     if m.action?
232       message = "#{m.sourcenick} #{message}"
233     end
234
235     @learning_queue.push message
236     random_markov(m, message) unless m.replied?
237   end
238
239   def learn(message)
240     # debug "learning #{message}"
241     wordlist = message.split(/\s+/)
242     return unless wordlist.length >= 2
243     word1, word2 = :nonword, :nonword
244     wordlist.each do |word3|
245       k = "#{word1} #{word2}"
246       @registry[k] = @registry[k].push(word3)
247       word1, word2 = word2, word3
248     end
249     k = "#{word1} #{word2}"
250     @registry[k] = @registry[k].push(:nonword)
251   end
252 end
253
254 plugin = MarkovPlugin.new
255 plugin.map 'markov ignore :action :option', :action => "ignore"
256 plugin.map 'markov ignore :action', :action => "ignore"
257 plugin.map 'markov ignore', :action => "ignore"
258 plugin.map 'markov enable', :action => "enable"
259 plugin.map 'markov disable', :action => "disable"
260 plugin.map 'markov status', :action => "status"
261 plugin.map 'chat about :seed1 :seed2', :action => "chat"
262 plugin.map 'chat', :action => "rand_chat"
263 plugin.map 'markov probability [:probability]', :action => "probability",
264            :requirements => {:probability => /^\d+%?$/}
265
266 plugin.default_auth('ignore', false)
267 plugin.default_auth('probability', false)
268