Don’t try to join channels that require a registration anymore.
[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 static java.lang.String.format;
21 import static net.pterodactylus.irc.event.ChannelNotJoined.Reason.registeredNicknamesOnly;
22 import static net.pterodactylus.irc.util.MessageCleaner.getDefaultInstance;
23 import static net.pterodactylus.xdcc.data.Channel.TO_NETWORK;
24 import static net.pterodactylus.xdcc.data.Download.FILTER_RUNNING;
25
26 import java.io.File;
27 import java.io.FileNotFoundException;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.OutputStream;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Map.Entry;
37 import java.util.concurrent.TimeUnit;
38
39 import net.pterodactylus.irc.Connection;
40 import net.pterodactylus.irc.ConnectionBuilder;
41 import net.pterodactylus.irc.DccReceiver;
42 import net.pterodactylus.irc.event.ChannelJoined;
43 import net.pterodactylus.irc.event.ChannelLeft;
44 import net.pterodactylus.irc.event.ChannelMessageReceived;
45 import net.pterodactylus.irc.event.ChannelNotJoined;
46 import net.pterodactylus.irc.event.ClientQuit;
47 import net.pterodactylus.irc.event.ConnectionClosed;
48 import net.pterodactylus.irc.event.ConnectionEstablished;
49 import net.pterodactylus.irc.event.ConnectionFailed;
50 import net.pterodactylus.irc.event.DccAcceptReceived;
51 import net.pterodactylus.irc.event.DccDownloadFailed;
52 import net.pterodactylus.irc.event.DccDownloadFinished;
53 import net.pterodactylus.irc.event.DccSendReceived;
54 import net.pterodactylus.irc.event.KickedFromChannel;
55 import net.pterodactylus.irc.event.NicknameChanged;
56 import net.pterodactylus.irc.event.PrivateMessageReceived;
57 import net.pterodactylus.irc.event.PrivateNoticeReceived;
58 import net.pterodactylus.irc.event.ReplyReceived;
59 import net.pterodactylus.irc.util.RandomNickname;
60 import net.pterodactylus.xdcc.core.event.BotAdded;
61 import net.pterodactylus.xdcc.core.event.CoreStarted;
62 import net.pterodactylus.xdcc.core.event.DownloadFailed;
63 import net.pterodactylus.xdcc.core.event.DownloadFinished;
64 import net.pterodactylus.xdcc.core.event.DownloadStarted;
65 import net.pterodactylus.xdcc.core.event.GenericError;
66 import net.pterodactylus.xdcc.core.event.GenericMessage;
67 import net.pterodactylus.xdcc.core.event.MessageReceived;
68 import net.pterodactylus.xdcc.data.Bot;
69 import net.pterodactylus.xdcc.data.Channel;
70 import net.pterodactylus.xdcc.data.Download;
71 import net.pterodactylus.xdcc.data.Network;
72 import net.pterodactylus.xdcc.data.Pack;
73 import net.pterodactylus.xdcc.data.Server;
74
75 import com.google.common.base.Function;
76 import com.google.common.base.Optional;
77 import com.google.common.base.Predicate;
78 import com.google.common.collect.FluentIterable;
79 import com.google.common.collect.HashBasedTable;
80 import com.google.common.collect.HashMultimap;
81 import com.google.common.collect.ImmutableList;
82 import com.google.common.collect.ImmutableSet;
83 import com.google.common.collect.Lists;
84 import com.google.common.collect.Maps;
85 import com.google.common.collect.Multimap;
86 import com.google.common.collect.Sets;
87 import com.google.common.collect.Table;
88 import com.google.common.eventbus.EventBus;
89 import com.google.common.eventbus.Subscribe;
90 import com.google.common.io.Closeables;
91 import com.google.common.util.concurrent.AbstractExecutionThreadService;
92 import com.google.inject.Inject;
93 import org.apache.log4j.Logger;
94
95 /**
96  * The core of XDCC Downloader.
97  *
98  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
99  */
100 public class Core extends AbstractExecutionThreadService {
101
102         /** The logger. */
103         private static final Logger logger = Logger.getLogger(Core.class.getName());
104
105         /** The event bus. */
106         private final EventBus eventBus;
107
108         /** The temporary directory to download files to. */
109         private final String temporaryDirectory;
110
111         /** The directory to move finished downloads to. */
112         private final String finalDirectory;
113
114         /** The channels that should be monitored. */
115         private final Collection<Channel> channels = Sets.newHashSet();
116
117         /** The channels that are currentlymonitored. */
118         private final Collection<Channel> joinedChannels = Sets.newHashSet();
119
120         /** The channels that are joined but not configured. */
121         private final Collection<Channel> extraChannels = Sets.newHashSet();
122
123         /** The current network connections. */
124         private final Map<Network, Connection> networkConnections = Collections.synchronizedMap(Maps.<Network, Connection>newHashMap());
125
126         /** The currently known bots. */
127         private final Table<Network, String, Bot> networkBots = HashBasedTable.create();
128
129         /** The current downloads. */
130         private final Multimap<String, Download> downloads = HashMultimap.create();
131
132         /** The current DCC receivers. */
133         private final Collection<DccReceiver> dccReceivers = Lists.newArrayList();
134
135         /**
136          * Creates a new core.
137          *
138          * @param eventBus
139          *              The event bus
140          * @param temporaryDirectory
141          *              The directory to download files to
142          * @param finalDirectory
143          *              The directory to move finished files to
144          */
145         @Inject
146         public Core(EventBus eventBus, String temporaryDirectory, String finalDirectory) {
147                 this.eventBus = eventBus;
148                 this.temporaryDirectory = temporaryDirectory;
149                 this.finalDirectory = finalDirectory;
150         }
151
152         //
153         // ACCESSORS
154         //
155
156         /**
157          * Returns all currently known connections.
158          *
159          * @return All currently known connections
160          */
161         public Collection<Connection> connections() {
162                 return networkConnections.values();
163         }
164
165         /**
166          * Returns all defined networks.
167          *
168          * @return All defined networks
169          */
170         public Collection<Network> networks() {
171                 return FluentIterable.from(channels).transform(TO_NETWORK).toSet();
172         }
173
174         /**
175          * Returns all connected networks.
176          *
177          * @return All connected networks
178          */
179         public Collection<Network> connectedNetworks() {
180                 return Lists.newArrayList(Optional.presentInstances(FluentIterable.from(networkConnections.values()).transform(new Function<Connection, Optional<Network>>() {
181                         @Override
182                         public Optional<Network> apply(Connection connection) {
183                                 return getNetwork(connection);
184                         }
185                 })));
186         }
187
188         /**
189          * Returns all configured channels. Due to various circumstances, configured
190          * channels might not actually be joined.
191          *
192          * @return All configured channels
193          */
194         public Collection<Channel> channels() {
195                 return ImmutableSet.copyOf(channels);
196         }
197
198         /**
199          * Returns all currently joined channels.
200          *
201          * @return All currently joined channels
202          */
203         public Collection<Channel> joinedChannels() {
204                 return ImmutableSet.copyOf(joinedChannels);
205         }
206
207         /**
208          * Returns all currently joined channels that are not configured.
209          *
210          * @return All currently joined but not configured channels
211          */
212         public Collection<Channel> extraChannels() {
213                 return ImmutableSet.copyOf(extraChannels);
214         }
215
216         /**
217          * Returns all currently known bots.
218          *
219          * @return All currently known bots
220          */
221         public Collection<Bot> bots() {
222                 return networkBots.values();
223         }
224
225         /**
226          * Returns all currently running downloads.
227          *
228          * @return All currently running downloads
229          */
230         public Collection<Download> downloads() {
231                 return downloads.values();
232         }
233
234         //
235         // ACTIONS
236         //
237
238         /**
239          * Adds a channel to monitor.
240          *
241          * @param channel
242          *              The channel to monitor
243          */
244         public void addChannel(Channel channel) {
245                 channels.add(channel);
246         }
247
248         /**
249          * Fetches the given pack from the given bot.
250          *
251          * @param bot
252          *              The bot to fetch the pack from
253          * @param pack
254          *              The pack to fetch
255          */
256         public void fetch(Bot bot, Pack pack) {
257                 Connection connection = networkConnections.get(bot.network());
258                 if (connection == null) {
259                         return;
260                 }
261
262                 /* check if we are already downloading the file? */
263                 if (downloads.containsKey(pack.name())) {
264                         Collection<Download> packDownloads = downloads.get(pack.name());
265                         Collection<Download> runningDownloads = FluentIterable.from(packDownloads).filter(FILTER_RUNNING).toSet();
266                         if (!runningDownloads.isEmpty()) {
267                                 Download download = runningDownloads.iterator().next();
268                                 eventBus.post(new GenericMessage(String.format("File %s is already downloading from %s (%s).", pack.name(), download.bot().name(), download.bot().network().name())));
269                                 return;
270                         }
271                         StringBuilder bots = new StringBuilder();
272                         for (Download download : packDownloads) {
273                                 if (bots.length() > 0) {
274                                         bots.append(", ");
275                                 }
276                                 bots.append(download.bot().name()).append(" (").append(download.bot().network().name()).append(')');
277                         }
278                         eventBus.post(new GenericMessage(String.format("File %s is already requested from %d bots (%s).", pack.name(), packDownloads.size(), bots.toString())));
279                 }
280
281                 Download download = new Download(bot, pack);
282                 downloads.put(pack.name(), download);
283
284                 try {
285                         connection.sendMessage(bot.name(), "XDCC SEND " + pack.id());
286                 } catch (IOException ioe1) {
287                         logger.warn("Could not send message to bot!", ioe1);
288                 }
289         }
290
291         /**
292          * Cancels the download of the given pack from the given bot.
293          *
294          * @param bot
295          *              The bot the pack is being downloaded from
296          * @param pack
297          *              The pack being downloaded
298          */
299         public void cancelDownload(Bot bot, Pack pack) {
300                 Optional<Download> download = getDownload(pack, bot);
301                 if (!download.isPresent()) {
302                         return;
303                 }
304
305                 /* get connection. */
306                 Connection connection = networkConnections.get(bot.network());
307                 if (connection == null) {
308                         /* request for unknown network? */
309                         return;
310                 }
311
312                 /* stop the DCC receiver. */
313                 if (download.get().dccReceiver() != null) {
314                         download.get().dccReceiver().stop();
315                 } else {
316                         /* remove download if it hasn’t started yet. */
317                         downloads.remove(pack.name(), download.get());
318                 }
319
320                 /* remove the request from the bot, too. */
321                 try {
322                         connection.sendMessage(bot.name(), String.format("XDCC %s", (download.get().dccReceiver() != null) ? "CANCEL" : "REMOVE"));
323                 } catch (IOException ioe1) {
324                         logger.warn(String.format("Could not cancel DCC from %s (%s)!", bot.name(), bot.network().name()), ioe1);
325                 }
326         }
327
328         /**
329          * Closes the given connection.
330          *
331          * @param connection
332          *              The connection to close
333          */
334         public void closeConnection(Connection connection) {
335                 try {
336                         connection.close();
337                 } catch (IOException ioe1) {
338                         /* TODO */
339                 }
340         }
341
342         //
343         // ABSTRACTIDLESERVICE METHODS
344         //
345
346         @Override
347         protected void startUp() {
348                 for (Channel channel : channels) {
349                         logger.info(String.format("Connecting to Channel %s on Network %s…", channel.name(), channel.network().name()));
350                         connectNetwork(channel.network());
351                 }
352
353                 /* notify listeners. */
354                 eventBus.post(new CoreStarted(this));
355         }
356
357         @Override
358         protected void run() throws Exception {
359                 while (isRunning()) {
360                         try {
361                                 Thread.sleep(TimeUnit.MINUTES.toMillis(1));
362                         } catch (InterruptedException ie1) {
363                                 /* ignore. */
364                         }
365
366                         /* find channels that should be monitored but are not. */
367                         for (Channel channel : channels) {
368                                 if (joinedChannels.contains(channel)) {
369                                         continue;
370                                 }
371
372                                 connectNetwork(channel.network());
373                                 Connection connection = networkConnections.get(channel.network());
374                                 if (connection.established()) {
375                                         eventBus.post(new GenericMessage(String.format("Trying to join %s on %s.", channel.name(), channel.network().name())));
376                                         try {
377                                                 connection.joinChannel(channel.name());
378                                         } catch (IOException ioe1) {
379                                                 eventBus.post(new GenericMessage(String.format("Could not join %s on %s.", channel.name(), channel.network().name())));
380                                         }
381                                 }
382                         }
383                 }
384         }
385
386         @Override
387         protected void shutDown() {
388         }
389
390         //
391         // PRIVATE METHODS
392         //
393
394         /**
395          * Starts a new connection for the given network if no such connection exists
396          * already.
397          *
398          * @param network
399          *              The network to connect to
400          */
401         private void connectNetwork(Network network) {
402                 if (!networkConnections.containsKey(network)) {
403                                 /* select a random server. */
404                         List<Server> servers = Lists.newArrayList(network.servers());
405                         if (servers.isEmpty()) {
406                                 eventBus.post(new GenericError(String.format("Network %s does not have any servers.", network.name())));
407                                 return;
408                         }
409                         Server server = servers.get((int) (Math.random() * servers.size()));
410                         Connection connection = new ConnectionBuilder(eventBus).connect(server.hostname()).port(server.unencryptedPorts().iterator().next()).build();
411                         connection.username(RandomNickname.get()).realName(RandomNickname.get());
412                         networkConnections.put(network, connection);
413                         connection.start();
414                 }
415         }
416
417         /**
418          * Removes the given connection and all its channels and bots.
419          *
420          * @param connection
421          *              The connection to remove
422          */
423         private void removeConnection(Connection connection) {
424                 Optional<Network> network = getNetwork(connection);
425                 if (!network.isPresent()) {
426                         return;
427                 }
428
429                 /* find all channels that need to be removed. */
430                 for (Collection<Channel> channels : ImmutableList.of(joinedChannels, extraChannels)) {
431                         for (Iterator<Channel> channelIterator = channels.iterator(); channelIterator.hasNext(); ) {
432                                 Channel joinedChannel = channelIterator.next();
433                                 if (!joinedChannel.network().equals(network.get())) {
434                                         continue;
435                                 }
436
437                                 channelIterator.remove();
438                         }
439                 }
440
441                 /* now remove all bots for that network. */
442                 Map<String, Bot> bots = networkBots.row(network.get());
443                 int botCount = bots.size();
444                 int packCount = 0;
445                 for (Bot bot : bots.values()) {
446                         packCount += bot.packs().size();
447                 }
448                 bots.clear();
449                 eventBus.post(new GenericMessage(String.format("Network %s disconnected, %d bots removed, %d packs removed.", network.get().name(), botCount, packCount)));
450
451                 /* now remove the network. */
452                 networkConnections.remove(network.get());
453         }
454
455         //
456         // EVENT HANDLERS
457         //
458
459         /**
460          * If a connection to a network has been established, the channels associated
461          * with this network are joined.
462          *
463          * @param connectionEstablished
464          *              The connection established event
465          */
466         @Subscribe
467         public void connectionEstablished(ConnectionEstablished connectionEstablished) {
468
469                 /* get network for connection. */
470                 Optional<Network> network = getNetwork(connectionEstablished.connection());
471
472                 /* found network? */
473                 if (!network.isPresent()) {
474                         eventBus.post(new GenericMessage(String.format("Connected to unknown network: %s", connectionEstablished.connection().hostname())));
475                         return;
476                 }
477
478                 eventBus.post(new GenericMessage(String.format("Connected to network %s.", network.get().name())));
479
480                 /* join all channels on this network. */
481                 for (Channel channel : channels) {
482                         if (channel.network().equals(network.get())) {
483                                 try {
484                                         eventBus.post(new GenericMessage(String.format("Trying to join %s on %s...", channel.name(), network.get().name())));
485                                         connectionEstablished.connection().joinChannel(channel.name());
486                                 } catch (IOException ioe1) {
487                                         logger.warn(String.format("Could not join %s on %s!", channel.name(), network.get().name()), ioe1);
488                                 }
489                         }
490                 }
491         }
492
493         /**
494          * Remove all data stored for a network if the connection is closed.
495          *
496          * @param connectionClosed
497          *              The connection closed event
498          */
499         @Subscribe
500         public void connectionClosed(ConnectionClosed connectionClosed) {
501                 removeConnection(connectionClosed.connection());
502         }
503
504         /**
505          * Remove all data stored for a network if the connection fails.
506          *
507          * @param connectionFailed
508          *              The connection failed event
509          */
510         @Subscribe
511         public void connectionFailed(ConnectionFailed connectionFailed) {
512                 removeConnection(connectionFailed.connection());
513         }
514
515         /**
516          * Shows a message when a channel was joined by us.
517          *
518          * @param channelJoined
519          *              The channel joined event
520          */
521         @Subscribe
522         public void channelJoined(ChannelJoined channelJoined) {
523                 if (channelJoined.connection().isSource(channelJoined.client())) {
524                         Optional<Network> network = getNetwork(channelJoined.connection());
525                         if (!network.isPresent()) {
526                                 return;
527                         }
528
529                         Optional<Channel> channel = getChannel(network.get(), channelJoined.channel());
530                         if (!channel.isPresent()) {
531                                 /* it’s an extra channel. */
532                                 extraChannels.add(new Channel(network.get(), channelJoined.channel()));
533                                 logger.info(String.format("Joined extra Channel %s on %s.", channelJoined.channel(), network.get().name()));
534                                 return;
535                         }
536
537                         joinedChannels.add(channel.get());
538                         logger.info(String.format("Joined Channel %s on %s.", channelJoined.channel(), network.get().name()));
539                 }
540         }
541
542         @Subscribe
543         public void channelNotJoined(ChannelNotJoined channelNotJoined) {
544                 Optional<Network> network = getNetwork(channelNotJoined.connection());
545                 if (!network.isPresent()) {
546                         return;
547                 }
548
549                 if (channelNotJoined.reason() == registeredNicknamesOnly) {
550                         Optional<Channel> channel = getChannel(network.get(), channelNotJoined.channel());
551                         if (channel.isPresent()) {
552                                 eventBus.post(new GenericMessage(format("Not trying to join %s anymore.", channel.get())));
553                                 channels.remove(channel.get());
554                         }
555                         return;
556                 }
557
558                 eventBus.post(new GenericMessage(
559                                 format("Could not join %s: %s", channelNotJoined.channel(),
560                                                 channelNotJoined.reason())));
561         }
562
563         /**
564          * Removes bots that leave a channel, or channels when it’s us that’s leaving.
565          *
566          * @param channelLeft
567          *              The channel left event
568          */
569         @Subscribe
570         public void channelLeft(ChannelLeft channelLeft) {
571                 Optional<Network> network = getNetwork(channelLeft.connection());
572                 if (!network.isPresent()) {
573                         return;
574                 }
575
576                 Bot bot = networkBots.get(network.get(), channelLeft.client().nick().get());
577                 if (bot == null) {
578                         /* maybe it was us? */
579                         if (channelLeft.connection().isSource(channelLeft.client())) {
580                                 Optional<Channel> channel = getChannel(network.get(), channelLeft.channel());
581                                 if (!channel.isPresent()) {
582                                         /* maybe it was an extra channel? */
583                                         channel = getExtraChannel(network.get(), channelLeft.channel());
584                                         if (!channel.isPresent()) {
585                                                 /* okay, whatever. */
586                                                 return;
587                                         }
588
589                                         extraChannels.remove(channel);
590                                 } else {
591                                         channels.remove(channel.get());
592                                 }
593
594                                 eventBus.post(new GenericMessage(String.format("Left Channel %s on %s.", channel.get().name(), channel.get().network().name())));
595                         }
596
597                         return;
598                 }
599
600                 networkBots.remove(network.get(), channelLeft.client().nick().get());
601         }
602
603         @Subscribe
604         public void kickedFromChannel(KickedFromChannel kickedFromChannel) {
605                 Optional<Network> network = getNetwork(kickedFromChannel.connection());
606                 if (!network.isPresent()) {
607                         return;
608                 }
609
610                 /* have we been kicked? */
611                 if (nicknameMatchesConnection(kickedFromChannel.connection(), kickedFromChannel.kickee())) {
612                         Optional<Channel> channel = getChannel(network.get(), kickedFromChannel.channel());
613                         if (!channel.isPresent()) {
614                                 /* maybe it was an extra channel? */
615                                 channel = getExtraChannel(network.get(), kickedFromChannel.channel());
616                                 if (!channel.isPresent()) {
617                                         /* okay, whatever. */
618                                         return;
619                                 }
620
621                                 extraChannels.remove(channel);
622                         } else {
623                                 channels.remove(channel.get());
624                         }
625                         eventBus.post(new GenericMessage(format(
626                                         "Kicked from %s by %s: %s",
627                                         kickedFromChannel.channel(),
628                                         kickedFromChannel.kicker(),
629                                         kickedFromChannel.reason().or("<unknown>")
630                         )));
631                 }
632         }
633
634         private boolean nicknameMatchesConnection(Connection connection, String nickname) {
635                 return connection.nickname().equalsIgnoreCase(nickname);
636         }
637
638         /**
639          * Removes a client (which may be a bot) from the table of known bots.
640          *
641          * @param clientQuit
642          *              The client quit event
643          */
644         @Subscribe
645         public void clientQuit(ClientQuit clientQuit) {
646                 Optional<Network> network = getNetwork(clientQuit.connection());
647                 if (!network.isPresent()) {
648                         return;
649                 }
650
651                 networkBots.remove(network.get(), clientQuit.client().nick().get());
652         }
653
654         /**
655          * If the nickname of a bit changes, remove it from the old name and store it
656          * under the new name.
657          *
658          * @param nicknameChanged
659          *              The nickname changed event
660          */
661         @Subscribe
662         public void nicknameChanged(NicknameChanged nicknameChanged) {
663                 Optional<Network> network = getNetwork(nicknameChanged.connection());
664                 if (!network.isPresent()) {
665                         return;
666                 }
667
668                 Bot bot = networkBots.remove(network.get(), nicknameChanged.client().nick().get());
669                 if (bot == null) {
670                         return;
671                 }
672
673                 networkBots.put(network.get(), nicknameChanged.newNickname(), bot);
674         }
675
676         /**
677          * If a message on a channel is received, it is parsed for pack information
678          * with is then added to a bot.
679          *
680          * @param channelMessageReceived
681          *              The channel message received event
682          */
683         @Subscribe
684         public void channelMessageReceived(ChannelMessageReceived channelMessageReceived) {
685                 String message = getDefaultInstance().clean(channelMessageReceived.message());
686                 if (!message.startsWith("#")) {
687                         /* most probably not a pack announcement. */
688                         return;
689                 }
690
691                 Optional<Network> network = getNetwork(channelMessageReceived.connection());
692                 if (!network.isPresent()) {
693                         /* message for unknown connection? */
694                         return;
695                 }
696
697                 /* parse pack information. */
698                 Optional<Pack> pack = parsePack(message);
699                 if (!pack.isPresent()) {
700                         return;
701                 }
702
703                 Bot bot;
704                 synchronized (networkBots) {
705                         if (!networkBots.contains(network.get(), channelMessageReceived.source().nick().get())) {
706                                 bot = new Bot(network.get(), channelMessageReceived.source().nick().get());
707                                 networkBots.put(network.get(), channelMessageReceived.source().nick().get(), bot);
708                                 eventBus.post(new BotAdded(bot));
709                         } else {
710                                 bot = networkBots.get(network.get(), channelMessageReceived.source().nick().get());
711                         }
712                 }
713
714                 /* add pack. */
715                 bot.addPack(pack.get());
716                 logger.debug(String.format("Bot %s now has %d packs.", bot, bot.packs().size()));
717         }
718
719         /**
720          * Forward all private messages to every console.
721          *
722          * @param privateMessageReceived
723          *              The private message recevied event
724          */
725         @Subscribe
726         public void privateMessageReceived(PrivateMessageReceived privateMessageReceived) {
727                 eventBus.post(new MessageReceived(privateMessageReceived.source(), privateMessageReceived.message()));
728         }
729
730         /**
731          * Sends a message to all console when a notice was received.
732          *
733          * @param privateNoticeReceived
734          *              The notice received event
735          */
736         @Subscribe
737         public void privateNoticeReceived(PrivateNoticeReceived privateNoticeReceived) {
738                 Optional<Network> network = getNetwork(privateNoticeReceived.connection());
739                 if (!network.isPresent()) {
740                         return;
741                 }
742
743                 eventBus.post(new GenericMessage(String.format("Notice from %s (%s): %s", privateNoticeReceived.source(), network.get(), privateNoticeReceived.text())));
744         }
745
746         /**
747          * Starts a DCC download.
748          *
749          * @param dccSendReceived
750          *              The DCC SEND event
751          */
752         @Subscribe
753         public void dccSendReceived(final DccSendReceived dccSendReceived) {
754                 final Optional<Network> network = getNetwork(dccSendReceived.connection());
755                 if (!network.isPresent()) {
756                         return;
757                 }
758
759                 Collection<Download> packDownloads = downloads.get(dccSendReceived.filename());
760                 if (packDownloads.isEmpty()) {
761                         /* unknown download, ignore. */
762                         return;
763                 }
764
765                 /* check if it’s already downloading. */
766                 Collection<Download> runningDownloads = FluentIterable.from(packDownloads).filter(FILTER_RUNNING).toSet();
767                 if (!runningDownloads.isEmpty()) {
768                         eventBus.post(new GenericMessage(String.format("Ignoring offer for %s, it’s already being downloaded.", dccSendReceived.filename())));
769                         return;
770                 }
771
772                 /* locate the correct download. */
773                 Collection<Download> requestedDownload = FluentIterable.from(packDownloads).filter(new Predicate<Download>() {
774
775                         @Override
776                         public boolean apply(Download download) {
777                                 return download.bot().network().equals(network.get()) && download.bot().name().equalsIgnoreCase(dccSendReceived.source().nick().get());
778                         }
779                 }).toSet();
780
781                 /* we did not request this download. */
782                 if (requestedDownload.isEmpty()) {
783                         return;
784                 }
785
786                 Download download = requestedDownload.iterator().next();
787
788                 /* check if the file already exists. */
789                 File outputFile = new File(temporaryDirectory, dccSendReceived.filename());
790                 if (outputFile.exists()) {
791                         long existingFileSize = outputFile.length();
792
793                         /* file already complete? */
794                         if ((dccSendReceived.filesize() > -1) && (existingFileSize >= dccSendReceived.filesize())) {
795                                 /* file is apparently already complete. just move it. */
796                                 if (outputFile.renameTo(new File(finalDirectory, download.pack().name()))) {
797                                         eventBus.post(new GenericMessage(String.format("File %s already downloaded.", download.pack().name())));
798                                 } else {
799                                         eventBus.post(new GenericMessage(String.format("File %s already downloaded but not moved to %s.", download.pack().name(), finalDirectory)));
800                                 }
801
802                                 /* remove download. */
803                                 downloads.removeAll(download.pack().name());
804                                 return;
805                         }
806
807                         /* file not complete yet, DCC resume it. */
808                         try {
809                                 download.remoteAddress(dccSendReceived.inetAddress()).filesize(dccSendReceived.filesize());
810                                 dccSendReceived.connection().sendDccResume(dccSendReceived.source().nick().get(), dccSendReceived.filename(), dccSendReceived.port(), existingFileSize);
811                         } catch (IOException ioe1) {
812                                 eventBus.post(new GenericError(String.format("Could not send DCC RESUME %s to %s (%s).", dccSendReceived.filename(), dccSendReceived.source().nick().get(), ioe1.getMessage())));
813                         }
814
815                         return;
816                 }
817
818                 /* file does not exist, start the download. */
819                 try {
820                         OutputStream fileOutputStream = new FileOutputStream(outputFile);
821                         DccReceiver dccReceiver = new DccReceiver(eventBus, dccSendReceived.inetAddress(), dccSendReceived.port(), dccSendReceived.filename(), dccSendReceived.filesize(), fileOutputStream);
822                         download.filename(outputFile.getPath()).outputStream(fileOutputStream).dccReceiver(dccReceiver);
823                         dccReceivers.add(dccReceiver);
824                         dccReceiver.start();
825                         eventBus.post(new DownloadStarted(download));
826                 } catch (FileNotFoundException fnfe1) {
827                         eventBus.post(new GenericError(String.format("Could not start download of %s from %s (%s).", dccSendReceived.filename(), dccSendReceived.source().nick().get(), fnfe1.getMessage())));
828                 }
829         }
830
831         @Subscribe
832         public void dccAcceptReceived(final DccAcceptReceived dccAcceptReceived) {
833                 final Optional<Network> network = getNetwork(dccAcceptReceived.connection());
834                 if (!network.isPresent()) {
835                         return;
836                 }
837
838                 Collection<Download> packDownloads = downloads.get(dccAcceptReceived.filename());
839                 if (packDownloads.isEmpty()) {
840                         /* unknown download, ignore. */
841                         return;
842                 }
843
844                 /* check if it’s already downloading. */
845                 Collection<Download> runningDownloads = FluentIterable.from(packDownloads).filter(FILTER_RUNNING).toSet();
846                 if (!runningDownloads.isEmpty()) {
847                         eventBus.post(new GenericMessage(String.format("Ignoring offer for %s, it’s already being downloaded.", dccAcceptReceived.filename())));
848                         return;
849                 }
850
851                 /* locate the correct download. */
852                 Collection<Download> requestedDownload = FluentIterable.from(packDownloads).filter(new Predicate<Download>() {
853
854                         @Override
855                         public boolean apply(Download download) {
856                                 return download.bot().network().equals(network.get()) && download.bot().name().equalsIgnoreCase(dccAcceptReceived.source().nick().get());
857                         }
858                 }).toSet();
859
860                 /* we did not request this download. */
861                 if (requestedDownload.isEmpty()) {
862                         return;
863                 }
864
865                 Download download = requestedDownload.iterator().next();
866
867                 try {
868                         File outputFile = new File(temporaryDirectory, dccAcceptReceived.filename());
869                         if (outputFile.length() != dccAcceptReceived.position()) {
870                                 eventBus.post(new GenericError(String.format("Download %s from %s does not start at the right position!")));
871                                 logger.warn(String.format("Download %s from %s: have %d bytes but wants to resume from %d!", dccAcceptReceived.filename(), dccAcceptReceived.source(), outputFile.length(), dccAcceptReceived.position()));
872
873                                 downloads.removeAll(download.pack().name());
874                                 return;
875                         }
876                         OutputStream outputStream = new FileOutputStream(outputFile, true);
877                         DccReceiver dccReceiver = new DccReceiver(eventBus, download.remoteAddress(), dccAcceptReceived.port(), dccAcceptReceived.filename(), dccAcceptReceived.position(), download.filesize(), outputStream);
878                         download.filename(outputFile.getPath()).outputStream(outputStream).dccReceiver(dccReceiver);
879                         dccReceivers.add(dccReceiver);
880                         dccReceiver.start();
881                         eventBus.post(new DownloadStarted(download));
882                 } catch (FileNotFoundException fnfe1) {
883                 }
884         }
885
886         /**
887          * Closes the output stream of the download and moves the file to the final
888          * location.
889          *
890          * @param dccDownloadFinished
891          *              The DCC download finished event
892          */
893         @Subscribe
894         public void dccDownloadFinished(DccDownloadFinished dccDownloadFinished) {
895
896                 /* locate the correct download. */
897                 Collection<Download> requestedDownload = FluentIterable.from(downloads.get(dccDownloadFinished.dccReceiver().filename())).filter(FILTER_RUNNING).toSet();
898                 if (requestedDownload.isEmpty()) {
899                         /* this seems wrong. */
900                         logger.warn("Download finished but could not be located.");
901                         return;
902                 }
903                 Download download = requestedDownload.iterator().next();
904
905                 try {
906                         download.outputStream().close();
907                         File file = new File(download.filename());
908                         file.renameTo(new File(finalDirectory, download.pack().name()));
909                         eventBus.post(new DownloadFinished(download));
910                         dccReceivers.remove(dccDownloadFinished.dccReceiver());
911                         downloads.removeAll(download.pack().name());
912                 } catch (IOException ioe1) {
913                         /* TODO - handle all the errors. */
914                         logger.warn(String.format("Could not move file %s to directory %s.", download.filename(), finalDirectory), ioe1);
915                 }
916         }
917
918         /**
919          * Closes the output stream and notifies all listeners of the failure.
920          *
921          * @param dccDownloadFailed
922          *              The DCC download failed event
923          */
924         @Subscribe
925         public void dccDownloadFailed(DccDownloadFailed dccDownloadFailed) {
926
927                 /* locate the correct download. */
928                 Collection<Download> requestedDownload = FluentIterable.from(downloads.get(dccDownloadFailed.dccReceiver().filename())).filter(FILTER_RUNNING).toSet();
929                 if (requestedDownload.isEmpty()) {
930                         /* this seems wrong. */
931                         logger.warn("Download finished but could not be located.");
932                         return;
933                 }
934                 Download download = requestedDownload.iterator().next();
935
936                 try {
937                         Closeables.close(download.outputStream(), true);
938                         eventBus.post(new DownloadFailed(download));
939                         dccReceivers.remove(dccDownloadFailed.dccReceiver());
940                         downloads.removeAll(download.pack().name());
941                 } catch (IOException ioe1) {
942                         /* swallow silently. */
943                 }
944         }
945
946         @Subscribe
947         public void replyReceived(ReplyReceived replyReceived) {
948                 logger.trace(String.format("%s: %s", replyReceived.connection().hostname(), replyReceived.reply()));
949         }
950
951         //
952         // PRIVATE METHODS
953         //
954
955         /**
956          * Returns the download of the given pack from the given bot.
957          *
958          * @param pack
959          *              The pack being downloaded
960          * @param bot
961          *              The bot the pack is being downloaded from
962          * @return The download, or {@link Optional#absent()} if the download could not
963          *         be found
964          */
965         private Optional<Download> getDownload(Pack pack, Bot bot) {
966                 if (!downloads.containsKey(pack.name())) {
967                         return Optional.absent();
968                 }
969                 for (Download download : Lists.newArrayList(downloads.get(pack.name()))) {
970                         if (download.bot().equals(bot)) {
971                                 return Optional.of(download);
972                         }
973                 }
974                 return Optional.absent();
975         }
976
977         /**
978          * Searches all current connections for the given connection, returning the
979          * associated network.
980          *
981          * @param connection
982          *              The connection to get the network for
983          * @return The network belonging to the connection, or {@link
984          *         Optional#absent()}
985          */
986         private Optional<Network> getNetwork(Connection connection) {
987                 for (Entry<Network, Connection> networkConnectionEntry : networkConnections.entrySet()) {
988                         if (networkConnectionEntry.getValue().equals(connection)) {
989                                 return Optional.of(networkConnectionEntry.getKey());
990                         }
991                 }
992                 return Optional.absent();
993         }
994
995         /**
996          * Returns the configured channel for the given network and name.
997          *
998          * @param network
999          *              The network the channel is located on
1000          * @param channelName
1001          *              The name of the channel
1002          * @return The configured channel, or {@link Optional#absent()} if no
1003          *         configured channel matching the given network and name was found
1004          */
1005         public Optional<Channel> getChannel(Network network, String channelName) {
1006                 for (Channel channel : channels) {
1007                         if (channel.network().equals(network) && (channel.name().equalsIgnoreCase(channelName))) {
1008                                 return Optional.of(channel);
1009                         }
1010                 }
1011                 return Optional.absent();
1012         }
1013
1014         /**
1015          * Returns the extra channel for the given network and name.
1016          *
1017          * @param network
1018          *              The network the channel is located on
1019          * @param channelName
1020          *              The name of the channel
1021          * @return The extra channel, or {@link Optional#absent()} if no extra channel
1022          *         matching the given network and name was found
1023          */
1024         public Optional<Channel> getExtraChannel(Network network, String channelName) {
1025                 for (Channel channel : extraChannels) {
1026                         if (channel.network().equals(network) && (channel.name().equalsIgnoreCase(channelName))) {
1027                                 return Optional.of(channel);
1028                         }
1029                 }
1030                 return Optional.absent();
1031         }
1032
1033         /**
1034          * Parses {@link Pack} information from the given message.
1035          *
1036          * @param message
1037          *              The message to parse pack information from
1038          * @return The parsed pack, or {@link Optional#absent()} if the message could
1039          *         not be parsed into a pack
1040          */
1041         private Optional<Pack> parsePack(String message) {
1042                 int squareOpen = message.indexOf('[');
1043                 int squareClose = message.indexOf(']', squareOpen);
1044                 if ((squareOpen == -1) && (squareClose == -1)) {
1045                         return Optional.absent();
1046                 }
1047                 String packSize = message.substring(squareOpen + 1, squareClose);
1048                 String packName = message.substring(message.lastIndexOf(' ') + 1);
1049                 String packIndex = message.substring(0, message.indexOf(' ')).substring(1);
1050                 return Optional.of(new Pack(packIndex, packSize, packName));
1051         }
1052
1053 }