X-Git-Url: https://git.pterodactylus.net/?p=xudocci.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fxdcc%2Fcore%2FCore.java;h=171683b9a71892438a3b8476080333776e5507c4;hp=3c3e4578168bf02376af4eb227b8ca01578cf04e;hb=c45f3f052f1ade1291732bc92e14e6c741a564ff;hpb=c0bc71bd241cb16659b07b1d83204f21af0c7695 diff --git a/src/main/java/net/pterodactylus/xdcc/core/Core.java b/src/main/java/net/pterodactylus/xdcc/core/Core.java index 3c3e457..171683b 100644 --- a/src/main/java/net/pterodactylus/xdcc/core/Core.java +++ b/src/main/java/net/pterodactylus/xdcc/core/Core.java @@ -17,6 +17,11 @@ package net.pterodactylus.xdcc.core; +import static java.lang.String.format; +import static net.pterodactylus.irc.util.MessageCleaner.getDefaultInstance; +import static net.pterodactylus.xdcc.data.Channel.TO_NETWORK; +import static net.pterodactylus.xdcc.data.Download.FILTER_RUNNING; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -29,8 +34,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; import net.pterodactylus.irc.Connection; import net.pterodactylus.irc.ConnectionBuilder; @@ -41,14 +44,16 @@ import net.pterodactylus.irc.event.ChannelMessageReceived; import net.pterodactylus.irc.event.ClientQuit; import net.pterodactylus.irc.event.ConnectionClosed; import net.pterodactylus.irc.event.ConnectionEstablished; +import net.pterodactylus.irc.event.ConnectionFailed; import net.pterodactylus.irc.event.DccAcceptReceived; import net.pterodactylus.irc.event.DccDownloadFailed; import net.pterodactylus.irc.event.DccDownloadFinished; import net.pterodactylus.irc.event.DccSendReceived; +import net.pterodactylus.irc.event.KickedFromChannel; import net.pterodactylus.irc.event.NicknameChanged; -import net.pterodactylus.irc.event.PrivateNoticeReceived; import net.pterodactylus.irc.event.PrivateMessageReceived; -import net.pterodactylus.irc.util.MessageCleaner; +import net.pterodactylus.irc.event.PrivateNoticeReceived; +import net.pterodactylus.irc.event.ReplyReceived; import net.pterodactylus.irc.util.RandomNickname; import net.pterodactylus.xdcc.core.event.BotAdded; import net.pterodactylus.xdcc.core.event.CoreStarted; @@ -65,12 +70,17 @@ import net.pterodactylus.xdcc.data.Network; import net.pterodactylus.xdcc.data.Pack; import net.pterodactylus.xdcc.data.Server; +import com.google.common.base.Function; import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; import com.google.common.collect.HashBasedTable; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.collect.Table; import com.google.common.eventbus.EventBus; @@ -78,6 +88,7 @@ import com.google.common.eventbus.Subscribe; import com.google.common.io.Closeables; import com.google.common.util.concurrent.AbstractExecutionThreadService; import com.google.inject.Inject; +import org.apache.log4j.Logger; /** * The core of XDCC Downloader. @@ -114,7 +125,7 @@ public class Core extends AbstractExecutionThreadService { private final Table networkBots = HashBasedTable.create(); /** The current downloads. */ - private final Map downloads = Maps.newHashMap(); + private final Multimap downloads = HashMultimap.create(); /** The current DCC receivers. */ private final Collection dccReceivers = Lists.newArrayList(); @@ -141,6 +152,38 @@ public class Core extends AbstractExecutionThreadService { // /** + * Returns all currently known connections. + * + * @return All currently known connections + */ + public Collection connections() { + return networkConnections.values(); + } + + /** + * Returns all defined networks. + * + * @return All defined networks + */ + public Collection networks() { + return FluentIterable.from(channels).transform(TO_NETWORK).toSet(); + } + + /** + * Returns all connected networks. + * + * @return All connected networks + */ + public Collection connectedNetworks() { + return Lists.newArrayList(Optional.presentInstances(FluentIterable.from(networkConnections.values()).transform(new Function>() { + @Override + public Optional apply(Connection connection) { + return getNetwork(connection); + } + }))); + } + + /** * Returns all configured channels. Due to various circumstances, configured * channels might not actually be joined. * @@ -178,12 +221,12 @@ public class Core extends AbstractExecutionThreadService { } /** - * Returns the currently active DCC receivers. + * Returns all currently running downloads. * - * @return The currently active DCC receivers + * @return All currently running downloads */ - public Collection dccReceivers() { - return dccReceivers; + public Collection downloads() { + return downloads.values(); } // @@ -214,13 +257,83 @@ public class Core extends AbstractExecutionThreadService { return; } + /* check if we are already downloading the file? */ + if (downloads.containsKey(pack.name())) { + Collection packDownloads = downloads.get(pack.name()); + Collection runningDownloads = FluentIterable.from(packDownloads).filter(FILTER_RUNNING).toSet(); + if (!runningDownloads.isEmpty()) { + Download download = runningDownloads.iterator().next(); + eventBus.post(new GenericMessage(String.format("File %s is already downloading from %s (%s).", pack.name(), download.bot().name(), download.bot().network().name()))); + return; + } + StringBuilder bots = new StringBuilder(); + for (Download download : packDownloads) { + if (bots.length() > 0) { + bots.append(", "); + } + bots.append(download.bot().name()).append(" (").append(download.bot().network().name()).append(')'); + } + eventBus.post(new GenericMessage(String.format("File %s is already requested from %d bots (%s).", pack.name(), packDownloads.size(), bots.toString()))); + } + Download download = new Download(bot, pack); downloads.put(pack.name(), download); try { connection.sendMessage(bot.name(), "XDCC SEND " + pack.id()); } catch (IOException ioe1) { - logger.log(Level.WARNING, "Could not send message to bot!", ioe1); + logger.warn("Could not send message to bot!", ioe1); + } + } + + /** + * Cancels the download of the given pack from the given bot. + * + * @param bot + * The bot the pack is being downloaded from + * @param pack + * The pack being downloaded + */ + public void cancelDownload(Bot bot, Pack pack) { + Optional download = getDownload(pack, bot); + if (!download.isPresent()) { + return; + } + + /* get connection. */ + Connection connection = networkConnections.get(bot.network()); + if (connection == null) { + /* request for unknown network? */ + return; + } + + /* stop the DCC receiver. */ + if (download.get().dccReceiver() != null) { + download.get().dccReceiver().stop(); + } else { + /* remove download if it hasn’t started yet. */ + downloads.remove(pack.name(), download.get()); + } + + /* remove the request from the bot, too. */ + try { + connection.sendMessage(bot.name(), String.format("XDCC %s", (download.get().dccReceiver() != null) ? "CANCEL" : "REMOVE")); + } catch (IOException ioe1) { + logger.warn(String.format("Could not cancel DCC from %s (%s)!", bot.name(), bot.network().name()), ioe1); + } + } + + /** + * Closes the given connection. + * + * @param connection + * The connection to close + */ + public void closeConnection(Connection connection) { + try { + connection.close(); + } catch (IOException ioe1) { + /* TODO */ } } @@ -258,7 +371,11 @@ public class Core extends AbstractExecutionThreadService { Connection connection = networkConnections.get(channel.network()); if (connection.established()) { eventBus.post(new GenericMessage(String.format("Trying to join %s on %s.", channel.name(), channel.network().name()))); - connection.joinChannel(channel.name()); + try { + connection.joinChannel(channel.name()); + } catch (IOException ioe1) { + eventBus.post(new GenericMessage(String.format("Could not join %s on %s.", channel.name(), channel.network().name()))); + } } } } @@ -295,6 +412,44 @@ public class Core extends AbstractExecutionThreadService { } } + /** + * Removes the given connection and all its channels and bots. + * + * @param connection + * The connection to remove + */ + private void removeConnection(Connection connection) { + Optional network = getNetwork(connection); + if (!network.isPresent()) { + return; + } + + /* find all channels that need to be removed. */ + for (Collection channels : ImmutableList.of(joinedChannels, extraChannels)) { + for (Iterator channelIterator = channels.iterator(); channelIterator.hasNext(); ) { + Channel joinedChannel = channelIterator.next(); + if (!joinedChannel.network().equals(network.get())) { + continue; + } + + channelIterator.remove(); + } + } + + /* now remove all bots for that network. */ + Map bots = networkBots.row(network.get()); + int botCount = bots.size(); + int packCount = 0; + for (Bot bot : bots.values()) { + packCount += bot.packs().size(); + } + bots.clear(); + eventBus.post(new GenericMessage(String.format("Network %s disconnected, %d bots removed, %d packs removed.", network.get().name(), botCount, packCount))); + + /* now remove the network. */ + networkConnections.remove(network.get()); + } + // // EVENT HANDLERS // @@ -314,16 +469,20 @@ public class Core extends AbstractExecutionThreadService { /* found network? */ if (!network.isPresent()) { + eventBus.post(new GenericMessage(String.format("Connected to unknown network: %s", connectionEstablished.connection().hostname()))); return; } + eventBus.post(new GenericMessage(String.format("Connected to network %s.", network.get().name()))); + /* join all channels on this network. */ for (Channel channel : channels) { if (channel.network().equals(network.get())) { try { + eventBus.post(new GenericMessage(String.format("Trying to join %s on %s...", channel.name(), network.get().name()))); connectionEstablished.connection().joinChannel(channel.name()); } catch (IOException ioe1) { - logger.log(Level.WARNING, String.format("Could not join %s on %s!", channel.name(), network.get().name()), ioe1); + logger.warn(String.format("Could not join %s on %s!", channel.name(), network.get().name()), ioe1); } } } @@ -337,35 +496,18 @@ public class Core extends AbstractExecutionThreadService { */ @Subscribe public void connectionClosed(ConnectionClosed connectionClosed) { - Optional network = getNetwork(connectionClosed.connection()); - if (!network.isPresent()) { - return; - } - - /* find all channels that need to be removed. */ - for (Collection channels : ImmutableList.of(joinedChannels, extraChannels)) { - for (Iterator channelIterator = channels.iterator(); channelIterator.hasNext(); ) { - Channel joinedChannel = channelIterator.next(); - if (!joinedChannel.network().equals(network.get())) { - continue; - } - - channelIterator.remove(); - } - } - - /* now remove all bots for that network. */ - Map bots = networkBots.row(network.get()); - int botCount = bots.size(); - int packCount = 0; - for (Bot bot : bots.values()) { - packCount += bot.packs().size(); - } - bots.clear(); - eventBus.post(new GenericMessage(String.format("Network %s disconnected, %d bots removed, %d packs removed.", network.get().name(), botCount, packCount))); + removeConnection(connectionClosed.connection()); + } - /* now remove the network. */ - networkConnections.remove(network.get()); + /** + * Remove all data stored for a network if the connection fails. + * + * @param connectionFailed + * The connection failed event + */ + @Subscribe + public void connectionFailed(ConnectionFailed connectionFailed) { + removeConnection(connectionFailed.connection()); } /** @@ -395,6 +537,18 @@ public class Core extends AbstractExecutionThreadService { } } + @Subscribe + public void channelNotJoined(ChannelNotJoined channelNotJoined) { + Optional network = getNetwork(channelNotJoined.connection()); + if (!network.isPresent()) { + return; + } + + eventBus.post(new GenericMessage( + format("Could not join %s/%s: %s", channelNotJoined.channel(), + network.get(), channelNotJoined.reason()))); + } + /** * Removes bots that leave a channel, or channels when it’s us that’s leaving. * @@ -432,12 +586,31 @@ public class Core extends AbstractExecutionThreadService { return; } - Bot removedBot = networkBots.remove(network.get(), channelLeft.client().nick().get()); - if (removedBot != null) { - eventBus.post(new GenericMessage(String.format("Bot %s (%s) was removed, %d packs removed.", removedBot.name(), removedBot.network().name(), removedBot.packs().size()))); + networkBots.remove(network.get(), channelLeft.client().nick().get()); + } + + @Subscribe + public void kickedFromChannel(KickedFromChannel kickedFromChannel) { + Optional network = getNetwork(kickedFromChannel.connection()); + if (!network.isPresent()) { + return; + } + + /* have we been kicked? */ + if (nicknameMatchesConnection(kickedFromChannel.connection(), kickedFromChannel.kickee())) { + eventBus.post(new GenericMessage(format( + "Kicked from %s/%s by %s: %s", + kickedFromChannel.channel(), network.get(), + kickedFromChannel.kicker(), + kickedFromChannel.reason().or("") + ))); } } + private boolean nicknameMatchesConnection(Connection connection, String nickname) { + return connection.nickname().equalsIgnoreCase(nickname); + } + /** * Removes a client (which may be a bot) from the table of known bots. * @@ -451,10 +624,7 @@ public class Core extends AbstractExecutionThreadService { return; } - Bot removedBot = networkBots.remove(network.get(), clientQuit.client().nick().get()); - if (removedBot != null) { - eventBus.post(new GenericMessage(String.format("Bot %s (%s) was removed, %d packs removed.", removedBot.name(), removedBot.network().name(), removedBot.packs().size()))); - } + networkBots.remove(network.get(), clientQuit.client().nick().get()); } /** @@ -488,7 +658,7 @@ public class Core extends AbstractExecutionThreadService { */ @Subscribe public void channelMessageReceived(ChannelMessageReceived channelMessageReceived) { - String message = MessageCleaner.getDefaultInstance().clean(channelMessageReceived.message()); + String message = getDefaultInstance().clean(channelMessageReceived.message()); if (!message.startsWith("#")) { /* most probably not a pack announcement. */ return; @@ -509,7 +679,7 @@ public class Core extends AbstractExecutionThreadService { Bot bot; synchronized (networkBots) { if (!networkBots.contains(network.get(), channelMessageReceived.source().nick().get())) { - bot = new Bot(network.get()).name(channelMessageReceived.source().nick().get()); + bot = new Bot(network.get(), channelMessageReceived.source().nick().get()); networkBots.put(network.get(), channelMessageReceived.source().nick().get(), bot); eventBus.post(new BotAdded(bot)); } else { @@ -519,7 +689,7 @@ public class Core extends AbstractExecutionThreadService { /* add pack. */ bot.addPack(pack.get()); - logger.fine(String.format("Bot %s now has %d packs.", bot, bot.packs().size())); + logger.debug(String.format("Bot %s now has %d packs.", bot, bot.packs().size())); } /** @@ -546,7 +716,7 @@ public class Core extends AbstractExecutionThreadService { return; } - eventBus.post(new GenericMessage(String.format("Notice from %s (%s): %s", privateNoticeReceived.reply().source(), network.get(), privateNoticeReceived.text()))); + eventBus.post(new GenericMessage(String.format("Notice from %s (%s): %s", privateNoticeReceived.reply().source().get(), network.get(), privateNoticeReceived.text()))); } /** @@ -556,18 +726,41 @@ public class Core extends AbstractExecutionThreadService { * The DCC SEND event */ @Subscribe - public void dccSendReceived(DccSendReceived dccSendReceived) { - Optional network = getNetwork(dccSendReceived.connection()); + public void dccSendReceived(final DccSendReceived dccSendReceived) { + final Optional network = getNetwork(dccSendReceived.connection()); if (!network.isPresent()) { return; } - Download download = downloads.get(dccSendReceived.filename()); - if (download == null) { + Collection packDownloads = downloads.get(dccSendReceived.filename()); + if (packDownloads.isEmpty()) { /* unknown download, ignore. */ return; } + /* check if it’s already downloading. */ + Collection runningDownloads = FluentIterable.from(packDownloads).filter(FILTER_RUNNING).toSet(); + if (!runningDownloads.isEmpty()) { + eventBus.post(new GenericMessage(String.format("Ignoring offer for %s, it’s already being downloaded.", dccSendReceived.filename()))); + return; + } + + /* locate the correct download. */ + Collection requestedDownload = FluentIterable.from(packDownloads).filter(new Predicate() { + + @Override + public boolean apply(Download download) { + return download.bot().network().equals(network.get()) && download.bot().name().equalsIgnoreCase(dccSendReceived.source().nick().get()); + } + }).toSet(); + + /* we did not request this download. */ + if (requestedDownload.isEmpty()) { + return; + } + + Download download = requestedDownload.iterator().next(); + /* check if the file already exists. */ File outputFile = new File(temporaryDirectory, dccSendReceived.filename()); if (outputFile.exists()) { @@ -583,7 +776,7 @@ public class Core extends AbstractExecutionThreadService { } /* remove download. */ - downloads.remove(download); + downloads.removeAll(download.pack().name()); return; } @@ -612,25 +805,48 @@ public class Core extends AbstractExecutionThreadService { } @Subscribe - public void dccAcceptReceived(DccAcceptReceived dccAcceptReceived) { - Optional network = getNetwork(dccAcceptReceived.connection()); + public void dccAcceptReceived(final DccAcceptReceived dccAcceptReceived) { + final Optional network = getNetwork(dccAcceptReceived.connection()); if (!network.isPresent()) { return; } - Download download = downloads.get(dccAcceptReceived.filename()); - if (download == null) { + Collection packDownloads = downloads.get(dccAcceptReceived.filename()); + if (packDownloads.isEmpty()) { /* unknown download, ignore. */ return; } + /* check if it’s already downloading. */ + Collection runningDownloads = FluentIterable.from(packDownloads).filter(FILTER_RUNNING).toSet(); + if (!runningDownloads.isEmpty()) { + eventBus.post(new GenericMessage(String.format("Ignoring offer for %s, it’s already being downloaded.", dccAcceptReceived.filename()))); + return; + } + + /* locate the correct download. */ + Collection requestedDownload = FluentIterable.from(packDownloads).filter(new Predicate() { + + @Override + public boolean apply(Download download) { + return download.bot().network().equals(network.get()) && download.bot().name().equalsIgnoreCase(dccAcceptReceived.source().nick().get()); + } + }).toSet(); + + /* we did not request this download. */ + if (requestedDownload.isEmpty()) { + return; + } + + Download download = requestedDownload.iterator().next(); + try { File outputFile = new File(temporaryDirectory, dccAcceptReceived.filename()); if (outputFile.length() != dccAcceptReceived.position()) { eventBus.post(new GenericError(String.format("Download %s from %s does not start at the right position!"))); - 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())); + 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())); - downloads.remove(download); + downloads.removeAll(download.pack().name()); return; } OutputStream outputStream = new FileOutputStream(outputFile, true); @@ -652,11 +868,15 @@ public class Core extends AbstractExecutionThreadService { */ @Subscribe public void dccDownloadFinished(DccDownloadFinished dccDownloadFinished) { - Download download = downloads.get(dccDownloadFinished.dccReceiver().filename()); - if (download == null) { - /* probably shouldn’t happen. */ + + /* locate the correct download. */ + Collection requestedDownload = FluentIterable.from(downloads.get(dccDownloadFinished.dccReceiver().filename())).filter(FILTER_RUNNING).toSet(); + if (requestedDownload.isEmpty()) { + /* this seems wrong. */ + logger.warn("Download finished but could not be located."); return; } + Download download = requestedDownload.iterator().next(); try { download.outputStream().close(); @@ -664,10 +884,10 @@ public class Core extends AbstractExecutionThreadService { file.renameTo(new File(finalDirectory, download.pack().name())); eventBus.post(new DownloadFinished(download)); dccReceivers.remove(dccDownloadFinished.dccReceiver()); - downloads.remove(download); + downloads.removeAll(download.pack().name()); } catch (IOException ioe1) { /* TODO - handle all the errors. */ - logger.log(Level.WARNING, String.format("Could not move file %s to directory %s.", download.filename(), finalDirectory), ioe1); + logger.warn(String.format("Could not move file %s to directory %s.", download.filename(), finalDirectory), ioe1); } } @@ -679,27 +899,58 @@ public class Core extends AbstractExecutionThreadService { */ @Subscribe public void dccDownloadFailed(DccDownloadFailed dccDownloadFailed) { - Download download = downloads.get(dccDownloadFailed.dccReceiver().filename()); - if (download == null) { - /* probably shouldn’t happen. */ + + /* locate the correct download. */ + Collection requestedDownload = FluentIterable.from(downloads.get(dccDownloadFailed.dccReceiver().filename())).filter(FILTER_RUNNING).toSet(); + if (requestedDownload.isEmpty()) { + /* this seems wrong. */ + logger.warn("Download finished but could not be located."); return; } + Download download = requestedDownload.iterator().next(); try { Closeables.close(download.outputStream(), true); eventBus.post(new DownloadFailed(download)); dccReceivers.remove(dccDownloadFailed.dccReceiver()); - downloads.remove(download); + downloads.removeAll(download.pack().name()); } catch (IOException ioe1) { /* swallow silently. */ } } + @Subscribe + public void replyReceived(ReplyReceived replyReceived) { + logger.trace(String.format("%s: %s", replyReceived.connection().hostname(), replyReceived.reply())); + } + // // PRIVATE METHODS // /** + * Returns the download of the given pack from the given bot. + * + * @param pack + * The pack being downloaded + * @param bot + * The bot the pack is being downloaded from + * @return The download, or {@link Optional#absent()} if the download could not + * be found + */ + private Optional getDownload(Pack pack, Bot bot) { + if (!downloads.containsKey(pack.name())) { + return Optional.absent(); + } + for (Download download : Lists.newArrayList(downloads.get(pack.name()))) { + if (download.bot().equals(bot)) { + return Optional.of(download); + } + } + return Optional.absent(); + } + + /** * Searches all current connections for the given connection, returning the * associated network. *