3 ## bring in the rubyvote library
4 require '/home/mako/bin/lib/rubyvote'
6 # list of support election methods
7 @voting_methods = { 'plurality' => PluralityVote,
8 'approval' => ApprovalVote,
9 'condorcet' => CondorcetVote,
10 'ssd' => CloneproofSSDVote,
12 'runoff' => InstantRunoffVote }
14 @default_method = 'ssd'
19 @from = "Selectricity Anywhere <vote@selectricity.org>" # from address
21 ## create an active record connection to an SQLite3 database where we
22 ## will store all of the vote data
24 require '/usr/share/rails/activerecord/lib/active_record.rb'
26 ActiveRecord::Base.establish_connection(
28 :dbfile=> '/home/mako/.votemail.db')
30 class Election < ActiveRecord::Base
34 class Vote < ActiveRecord::Base
38 # expire old elections
40 Election.find_all(["active = 1"]).each do |election|
41 if Time.now - election.startdate > 1.hour
48 # create a new election object
49 def new_election(name, candidate_string=nil, options={})
51 # check to see if the name is in use
52 if Election.find_all(["active = 1 and name = ?", name]).length > 0
53 reply "Vote name \"%s\" in use. Try later or with new name." % name
57 # check to see if it is a verboten name
58 if name == 'new' or name == 'create' or name =~ /^res/
59 reply "Sorry. You can't create a vote with that name."
63 # see if there are any repeat candidates
64 if candidate_string.split(//).any? {|i| candidate_string.split(//).grep(i).length > 1}
65 reply "The choice list cannot contain repeated characters."
69 # make sure there is a candidate list
70 if candidate_string.split(//).length < 2
71 reply "A vote needs at least two candidates"
75 # otherwise, we have what we need to create and populate the new
77 election = Election.new
79 election.candidate_string = candidate_string.split(//).sort.join("")
80 election.startdate = Time.now
83 # set the voting method
84 if options.has_key?('method')
85 if @voting_methods.has_key?(options['method'])
86 election.voting_method = options['method']
88 reply "Unsupported voting method. Vote not created."
92 election.voting_method = @default_method
95 # save the info string on candidates if anything is provided
96 if options.has_key?('candidates')
98 # generate the info string
99 election.info = candidate_string.split(//).collect do |c|
100 if options['candidates'].has_key?(c)
101 description = options['candidates'][c]
103 description = 'unknown'
105 [c.upcase, description].join(":")
110 reply ("Vote created. Vote like: \"%s %s\". " +
111 "Arrange capital letters in order of voter preference. " +
112 "Send \"res %s\" for results.") % \
113 [name, candidate_string.upcase, name]
116 reply "Failure creating vote."
121 def register_vote(name=nil, votestring=nil)
123 election = Election.find_all(["active = 1 and name = ?", name])[0]
125 # check that the vote string doesn't contain duplicates
126 if votestring.split(//).any? {|i| votestring.split(//).grep(i).length > 1}
127 reply "The choice list cannot contain repeated characters."
131 elsif votestring.split(//).any? {|i| not election.candidate_string.split(//).include?(i)}
132 reply "The choice list must only include these characters: %s" % election.candidate_string
136 unless election.voting_method == 'plurality' or election.voting_method == 'approval'
137 # OR doesn't list all of the necessary options
138 if votestring.length != election.candidate_string.length
139 reply "You must list ALL of these choices: \"%s\"" % election.candidate_string
144 # if this person has voted already, blow away the vote
145 Vote.find_all(["election_id = ? and email = ?", election.id, @email]).each do |vote|
149 # create the new vote and populate the string
151 vote.votestring = votestring
153 vote.election = election
155 # attach the vote to the election
157 reply "Vote recorded successfully as: %s" % vote.votestring.upcase
160 reply "Failed to register your vote."
165 def results(name=nil)
167 # ensure that we can find a vote by the name
168 if Election.find_all(["active = 1 and name = ?", name]).length != 1
169 reply "Sorry! I cannot find active vote by that name."
172 election = Election.find_all(["active = 1 and name = ?", name])[0]
175 if election.votes.length == 0
176 reply "I have recorded no votes for that election."
180 # build the tally of the number of votes
182 election.votes.each do |vote|
183 tally << vote.votestring.split(//)
186 result = @voting_methods[election.voting_method].new(tally).result
189 if result.winners.length == 1
190 message = "The winner is: %s" % result.winners[0].upcase
192 message = "The winners are: %s" % result.winners.join(", ")
195 message = "There was no winner."
198 message += (" (Total votes: %s)" % election.votes.length )
203 def provide_info(name=nil)
204 if not name or Election.find_all(["active = 1 and name = ?", name]).length != 1
205 reply "Sorry! I cannot find an active vote by that name."
208 election = Election.find_all(["active = 1 and name = ?", name])[0]
212 info_string = election.info
214 info_string = election.candidate_string.upcase.split(//).join(", ")
217 reply ("Choices include %s. Please list ALL and reply with " +
218 "\"%s %s\" with %s being in the order of your preference.") \
219 % [ info_string, election.name,
220 election.candidate_string.upcase,
221 election.candidate_string.upcase ]
225 ## samller helper functions
226 #############################
228 def trim_body(body="")
230 body.sub!(/^(.*?)(-- |\n\n).*$/m, '\1')
231 body.gsub!(/\s+/, ' ')
236 # this message takes a message and sends it back to the person who is
239 # create a new blank message
240 outgoing = RMail::Message.new
241 header = outgoing.header
246 header.subject = "vote"
249 # send the mail over SMTP
250 Net::SMTP.start('localhost', 25) do |smtp|
251 smtp.send_message outgoing.to_s, @from, @email
255 def build_options(args=[])
257 candidates = Hash.new
261 key, value = *item.split(':')
263 candidates[key] = value
270 if candidates.length > 0
271 options['candidates'] = candidates
278 ## main logic of the program
279 #######################################################
282 # run this each time to turn off any old emails
285 # grab the command out of the email
286 incoming_msg = RMail::Parser.read(STDIN)
288 # record the address from the incoming mail
289 @email = incoming_msg.header.from.first.address
291 ## try to extract the body of the mail
292 # first test to see if its multipart
293 if incoming_msg.multipart?
294 incoming_msg.body.each do |msg|
295 if msg.header.media_type == 'text' and msg.header.subtype == 'plain'
296 command = trim_body(incoming_msg.body)
301 # if it's not multipart, just grab the contents
303 command = trim_body(incoming_msg.body)
306 # parse the commands command
307 args = command.split.collect {|s| s.downcase}
309 # if the person is creating a new vote
310 if args[0] == 'new' or args[0] == 'create'
312 # verify that we have been given the right number of arguments
313 if args[1] and args[2]
315 # attempt to create the new election
316 new_election(args[1], args[2], build_options(args[3..-1]))
319 # otherwise, provide feedback
320 reply "Command be of the form: \"new votename ABCD\" where ABCD is the list of candidates."
323 # if the person is asking for help
324 elsif args[0] == "help" or args[0] == '?'
325 reply "Commands: \"new yourvotename ABCD\"; \"yourvotename BDCA\" (in preferred order); \"res yourvotename\", "
327 # if the person is trying to get results
328 elsif args[0] =~ /^res/
331 elsif args[0] == 'info'
332 provide_info(args[1])
334 elsif Election.find_all(["active = 1 and name = ?", args[0]]).length == 1 \
336 provide_info(args[0])
338 # otherwise, check to see if it's a vote prefixed by a votename
339 elsif Election.find_all(["active = 1 and name = ?", args[0]]).length == 1
340 register_vote args[0], args[1]
342 # otherwise, we don't know, give an error
344 reply "I don't understand your command. Perhaps you have the wrong " +
345 "vote name? Reply \"help\" for more information."