]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/poll.rb
ad80bdff626de215bf72d1ae22b132953a1375ea
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / poll.rb
1 #-- vim:ts=2:et:sw=2
2 #++
3 #
4 # :title: Voting plugin for rbot
5 # Author:: David Gadling <dave@toasterwaffles.com>
6 # Copyright:: (C) 2010 David Gadling
7 # License:: BSD
8 #
9 # Submit a poll question to a channel, wait for glorious outcome.
10 #
11
12 class ::Poll
13   attr_accessor :id, :author, :channel, :running, :ends_at, :started
14   attr_accessor :question, :answers, :duration, :voters, :outcome
15
16   def initialize(originating_message, question, answers, duration)
17     @author = originating_message.sourcenick
18     @channel = originating_message.channel
19     @question = question
20     @running = false
21     @duration = duration
22
23     @answers = Hash.new
24     @voters  = Hash.new
25
26     answer_index = "A"
27     answers.each do |ans|
28       @answers[answer_index] = {
29         :value => ans,
30         :count => 0
31       }
32       answer_index.next!
33     end
34   end
35
36   def start!
37     return if @running
38
39     @started = Time.now
40     @ends_at = @started + @duration
41     @running = true
42   end
43
44   def stop!
45     return if @running == false
46     @running = false
47   end
48
49   def record_vote(voter, choice)
50     if @running == false
51       return _("Poll's closed!")
52     end
53
54     if @voters.has_key? voter
55       return _("You already voted for #{@voters[voter]}!")
56     end
57
58     choice.upcase!
59     if @answers.has_key? choice
60       @answers[choice][:count] += 1
61       @voters[voter] = choice
62
63       return _("Recorded your vote for #{choice}: #{@answers[choice][:value]}")
64     else
65       return _("Don't have an option #{choice}")
66     end
67   end
68
69   def printing_values
70     return Hash[:question => @question,
71             :answers => @answers.keys.collect { |a| [a, @answers[a][:value]] }
72     ]
73   end
74
75   def to_s
76     return @question
77   end
78
79   def options
80     options = _("Options are: ")
81     @answers.each { |letter, info|
82       options << "#{Bold}#{letter}#{NormalText}) #{info[:value]} "
83     }
84     return options
85   end
86 end
87
88 class PollPlugin < Plugin
89   Config.register Config::IntegerValue.new('poll.max_concurrent_polls',
90     :default => 2,
91     :desc => _("How many polls a user can have running at once"))
92   Config.register Config::StringValue.new('poll.default_duration',
93     :default => "2 minutes",
94     :desc => _("How long a poll will accept answers, by default."))
95   Config.register Config::BooleanValue.new('poll.save_results',
96     :default => true,
97     :desc => _("Should we save results until we see the nick of the pollster?"))
98
99   def init_reg_entry(sym, default)
100     unless @registry.has_key?(sym)
101       @registry[sym] = default
102     end
103   end
104
105   def initialize()
106     super
107     init_reg_entry :running, Hash.new
108     init_reg_entry :archives, Hash.new
109     init_reg_entry :last_poll_id, 0
110   end
111
112   MULTIPLIERS = {
113     :seconds => 1,
114     :minutes => 60,
115     :hours   => 60*60,
116     :days    => 24*60*60,
117     :weeks   => 7*24*60*60
118   }
119
120   def authors_running_count(victim)
121     return @registry[:running].values.collect { |p|
122       if p.author == victim
123         1
124       else
125         0
126       end
127     }.inject(0) { |acc, v| acc + v }
128   end
129
130   def start(m, params)
131     author = m.sourcenick
132     chan = m.channel
133
134     max_concurrent = @bot.config['poll.max_concurrent_polls']
135     if authors_running_count(author) == max_concurrent
136       m.reply _("Sorry, you're already at the limit (#{max_concurrent}) polls")
137       return
138     end
139
140     input_blob = params[:blob].join(" ")
141     quote_character = input_blob[0].chr()
142     chunks = input_blob.split(/#{quote_character}\s+#{quote_character}/)
143     if chunks.length <= 2
144       m.reply _("This isn't a dictatorship!")
145       return
146     end
147
148     question = chunks[0].gsub(/"/, '')
149     question = question + "?" if question[-1].chr != "?"
150     answers = chunks[1, chunks.length()-1].map { |a| a.gsub(/"/, '') }
151
152     params[:duration] = params[:duration].join(' ')
153     if params[:duration] == ''
154       target_duration = @bot.config['poll.default_duration']
155     else
156       target_duration = params[:duration]
157     end
158
159     val, units = target_duration.split(' ')
160     if MULTIPLIERS.has_key? units.to_sym
161       duration = val.to_i * MULTIPLIERS[units.to_sym]
162     else
163       m.reply _("I don't understand the #{Bold}#{units}#{NormalText} unit")
164       return
165     end
166
167     poll = Poll.new(m, question, answers, duration)
168
169     m.reply _("New poll from #{author}: #{Bold}#{question}#{NormalText}")
170     m.reply poll.options
171
172     poll.id = @registry[:last_poll_id] + 1
173     poll.start!
174     command = _("poll vote #{poll.id} <SINGLE-LETTER>")
175     m.reply _("You have #{Bold}#{target_duration}#{NormalText} to: " +
176             "#{Bold}/msg #{@bot.nick} #{command}#{NormalText} or " +
177             "#{Bold}#{@bot.config['core.address_prefix']}#{command}#{NormalText} ")
178
179     running = @registry[:running]
180     running[poll.id] = poll
181     @registry[:running] = running
182     @bot.timer.add_once(duration) { count_votes(poll.id) }
183     @registry[:last_poll_id] = poll.id
184   end
185
186   def count_votes(poll_id)
187     poll = @registry[:running][poll_id]
188
189     # Hrm, it vanished!
190     return if poll == nil
191     poll.stop!
192
193     @bot.say(poll.channel, _("Let's find the answer to: #{Bold}#{poll.question}#{NormalText}"))
194
195     sorted = poll.answers.sort { |a,b| b[1][:count]<=>a[1][:count] }
196
197     winner_info = sorted.first
198     winner_info << sorted.inject(0) { |accum, choice| accum + choice[1][:count] }
199
200     if winner_info[2] == 0
201       poll.outcome = _("Nobody voted")
202     else
203       if sorted[0][1][:count] == sorted[1][1][:count]
204         poll.outcome = _("No clear winner: ") +
205           sorted.select { |a|
206             a[1][:count] > 0
207           }.collect { |a|
208             _("'#{a[1][:value]}' got #{a[1][:count]} vote#{a[1][:count] > 1 ? 's' : ''}")
209           }.join(", ")
210       else
211         winning_pct = "%3.0f%%" % [ 100 * (winner_info[1][:count] / winner_info[2]) ]
212         poll.outcome = _("The winner was choice #{winner_info[0]}: " +
213                        "'#{winner_info[1][:value]}' " +
214                        "with #{winner_info[1][:count]} " +
215                        "vote#{winner_info[1][:count] > 1 ? 's' : ''} (#{winning_pct})")
216       end
217     end
218
219     @bot.say poll.channel, poll.outcome
220
221     # Now that we're done, move it to the archives
222     archives = @registry[:archives]
223     archives[poll_id] = poll
224     @registry[:archives] = archives
225
226     # ... and take it out of the running list
227     running = @registry[:running]
228     running.delete(poll_id)
229     @registry[:running] = running
230   end
231
232   def list(m, params)
233     if @registry[:running].keys.length == 0
234       m.reply _("No polls running right now")
235       return
236     end
237
238     @registry[:running].each { |id, p|
239       m.reply _("#{p.author}'s poll \"#{p.question}\" (id ##{p.id}) runs until #{p.ends_at}")
240     }
241   end
242
243   def record_vote(m, params)
244     poll_id = params[:id].to_i
245     if @registry[:running].has_key?(poll_id) == false
246       m.reply _("I don't have poll ##{poll_id} running :(")
247       return
248     end
249
250     running = @registry[:running]
251
252     poll = running[poll_id]
253     result = poll.record_vote(m.sourcenick, params[:choice])
254
255     running[poll_id] = poll
256     @registry[:running] = running
257     m.reply result
258   end
259
260   def info(m, params)
261     params[:id] = params[:id].to_i
262     if @registry[:running].has_key? params[:id]
263       poll = @registry[:running][params[:id]]
264     elsif @registry[:archives].has_key? params[:id]
265       poll = @registry[:archives][params[:id]]
266     else
267       m.reply _("Sorry, couldn't find poll ##{Bold}#{params[:id]}#{NormalText}")
268       return
269     end
270
271     to_reply = _("Poll ##{poll.id} was asked by #{Bold}#{poll.author}#{NormalText} " +
272                  "in #{Bold}#{poll.channel}#{NormalText} #{poll.started}.")
273     if poll.running
274       to_reply << _(" It's still running!")
275       if poll.voters.has_key? m.sourcenick
276         to_reply << _(" Be patient, it'll end #{poll.ends_at}")
277       else
278         to_reply << _(" You have until #{poll.ends_at} to vote if you haven't!")
279         to_reply << " #{poll.options}"
280       end
281     else
282       to_reply << " #{poll.outcome}"
283     end
284
285     m.reply _(to_reply)
286   end
287
288   def help(plugin,topic="")
289     case topic
290     when "start"
291       _("poll start 'my question' 'answer1' 'answer2' ['answer3' ...] " +
292         "[for 5 minutes] : Start a poll for the given duration. " +
293         "If you don't specify a duration the default will be used.")
294     when "list"
295       _("poll list : Give some info about currently active polls")
296     when "info"
297       _("poll info #{Bold}id#{Bold} : Get info about /results from a given poll")
298     when "vote"
299       _("poll vote #{Bold}id choice#{Bold} : Vote on the given poll with your choice")
300     else
301       _("Hold informative polls: poll start|list|info|vote")
302     end
303   end
304 end
305
306 plugin = PollPlugin.new
307 plugin.map 'poll start *blob [for *duration]', :action => 'start'
308 plugin.map 'poll list', :action => 'list'
309 plugin.map 'poll info :id', :action => 'info'
310 plugin.map 'poll vote :id :choice', :action => 'record_vote', :threaded => true