X-Git-Url: https://git.pterodactylus.net/?a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fxdcc%2Fcore%2FCore.java;h=33a22f81c3c6389e9bcd8681d04ec24f015003db;hb=287af7be5a3fe79882649783649889f079451d27;hp=36768a4b347144c895dec11c82021eb82f9ebaaa;hpb=907f0d44b3905788e3984f4d19d79a444eafe9f2;p=xudocci.git diff --git a/src/main/java/net/pterodactylus/xdcc/core/Core.java b/src/main/java/net/pterodactylus/xdcc/core/Core.java index 36768a4..33a22f8 100644 --- a/src/main/java/net/pterodactylus/xdcc/core/Core.java +++ b/src/main/java/net/pterodactylus/xdcc/core/Core.java @@ -24,9 +24,11 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; 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; @@ -34,21 +36,38 @@ import net.pterodactylus.irc.Connection; import net.pterodactylus.irc.ConnectionBuilder; import net.pterodactylus.irc.DccReceiver; import net.pterodactylus.irc.event.ChannelJoined; +import net.pterodactylus.irc.event.ChannelLeft; 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.DccAcceptReceived; +import net.pterodactylus.irc.event.DccDownloadFailed; +import net.pterodactylus.irc.event.DccDownloadFinished; import net.pterodactylus.irc.event.DccSendReceived; +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.util.RandomNickname; import net.pterodactylus.xdcc.core.event.BotAdded; import net.pterodactylus.xdcc.core.event.CoreStarted; +import net.pterodactylus.xdcc.core.event.DownloadFailed; +import net.pterodactylus.xdcc.core.event.DownloadFinished; +import net.pterodactylus.xdcc.core.event.DownloadStarted; +import net.pterodactylus.xdcc.core.event.GenericError; +import net.pterodactylus.xdcc.core.event.GenericMessage; +import net.pterodactylus.xdcc.core.event.MessageReceived; import net.pterodactylus.xdcc.data.Bot; import net.pterodactylus.xdcc.data.Channel; +import net.pterodactylus.xdcc.data.Download; import net.pterodactylus.xdcc.data.Network; import net.pterodactylus.xdcc.data.Pack; import net.pterodactylus.xdcc.data.Server; import com.google.common.base.Optional; import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -56,7 +75,8 @@ import com.google.common.collect.Sets; import com.google.common.collect.Table; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; -import com.google.common.util.concurrent.AbstractIdleService; +import com.google.common.io.Closeables; +import com.google.common.util.concurrent.AbstractExecutionThreadService; import com.google.inject.Inject; /** @@ -64,7 +84,7 @@ import com.google.inject.Inject; * * @author David ‘Bombe’ Roden */ -public class Core extends AbstractIdleService { +public class Core extends AbstractExecutionThreadService { /** The logger. */ private static final Logger logger = Logger.getLogger(Core.class.getName()); @@ -72,6 +92,12 @@ public class Core extends AbstractIdleService { /** The event bus. */ private final EventBus eventBus; + /** The temporary directory to download files to. */ + private final String temporaryDirectory; + + /** The directory to move finished downloads to. */ + private final String finalDirectory; + /** The channels that should be monitored. */ private final Collection channels = Sets.newHashSet(); @@ -87,18 +113,27 @@ public class Core extends AbstractIdleService { /** The currently known bots. */ private final Table networkBots = HashBasedTable.create(); + /** The current downloads. */ + private final Map downloads = Maps.newHashMap(); + /** The current DCC receivers. */ - private final Collection dccReceivers = Sets.newHashSet(); + private final Collection dccReceivers = Lists.newArrayList(); /** * Creates a new core. * * @param eventBus * The event bus + * @param temporaryDirectory + * The directory to download files to + * @param finalDirectory + * The directory to move finished files to */ @Inject - public Core(EventBus eventBus) { + public Core(EventBus eventBus, String temporaryDirectory, String finalDirectory) { this.eventBus = eventBus; + this.temporaryDirectory = temporaryDirectory; + this.finalDirectory = finalDirectory; } // @@ -143,12 +178,12 @@ public class Core extends AbstractIdleService { } /** - * 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(); } // @@ -179,6 +214,9 @@ public class Core extends AbstractIdleService { return; } + Download download = new Download(bot, pack); + downloads.put(pack.name(), download); + try { connection.sendMessage(bot.name(), "XDCC SEND " + pack.id()); } catch (IOException ioe1) { @@ -194,15 +232,7 @@ public class Core extends AbstractIdleService { protected void startUp() { for (Channel channel : channels) { logger.info(String.format("Connecting to Channel %s on Network %s…", channel.name(), channel.network().name())); - if (!networkConnections.containsKey(channel.network())) { - /* select a random server. */ - List servers = Lists.newArrayList(channel.network().servers()); - Server server = servers.get((int) (Math.random() * servers.size())); - Connection connection = new ConnectionBuilder(eventBus).connect(server.hostname()).port(server.unencryptedPorts().iterator().next()).build(); - connection.username(RandomNickname.get()).realName(RandomNickname.get()); - networkConnections.put(channel.network(), connection); - connection.start(); - } + connectNetwork(channel.network()); } /* notify listeners. */ @@ -210,10 +240,62 @@ public class Core extends AbstractIdleService { } @Override + protected void run() throws Exception { + while (isRunning()) { + try { + Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + } catch (InterruptedException ie1) { + /* ignore. */ + } + + /* find channels that should be monitored but are not. */ + for (Channel channel : channels) { + if (joinedChannels.contains(channel)) { + continue; + } + + connectNetwork(channel.network()); + 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()); + } + } + } + } + + @Override protected void shutDown() { } // + // PRIVATE METHODS + // + + /** + * Starts a new connection for the given network if no such connection exists + * already. + * + * @param network + * The network to connect to + */ + private void connectNetwork(Network network) { + if (!networkConnections.containsKey(network)) { + /* select a random server. */ + List servers = Lists.newArrayList(network.servers()); + if (servers.isEmpty()) { + eventBus.post(new GenericError(String.format("Network %s does not have any servers.", network.name()))); + return; + } + Server server = servers.get((int) (Math.random() * servers.size())); + Connection connection = new ConnectionBuilder(eventBus).connect(server.hostname()).port(server.unencryptedPorts().iterator().next()).build(); + connection.username(RandomNickname.get()).realName(RandomNickname.get()); + networkConnections.put(network, connection); + connection.start(); + } + } + + // // EVENT HANDLERS // @@ -248,6 +330,45 @@ public class Core extends AbstractIdleService { } /** + * Remove all data stored for a network if the connection is closed. + * + * @param connectionClosed + * The connection closed event + */ + @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))); + + /* now remove the network. */ + networkConnections.remove(network.get()); + } + + /** * Shows a message when a channel was joined by us. * * @param channelJoined @@ -275,6 +396,90 @@ public class Core extends AbstractIdleService { } /** + * Removes bots that leave a channel, or channels when it’s us that’s leaving. + * + * @param channelLeft + * The channel left event + */ + @Subscribe + public void channelLeft(ChannelLeft channelLeft) { + Optional network = getNetwork(channelLeft.connection()); + if (!network.isPresent()) { + return; + } + + Bot bot = networkBots.get(network.get(), channelLeft.client().nick().get()); + if (bot == null) { + /* maybe it was us? */ + if (channelLeft.connection().isSource(channelLeft.client())) { + Optional channel = getChannel(network.get(), channelLeft.channel()); + if (!channel.isPresent()) { + /* maybe it was an extra channel? */ + channel = getExtraChannel(network.get(), channelLeft.channel()); + if (!channel.isPresent()) { + /* okay, whatever. */ + return; + } + + extraChannels.remove(channel); + } else { + channels.remove(channel.get()); + } + + eventBus.post(new GenericMessage(String.format("Left Channel %s on %s.", channel.get().name(), channel.get().network().name()))); + } + + 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()))); + } + } + + /** + * Removes a client (which may be a bot) from the table of known bots. + * + * @param clientQuit + * The client quit event + */ + @Subscribe + public void clientQuit(ClientQuit clientQuit) { + Optional network = getNetwork(clientQuit.connection()); + if (!network.isPresent()) { + 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()))); + } + } + + /** + * If the nickname of a bit changes, remove it from the old name and store it + * under the new name. + * + * @param nicknameChanged + * The nickname changed event + */ + @Subscribe + public void nicknameChanged(NicknameChanged nicknameChanged) { + Optional network = getNetwork(nicknameChanged.connection()); + if (!network.isPresent()) { + return; + } + + Bot bot = networkBots.remove(network.get(), nicknameChanged.client().nick().get()); + if (bot == null) { + return; + } + + networkBots.put(network.get(), nicknameChanged.newNickname(), bot); + } + + /** * If a message on a channel is received, it is parsed for pack information * with is then added to a bot. * @@ -318,6 +523,33 @@ public class Core extends AbstractIdleService { } /** + * Forward all private messages to every console. + * + * @param privateMessageReceived + * The private message recevied event + */ + @Subscribe + public void privateMessageReceived(PrivateMessageReceived privateMessageReceived) { + eventBus.post(new MessageReceived(privateMessageReceived.source(), privateMessageReceived.message())); + } + + /** + * Sends a message to all console when a notice was received. + * + * @param privateNoticeReceived + * The notice received event + */ + @Subscribe + public void privateNoticeReceived(PrivateNoticeReceived privateNoticeReceived) { + Optional network = getNetwork(privateNoticeReceived.connection()); + if (!network.isPresent()) { + return; + } + + eventBus.post(new GenericMessage(String.format("Notice from %s (%s): %s", privateNoticeReceived.reply().source(), network.get(), privateNoticeReceived.text()))); + } + + /** * Starts a DCC download. * * @param dccSendReceived @@ -325,14 +557,141 @@ public class Core extends AbstractIdleService { */ @Subscribe public void dccSendReceived(DccSendReceived dccSendReceived) { - logger.info(String.format("Starting download of %s.", dccSendReceived.filename())); + Optional network = getNetwork(dccSendReceived.connection()); + if (!network.isPresent()) { + return; + } + + Download download = downloads.get(dccSendReceived.filename()); + if (download == null) { + /* unknown download, ignore. */ + return; + } + + /* check if the file already exists. */ + File outputFile = new File(temporaryDirectory, dccSendReceived.filename()); + if (outputFile.exists()) { + long existingFileSize = outputFile.length(); + + /* file already complete? */ + if ((dccSendReceived.filesize() > -1) && (existingFileSize >= dccSendReceived.filesize())) { + /* file is apparently already complete. just move it. */ + if (outputFile.renameTo(new File(finalDirectory, download.pack().name()))) { + eventBus.post(new GenericMessage(String.format("File %s already downloaded.", download.pack().name()))); + } else { + eventBus.post(new GenericMessage(String.format("File %s already downloaded but not moved to %s.", download.pack().name(), finalDirectory))); + } + + /* remove download. */ + downloads.remove(download.pack().name()); + return; + } + + /* file not complete yet, DCC resume it. */ + try { + download.remoteAddress(dccSendReceived.inetAddress()).filesize(dccSendReceived.filesize()); + dccSendReceived.connection().sendDccResume(dccSendReceived.source().nick().get(), dccSendReceived.filename(), dccSendReceived.port(), existingFileSize); + } catch (IOException ioe1) { + eventBus.post(new GenericError(String.format("Could not send DCC RESUME %s to %s (%s).", dccSendReceived.filename(), dccSendReceived.source().nick().get(), ioe1.getMessage()))); + } + + return; + } + + /* file does not exist, start the download. */ try { - OutputStream fileOutputStream = new FileOutputStream(new File("/home/bombe/Temp", dccSendReceived.filename())); + OutputStream fileOutputStream = new FileOutputStream(outputFile); DccReceiver dccReceiver = new DccReceiver(eventBus, dccSendReceived.inetAddress(), dccSendReceived.port(), dccSendReceived.filename(), dccSendReceived.filesize(), fileOutputStream); + download.filename(outputFile.getPath()).outputStream(fileOutputStream).dccReceiver(dccReceiver); + dccReceivers.add(dccReceiver); + dccReceiver.start(); + eventBus.post(new DownloadStarted(download)); + } catch (FileNotFoundException fnfe1) { + eventBus.post(new GenericError(String.format("Could not start download of %s from %s (%s).", dccSendReceived.filename(), dccSendReceived.source().nick().get(), fnfe1.getMessage()))); + } + } + + @Subscribe + public void dccAcceptReceived(DccAcceptReceived dccAcceptReceived) { + Optional network = getNetwork(dccAcceptReceived.connection()); + if (!network.isPresent()) { + return; + } + + Download download = downloads.get(dccAcceptReceived.filename()); + if (download == null) { + /* unknown download, ignore. */ + return; + } + + 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())); + + downloads.remove(download.pack().name()); + return; + } + OutputStream outputStream = new FileOutputStream(outputFile, true); + DccReceiver dccReceiver = new DccReceiver(eventBus, download.remoteAddress(), dccAcceptReceived.port(), dccAcceptReceived.filename(), dccAcceptReceived.position(), download.filesize(), outputStream); + download.filename(outputFile.getPath()).outputStream(outputStream).dccReceiver(dccReceiver); dccReceivers.add(dccReceiver); dccReceiver.start(); + eventBus.post(new DownloadStarted(download)); } catch (FileNotFoundException fnfe1) { - logger.log(Level.WARNING, "Could not open file for download!", fnfe1); + } + } + + /** + * Closes the output stream of the download and moves the file to the final + * location. + * + * @param dccDownloadFinished + * The DCC download finished event + */ + @Subscribe + public void dccDownloadFinished(DccDownloadFinished dccDownloadFinished) { + Download download = downloads.get(dccDownloadFinished.dccReceiver().filename()); + if (download == null) { + /* probably shouldn’t happen. */ + return; + } + + try { + download.outputStream().close(); + File file = new File(download.filename()); + file.renameTo(new File(finalDirectory, download.pack().name())); + eventBus.post(new DownloadFinished(download)); + dccReceivers.remove(dccDownloadFinished.dccReceiver()); + downloads.remove(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); + } + } + + /** + * Closes the output stream and notifies all listeners of the failure. + * + * @param dccDownloadFailed + * The DCC download failed event + */ + @Subscribe + public void dccDownloadFailed(DccDownloadFailed dccDownloadFailed) { + Download download = downloads.get(dccDownloadFailed.dccReceiver().filename()); + if (download == null) { + /* probably shouldn’t happen. */ + return; + } + + try { + Closeables.close(download.outputStream(), true); + eventBus.post(new DownloadFailed(download)); + dccReceivers.remove(dccDownloadFailed.dccReceiver()); + downloads.remove(download.pack().name()); + } catch (IOException ioe1) { + /* swallow silently. */ } } @@ -370,7 +729,26 @@ public class Core extends AbstractIdleService { */ public Optional getChannel(Network network, String channelName) { for (Channel channel : channels) { - if (channel.network().equals(network) && (channel.name().equals(channelName))) { + if (channel.network().equals(network) && (channel.name().equalsIgnoreCase(channelName))) { + return Optional.of(channel); + } + } + return Optional.absent(); + } + + /** + * Returns the extra channel for the given network and name. + * + * @param network + * The network the channel is located on + * @param channelName + * The name of the channel + * @return The extra channel, or {@link Optional#absent()} if no extra channel + * matching the given network and name was found + */ + public Optional getExtraChannel(Network network, String channelName) { + for (Channel channel : extraChannels) { + if (channel.network().equals(network) && (channel.name().equalsIgnoreCase(channelName))) { return Optional.of(channel); } }