#!/usr/bin/env ruby ## bring in the rubyvote library require '/home/mako/bin/lib/rubyvote' # list of support election methods @voting_methods = { 'plurality' => PluralityVote, 'approval' => ApprovalVote, 'condorcet' => CondorcetVote, 'ssd' => CloneproofSSDVote, 'borda' => BordaVote, 'runoff' => InstantRunoffVote } @default_method = 'ssd' # ruby mail handling require 'rmail' require 'net/smtp' @from = "Selectricity Anywhere " # from address ## create an active record connection to an SQLite3 database where we ## will store all of the vote data require 'sqlite3' require '/usr/share/rails/activerecord/lib/active_record.rb' ActiveRecord::Base.establish_connection( :adapter=> 'sqlite3', :dbfile=> '/home/mako/.votemail.db') class Election < ActiveRecord::Base has_many :votes end class Vote < ActiveRecord::Base belongs_to :election end # expire old elections def cleanup Election.find_all(["active = 1"]).each do |election| if Time.now - election.startdate > 1.hour election.active = 0 election.save end end end # create a new election object def new_election(name, candidate_string=nil, options={}) # check to see if the name is in use if Election.find_all(["active = 1 and name = ?", name]).length > 0 reply "Vote name \"%s\" in use. Try later or with new name." % name return false end # check to see if it is a verboten name if name == 'new' or name == 'create' or name =~ /^res/ reply "Sorry. You can't create a vote with that name." return false end # see if there are any repeat candidates if candidate_string.split(//).any? {|i| candidate_string.split(//).grep(i).length > 1} reply "The choice list cannot contain repeated characters." return false end # make sure there is a candidate list if candidate_string.split(//).length < 2 reply "A vote needs at least two candidates" return false end # otherwise, we have what we need to create and populate the new # election election = Election.new election.name = name election.candidate_string = candidate_string.split(//).sort.join("") election.startdate = Time.now election.active = 1 # set the voting method if options.has_key?('method') if @voting_methods.has_key?(options['method']) election.voting_method = options['method'] else reply "Unsupported voting method. Vote not created." return false end else election.voting_method = @default_method end # save the info string on candidates if anything is provided if options.has_key?('candidates') # generate the info string election.info = candidate_string.split(//).collect do |c| if options['candidates'].has_key?(c) description = options['candidates'][c] else description = 'unknown' end [c.upcase, description].join(":") end.join(", ") end if election.save reply ("Vote created. Vote like: \"%s %s\". " + "Arrange capital letters in order of voter preference. " + "Send \"res %s\" for results.") % \ [name, candidate_string.upcase, name] return true else reply "Failure creating vote." return false end end def register_vote(name=nil, votestring=nil) election = Election.find_all(["active = 1 and name = ?", name])[0] # check that the vote string doesn't contain duplicates if votestring.split(//).any? {|i| votestring.split(//).grep(i).length > 1} reply "The choice list cannot contain repeated characters." return false # OR extras elsif votestring.split(//).any? {|i| not election.candidate_string.split(//).include?(i)} reply "The choice list must only include these characters: %s" % election.candidate_string return false end unless election.voting_method == 'plurality' or election.voting_method == 'approval' # OR doesn't list all of the necessary options if votestring.length != election.candidate_string.length reply "You must list ALL of these choices: \"%s\"" % election.candidate_string return false end end # if this person has voted already, blow away the vote Vote.find_all(["election_id = ? and email = ?", election.id, @email]).each do |vote| vote.destroy end # create the new vote and populate the string vote = Vote.new vote.votestring = votestring vote.email = @email vote.election = election # attach the vote to the election if vote.save reply "Vote recorded successfully as: %s" % vote.votestring.upcase return true else reply "Failed to register your vote." return false end end def results(name=nil) # ensure that we can find a vote by the name if Election.find_all(["active = 1 and name = ?", name]).length != 1 reply "Sorry! I cannot find active vote by that name." return false else election = Election.find_all(["active = 1 and name = ?", name])[0] end if election.votes.length == 0 reply "I have recorded no votes for that election." return false end # build the tally of the number of votes tally = Array.new election.votes.each do |vote| tally << vote.votestring.split(//) end result = @voting_methods[election.voting_method].new(tally).result if result.winner? if result.winners.length == 1 message = "The winner is: %s" % result.winners[0].upcase else message = "The winners are: %s" % result.winners.join(", ") end else message = "There was no winner." end message += (" (Total votes: %s)" % election.votes.length ) reply(message) return true end def provide_info(name=nil) if not name or Election.find_all(["active = 1 and name = ?", name]).length != 1 reply "Sorry! I cannot find an active vote by that name." return false else election = Election.find_all(["active = 1 and name = ?", name])[0] end if election.info info_string = election.info else info_string = election.candidate_string.upcase.split(//).join(", ") end reply ("Choices include %s. Please list ALL and reply with " + "\"%s %s\" with %s being in the order of your preference.") \ % [ info_string, election.name, election.candidate_string.upcase, election.candidate_string.upcase ] return true end ## samller helper functions ############################# def trim_body(body="") body.strip! body.sub!(/^(.*?)(-- |\n\n).*$/m, '\1') body.gsub!(/\s+/, ' ') body.strip! return body end # this message takes a message and sends it back to the person who is # calling the script def reply(msg=nil) # create a new blank message outgoing = RMail::Message.new header = outgoing.header # fill the values in header.to = @email header.from = @from header.subject = "vote" outgoing.body = msg # send the mail over SMTP Net::SMTP.start('localhost', 25) do |smtp| smtp.send_message outgoing.to_s, @from, @email end end def build_options(args=[]) options = Hash.new candidates = Hash.new args.each do |item| if item =~ /\w+:\w+/ key, value = *item.split(':') if key.length == 1 candidates[key] = value else options[key] = value end end end if candidates.length > 0 options['candidates'] = candidates end options end ## main logic of the program ####################################################### # run this each time to turn off any old emails cleanup() # grab the command out of the email incoming_msg = RMail::Parser.read(STDIN) # record the address from the incoming mail @email = incoming_msg.header.from.first.address ## try to extract the body of the mail # first test to see if its multipart if incoming_msg.multipart? incoming_msg.body.each do |msg| if msg.header.media_type == 'text' and msg.header.subtype == 'plain' command = trim_body(incoming_msg.body) break end end # if it's not multipart, just grab the contents else command = trim_body(incoming_msg.body) end # parse the commands command args = command.split.collect {|s| s.downcase} # if the person is creating a new vote if args[0] == 'new' or args[0] == 'create' # verify that we have been given the right number of arguments if args[1] and args[2] # attempt to create the new election new_election(args[1], args[2], build_options(args[3..-1])) else # otherwise, provide feedback reply "Command be of the form: \"new votename ABCD\" where ABCD is the list of candidates." end # if the person is asking for help elsif args[0] == "help" or args[0] == '?' reply "Commands: \"new yourvotename ABCD\"; \"yourvotename BDCA\" (in preferred order); \"res yourvotename\", " # if the person is trying to get results elsif args[0] =~ /^res/ results(args[1]) elsif args[0] == 'info' provide_info(args[1]) elsif Election.find_all(["active = 1 and name = ?", args[0]]).length == 1 \ and not args[1] provide_info(args[0]) # otherwise, check to see if it's a vote prefixed by a votename elsif Election.find_all(["active = 1 and name = ?", args[0]]).length == 1 register_vote args[0], args[1] # otherwise, we don't know, give an error else reply "I don't understand your command. Perhaps you have the wrong " + "vote name? Reply \"help\" for more information." end