Remove bots when they leave the channel; handle our own leaving of channels.
[xudocci.git] / src / main / java / net / pterodactylus / xdcc / core / Core.java
1 /*
2  * XdccDownloader - Core.java - Copyright © 2013 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.xdcc.core;
19
20 import java.io.File;
21 import java.io.FileNotFoundException;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.OutputStream;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.logging.Level;
31 import java.util.logging.Logger;
32
33 import net.pterodactylus.irc.Connection;
34 import net.pterodactylus.irc.ConnectionBuilder;
35 import net.pterodactylus.irc.DccReceiver;
36 import net.pterodactylus.irc.event.ChannelJoined;
37 import net.pterodactylus.irc.event.ChannelLeft;
38 import net.pterodactylus.irc.event.ChannelMessageReceived;
39 import net.pterodactylus.irc.event.ConnectionEstablished;
40 import net.pterodactylus.irc.event.DccDownloadFailed;
41 import net.pterodactylus.irc.event.DccDownloadFinished;
42 import net.pterodactylus.irc.event.DccSendReceived;
43 import net.pterodactylus.irc.event.PrivateMessageReceived;
44 import net.pterodactylus.irc.util.MessageCleaner;
45 import net.pterodactylus.irc.util.RandomNickname;
46 import net.pterodactylus.xdcc.core.event.BotAdded;
47 import net.pterodactylus.xdcc.core.event.CoreStarted;
48 import net.pterodactylus.xdcc.core.event.DownloadFailed;
49 import net.pterodactylus.xdcc.core.event.DownloadFinished;
50 import net.pterodactylus.xdcc.core.event.DownloadStarted;
51 import net.pterodactylus.xdcc.core.event.GenericMessage;
52 import net.pterodactylus.xdcc.core.event.MessageReceived;
53 import net.pterodactylus.xdcc.data.Bot;
54 import net.pterodactylus.xdcc.data.Channel;
55 import net.pterodactylus.xdcc.data.Download;
56 import net.pterodactylus.xdcc.data.Network;
57 import net.pterodactylus.xdcc.data.Pack;
58 import net.pterodactylus.xdcc.data.Server;
59
60 import com.google.common.base.Optional;
61 import com.google.common.collect.HashBasedTable;
62 import com.google.common.collect.ImmutableSet;
63 import com.google.common.collect.Lists;
64 import com.google.common.collect.Maps;
65 import com.google.common.collect.Sets;
66 import com.google.common.collect.Table;
67 import com.google.common.eventbus.EventBus;
68 import com.google.common.eventbus.Subscribe;
69 import com.google.common.io.Closeables;
70 import com.google.common.util.concurrent.AbstractIdleService;
71 import com.google.inject.Inject;
72
73 /**
74  * The core of XDCC Downloader.
75  *
76  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
77  */
78 public class Core extends AbstractIdleService {
79
80         /** The logger. */
81         private static final Logger logger = Logger.getLogger(Core.class.getName());
82
83         /** The event bus. */
84         private final EventBus eventBus;
85
86         /** The temporary directory to download files to. */
87         private final String temporaryDirectory;
88
89         /** The directory to move finished downloads to. */
90         private final String finalDirectory;
91
92         /** The channels that should be monitored. */
93         private final Collection<Channel> channels = Sets.newHashSet();
94
95         /** The channels that are currentlymonitored. */
96         private final Collection<Channel> joinedChannels = Sets.newHashSet();
97
98         /** The channels that are joined but not configured. */
99         private final Collection<Channel> extraChannels = Sets.newHashSet();
100
101         /** The current network connections. */
102         private final Map<Network, Connection> networkConnections = Collections.synchronizedMap(Maps.<Network, Connection>newHashMap());
103
104         /** The currently known bots. */
105         private final Table<Network, String, Bot> networkBots = HashBasedTable.create();
106
107         /** The current downloads. */
108         private final Map<String, Download> downloads = Maps.newHashMap();
109
110         /** The current DCC receivers. */
111         private final Collection<DccReceiver> dccReceivers = Lists.newArrayList();
112
113         /**
114          * Creates a new core.
115          *
116          * @param eventBus
117          *              The event bus
118          * @param temporaryDirectory
119          *              The directory to download files to
120          * @param finalDirectory
121          *              The directory to move finished files to
122          */
123         @Inject
124         public Core(EventBus eventBus, String temporaryDirectory, String finalDirectory) {
125                 this.eventBus = eventBus;
126                 this.temporaryDirectory = temporaryDirectory;
127                 this.finalDirectory = finalDirectory;
128         }
129
130         //
131         // ACCESSORS
132         //
133
134         /**
135          * Returns all configured channels. Due to various circumstances, configured
136          * channels might not actually be joined.
137          *
138          * @return All configured channels
139          */
140         public Collection<Channel> channels() {
141                 return ImmutableSet.copyOf(channels);
142         }
143
144         /**
145          * Returns all currently joined channels.
146          *
147          * @return All currently joined channels
148          */
149         public Collection<Channel> joinedChannels() {
150                 return ImmutableSet.copyOf(joinedChannels);
151         }
152
153         /**
154          * Returns all currently joined channels that are not configured.
155          *
156          * @return All currently joined but not configured channels
157          */
158         public Collection<Channel> extraChannels() {
159                 return ImmutableSet.copyOf(extraChannels);
160         }
161
162         /**
163          * Returns all currently known bots.
164          *
165          * @return All currently known bots
166          */
167         public Collection<Bot> bots() {
168                 return networkBots.values();
169         }
170
171         /**
172          * Returns the currently active DCC receivers.
173          *
174          * @return The currently active DCC receivers
175          */
176         public Collection<DccReceiver> dccReceivers() {
177                 return dccReceivers;
178         }
179
180         //
181         // ACTIONS
182         //
183
184         /**
185          * Adds a channel to monitor.
186          *
187          * @param channel
188          *              The channel to monitor
189          */
190         public void addChannel(Channel channel) {
191                 channels.add(channel);
192         }
193
194         /**
195          * Fetches the given pack from the given bot.
196          *
197          * @param bot
198          *              The bot to fetch the pack from
199          * @param pack
200          *              The pack to fetch
201          */
202         public void fetch(Bot bot, Pack pack) {
203                 Connection connection = networkConnections.get(bot.network());
204                 if (connection == null) {
205                         return;
206                 }
207
208                 Download download = new Download(bot, pack);
209                 downloads.put(pack.name(), download);
210
211                 try {
212                         connection.sendMessage(bot.name(), "XDCC SEND " + pack.id());
213                 } catch (IOException ioe1) {
214                         logger.log(Level.WARNING, "Could not send message to bot!", ioe1);
215                 }
216         }
217
218         //
219         // ABSTRACTIDLESERVICE METHODS
220         //
221
222         @Override
223         protected void startUp() {
224                 for (Channel channel : channels) {
225                         logger.info(String.format("Connecting to Channel %s on Network %s…", channel.name(), channel.network().name()));
226                         if (!networkConnections.containsKey(channel.network())) {
227                                 /* select a random server. */
228                                 List<Server> servers = Lists.newArrayList(channel.network().servers());
229                                 Server server = servers.get((int) (Math.random() * servers.size()));
230                                 Connection connection = new ConnectionBuilder(eventBus).connect(server.hostname()).port(server.unencryptedPorts().iterator().next()).build();
231                                 connection.username(RandomNickname.get()).realName(RandomNickname.get());
232                                 networkConnections.put(channel.network(), connection);
233                                 connection.start();
234                         }
235                 }
236
237                 /* notify listeners. */
238                 eventBus.post(new CoreStarted(this));
239         }
240
241         @Override
242         protected void shutDown() {
243         }
244
245         //
246         // EVENT HANDLERS
247         //
248
249         /**
250          * If a connection to a network has been established, the channels associated
251          * with this network are joined.
252          *
253          * @param connectionEstablished
254          *              The connection established event
255          */
256         @Subscribe
257         public void connectionEstablished(ConnectionEstablished connectionEstablished) {
258
259                 /* get network for connection. */
260                 Optional<Network> network = getNetwork(connectionEstablished.connection());
261
262                 /* found network? */
263                 if (!network.isPresent()) {
264                         return;
265                 }
266
267                 /* join all channels on this network. */
268                 for (Channel channel : channels) {
269                         if (channel.network().equals(network.get())) {
270                                 try {
271                                         connectionEstablished.connection().joinChannel(channel.name());
272                                 } catch (IOException ioe1) {
273                                         logger.log(Level.WARNING, String.format("Could not join %s on %s!", channel.name(), network.get().name()), ioe1);
274                                 }
275                         }
276                 }
277         }
278
279         /**
280          * Shows a message when a channel was joined by us.
281          *
282          * @param channelJoined
283          *              The channel joined event
284          */
285         @Subscribe
286         public void channelJoined(ChannelJoined channelJoined) {
287                 if (channelJoined.connection().isSource(channelJoined.client())) {
288                         Optional<Network> network = getNetwork(channelJoined.connection());
289                         if (!network.isPresent()) {
290                                 return;
291                         }
292
293                         Optional<Channel> channel = getChannel(network.get(), channelJoined.channel());
294                         if (!channel.isPresent()) {
295                                 /* it’s an extra channel. */
296                                 extraChannels.add(new Channel(network.get(), channelJoined.channel()));
297                                 logger.info(String.format("Joined extra Channel %s on %s.", channelJoined.channel(), network.get().name()));
298                                 return;
299                         }
300
301                         joinedChannels.add(channel.get());
302                         logger.info(String.format("Joined Channel %s on %s.", channelJoined.channel(), network.get().name()));
303                 }
304         }
305
306         /**
307          * Removes bots that leave a channel, or channels when it’s us that’s leaving.
308          *
309          * @param channelLeft
310          *              The channel left event
311          */
312         @Subscribe
313         public void channelLeft(ChannelLeft channelLeft) {
314                 Optional<Network> network = getNetwork(channelLeft.connection());
315                 if (!network.isPresent()) {
316                         return;
317                 }
318
319                 Bot bot = networkBots.get(network.get(), channelLeft.client().nick().get());
320                 if (bot == null) {
321                         /* maybe it was us? */
322                         if (channelLeft.connection().isSource(channelLeft.client())) {
323                                 Optional<Channel> channel = getChannel(network.get(), channelLeft.channel());
324                                 if (!channel.isPresent()) {
325                                         /* maybe it was an extra channel? */
326                                         channel = getExtraChannel(network.get(), channelLeft.channel());
327                                         if (!channel.isPresent()) {
328                                                 /* okay, whatever. */
329                                                 return;
330                                         }
331
332                                         extraChannels.remove(channel);
333                                 } else {
334                                         channels.remove(channel.get());
335                                 }
336
337                                 eventBus.post(new GenericMessage(String.format("Left Channel %s on %s.", channel.get().name(), channel.get().network().name())));
338                         }
339
340                         return;
341                 }
342
343                 networkBots.remove(network.get(), channelLeft.client().nick().get());
344         }
345
346         /**
347          * If a message on a channel is received, it is parsed for pack information
348          * with is then added to a bot.
349          *
350          * @param channelMessageReceived
351          *              The channel message received event
352          */
353         @Subscribe
354         public void channelMessageReceived(ChannelMessageReceived channelMessageReceived) {
355                 String message = MessageCleaner.getDefaultInstance().clean(channelMessageReceived.message());
356                 if (!message.startsWith("#")) {
357                         /* most probably not a pack announcement. */
358                         return;
359                 }
360
361                 Optional<Network> network = getNetwork(channelMessageReceived.connection());
362                 if (!network.isPresent()) {
363                         /* message for unknown connection? */
364                         return;
365                 }
366
367                 /* parse pack information. */
368                 Optional<Pack> pack = parsePack(message);
369                 if (!pack.isPresent()) {
370                         return;
371                 }
372
373                 Bot bot;
374                 synchronized (networkBots) {
375                         if (!networkBots.contains(network.get(), channelMessageReceived.source().nick().get())) {
376                                 bot = new Bot(network.get()).name(channelMessageReceived.source().nick().get());
377                                 networkBots.put(network.get(), channelMessageReceived.source().nick().get(), bot);
378                                 eventBus.post(new BotAdded(bot));
379                         } else {
380                                 bot = networkBots.get(network.get(), channelMessageReceived.source().nick().get());
381                         }
382                 }
383
384                 /* add pack. */
385                 bot.addPack(pack.get());
386                 logger.fine(String.format("Bot %s now has %d packs.", bot, bot.packs().size()));
387         }
388
389         /**
390          * Forward all private messages to every console.
391          *
392          * @param privateMessageReceived
393          *              The private message recevied event
394          */
395         @Subscribe
396         public void privateMessageReceived(PrivateMessageReceived privateMessageReceived) {
397                 eventBus.post(new MessageReceived(privateMessageReceived.source(), privateMessageReceived.message()));
398         }
399
400         /**
401          * Starts a DCC download.
402          *
403          * @param dccSendReceived
404          *              The DCC SEND event
405          */
406         @Subscribe
407         public void dccSendReceived(DccSendReceived dccSendReceived) {
408                 Optional<Network> network = getNetwork(dccSendReceived.connection());
409                 if (!network.isPresent()) {
410                         return;
411                 }
412
413                 Download download = downloads.get(dccSendReceived.filename());
414                 if (download == null) {
415                         /* unknown download, ignore. */
416                         return;
417                 }
418
419                 logger.info(String.format("Starting download of %s.", dccSendReceived.filename()));
420                 try {
421                         File outputFile = new File(temporaryDirectory, dccSendReceived.filename());
422                         OutputStream fileOutputStream = new FileOutputStream(outputFile);
423                         DccReceiver dccReceiver = new DccReceiver(eventBus, dccSendReceived.inetAddress(), dccSendReceived.port(), dccSendReceived.filename(), dccSendReceived.filesize(), fileOutputStream);
424                         download.filename(outputFile.getPath()).outputStream(fileOutputStream).dccReceiver(dccReceiver);
425                         dccReceivers.add(dccReceiver);
426                         dccReceiver.start();
427                         eventBus.post(new DownloadStarted(download));
428                 } catch (FileNotFoundException fnfe1) {
429                         logger.log(Level.WARNING, "Could not open file for download!", fnfe1);
430                 }
431         }
432
433         /**
434          * Closes the output stream of the download and moves the file to the final
435          * location.
436          *
437          * @param dccDownloadFinished
438          *              The DCC download finished event
439          */
440         @Subscribe
441         public void dccDownloadFinished(DccDownloadFinished dccDownloadFinished) {
442                 Download download = downloads.get(dccDownloadFinished.dccReceiver().filename());
443                 if (download == null) {
444                         /* probably shouldn’t happen. */
445                         return;
446                 }
447
448                 try {
449                         download.outputStream().close();
450                         File file = new File(download.filename());
451                         file.renameTo(new File(finalDirectory, download.pack().name()));
452                         eventBus.post(new DownloadFinished(download));
453                         dccReceivers.remove(dccDownloadFinished.dccReceiver());
454                         downloads.remove(download);
455                 } catch (IOException ioe1) {
456                         /* TODO - handle all the errors. */
457                         logger.log(Level.WARNING, String.format("Could not move file %s to directory %s.", download.filename(), finalDirectory), ioe1);
458                 }
459         }
460
461         /**
462          * Closes the output stream and notifies all listeners of the failure.
463          *
464          * @param dccDownloadFailed
465          *              The DCC download failed event
466          */
467         @Subscribe
468         public void dccDownloadFailed(DccDownloadFailed dccDownloadFailed) {
469                 Download download = downloads.get(dccDownloadFailed.dccReceiver().filename());
470                 if (download == null) {
471                         /* probably shouldn’t happen. */
472                         return;
473                 }
474
475                 try {
476                         Closeables.close(download.outputStream(), true);
477                         eventBus.post(new DownloadFailed(download));
478                         dccReceivers.remove(dccDownloadFailed.dccReceiver());
479                         downloads.remove(download);
480                 } catch (IOException ioe1) {
481                         /* swallow silently. */
482                 }
483         }
484
485         //
486         // PRIVATE METHODS
487         //
488
489         /**
490          * Searches all current connections for the given connection, returning the
491          * associated network.
492          *
493          * @param connection
494          *              The connection to get the network for
495          * @return The network belonging to the connection, or {@link
496          *         Optional#absent()}
497          */
498         private Optional<Network> getNetwork(Connection connection) {
499                 for (Entry<Network, Connection> networkConnectionEntry : networkConnections.entrySet()) {
500                         if (networkConnectionEntry.getValue().equals(connection)) {
501                                 return Optional.of(networkConnectionEntry.getKey());
502                         }
503                 }
504                 return Optional.absent();
505         }
506
507         /**
508          * Returns the configured channel for the given network and name.
509          *
510          * @param network
511          *              The network the channel is located on
512          * @param channelName
513          *              The name of the channel
514          * @return The configured channel, or {@link Optional#absent()} if no
515          *         configured channel matching the given network and name was found
516          */
517         public Optional<Channel> getChannel(Network network, String channelName) {
518                 for (Channel channel : channels) {
519                         if (channel.network().equals(network) && (channel.name().equalsIgnoreCase(channelName))) {
520                                 return Optional.of(channel);
521                         }
522                 }
523                 return Optional.absent();
524         }
525
526         /**
527          * Returns the extra channel for the given network and name.
528          *
529          * @param network
530          *              The network the channel is located on
531          * @param channelName
532          *              The name of the channel
533          * @return The extra channel, or {@link Optional#absent()} if no extra channel
534          *         matching the given network and name was found
535          */
536         public Optional<Channel> getExtraChannel(Network network, String channelName) {
537                 for (Channel channel : extraChannels) {
538                         if (channel.network().equals(network) && (channel.name().equalsIgnoreCase(channelName))) {
539                                 return Optional.of(channel);
540                         }
541                 }
542                 return Optional.absent();
543         }
544
545         /**
546          * Parses {@link Pack} information from the given message.
547          *
548          * @param message
549          *              The message to parse pack information from
550          * @return The parsed pack, or {@link Optional#absent()} if the message could
551          *         not be parsed into a pack
552          */
553         private Optional<Pack> parsePack(String message) {
554                 int squareOpen = message.indexOf('[');
555                 int squareClose = message.indexOf(']', squareOpen);
556                 if ((squareOpen == -1) && (squareClose == -1)) {
557                         return Optional.absent();
558                 }
559                 String packSize = message.substring(squareOpen + 1, squareClose);
560                 String packName = message.substring(message.lastIndexOf(' ') + 1);
561                 String packIndex = message.substring(0, message.indexOf(' ')).substring(1);
562                 return Optional.of(new Pack(packIndex, packSize, packName));
563         }
564
565 }