summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Dittberner <jandd@cacert.org>2016-06-13 00:22:59 +0200
committerJan Dittberner <jandd@cacert.org>2016-06-13 00:22:59 +0200
commitd359d4697784147de1245940477af041c2817c13 (patch)
tree662e772dc7de53dfb0f3d76e5e782ca1129d4ba5
parent75de011666eacb7894507495f640ade0afd1aa33 (diff)
downloadcacert-votebot-d359d4697784147de1245940477af041c2817c13.tar.gz
cacert-votebot-d359d4697784147de1245940477af041c2817c13.tar.xz
cacert-votebot-d359d4697784147de1245940477af041c2817c13.zip
Refactor Votebot to use spring-boot
This commit moves the vote bot and audit bot code to separate packages, adds spring-boot annotations and adds some more refactorings to improve the code base.
-rw-r--r--build.gradle31
-rw-r--r--src/main/java/org/cacert/votebot/CAcertVoteAuditor.java109
-rw-r--r--src/main/java/org/cacert/votebot/CAcertVoteBot.java94
-rw-r--r--src/main/java/org/cacert/votebot/CAcertVoteMechanics.java171
-rw-r--r--src/main/java/org/cacert/votebot/IRCBot.java28
-rw-r--r--src/main/java/org/cacert/votebot/IRCClient.java256
-rw-r--r--src/main/java/org/cacert/votebot/audit/CAcertVoteAuditor.java189
-rw-r--r--src/main/java/org/cacert/votebot/audit/package-info.java25
-rw-r--r--src/main/java/org/cacert/votebot/package-info.java25
-rw-r--r--src/main/java/org/cacert/votebot/shared/CAcertVoteMechanics.java174
-rw-r--r--src/main/java/org/cacert/votebot/shared/IRCBot.java94
-rw-r--r--src/main/java/org/cacert/votebot/shared/IRCClient.java430
-rw-r--r--src/main/java/org/cacert/votebot/shared/VoteType.java72
-rw-r--r--src/main/java/org/cacert/votebot/shared/exceptions/IRCClientException.java34
-rw-r--r--src/main/java/org/cacert/votebot/shared/exceptions/InvalidChannelName.java36
-rw-r--r--src/main/java/org/cacert/votebot/shared/exceptions/InvalidNickName.java36
-rw-r--r--src/main/java/org/cacert/votebot/shared/exceptions/NoBotAssigned.java36
-rw-r--r--src/main/java/org/cacert/votebot/shared/exceptions/package-info.java25
-rw-r--r--src/main/java/org/cacert/votebot/shared/package-info.java25
-rw-r--r--src/main/java/org/cacert/votebot/vote/CAcertVoteBot.java177
-rw-r--r--src/main/java/org/cacert/votebot/vote/package-info.java25
-rw-r--r--src/main/resources/application.properties26
-rw-r--r--src/main/resources/messages.properties28
23 files changed, 1486 insertions, 660 deletions
diff --git a/build.gradle b/build.gradle
index fe5908e..fd2b8c7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,14 +1,33 @@
description = "IRC vote bot for CAcert.org"
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE"
+ }
+}
+
apply plugin: 'java'
+apply plugin: 'application'
+apply plugin: 'spring-boot'
-sourceCompatibility = 1.7
-targetCompatibility = 1.7
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+mainClassName = 'org.cacert.votebot.vote.CAcertVoteBot'
repositories {
mavenCentral()
}
+dependencies {
+ compile "org.springframework.boot:spring-boot-starter:1.3.5.RELEASE"
+ compile "commons-cli:commons-cli:1.3.1"
+ testCompile "org.springframework.boot:spring-boot-starter-test:1.3.5.RELEASE"
+}
+
group = 'org.cacert'
version = '0.1.0-SNAPSHOT'
@@ -16,3 +35,11 @@ task wrapper(type: Wrapper) {
gradleVersion = '2.13'
}
+bootRun {
+ args System.getProperty("exec.args").split()
+}
+
+bootRepackage {
+ enabled = false
+ executable = true
+}
diff --git a/src/main/java/org/cacert/votebot/CAcertVoteAuditor.java b/src/main/java/org/cacert/votebot/CAcertVoteAuditor.java
deleted file mode 100644
index 95fc87d..0000000
--- a/src/main/java/org/cacert/votebot/CAcertVoteAuditor.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package org.cacert.votebot;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.cacert.votebot.CAcertVoteMechanics.VoteType;
-
-public class CAcertVoteAuditor extends IRCBot {
-
- public CAcertVoteAuditor(IRCClient c, String toAudit) {
- super(c);
- this.toAudit = toAudit;
- c.join(voteAuxChn);
- }
-
- String voteAuxChn = System.getProperty("auditor.target.voteChn", "vote");
-
- String toAudit;
-
- long warn = Long.parseLong(System.getProperty("voteBot.warnSecs", "90"));
-
- long timeout = Long.parseLong(System.getProperty("voteBot.timeoutSecs", "120"));
-
- CAcertVoteMechanics mech = new CAcertVoteMechanics();
-
- String[] capturedResults = new String[VoteType.values().length];
-
- int ctr = -1;
-
- @Override
- public synchronized void publicMessage(String from, String channel, String message) {
- if (channel.equals(voteAuxChn)) {
- if (from.equals(toAudit)) {
- if (ctr >= 0) {
- capturedResults[ctr++] = message;
-
- if (ctr == capturedResults.length) {
- String[] reals = mech.closeVote();
-
- if (Arrays.equals(reals, capturedResults)) {
- System.out.println("Audit for vote was successful.");
- } else {
- System.out.println("Audit failed! Vote Bot (or Auditor) is probably broken.");
- }
-
- ctr = -1;
- }
-
- return;
- }
- if (message.startsWith("New Vote: ")) {
- System.out.println("detected vote-start");
-
- Pattern p = Pattern.compile("New Vote: (.*) has started a vote on \"(.*)\"");
- Matcher m = p.matcher(message);
-
- if ( !m.matches()) {
- System.out.println("error: vote-start malformed");
- return;
- }
-
- mech.callVote(m.group(1), m.group(2));
- } else if (message.startsWith("Results: ")) {
- System.out.println("detected vote-end. Reading results");
-
- ctr = 0;
- }
- } else {
- if (ctr != -1) {
- System.out.println("Vote after end.");
- return;
- }
-
- System.out.println("detected vote");
- mech.evaluateVote(from, message);
- System.out.println("Current state: " + mech.getCurrentResult());
- }
- }
- }
-
- @Override
- public synchronized void privateMessage(String from, String message) {
-
- }
-
- @Override
- public synchronized void join(String cleanReferent, String chn) {
-
- }
-
- @Override
- public synchronized void part(String cleanReferent, String chn) {
-
- }
-
- public static void main(String[] args) throws IOException, InterruptedException {
- IRCClient ic = IRCClient.parseCommandLine(args, "dogcraft_de-Auditor");
- String targetNick = System.getProperty("auditor.target.nick");
-
- if (targetNick == null) {
- System.out.println("use -Dauditor.target.nick=TargetNick to set a target nick");
- System.exit(0);
- }
-
- ic.setBot(new CAcertVoteAuditor(ic, targetNick));
- }
-}
diff --git a/src/main/java/org/cacert/votebot/CAcertVoteBot.java b/src/main/java/org/cacert/votebot/CAcertVoteBot.java
deleted file mode 100644
index 94c2f2d..0000000
--- a/src/main/java/org/cacert/votebot/CAcertVoteBot.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.cacert.votebot;
-
-import java.io.IOException;
-
-import org.cacert.votebot.CAcertVoteMechanics.State;
-
-public class CAcertVoteBot extends IRCBot implements Runnable {
-
- public CAcertVoteBot(IRCClient c) {
- super(c);
-
- c.join(meetingChn);
- c.join(voteAuxChn);
-
- new Thread(this).start();
- }
-
- String meetingChn = System.getProperty("voteBot.meetingChn", "agm");
-
- String voteAuxChn = System.getProperty("voteBot.voteChn", "vote");
-
- long warn = Long.parseLong(System.getProperty("voteBot.warnSecs", "90"));
-
- long timeout = Long.parseLong(System.getProperty("voteBot.timeoutSecs", "120"));
-
- CAcertVoteMechanics mech = new CAcertVoteMechanics();
-
- @Override
- public synchronized void publicMessage(String from, String channel, String message) {
- if (channel.equals(voteAuxChn)) {
- sendPublicMessage(voteAuxChn, mech.evaluateVote(from, message));
- }
- }
-
- @Override
- public synchronized void privateMessage(String from, String message) {
- if (message.startsWith("vote ")) {
- String response = mech.callVote(from, message.substring(5));
- sendPrivateMessage(from, response);
-
- if (response.startsWith("Sorry,")) {
- return;
- }
-
- anounce("New Vote: " + from + " has started a vote on \"" + mech.getTopic() + "\"");
- sendPublicMessage(meetingChn, "Please cast your vote in #vote");
- sendPublicMessage(voteAuxChn, "Please cast your vote in the next " + timeout + " seconds.");
- }
- }
-
- private synchronized void anounce(String msg) {
- sendPublicMessage(meetingChn, msg);
- sendPublicMessage(voteAuxChn, msg);
- }
-
- public void run() {
- try {
- while (true) {
- while (mech.getState() == State.IDLE) {
- Thread.sleep(1000);
- }
-
- Thread.sleep(warn * 1000);
- anounce("Voting on " + mech.getTopic() + " will end in " + (timeout - warn) + " seconds.");
- Thread.sleep((timeout - warn) * 1000);
- anounce("Voting on " + mech.getTopic() + " has closed.");
- String[] res = mech.closeVote();
- anounce("Results: for " + mech.getTopic() + ":");
-
- for (int i = 0; i < res.length; i++) {
- anounce(res[i]);
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public synchronized void join(String cleanReferent, String chn) {
-
- }
-
- @Override
- public synchronized void part(String cleanReferent, String chn) {
-
- }
-
- public static void main(String[] args) throws IOException, InterruptedException {
- IRCClient ic = IRCClient.parseCommandLine(args, "dogcraft_de");
- ic.setBot(new CAcertVoteBot(ic));
- }
-
-}
diff --git a/src/main/java/org/cacert/votebot/CAcertVoteMechanics.java b/src/main/java/org/cacert/votebot/CAcertVoteMechanics.java
deleted file mode 100644
index e331eac..0000000
--- a/src/main/java/org/cacert/votebot/CAcertVoteMechanics.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package org.cacert.votebot;
-
-import java.util.HashMap;
-import java.util.Map.Entry;
-
-/**
- * Reprenents the voting-automata for voting in IRC chanenls.
- */
-public class CAcertVoteMechanics {
-
- public static enum VoteType {
- AYE, NAYE, ABSTAIN
- }
-
- public static enum State {
- RUNNING, IDLE
- }
-
- State state = State.IDLE;
-
- String voteCaller;
-
- String topic;
-
- private String vote(String voter, String actor, VoteType type) {
- votes.put(voter, type);
-
- if (voter.equals(actor)) {
- return "Thanks " + actor + " I count your vote as " + type;
- } else {
- return "Thanks " + actor + " I count your vote for " + voter + " as " + type;
- }
- }
-
- HashMap<String, VoteType> votes = new HashMap<>();
-
- private String voteError(String actor) {
- return "Sorry " + actor + ", I did not understand your vote, your current vote state remains unchanged!";
- }
-
- /**
- * Adds a vote to the current topic. This interprets proxies.
- *
- * @param actor
- * the person that sent this vote
- * @param txt
- * the text that the person sent.
- * @return A message to
- *
- * <pre>
- * actor
- * </pre>
- *
- * indicating tha result of his action.
- */
- public synchronized String evaluateVote(String actor, String txt) {
- if (state != State.RUNNING) {
- return "Sorry " + actor + ", but currently no vote is running.";
- }
-
- String voter = actor;
- String value = null;
-
- if (txt.toLowerCase().matches("^\\s*proxy\\s.*")) {
- String[] parts = txt.split("\\s+");
- if (parts.length == 3) {
- voter = parts[1];
- value = parts[2];
- }
- } else {
- value = txt.replaceAll("^\\s*|\\s*$", "");
- }
-
- if (value == null) {
- return voteError(actor);
- } else {
- value = value.toLowerCase();
-
- switch (value) {
- case "aye":
- case "yes":
- case "oui":
- case "ja": {
- return vote(voter, actor, VoteType.AYE);
- }
- case "naye":
- case "nay":
- case "no":
- case "non":
- case "nein": {
- return vote(voter, actor, VoteType.NAYE);
- }
- case "abstain":
- case "enthaltung":
- case "enthalten":
- case "abs": {
- return vote(voter, actor, VoteType.ABSTAIN);
- }
- }
- }
-
- return voteError(actor);
- }
-
- /**
- * A new vote begins.
- *
- * @param from
- * the nick that called the vote
- * @param topic
- * the topic of the vote
- * @return A response to
- *
- * <pre>
- * from
- * </pre>
- *
- * indicating success or failure.
- */
- public synchronized String callVote(String from, String topic) {
- if (state != State.IDLE) {
- return "Sorry, a vote is already running";
- }
-
- voteCaller = from;
- this.topic = topic;
- votes.clear();
-
- state = State.RUNNING;
-
- return "Vote started.";
- }
-
- /**
- * Ends a vote.
- *
- * @return An array of Strings containing result status messages.
- */
- public synchronized String[] closeVote() {
- int[] res = new int[VoteType.values().length];
-
- for (Entry<String, VoteType> i : votes.entrySet()) {
- res[i.getValue().ordinal()]++;
- }
-
- String[] results = new String[VoteType.values().length];
-
- for (int i = 0; i < res.length; i++) {
- results[i] = (VoteType.values()[i] + ": " + res[i]);
- }
-
- votes.clear();
- voteCaller = null;
- state = State.IDLE;
-
- return results;
- }
-
- public String getTopic() {
- return topic;
- }
-
- public State getState() {
- return state;
- }
-
- public String getCurrentResult() {
- return votes.toString();
- }
-
-}
diff --git a/src/main/java/org/cacert/votebot/IRCBot.java b/src/main/java/org/cacert/votebot/IRCBot.java
deleted file mode 100644
index 2fe40ad..0000000
--- a/src/main/java/org/cacert/votebot/IRCBot.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.cacert.votebot;
-
-public abstract class IRCBot {
-
- private IRCClient c;
-
- public IRCBot(IRCClient c) {
- this.c = c;
- }
-
- public abstract void publicMessage(String from, String channel,
- String message);
-
- public abstract void privateMessage(String from, String message);
-
- public void sendPublicMessage(String channel, String message) {
- c.send(message, channel);
- }
-
- public void sendPrivateMessage(String to, String message) {
- c.sendPrivate(message, to);
- }
-
- public abstract void part(String cleanReferent, String chn);
-
- public abstract void join(String cleanReferent, String chn);
-
-}
diff --git a/src/main/java/org/cacert/votebot/IRCClient.java b/src/main/java/org/cacert/votebot/IRCClient.java
deleted file mode 100644
index 52aad37..0000000
--- a/src/main/java/org/cacert/votebot/IRCClient.java
+++ /dev/null
@@ -1,256 +0,0 @@
-package org.cacert.votebot;
-
-import java.io.BufferedReader;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.net.Socket;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.concurrent.Semaphore;
-
-import javax.net.ssl.SSLSocketFactory;
-
-public class IRCClient {
-
- private Semaphore loggedin = new Semaphore(0);
-
- private PrintWriter out;
-
- private Socket s;
-
- class ServerReader implements Runnable {
-
- private BufferedReader br;
-
- public ServerReader(BufferedReader br) {
- this.br = br;
-
- new Thread(this).start();
- }
-
- @Override
- public void run() {
- String l;
-
- try {
- while ((l = br.readLine()) != null) {
- String fullline = l;
- // System.out.println(l);
-
- if (l.startsWith("PING ")) {
- System.out.println("PONG");
- out.println("PONG " + l.substring(5));
- }
-
- String referent = "";
-
- if (l.startsWith(":")) {
- String[] parts = l.split(" ", 2);
- referent = parts[0];
- l = parts[1];
- }
-
- String[] command = l.split(" ", 3);
-
- if (command[0].equals("001")) {
- loggedin.release();
- }
-
- if (command[0].equals("PRIVMSG")) {
- String msg = command[2].substring(1);
- String chnl = command[1];
-
- if ( !chnl.startsWith("#")) {
- handlePrivMsg(referent, msg);
- } else {
- handleMsg(referent, chnl, msg);
- }
-
- log(chnl, fullline);
- } else if (command[0].equals("JOIN")) {
- String chn = command[1].substring(1);
- targetBot.join(cleanReferent(referent), chn.substring(1));
- log(chn, fullline);
- } else if (command[0].equals("PART")) {
- String chn = command[1];
- targetBot.part(cleanReferent(referent), chn);
- log(chn, fullline);
- } else {
- System.out.println("unknown line: ");
- System.out.println(l);
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- }
-
- private String cleanReferent(String referent) {
- String[] parts = referent.split("!");
-
- if ( !parts[0].startsWith(":")) {
- System.err.println("invalid public message");
- return "unknown";
- }
-
- return parts[0];
- }
-
- HashMap<String, PrintWriter> logs = new HashMap<String, PrintWriter>();
-
- private void log(String chn, String l) throws IOException {
- PrintWriter log = logs.get(chn);
-
- if (log == null) {
- log = new PrintWriter(new FileOutputStream("irc/log_" + chn), true);
- logs.put(chn, log);
- }
-
- log.println(l);
- }
-
- private void handlePrivMsg(String referent, String msg) {
- String[] parts = referent.split("!");
-
- if ( !parts[0].startsWith(":")) {
- System.err.println("invalid private message");
- return;
- }
-
- if (targetBot == null) {
- System.out.println("dropping message");
- return;
- }
-
- targetBot.privateMessage(parts[0].substring(1), msg);
- }
-
- private void handleMsg(String referent, String chnl, String msg) {
- String[] parts = referent.split("!");
-
- if ( !parts[0].startsWith(":")) {
- System.err.println("invalid public message");
- return;
- }
-
- if (targetBot == null) {
- System.out.println("dropping message");
- return;
- }
-
- if ( !chnl.startsWith("#")) {
- System.err.println("invalid public message (chnl)");
- return;
- }
-
- targetBot.publicMessage(parts[0].substring(1), chnl.substring(1), msg);
- }
-
- }
-
- public IRCClient(String nick, String server, int port, boolean ssl) throws IOException, InterruptedException {
- if ( !nick.matches("[a-zA-Z0-9_-]+")) {
- throw new Error("malformed");
- }
-
- if (ssl) {
- s = SSLSocketFactory.getDefault().createSocket(server, port);//default-ssl = 7000
- // default-ssl = 7000
- } else {
- s = new Socket(server, port);
- // default-plain = 6667
- }
-
- out = new PrintWriter(s.getOutputStream(), true);
- BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
-
- new ServerReader(in);
-
- out.println("USER " + nick + " 0 * :unknown");
- out.println("NICK " + nick);
-
- loggedin.acquire();
- }
-
- HashSet<String> joined = new HashSet<String>();
-
- private IRCBot targetBot;
-
- public void join(String channel) {
- if ( !channel.matches("[a-zA-Z0-9_-]+")) {
- return;
- }
-
- if (joined.add(channel)) {
- out.println("JOIN #" + channel);
- }
- }
-
- public void leave(String channel) {
- if ( !channel.matches("[a-zA-Z0-9_-]+")) {
- return;
- }
-
- if (joined.remove(channel)) {
- out.println("PART #" + channel);
- }
- }
-
- public void send(String msg, String channel) {
- if ( !channel.matches("[a-zA-Z0-9_-]+")) {
- return;
- }
-
- out.println("PRIVMSG #" + channel + " :" + msg);
- }
-
- public void sendPrivate(String msg, String to) {
- if ( !to.matches("[a-zA-Z0-9_-]+")) {
- return;
- }
-
- out.println("PRIVMSG " + to + " :" + msg);
- }
-
- public void setBot(IRCBot bot) {
- this.targetBot = bot;
- }
-
- public static IRCClient parseCommandLine(String[] commandLine, String nick) throws IOException, InterruptedException {
- String host = null;
- int port = 7000;
- boolean ssl = true;
-
- for (int i = 0; i < commandLine.length; i++) {
- String cmd = commandLine[i];
-
- if (cmd.equals("--no-ssl") || cmd.equals("-u")) {
- ssl = false;
- } else if (cmd.equals("--ssl") || cmd.equals("-s")) {
- ssl = true;
- } else if (cmd.equals("--host") || cmd.equals("-h")) {
- host = commandLine[++i];
- } else if (cmd.equals("--port") || cmd.equals("-p")) {
- port = Integer.parseInt(commandLine[++i]);
- } else if (cmd.equals("--nick") || cmd.equals("-n")) {
- nick = commandLine[++i];
- } else if (cmd.equals("--help") || cmd.equals("-h")) {
- System.out.println("Options: [--no-ssl|-u|--ssl|-s|[--host|-h] <host>|[--port|-p] <port>|[--nick|-n] <nick>]*");
- System.out.println("Requires the -host argument, --ssl is default, last argument of a kind is significant.");
- throw new Error("Operation caneled");
- } else {
- throw new Error("Invalid option (usage with --help): " + cmd);
- }
- }
-
- if (host == null) {
- throw new Error("--host <host> is missing");
- }
-
- return new IRCClient(nick, host, port, ssl);
- }
-
-}
diff --git a/src/main/java/org/cacert/votebot/audit/CAcertVoteAuditor.java b/src/main/java/org/cacert/votebot/audit/CAcertVoteAuditor.java
new file mode 100644
index 0000000..b5afc58
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/audit/CAcertVoteAuditor.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.cacert.votebot.audit;
+
+import org.apache.commons.cli.ParseException;
+import org.cacert.votebot.shared.CAcertVoteMechanics;
+import org.cacert.votebot.shared.IRCBot;
+import org.cacert.votebot.shared.IRCClient;
+import org.cacert.votebot.shared.VoteType;
+import org.cacert.votebot.shared.exceptions.IRCClientException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Auditor bot for votes.
+ *
+ * @author Felix Doerre
+ * @author Jan Dittberner
+ */
+@SpringBootApplication(scanBasePackageClasses = {CAcertVoteAuditor.class, IRCClient.class})
+@Component
+public class CAcertVoteAuditor extends IRCBot implements CommandLineRunner {
+ private static final Logger LOGGER = LoggerFactory.getLogger(
+ CAcertVoteAuditor.class);
+ private static final String NEW_VOTE_REGEX =
+ "New Vote: (.*) has started a vote on \"(.*)\"";
+
+ @Value("${auditor.target.nick}")
+ private String toAudit;
+
+ @Value("${auditor.target.voteChn}")
+ private String voteAuxChn;
+
+ @Autowired
+ private IRCClient ircClient;
+
+ @Autowired
+ private final CAcertVoteMechanics voteMechanics = new CAcertVoteMechanics();
+
+ private final String[] capturedResults = new String[VoteType.values().length];
+
+ private int counter = -1;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected final IRCClient getIrcClient() {
+ return ircClient;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final synchronized void publicMessage(final String from, final String channel, final String message) {
+ if (channel.equals(voteAuxChn)) {
+ if (from.equals(toAudit)) {
+ if (counter >= 0) {
+ capturedResults[counter++] = message;
+
+ if (counter == capturedResults.length) {
+ final String[] reals = voteMechanics.closeVote();
+
+ if (Arrays.equals(reals, capturedResults)) {
+ LOGGER.info("Audit for vote was successful.");
+ } else {
+ LOGGER.warn("Audit failed! Vote Bot (or Auditor) is probably broken.");
+ }
+
+ counter = -1;
+ }
+
+ return;
+ }
+ if (message.startsWith("New Vote: ")) {
+ LOGGER.info("detected vote-start");
+
+ final Pattern pattern = Pattern.compile(NEW_VOTE_REGEX);
+ final Matcher matcher = pattern.matcher(message);
+
+ if (!matcher.matches()) {
+ LOGGER.warn("error: vote-start malformed");
+ return;
+ }
+
+ voteMechanics.callVote(matcher.group(2));
+ } else if (message.startsWith("Results: ")) {
+ LOGGER.info("detected vote-end. Reading results");
+
+ counter = 0;
+ }
+ } else {
+ if (counter != -1) {
+ LOGGER.info("Vote after end.");
+ return;
+ }
+
+ LOGGER.info("detected vote");
+ voteMechanics.evaluateVote(from, message);
+ final String currentResult = voteMechanics.getCurrentResult();
+ LOGGER.info("Current state: {}", currentResult);
+ }
+ }
+ }
+
+ /**
+ * Do nothing for private messages.
+ *
+ * @param from source nick for the message
+ * @param message message text
+ */
+ @Override
+ public synchronized void privateMessage(final String from, final String message) {
+ }
+
+ /**
+ * Do nothing on join messages.
+ *
+ * @param referent joining nick
+ * @param channel channel name
+ */
+ @Override
+ public synchronized void join(final String referent, final String channel) {
+ }
+
+ /**
+ * Do nothing on part messages.
+ *
+ * @param referent leaving nick
+ * @param channel channel name
+ */
+ @Override
+ public synchronized void part(final String referent, final String channel) {
+ }
+
+ /**
+ * Run the audit bot.
+ * {@inheritDoc}
+ *
+ * @param args command line arguments
+ */
+ @Override
+ public final void run(final String... args) {
+ try {
+ getIrcClient().initializeFromArgs(args).assignBot(this);
+
+ getIrcClient().join(voteAuxChn);
+ } catch (IOException | InterruptedException | ParseException | IRCClientException e) {
+ LOGGER.error("error running votebot {}", e.getMessage());
+ }
+ }
+ /**
+ * Entry point for the audit bot.
+ *
+ * @param args command line arguments
+ */
+ public static void main(final String... args) {
+ SpringApplication.run(CAcertVoteAuditor.class, args);
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/audit/package-info.java b/src/main/java/org/cacert/votebot/audit/package-info.java
new file mode 100644
index 0000000..195ce83
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/audit/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * The audit package contains an IRC bot for auditing votes.
+ *
+ * @author Jan Dittberner
+ */
+package org.cacert.votebot.audit;
diff --git a/src/main/java/org/cacert/votebot/package-info.java b/src/main/java/org/cacert/votebot/package-info.java
new file mode 100644
index 0000000..f0abf72
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * The CAcert vote bot is used to record votes during CAcert IRC meetings.
+ *
+ * @author Jan Dittberner
+ */
+package org.cacert.votebot;
diff --git a/src/main/java/org/cacert/votebot/shared/CAcertVoteMechanics.java b/src/main/java/org/cacert/votebot/shared/CAcertVoteMechanics.java
new file mode 100644
index 0000000..2eb8124
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/CAcertVoteMechanics.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.cacert.votebot.shared;
+
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.ResourceBundle;
+
+/**
+ * Represents the voting-automate for voting in IRC channels.
+ */
+@Component
+public final class CAcertVoteMechanics {
+ private static final String PROXY_RE = "^\\s*proxy\\s.*";
+ private static final int VOTE_MESSAGE_PART_COUNT = 3;
+
+ private State state = State.IDLE;
+ private String topic;
+ private final Map<String, VoteType> votes = new HashMap<>();
+ private final ResourceBundle resourceBundle = ResourceBundle.getBundle("messages");
+
+ /**
+ * Voting state indicating whether a vote is currently running or not.
+ */
+ public enum State {
+ /**
+ * A vote is currently running.
+ */
+ RUNNING,
+ /**
+ * No vote is running.
+ */
+ IDLE
+ }
+
+ private String vote(final String voter, final String actor, final VoteType type) {
+ votes.put(voter, type);
+
+ if (voter.equals(actor)) {
+ return String.format(resourceBundle.getString("count_vote"), actor, type);
+ } else {
+ return String.format(resourceBundle.getString("count_proxy_vote"), actor, voter, type);
+ }
+ }
+
+ private String voteError(final String actor) {
+ return String.format(resourceBundle.getString("vote_not_understood"), actor);
+ }
+
+ private String proxyVoteError(final String actor) {
+ return String.format(resourceBundle.getString("invalid_proxy_vote"), actor);
+ }
+
+ /**
+ * Adds a vote to the current topic. This interprets proxies.
+ *
+ * @param actor the person that sent this vote
+ * @param txt the text that the person sent.
+ * @return A message to <code>actor</code> indicating the result of his action.
+ */
+ public synchronized String evaluateVote(final String actor, final String txt) {
+ if (state != State.RUNNING) {
+ return String.format(resourceBundle.getString("no_vote_running"), actor);
+ }
+
+ final String voter;
+ final String value;
+
+ if (txt.toLowerCase().matches(PROXY_RE)) {
+ String[] parts = txt.split("\\s+");
+ if (parts.length == VOTE_MESSAGE_PART_COUNT) {
+ voter = parts[1];
+ value = parts[2];
+ } else {
+ return proxyVoteError(actor);
+ }
+ } else {
+ voter = actor;
+ value = txt.trim();
+ }
+
+ try {
+ return vote(voter, actor, VoteType.evaluate(value));
+ } catch (IllegalArgumentException iae) {
+ return voteError(actor);
+ }
+ }
+
+ /**
+ * A new vote begins.
+ *
+ * @param topic the topic of the vote
+ * @return A response to <code>from</code> indicating success or failure.
+ */
+ public synchronized String callVote(final String topic) {
+ if (state != State.IDLE) {
+ return resourceBundle.getString("vote_running");
+ }
+
+ this.topic = topic;
+ votes.clear();
+
+ state = State.RUNNING;
+
+ return resourceBundle.getString("vote_started");
+ }
+
+ /**
+ * Ends a vote.
+ *
+ * @return An array of Strings containing result status messages.
+ */
+ public synchronized String[] closeVote() {
+ final int[] resultCounts = new int[VoteType.values().length];
+
+ for (final Entry<String, VoteType> voteEntry : votes.entrySet()) {
+ resultCounts[voteEntry.getValue().ordinal()]++;
+ }
+
+ final String[] results = new String[VoteType.values().length];
+
+ for (int i = 0; i < results.length; i++) {
+ results[i] = String.format("%s: %d", VoteType.values()[i], resultCounts[i]);
+ }
+
+ votes.clear();
+ state = State.IDLE;
+ topic = "";
+
+ return results;
+ }
+
+ /**
+ * @return Topic of the current vote.
+ */
+ public String getTopic() {
+ return topic;
+ }
+
+ /**
+ * @return Voting state
+ */
+ public State getState() {
+ return state;
+ }
+
+ /**
+ * @return current vote results as string
+ */
+ public String getCurrentResult() {
+ return votes.toString();
+ }
+
+}
diff --git a/src/main/java/org/cacert/votebot/shared/IRCBot.java b/src/main/java/org/cacert/votebot/shared/IRCBot.java
new file mode 100644
index 0000000..9f90b27
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/IRCBot.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2015 Felix Doerre
+ * Copyright (c) 2016 Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.cacert.votebot.shared;
+
+import org.cacert.votebot.shared.exceptions.IRCClientException;
+
+/**
+ * Base class for IRC bot implementations.
+ *
+ * @author Felix Doerre
+ * @author Jan Dittberner
+ */
+public abstract class IRCBot {
+ /**
+ * @return IRC client implementation associated with the bot
+ */
+ protected abstract IRCClient getIrcClient();
+
+ /**
+ * Handle a received public message.
+ *
+ * @param from sender nick name
+ * @param channel channel name
+ * @param message message text
+ * @throws IRCClientException for IRC client problems
+ */
+ public abstract void publicMessage(String from, String channel,
+ String message) throws IRCClientException;
+
+ /**
+ * Handle a received private message.
+ *
+ * @param from sender nick name
+ * @param message message text
+ * @throws IRCClientException for IRC client problems
+ */
+ public abstract void privateMessage(String from, String message) throws IRCClientException;
+
+ /**
+ * Send a public message.
+ *
+ * @param channel channel name
+ * @param message message text
+ * @throws IRCClientException for IRC client problems
+ */
+ protected final void sendPublicMessage(final String channel, final String message) throws IRCClientException {
+ getIrcClient().send(message, channel);
+ }
+
+ /**
+ * Send a private message.
+ *
+ * @param to recipient nick name
+ * @param message message text
+ * @throws IRCClientException for IRC client problems
+ */
+ protected final void sendPrivateMessage(final String to, final String message) throws IRCClientException {
+ getIrcClient().sendPrivate(message, to);
+ }
+
+ /**
+ * Handle leave message.
+ *
+ * @param referent nick name
+ * @param channel channel name
+ */
+ public abstract void part(String referent, String channel);
+
+ /**
+ * Handle join message.
+ *
+ * @param referent nick name
+ * @param channel channel name
+ */
+ public abstract void join(String referent, String channel);
+}
diff --git a/src/main/java/org/cacert/votebot/shared/IRCClient.java b/src/main/java/org/cacert/votebot/shared/IRCClient.java
new file mode 100644
index 0000000..ee56ecc
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/IRCClient.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (c) 2015 Felix Doerre
+ * Copyright (c) 2016 Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.cacert.votebot.shared;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.cacert.votebot.shared.exceptions.IRCClientException;
+import org.cacert.votebot.shared.exceptions.InvalidChannelName;
+import org.cacert.votebot.shared.exceptions.InvalidNickName;
+import org.cacert.votebot.shared.exceptions.NoBotAssigned;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+
+/**
+ * This class encapsulates the communication with the IRC server.
+ *
+ * @author Felix Doerre
+ * @author Jan Dittberner
+ */
+@Component
+public final class IRCClient {
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = LoggerFactory.getLogger(IRCClient.class);
+ /**
+ * Regular expression to validate IRC nick names.
+ */
+ private static final String NICK_RE = "[a-zA-Z0-9_-]+";
+ /**
+ * Regular expression to validate IRC channel names.
+ */
+ private static final String CHANNEL_RE = "[a-zA-Z0-9_-]+";
+
+ private final Semaphore loggedin = new Semaphore(0);
+ private PrintWriter out;
+ private final Set<String> joinedChannels = new HashSet<>();
+ private IRCBot targetBot;
+
+ /**
+ * Initialize the IRC client based on command line arguments.
+ *
+ * @param args command line arguments
+ * @return the instance itself
+ * @throws IOException in case of network IO problems
+ * @throws InterruptedException in case of thread interruption
+ * @throws ParseException in case of problems parsing the command line arguments
+ * @throws IRCClientException in case of syntactic errors related to the IRC protocol
+ */
+ public IRCClient initializeFromArgs(final String... args)
+ throws IOException, InterruptedException, ParseException, IRCClientException {
+ final Options opts = new Options();
+ opts.addOption(
+ Option.builder("u").longOpt("no-ssl")
+ .desc("disable SSL").build());
+ opts.addOption(
+ Option.builder("h").longOpt("host").hasArg(true).required()
+ .desc("hostname of the IRC server").build());
+ opts.addOption(
+ Option.builder("p").longOpt("port").hasArg(true)
+ .desc("tcp port of the IRC server").type(Integer.class).build());
+ opts.addOption(
+ Option.builder("n").longOpt("nick").hasArg(true).argName("nick").required()
+ .desc("IRC nick name").build());
+
+ final CommandLineParser commandLineParser = new DefaultParser();
+ try {
+ final CommandLine commandLine = commandLineParser.parse(opts, args);
+ initialize(
+ commandLine.getOptionValue("nick"),
+ commandLine.getOptionValue("host"),
+ Integer.parseInt(commandLine.getOptionValue("port", "7000")),
+ !commandLine.hasOption("no-ssl"));
+ } catch (final ParseException pe) {
+ final HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp("votebot", opts);
+ throw pe;
+ }
+ return this;
+ }
+
+ private void initialize(final String nick, final String server, final int port, final boolean ssl)
+ throws IOException,
+ InterruptedException, IRCClientException {
+ if (!nick.matches(NICK_RE)) {
+ throw new IRCClientException(String.format("malformed nickname %s", nick));
+ }
+
+ final Socket socket;
+ if (ssl) {
+ socket = SSLSocketFactory.getDefault().createSocket(server, port); //default-ssl = 7000
+ } else {
+ socket = new Socket(server, port); // default-plain = 6667
+ }
+
+ out = new PrintWriter(socket.getOutputStream(), true);
+ final BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+ new ServerReader(in);
+
+ out.println("USER " + nick + " 0 * :unknown");
+ out.println("NICK " + nick);
+
+ loggedin.acquire();
+ }
+
+ /**
+ * Check whether preconditions for a channel command are met.
+ *
+ * @param channel channel name
+ * @throws NoBotAssigned when no bot is associated with this client
+ * @throws InvalidChannelName when the channel name is invalid
+ */
+ private void checkChannelPreconditions(final String channel) throws NoBotAssigned, InvalidChannelName {
+ if (targetBot == null) {
+ throw new NoBotAssigned();
+ }
+
+ if (!channel.matches(CHANNEL_RE)) {
+ throw new InvalidChannelName(channel);
+ }
+ }
+
+ /**
+ * Check whether preconditions for a private message command are met.
+ *
+ * @param nick nick name
+ * @throws NoBotAssigned when no bot is associated with this client
+ * @throws InvalidNickName when the nick name is invalid
+ */
+ private void checkPrivateMessagePreconditions(final String nick) throws NoBotAssigned, InvalidNickName {
+ if (targetBot == null) {
+ throw new NoBotAssigned();
+ }
+
+ if (!nick.matches(NICK_RE)) {
+ throw new InvalidNickName(nick);
+ }
+ }
+
+ /**
+ * Let the associated bot join the given channel.
+ *
+ * @param channel channel name
+ * @throws IRCClientException for IRC client issue
+ */
+ public void join(final String channel) throws IRCClientException {
+ checkChannelPreconditions(channel);
+
+ if (joinedChannels.add(channel)) {
+ out.println("JOIN #" + channel);
+ }
+ }
+
+ /**
+ * Let the associated bot leave the given channel.
+ *
+ * @param channel channel name
+ * @throws IRCClientException for IRC client issues
+ */
+ public void leave(final String channel) throws IRCClientException {
+ checkChannelPreconditions(channel);
+
+ if (joinedChannels.remove(channel)) {
+ out.println("PART #" + channel);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @PreDestroy
+ private void leaveAll() {
+ List<String> channels = new ArrayList<>(joinedChannels);
+ for (String channel : channels) {
+ try {
+ leave(channel);
+ } catch (IRCClientException e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Send a message to the given channel.
+ *
+ * @param msg message
+ * @param channel channel name
+ * @throws IRCClientException for IRC client issues
+ */
+ public void send(final String msg, final String channel) throws IRCClientException {
+ checkChannelPreconditions(channel);
+
+ out.println(String.format("PRIVMSG #%s :%s", channel, msg));
+ }
+
+ /**
+ * Send a private message to the given nick name.
+ *
+ * @param msg message
+ * @param to nickname
+ * @throws IRCClientException for IRC client issues
+ */
+ public void sendPrivate(final String msg, final String to) throws IRCClientException {
+ checkPrivateMessagePreconditions(to);
+
+ out.println(String.format("PRIVMSG %s :%s", to, msg));
+ }
+
+ /**
+ * Assign a bot to this client.
+ *
+ * @param bot IRC bot instance
+ */
+ public void assignBot(final IRCBot bot) {
+ targetBot = bot;
+ }
+
+ /**
+ * Reader thread for handling the IRC connection.
+ */
+ private class ServerReader implements Runnable {
+ private final BufferedReader bufferedReader;
+ private final Map<String, PrintWriter> logs = new HashMap<>();
+
+ ServerReader(final BufferedReader bufferedReader) {
+ this.bufferedReader = bufferedReader;
+
+ new Thread(this).start();
+ }
+
+ @Override
+ public void run() {
+ String line;
+
+ try {
+ while ((line = bufferedReader.readLine()) != null) {
+ final String fullLine = line;
+
+ if (line.startsWith("PING ")) {
+ handleIrcPing(line);
+ continue;
+ }
+
+ String referent = "";
+
+ if (line.startsWith(":")) {
+ final String[] parts = line.split(" ", 2);
+ referent = parts[0];
+ line = parts[1];
+ }
+
+ final String[] command = line.split(" ", 3);
+
+ if (command[0].equals("001")) {
+ loggedin.release();
+ }
+
+ switch (command[0]) {
+ case "PRIVMSG":
+ final String msg = command[2].substring(1);
+ final String chnl = command[1];
+
+ if (chnl.startsWith("#")) {
+ handleMsg(referent, chnl, msg);
+ } else {
+ handlePrivMsg(referent, msg);
+ }
+
+ log(chnl, fullLine);
+ break;
+ case "JOIN": {
+ final String channel = command[1].substring(1);
+ targetBot.join(cleanReferent(referent), channel.substring(1));
+ log(channel, fullLine);
+ break;
+ }
+ case "PART":
+ final String channel = command[1];
+ targetBot.part(cleanReferent(referent), channel);
+ log(channel, fullLine);
+ break;
+ default:
+ LOGGER.info("unknown line: {}", line);
+ break;
+ }
+ }
+ } catch (final IOException | IRCClientException e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ }
+
+ private void handleIrcPing(final String line) {
+ LOGGER.debug("PONG");
+ out.println("PONG " + line.substring("PING ".length()));
+ }
+
+ private String cleanReferent(final String referent) {
+ final String[] parts = referent.split("!");
+
+ if (!parts[0].startsWith(":")) {
+ LOGGER.error("invalid public message");
+ return "unknown";
+ }
+
+ return parts[0];
+ }
+
+ private void log(final String channel, final String logline) {
+ PrintWriter log = logs.get(channel);
+
+ if (log == null) {
+ final Path dirPath = Paths.get("irc");
+ if (!Files.exists(dirPath)) {
+ final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rwxr-x---");
+ final FileAttribute fileAttributes = PosixFilePermissions.asFileAttribute(permissions);
+ try {
+ Files.createDirectory(dirPath, fileAttributes);
+ } catch (final IOException e) {
+ LOGGER.error("error creating directory 'irc': {}", e.getMessage());
+ return;
+ }
+ }
+ final Path filePath = dirPath.resolve(String.format("log_%s", channel));
+ try {
+ log = new PrintWriter(Files
+ .newBufferedWriter(filePath, StandardCharsets.UTF_8, StandardOpenOption.APPEND,
+ StandardOpenOption.CREATE, StandardOpenOption.WRITE));
+ } catch (final IOException e) {
+ LOGGER.error("error opening log file '{}' for writing: {}", filePath, e.getMessage());
+ return;
+ }
+ logs.put(channel, log);
+ }
+
+ log.println(logline);
+ log.flush();
+ }
+
+ @SuppressWarnings("unused")
+ @PreDestroy
+ private void closeLogs() {
+ for (final PrintWriter pwr : logs.values()) {
+ pwr.flush();
+ pwr.close();
+ }
+ logs.clear();
+ }
+
+ private void handlePrivMsg(final String referent, final String msg) throws IRCClientException {
+ if (targetBot == null) {
+ throw new NoBotAssigned();
+ }
+
+ final String[] parts = referent.split("!");
+
+ if (!parts[0].startsWith(":")) {
+ LOGGER.warn("invalid private message: {}", msg);
+ return;
+ }
+
+ targetBot.privateMessage(parts[0].substring(1), msg);
+ }
+
+ private void handleMsg(final String referent, final String chnl, final String msg) throws IRCClientException {
+ if (targetBot == null) {
+ throw new NoBotAssigned();
+ }
+
+ final String[] parts = referent.split("!");
+
+ if (!parts[0].startsWith(":")) {
+ LOGGER.warn("invalid public message");
+ return;
+ }
+
+ if (!chnl.startsWith("#")) {
+ LOGGER.warn("invalid public message (chnl)");
+ return;
+ }
+
+ targetBot.publicMessage(parts[0].substring(1), chnl.substring(1), msg);
+ }
+
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/shared/VoteType.java b/src/main/java/org/cacert/votebot/shared/VoteType.java
new file mode 100644
index 0000000..5ebcaf9
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/VoteType.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2015 Felix Doerre
+ * Copyright (c) 2016 Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.cacert.votebot.shared;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Type for vote values.
+ *
+ * @author Felix Doerre
+ * @author Jan Dittberner
+ */
+public enum VoteType {
+ /**
+ * Vote counts as yes.
+ */
+ AYE("aye", "yes", "oui", "ja"),
+ /**
+ * Vote counts as no.
+ */
+ NAYE("naye", "nay", "no", "non", "nein"),
+ /**
+ * Vote counts as abstain.
+ */
+ ABSTAIN("abstain", "enthaltung", "abs");
+
+ private final Set<String> variants;
+
+ /**
+ * @param variants words that are counted as this vote type
+ */
+ VoteType(final String... variants) {
+ this.variants = new HashSet<>(Arrays.asList(variants));
+ }
+
+ /**
+ * Evaluate a given word to a VoteType value.
+ *
+ * @param vote word
+ * @return VoteType value
+ * @throws IllegalArgumentException if the word can not be evaluated
+ */
+ public static VoteType evaluate(final String vote) {
+ final String normalized = vote.trim().toLowerCase();
+ for (final VoteType value : values()) {
+ if (value.variants.contains(normalized)) {
+ return value;
+ }
+ }
+ throw new IllegalArgumentException(
+ String.format("%s is no valid vote", vote));
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/shared/exceptions/IRCClientException.java b/src/main/java/org/cacert/votebot/shared/exceptions/IRCClientException.java
new file mode 100644
index 0000000..4b89b7b
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/exceptions/IRCClientException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.cacert.votebot.shared.exceptions;
+
+/**
+ * Exception for IRC client errors.
+ *
+ * @author Jan Dittberner
+ */
+public class IRCClientException extends Exception {
+ /**
+ * @param message error message
+ */
+ public IRCClientException(final String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/shared/exceptions/InvalidChannelName.java b/src/main/java/org/cacert/votebot/shared/exceptions/InvalidChannelName.java
new file mode 100644
index 0000000..cd4063a
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/exceptions/InvalidChannelName.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.cacert.votebot.shared.exceptions;
+
+import java.util.ResourceBundle;
+
+/**
+ * Exception indicating an invalid IRC channel name.
+ *
+ * @author Jan Dittberner
+ */
+public class InvalidChannelName extends IRCClientException {
+ /**
+ * @param channel channel name
+ */
+ public InvalidChannelName(final String channel) {
+ super(String.format(ResourceBundle.getBundle("messages").getString("invalid_channel_name"), channel));
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/shared/exceptions/InvalidNickName.java b/src/main/java/org/cacert/votebot/shared/exceptions/InvalidNickName.java
new file mode 100644
index 0000000..bd03fdc
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/exceptions/InvalidNickName.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.cacert.votebot.shared.exceptions;
+
+import java.util.ResourceBundle;
+
+/**
+ * Exception indicating an invalid IRC nick name.
+ *
+ * @author Jan Dittberner
+ */
+public class InvalidNickName extends IRCClientException {
+ /**
+ * @param nickname IRC nick name
+ */
+ public InvalidNickName(final String nickname) {
+ super(String.format(ResourceBundle.getBundle("messages").getString("invalid_nick_name"), nickname));
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/shared/exceptions/NoBotAssigned.java b/src/main/java/org/cacert/votebot/shared/exceptions/NoBotAssigned.java
new file mode 100644
index 0000000..c30a35b
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/exceptions/NoBotAssigned.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.cacert.votebot.shared.exceptions;
+
+import java.util.ResourceBundle;
+
+/**
+ * Exception indicating a missing bot assignment.
+ *
+ * @author Jan Dittberner
+ */
+public class NoBotAssigned extends IRCClientException {
+ /**
+ * Create a new exception.
+ */
+ public NoBotAssigned() {
+ super(ResourceBundle.getBundle("messages").getString("assign_bot_not_called"));
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/shared/exceptions/package-info.java b/src/main/java/org/cacert/votebot/shared/exceptions/package-info.java
new file mode 100644
index 0000000..89d3501
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/exceptions/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Exception classes for IRC client and bots.
+ *
+ * @author Jan Dittberner
+ */
+package org.cacert.votebot.shared.exceptions;
diff --git a/src/main/java/org/cacert/votebot/shared/package-info.java b/src/main/java/org/cacert/votebot/shared/package-info.java
new file mode 100644
index 0000000..7765cbb
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/shared/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * The shared package contains classes that are used by both the vote and the audit bot.
+ *
+ * @author Jan Dittberner
+ */
+package org.cacert.votebot.shared;
diff --git a/src/main/java/org/cacert/votebot/vote/CAcertVoteBot.java b/src/main/java/org/cacert/votebot/vote/CAcertVoteBot.java
new file mode 100644
index 0000000..25cbb9c
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/vote/CAcertVoteBot.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.cacert.votebot.vote;
+
+import org.apache.commons.cli.ParseException;
+import org.cacert.votebot.shared.CAcertVoteMechanics;
+import org.cacert.votebot.shared.CAcertVoteMechanics.State;
+import org.cacert.votebot.shared.IRCBot;
+import org.cacert.votebot.shared.IRCClient;
+import org.cacert.votebot.shared.exceptions.IRCClientException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+
+/**
+ * Vote bot.
+ *
+ * @author Felix Doerre
+ * @author Jan Dittberner
+ */
+@SpringBootApplication(scanBasePackageClasses = {IRCClient.class, CAcertVoteBot.class})
+@Component
+public class CAcertVoteBot extends IRCBot implements Runnable, CommandLineRunner {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CAcertVoteBot.class);
+ private static final int MILLIS_ONE_SECOND = 1000;
+
+ /**
+ * Meeting channel where votes and results are published.
+ */
+ @Value("${voteBot.meetingChn}")
+ private String meetingChannel;
+
+ /**
+ * Channel name where voting is performed.
+ */
+ @Value("${voteBot.voteChn}")
+ private String voteChannel;
+
+ /**
+ * Seconds to warn before a vote ends.
+ */
+ @Value("${voteBot.warnSecs}")
+ private long warn;
+
+ /**
+ * Seconds before a vote times out.
+ */
+ @Value("${voteBot.timeoutSecs}")
+ private long timeout;
+
+ @Autowired
+ private CAcertVoteMechanics voteMechanics;
+
+ @Autowired
+ private IRCClient ircClient;
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param args command line arguments
+ */
+ @Override
+ public final void run(final String... args) {
+ try {
+ getIrcClient().initializeFromArgs(args).assignBot(this);
+
+ getIrcClient().join(meetingChannel);
+ getIrcClient().join(voteChannel);
+
+ new Thread(this).start();
+ } catch (IOException | InterruptedException | ParseException | IRCClientException e) {
+ LOGGER.error(String.format("error running votebot %s", e.getMessage()));
+ }
+ }
+
+ @Override
+ protected final IRCClient getIrcClient() {
+ return ircClient;
+ }
+
+ @Override
+ public final synchronized void publicMessage(final String from, final String channel, final String message) throws
+ IRCClientException {
+ if (channel.equals(voteChannel)) {
+ sendPublicMessage(voteChannel, voteMechanics.evaluateVote(from, message));
+ }
+ }
+
+ @Override
+ public final synchronized void privateMessage(final String from, final String message) throws IRCClientException {
+ if (message.startsWith("vote ")) {
+ final String response = voteMechanics.callVote(message.substring(5));
+ sendPrivateMessage(from, response);
+
+ if (response.startsWith("Sorry,")) {
+ return;
+ }
+
+ announce("New Vote: " + from + " has started a vote on \"" + voteMechanics.getTopic() + "\"");
+ sendPublicMessage(meetingChannel, "Please cast your vote in #vote");
+ sendPublicMessage(voteChannel, "Please cast your vote in the next " + timeout + " seconds.");
+ }
+ }
+
+ private synchronized void announce(final String msg) throws IRCClientException {
+ sendPublicMessage(meetingChannel, msg);
+ sendPublicMessage(voteChannel, msg);
+ }
+
+ @Override
+ public final void run() {
+ try {
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ while (voteMechanics.getState() == State.IDLE) {
+ Thread.sleep(MILLIS_ONE_SECOND);
+ }
+
+ Thread.sleep(warn * MILLIS_ONE_SECOND);
+ announce("Voting on " + voteMechanics.getTopic() + " will end in " + (timeout - warn) + " seconds.");
+ Thread.sleep((timeout - warn) * MILLIS_ONE_SECOND);
+ announce("Voting on " + voteMechanics.getTopic() + " has closed.");
+ final String[] res = voteMechanics.closeVote();
+ announce("Results: for " + voteMechanics.getTopic() + ":");
+
+ for (final String re : res) {
+ announce(re);
+ }
+ }
+ } catch (final InterruptedException | IRCClientException e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public synchronized void join(final String referent, final String chn) {
+
+ }
+
+ @Override
+ public synchronized void part(final String referent, final String channel) {
+
+ }
+
+ /**
+ * Entry point for the vote bot.
+ *
+ * @param args command line arguments
+ */
+ public static void main(final String... args) {
+ SpringApplication.run(CAcertVoteBot.class, args);
+ }
+}
diff --git a/src/main/java/org/cacert/votebot/vote/package-info.java b/src/main/java/org/cacert/votebot/vote/package-info.java
new file mode 100644
index 0000000..8c8c6d2
--- /dev/null
+++ b/src/main/java/org/cacert/votebot/vote/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016. Jan Dittberner
+ *
+ * This file is part of CAcert votebot.
+ *
+ * CAcert votebot is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * CAcert votebot is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * The vote package contains the vote bot.
+ *
+ * @author Jan Dittberner
+ */
+package org.cacert.votebot.vote;
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 0000000..f417a70
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1,26 @@
+#
+# Copyright (c) 2016. Jan Dittberner
+#
+# This file is part of CAcert votebot.
+#
+# CAcert votebot is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# CAcert votebot is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+#
+
+voteBot.meetingChn=${meetingChn:agm}
+voteBot.voteChn=${voteChn:vote}
+voteBot.warnSecs=${warnSecs:90}
+voteBot.timeoutSecs=${timeoutSecs:120}
+
+auditor.target.voteChn=${voteChn:vote}
+auditor.target.nick=${auditor.nick:} \ No newline at end of file
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
new file mode 100644
index 0000000..ff24c5b
--- /dev/null
+++ b/src/main/resources/messages.properties
@@ -0,0 +1,28 @@
+#
+# Copyright (c) 2016. Jan Dittberner
+#
+# This file is part of CAcert votebot.
+#
+# CAcert votebot is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# CAcert votebot is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# CAcert votebot. If not, see <http://www.gnu.org/licenses/>.
+#
+assign_bot_not_called=assignBot() has not been called.
+count_proxy_vote=Thanks %s I count your vote for %s as %s
+count_vote=Thanks %s I count your vote as %s
+invalid_channel_name=%s is not a valid channel name
+invalid_nick_name=%s is not a valid nick name.
+invalid_proxy_vote=Sorry %s, you tried an invalid proxy vote. Please use 'proxy <voter> <vote>'
+no_vote_running=Sorry %s, but currently no vote is running.
+vote_not_understood=Sorry %s, I did not understand your vote, your current vote state remains unchanged!
+vote_running=Sorry, a vote is already running
+vote_started=Vote started. \ No newline at end of file