Implement a cancel command for VoteBot
[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.IRCBot;
26 import org.cacert.votebot.shared.IRCClient;
27 import org.cacert.votebot.shared.exceptions.IRCClientException;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
30 import org.springframework.beans.factory.annotation.Autowired;
31 import org.springframework.beans.factory.annotation.Value;
32 import org.springframework.boot.CommandLineRunner;
33 import org.springframework.boot.SpringApplication;
34 import org.springframework.boot.autoconfigure.SpringBootApplication;
35 import org.springframework.stereotype.Component;
36
37 import java.io.IOException;
38 import java.text.MessageFormat;
39 import java.time.Duration;
40 import java.util.Calendar;
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 final ResourceBundle messages = ResourceBundle.getBundle("messages");
56
57 /**
58 * Meeting channel where votes and results are published.
59 */
60 @Value("${voteBot.meetingChn:meeting}")
61 private String meetingChannel;
62
63 /**
64 * Channel name where voting is performed.
65 */
66 @Value("${voteBot.voteChn:vote}")
67 private String voteChannel;
68
69 /**
70 * Seconds to warn before a vote ends.
71 */
72 @Value("${voteBot.warnSecs:90}")
73 private long warn;
74
75 /**
76 * Seconds before a vote times out.
77 */
78 @Value("${voteBot.timeoutSecs:120}")
79 private long timeout;
80
81 private final CAcertVoteMechanics voteMechanics;
82
83 private final IRCClient ircClient;
84
85 @Autowired
86 public CAcertVoteBot(CAcertVoteMechanics voteMechanics, IRCClient ircClient) {
87 this.voteMechanics = voteMechanics;
88 this.ircClient = ircClient;
89 }
90
91 /**
92 * {@inheritDoc}
93 *
94 * @param args command line arguments
95 */
96 @Override
97 public final void run(final String... args) {
98 try {
99 getIrcClient().initializeFromArgs(args).assignBot(this);
100
101 Thread.sleep(Duration.ofSeconds(3).toMillis());
102 getIrcClient().join(meetingChannel);
103 Thread.sleep(Duration.ofSeconds(1).toMillis());
104 getIrcClient().join(voteChannel);
105
106 new Thread(this).start();
107 } catch (IOException | InterruptedException | ParseException | IRCClientException e) {
108 LOGGER.error(MessageFormat.format(messages.getString("error_running_votebot"), e.getMessage()));
109 }
110 }
111
112 @Override
113 protected final IRCClient getIrcClient() {
114 return ircClient;
115 }
116
117 @Override
118 public final synchronized void publicMessage(final String from, final String channel, final String message) throws
119 IRCClientException {
120 if (channel.equals(voteChannel)) {
121 sendPublicMessage(voteChannel, voteMechanics.evaluateVote(from, message));
122 }
123 }
124
125 @Override
126 public final synchronized void privateMessage(final String from, final String message) throws IRCClientException {
127 if (message != null && message.length() > 0) {
128 String[] parts = message.split("\\s+", 2);
129 try {
130 VoteBotCommand command = VoteBotCommand.valueOf(parts[0].toUpperCase(Locale.ENGLISH));
131 switch (command) {
132 case VOTE:
133 startVote(from, parts[1]);
134 break;
135 case HELP:
136 giveHelp(from);
137 break;
138 case CANCEL:
139 cancelVote(from);
140 break;
141 }
142 } catch (IllegalArgumentException e) {
143 sendUnknownCommand(from, parts[0]);
144 }
145 }
146 }
147
148 /**
149 * Cancel a running vote before the end of the voting period.
150 *
151 * @param from initiator of the cancel command
152 */
153 private void cancelVote(String from) throws IRCClientException {
154 LOGGER.debug(String.format("received cancel vote command from %s", from));
155 try {
156 announce(voteMechanics.stopVote(from));
157 sendPrivateMessage(from, messages.getString("vote_canceled"));
158 } catch (IllegalStateException e) {
159 sendPrivateMessage(from, e.getMessage());
160 }
161 }
162
163 private void sendUnknownCommand(String from, String command) throws IRCClientException {
164 sendPrivateMessage(from, MessageFormat.format(messages.getString("unknown_command"), command));
165 }
166
167 private void giveHelp(String from) throws IRCClientException {
168 sendPrivateMessage(from, messages.getString("help_message"));
169 }
170
171 private void startVote(final String from, final String message) throws IRCClientException {
172 final String response = voteMechanics.callVote(message, warn, timeout);
173 sendPrivateMessage(from, response);
174
175 if (response.startsWith("Sorry,")) {
176 return;
177 }
178
179 announce(MessageFormat.format(messages.getString("new_vote"), from, voteMechanics.getTopic()));
180 sendPublicMessage(
181 meetingChannel,
182 MessageFormat.format(messages.getString("cast_vote_in_vote_channel"), voteChannel));
183 sendPublicMessage(
184 voteChannel, MessageFormat.format(messages.getString("cast_vote_in_next_seconds"), timeout));
185 }
186
187 private synchronized void announce(final String msg) throws IRCClientException {
188 sendPublicMessage(meetingChannel, msg);
189 sendPublicMessage(voteChannel, msg);
190 }
191
192 @Override
193 public final void run() {
194 try {
195 //noinspection InfiniteLoopStatement
196 while (true) {
197 Thread.sleep(Duration.ofSeconds(1).toMillis());
198 String topic = voteMechanics.getTopic();
199
200 switch (voteMechanics.getState()) {
201 case IDLE:
202 break;
203 case RUNNING:
204 Calendar now = Calendar.getInstance();
205 if (now.after(voteMechanics.getEndTime())) {
206 announce(voteMechanics.stopVote("timeout"));
207 } else if (now.after(voteMechanics.getWarnTime()) && !voteMechanics.isWarned()) {
208 announce(MessageFormat.format(
209 messages.getString("voting_will_end_in_n_seconds"),
210 topic, timeout - warn));
211 voteMechanics.setWarned();
212 }
213 break;
214 case STOPPING:
215 announce(MessageFormat.format(
216 messages.getString("voting_has_closed"), topic));
217 final String[] res = voteMechanics.closeVote();
218 announce(MessageFormat.format(messages.getString("results_for_vote"), topic));
219
220 for (final String re : res) {
221 announce(re);
222 }
223 break;
224 default:
225 throw new IllegalStateException(messages.getString("illegal_vote_mechanics_state"));
226 }
227 }
228 } catch (final InterruptedException | IRCClientException e) {
229 LOGGER.error(e.getMessage(), e);
230 }
231 }
232
233 @Override
234 public synchronized void join(final String referent, final String chn) {
235
236 }
237
238 @Override
239 public synchronized void part(final String referent, final String channel) {
240
241 }
242
243 /**
244 * Entry point for the vote bot.
245 *
246 * @param args command line arguments
247 */
248 public static void main(final String... args) {
249 SpringApplication.run(CAcertVoteBot.class, args);
250 }
251 }