Refactor Votebot to use spring-boot
authorJan Dittberner <jandd@cacert.org>
Sun, 12 Jun 2016 22:22:59 +0000 (00:22 +0200)
committerJan Dittberner <jandd@cacert.org>
Sun, 12 Jun 2016 22:22:59 +0000 (00:22 +0200)
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.

23 files changed:
build.gradle
src/main/java/org/cacert/votebot/CAcertVoteAuditor.java [deleted file]
src/main/java/org/cacert/votebot/CAcertVoteBot.java [deleted file]
src/main/java/org/cacert/votebot/CAcertVoteMechanics.java [deleted file]
src/main/java/org/cacert/votebot/IRCBot.java [deleted file]
src/main/java/org/cacert/votebot/IRCClient.java [deleted file]
src/main/java/org/cacert/votebot/audit/CAcertVoteAuditor.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/audit/package-info.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/package-info.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/CAcertVoteMechanics.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/IRCBot.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/IRCClient.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/VoteType.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/exceptions/IRCClientException.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/exceptions/InvalidChannelName.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/exceptions/InvalidNickName.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/exceptions/NoBotAssigned.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/exceptions/package-info.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/shared/package-info.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/vote/CAcertVoteBot.java [new file with mode: 0644]
src/main/java/org/cacert/votebot/vote/package-info.java [new file with mode: 0644]
src/main/resources/application.properties [new file with mode: 0644]
src/main/resources/messages.properties [new file with mode: 0644]

index fe5908e..fd2b8c7 100644 (file)
@@ -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 (file)
index 95fc87d..0000000
+++ /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 (file)
index 94c2f2d..0000000
+++ /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 (file)
index e331eac..0000000
+++ /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 (file)
index 2fe40ad..0000000
+++ /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 (file)
index 52aad37..0000000
+++ /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 (file)
index 0000000..b5afc58
--- /dev/null
@@ -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 (file)
index 0000000..195ce83
--- /dev/null
@@ -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 (file)
index 0000000..f0abf72
--- /dev/null
@@ -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 (file)
index 0000000..2eb8124
--- /dev/null
@@ -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 (file)
index 0000000..9f90b27
--- /dev/null
@@ -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 (file)
index 0000000..ee56ecc
--- /dev/null
@@ -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 (file)
index 0000000..5ebcaf9
--- /dev/null
@@ -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 (file)
index 0000000..4b89b7b
--- /dev/null
@@ -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 (file)
index 0000000..cd4063a
--- /dev/null
@@ -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 (file)
index 0000000..bd03fdc
--- /dev/null
@@ -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 (file)
index 0000000..c30a35b
--- /dev/null
@@ -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 (file)
index 0000000..89d3501
--- /dev/null
@@ -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 (file)
index 0000000..7765cbb
--- /dev/null
@@ -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 (file)
index 0000000..25cbb9c
--- /dev/null
@@ -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 (file)
index 0000000..8c8c6d2
--- /dev/null
@@ -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 (file)
index 0000000..f417a70
--- /dev/null
@@ -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 (file)
index 0000000..ff24c5b
--- /dev/null
@@ -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