Implement help command, add more tests, improve messages
[cacert-votebot.git] / src / main / java / org / cacert / votebot / vote / CAcertVoteBot.java
1 /*
2 * Copyright (c) 2015 Felix Doerre
3 * Copyright (c) 2015 Benny Baumann
4 * Copyright (c) 2016, 2018 Jan Dittberner
5 *
6 * This file is part of CAcert VoteBot.
7 *
8 * CAcert VoteBot is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License as published by the Free
10 * Software Foundation, either version 3 of the License, or (at your option)
11 * any later version.
12 *
13 * CAcert VoteBot is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 * more details.
17 *
18 * You should have received a copy of the GNU General Public License along with
19 * CAcert VoteBot. If not, see <http://www.gnu.org/licenses/>.
20 */
21 package org.cacert.votebot.vote;
22
23 import org.apache.commons.cli.ParseException;
24 import org.cacert.votebot.shared.CAcertVoteMechanics;
25 import org.cacert.votebot.shared.CAcertVoteMechanics.State;
26 import org.cacert.votebot.shared.IRCBot;
27 import org.cacert.votebot.shared.IRCClient;
28 import org.cacert.votebot.shared.exceptions.IRCClientException;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.springframework.beans.factory.annotation.Autowired;
32 import org.springframework.beans.factory.annotation.Value;
33 import org.springframework.boot.CommandLineRunner;
34 import org.springframework.boot.SpringApplication;
35 import org.springframework.boot.autoconfigure.SpringBootApplication;
36 import org.springframework.stereotype.Component;
37
38 import java.io.IOException;
39 import java.text.MessageFormat;
40 import java.time.Duration;
41 import java.util.Locale;
42 import java.util.ResourceBundle;
43
44
45 /**
46 * VoteBot main class.
47 *
48 * @author Felix Doerre
49 * @author Jan Dittberner
50 */
51 @SpringBootApplication(scanBasePackageClasses = {IRCClient.class, CAcertVoteBot.class})
52 @Component
53 public class CAcertVoteBot extends IRCBot implements Runnable, CommandLineRunner {
54 private static final Logger LOGGER = LoggerFactory.getLogger(CAcertVoteBot.class);
55 private static final long MILLIS_ONE_SECOND = Duration.ofSeconds(1).toMillis();
56 private final ResourceBundle messages = ResourceBundle.getBundle("messages");
57
58 /**
59 * Meeting channel where votes and results are published.
60 */
61 @Value("${voteBot.meetingChn:meeting}")
62 private String meetingChannel;
63
64 /**
65 * Channel name where voting is performed.
66 */
67 @Value("${voteBot.voteChn:vote}")
68 private String voteChannel;
69
70 /**
71 * Seconds to warn before a vote ends.
72 */
73 @Value("${voteBot.warnSecs:90}")
74 private long warn;
75
76 /**
77 * Seconds before a vote times out.
78 */
79 @Value("${voteBot.timeoutSecs:120}")
80 private long timeout;
81
82 private final CAcertVoteMechanics voteMechanics;
83
84 private final IRCClient ircClient;
85
86 @Autowired
87 public CAcertVoteBot(CAcertVoteMechanics voteMechanics, IRCClient ircClient) {
88 this.voteMechanics = voteMechanics;
89 this.ircClient = ircClient;
90 }
91
92 /**
93 * {@inheritDoc}
94 *
95 * @param args command line arguments
96 */
97 @Override
98 public final void run(final String... args) {
99 try {
100 getIrcClient().initializeFromArgs(args).assignBot(this);
101
102 getIrcClient().join(meetingChannel);
103 getIrcClient().join(voteChannel);
104
105 new Thread(this).start();
106 } catch (IOException | InterruptedException | ParseException | IRCClientException e) {
107 LOGGER.error(String.format("error running votebot %s", e.getMessage()));
108 }
109 }
110
111 @Override
112 protected final IRCClient getIrcClient() {
113 return ircClient;
114 }
115
116 @Override
117 public final synchronized void publicMessage(final String from, final String channel, final String message) throws
118 IRCClientException {
119 if (channel.equals(voteChannel)) {
120 sendPublicMessage(voteChannel, voteMechanics.evaluateVote(from, message));
121 }
122 }
123
124 @Override
125 public final synchronized void privateMessage(final String from, final String message) throws IRCClientException {
126 if (message != null && message.length() > 0) {
127 String[] parts = message.split("\\s+", 2);
128 try {
129 VoteBotCommand command = VoteBotCommand.valueOf(parts[0].toUpperCase(Locale.ENGLISH));
130 switch (command) {
131 case VOTE:
132 startVote(from, parts[1]);
133 break;
134 case HELP:
135 giveHelp(from);
136 break;
137 }
138 } catch (IllegalArgumentException e) {
139 sendUnknownCommand(from, parts[0]);
140 }
141 }
142 }
143
144 private void sendUnknownCommand(String from, String command) throws IRCClientException {
145 sendPrivateMessage(from, String.format(messages.getString("unknown_command"), command));
146 }
147
148 private void giveHelp(String from) throws IRCClientException {
149 sendPrivateMessage(from, messages.getString("help_message"));
150 }
151
152 private void startVote(final String from, final String message) throws IRCClientException {
153 final String response = voteMechanics.callVote(message);
154 sendPrivateMessage(from, response);
155
156 if (response.startsWith("Sorry,")) {
157 return;
158 }
159
160 announce(MessageFormat.format(messages.getString("new_vote"), from, voteMechanics.getTopic()));
161 sendPublicMessage(
162 meetingChannel,
163 MessageFormat.format(messages.getString("cast_vote_in_vote_channel"), voteChannel));
164 sendPublicMessage(
165 voteChannel, MessageFormat.format(messages.getString("cast_vote_in_next_seconds"), timeout));
166 }
167
168 private synchronized void announce(final String msg) throws IRCClientException {
169 sendPublicMessage(meetingChannel, msg);
170 sendPublicMessage(voteChannel, msg);
171 }
172
173 @Override
174 public final void run() {
175 try {
176 //noinspection InfiniteLoopStatement
177 while (true) {
178 while (voteMechanics.getState() == State.IDLE) {
179 Thread.sleep(MILLIS_ONE_SECOND);
180 }
181
182 Thread.sleep(warn * MILLIS_ONE_SECOND);
183 announce("Voting on " + voteMechanics.getTopic() + " will end in " + (timeout - warn) + " seconds.");
184 Thread.sleep((timeout - warn) * MILLIS_ONE_SECOND);
185 announce("Voting on " + voteMechanics.getTopic() + " has closed.");
186 final String[] res = voteMechanics.closeVote();
187 announce("Results: for " + voteMechanics.getTopic() + ":");
188
189 for (final String re : res) {
190 announce(re);
191 }
192 }
193 } catch (final InterruptedException | IRCClientException e) {
194 LOGGER.error(e.getMessage(), e);
195 }
196 }
197
198 @Override
199 public synchronized void join(final String referent, final String chn) {
200
201 }
202
203 @Override
204 public synchronized void part(final String referent, final String channel) {
205
206 }
207
208 /**
209 * Entry point for the vote bot.
210 *
211 * @param args command line arguments
212 */
213 public static void main(final String... args) {
214 SpringApplication.run(CAcertVoteBot.class, args);
215 }
216 }