4 # :title: Voting plugin for rbot
5 # Author:: David Gadling <dave@toasterwaffles.com>
6 # Copyright:: (C) 2010 David Gadling
9 # Submit a poll question to a channel, wait for glorious outcome.
13 attr_accessor :id, :author, :channel, :running, :ends_at, :started
14 attr_accessor :question, :answers, :duration, :voters, :outcome
16 def initialize(originating_message, question, answers, duration)
17 @author = originating_message.sourcenick
18 @channel = originating_message.channel
28 @answers[answer_index] = {
40 @ends_at = @started + @duration
45 return if @running == false
49 def record_vote(voter, choice)
51 return _("Poll's closed!")
54 if @voters.has_key? voter
55 return _("You already voted for #{@voters[voter]}!")
59 if @answers.has_key? choice
60 @answers[choice][:count] += 1
61 @voters[voter] = choice
63 return _("Recorded your vote for #{choice}: #{@answers[choice][:value]}")
65 return _("Don't have an option #{choice}")
70 return Hash[:question => @question,
71 :answers => @answers.keys.collect { |a| [a, @answers[a][:value]] }
80 options = _("Options are: ")
81 @answers.each { |letter, info|
82 options << "#{Bold}#{letter}#{NormalText}) #{info[:value]} "
88 class PollPlugin < Plugin
89 Config.register Config::IntegerValue.new('poll.max_concurrent_polls',
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',
97 :desc => _("Should we save results until we see the nick of the pollster?"))
99 def init_reg_entry(sym, default)
100 unless @registry.has_key?(sym)
101 @registry[sym] = default
107 init_reg_entry :running, Hash.new
108 init_reg_entry :archives, Hash.new
109 init_reg_entry :last_poll_id, 0
112 def authors_running_count(victim)
113 return @registry[:running].values.collect { |p|
114 if p.author == victim
119 }.inject(0) { |acc, v| acc + v }
123 author = m.sourcenick
126 max_concurrent = @bot.config['poll.max_concurrent_polls']
127 if authors_running_count(author) == max_concurrent
128 m.reply _("Sorry, you're already at the limit (#{max_concurrent}) polls")
132 input_blob = params[:blob].to_s.strip
133 quote_character = input_blob[0,1]
134 chunks = input_blob.split(/#{quote_character}\s+#{quote_character}/)
135 if chunks.length <= 2
136 m.reply _("This isn't a dictatorship!")
140 # grab the question, removing the leading quote character
141 question = chunks[0][1..-1].strip
142 question << "?" unless question[-1,1] == "?"
143 answers = chunks[1..-1].map { |a| a.strip }
145 # if the last answer terminates with a quote character,
146 # there is no time specification, so strip the quote character
147 # and assume default duration
148 if answers.last[-1,1] == quote_character
149 answers.last.chomp!(quote_character)
151 target_duration = @bot.config['poll.default_duration']
153 last_quote = answers.last.rindex(quote_character)
154 time_spec = answers.last[(last_quote+1)..-1].strip
155 answers.last[last_quote..-1] = String.new
157 # now answers.last is really the (cleaned-up) last answer,
158 # while time_spec holds the (cleaned-up) time spec, which
159 # should start with 'for' or 'until'
160 time_word, target_duration = time_spec.split(/\s+/, 2)
161 time_word = time_word.strip.intern rescue nil
166 duration = Utils.parse_time_offset(target_duration) rescue nil
168 # TODO "until <some moment in time>"
173 m.reply _("I don't understand the time spec %{timespec}") % {
174 :timespec => "'#{time_word} #{target_duration}'"
179 poll = Poll.new(m, question, answers, duration)
181 m.reply _("New poll from #{author}: #{Bold}#{question}#{NormalText}")
184 poll.id = @registry[:last_poll_id] + 1
186 command = _("poll vote #{poll.id} <SINGLE-LETTER>")
187 m.reply _("You have #{Bold}#{target_duration}#{NormalText} to: " +
188 "#{Bold}/msg #{@bot.nick} #{command}#{NormalText} or " +
189 "#{Bold}#{@bot.config['core.address_prefix']}#{command}#{NormalText} ")
191 running = @registry[:running]
192 running[poll.id] = poll
193 @registry[:running] = running
194 @bot.timer.add_once(duration) { count_votes(poll.id) }
195 @registry[:last_poll_id] = poll.id
198 def count_votes(poll_id)
199 poll = @registry[:running][poll_id]
202 return if poll == nil
205 @bot.say(poll.channel, _("Let's find the answer to: #{Bold}#{poll.question}#{NormalText}"))
207 sorted = poll.answers.sort { |a,b| b[1][:count]<=>a[1][:count] }
209 winner_info = sorted.first
210 winner_info << sorted.inject(0) { |accum, choice| accum + choice[1][:count] }
212 if winner_info[2] == 0
213 poll.outcome = _("Nobody voted")
215 if sorted[0][1][:count] == sorted[1][1][:count]
216 poll.outcome = _("No clear winner: ") +
220 _("'#{a[1][:value]}' got #{a[1][:count]} vote#{a[1][:count] > 1 ? 's' : ''}")
223 winning_pct = "%3.0f%%" % [ 100 * (winner_info[1][:count] / winner_info[2]) ]
224 poll.outcome = _("The winner was choice #{winner_info[0]}: " +
225 "'#{winner_info[1][:value]}' " +
226 "with #{winner_info[1][:count]} " +
227 "vote#{winner_info[1][:count] > 1 ? 's' : ''} (#{winning_pct})")
231 @bot.say poll.channel, poll.outcome
233 # Now that we're done, move it to the archives
234 archives = @registry[:archives]
235 archives[poll_id] = poll
236 @registry[:archives] = archives
238 # ... and take it out of the running list
239 running = @registry[:running]
240 running.delete(poll_id)
241 @registry[:running] = running
245 if @registry[:running].keys.length == 0
246 m.reply _("No polls running right now")
250 @registry[:running].each { |id, p|
251 m.reply _("#{p.author}'s poll \"#{p.question}\" (id ##{p.id}) runs until #{p.ends_at}")
255 def record_vote(m, params)
256 poll_id = params[:id].to_i
257 if @registry[:running].has_key?(poll_id) == false
258 m.reply _("I don't have poll ##{poll_id} running :(")
262 running = @registry[:running]
264 poll = running[poll_id]
265 result = poll.record_vote(m.sourcenick, params[:choice])
267 running[poll_id] = poll
268 @registry[:running] = running
273 params[:id] = params[:id].to_i
274 if @registry[:running].has_key? params[:id]
275 poll = @registry[:running][params[:id]]
276 elsif @registry[:archives].has_key? params[:id]
277 poll = @registry[:archives][params[:id]]
279 m.reply _("Sorry, couldn't find poll ##{Bold}#{params[:id]}#{NormalText}")
283 to_reply = _("Poll ##{poll.id} was asked by #{Bold}#{poll.author}#{NormalText} " +
284 "in #{Bold}#{poll.channel}#{NormalText} #{poll.started}.")
286 to_reply << _(" It's still running!")
287 if poll.voters.has_key? m.sourcenick
288 to_reply << _(" Be patient, it'll end #{poll.ends_at}")
290 to_reply << _(" You have until #{poll.ends_at} to vote if you haven't!")
291 to_reply << " #{poll.options}"
294 to_reply << " #{poll.outcome}"
300 def help(plugin,topic="")
303 _("poll start 'my question' 'answer1' 'answer2' ['answer3' ...] " +
304 "[for 5 minutes] : Start a poll for the given duration. " +
305 "If you don't specify a duration the default will be used.")
307 _("poll list : Give some info about currently active polls")
309 _("poll info #{Bold}id#{Bold} : Get info about /results from a given poll")
311 _("poll vote #{Bold}id choice#{Bold} : Vote on the given poll with your choice")
313 _("Hold informative polls: poll start|list|info|vote")
318 plugin = PollPlugin.new
319 plugin.map 'poll start *blob', :action => 'start'
320 plugin.map 'poll list', :action => 'list'
321 plugin.map 'poll info :id', :action => 'info'
322 plugin.map 'poll vote :id :choice', :action => 'record_vote', :threaded => true