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