X-Git-Url: https://git.pterodactylus.net/?p=xudocci.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fxdcc%2Fcore%2FCore.java;h=d39ee2bd0ffe241a933ad8ae0c844f99c21a93f4;hp=c59c9d5592d4ecaadaba6647ce5867f0df055591;hb=d8c6d72caaa1ba75d09bd28f2bb10ae6f4964bf4;hpb=103a6342cdf0c852da2a9aef1d2129e51782d9ad diff --git a/src/main/java/net/pterodactylus/xdcc/core/Core.java b/src/main/java/net/pterodactylus/xdcc/core/Core.java index c59c9d5..d39ee2b 100644 --- a/src/main/java/net/pterodactylus/xdcc/core/Core.java +++ b/src/main/java/net/pterodactylus/xdcc/core/Core.java @@ -17,71 +17,245 @@ package net.pterodactylus.xdcc.core; +import static java.lang.String.format; +import static net.pterodactylus.irc.event.ChannelNotJoined.Reason.banned; +import static net.pterodactylus.irc.event.ChannelNotJoined.Reason.inviteOnly; +import static net.pterodactylus.irc.event.ChannelNotJoined.Reason.registeredNicknamesOnly; +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; import java.io.IOException; +import java.io.OutputStream; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; import net.pterodactylus.irc.Connection; -import net.pterodactylus.irc.ConnectionBuilder; +import net.pterodactylus.irc.ConnectionFactory; +import net.pterodactylus.irc.DccReceiver; +import net.pterodactylus.irc.DefaultConnection; +import net.pterodactylus.irc.event.ChannelJoined; +import net.pterodactylus.irc.event.ChannelLeft; import net.pterodactylus.irc.event.ChannelMessageReceived; +import net.pterodactylus.irc.event.ChannelNotJoined; +import net.pterodactylus.irc.event.ClientQuit; +import net.pterodactylus.irc.event.ConnectionClosed; import net.pterodactylus.irc.event.ConnectionEstablished; -import net.pterodactylus.irc.util.MessageCleaner; +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.PrivateMessageReceived; +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; +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.ConnectedNetwork; +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.beust.jcommander.internal.Maps; -import com.beust.jcommander.internal.Sets; 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; 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; +import org.apache.log4j.Logger; /** * The core of XDCC Downloader. * * @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()); + private final Object syncObject = new Object(); /** The event bus. */ private final EventBus eventBus; + private final ConnectionFactory connectionFactory; + private final ChannelBanManager channelBanManager = + new ChannelBanManager(); + private final ConnectionBackoff connectionBackoff = new ConnectionBackoff(); + + /** 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(); + /** The channels that are currentlymonitored. */ + private final Collection joinedChannels = Sets.newHashSet(); + private final Set channelsBeingJoined = new HashSet<>(); + + /** The channels that are joined but not configured. */ + private final Collection extraChannels = Sets.newHashSet(); + /** The current network connections. */ private final Map networkConnections = Collections.synchronizedMap(Maps.newHashMap()); /** The currently known bots. */ private final Table networkBots = HashBasedTable.create(); + /** The current downloads. */ + private final Multimap downloads = HashMultimap.create(); + + /** The current DCC receivers. */ + 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, ConnectionFactory connectionFactory, String temporaryDirectory, String finalDirectory) { this.eventBus = eventBus; + this.connectionFactory = connectionFactory; + this.temporaryDirectory = temporaryDirectory; + this.finalDirectory = finalDirectory; + } + + // + // ACCESSORS + // + + /** + * 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 networkConnections.entrySet().stream().map((entry) -> { + Network network = entry.getKey(); + Collection bots = networkBots.row(network).values(); + int packCount = bots.stream().mapToInt((bot) -> bot.packs().size()).reduce((a, b) -> a + b).orElse(0); + return new ConnectedNetwork(network, entry.getValue().hostname(), + entry.getValue().port(), entry.getValue().nickname(), + channels.stream() + .filter((channel) -> channel.network() + .equals(network)) + .map(Channel::name) + .collect(Collectors.toList()), + extraChannels.stream() + .filter((channel) -> channel.network() + .equals(network)) + .map(Channel::name) + .collect(Collectors.toList()), + bots.size(), packCount); + }).collect(Collectors.toList()); + } + + /** + * Returns all configured channels. Due to various circumstances, configured + * channels might not actually be joined. + * + * @return All configured channels + */ + public Collection channels() { + return ImmutableSet.copyOf(channels); + } + + /** + * Returns all currently joined channels. + * + * @return All currently joined channels + */ + public Collection joinedChannels() { + return ImmutableSet.copyOf(joinedChannels); + } + + /** + * Returns all currently joined channels that are not configured. + * + * @return All currently joined but not configured channels + */ + public Collection extraChannels() { + return ImmutableSet.copyOf(extraChannels); + } + + /** + * Returns all currently known bots. + * + * @return All currently known bots + */ + public Collection bots() { + return networkBots.values(); + } + + /** + * Returns all currently running downloads. + * + * @return All currently running downloads + */ + public Collection downloads() { + return downloads.values(); } // @@ -98,6 +272,100 @@ public class Core extends AbstractIdleService { channels.add(channel); } + /** + * Fetches the given pack from the given bot. + * + * @param bot + * The bot to fetch the pack from + * @param pack + * The pack to fetch + */ + public void fetch(Bot bot, Pack pack) { + Connection connection = networkConnections.get(bot.network()); + if (connection == null) { + 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.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 */ + } + } + // // ABSTRACTIDLESERVICE METHODS // @@ -106,15 +374,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. */ @@ -122,7 +382,152 @@ public class Core extends AbstractIdleService { } @Override - protected void shutDown() { + protected void run() throws Exception { + while (isRunning()) { + + Set missingChannels = new HashSet<>(); + for (Channel channel : channels) { + if (joinedChannels.contains(channel) || channelsBeingJoined.contains(channel)) { + continue; + } + if (channelBanManager.isBanned(channel)) { + continue; + } + if (!networkConnections.containsKey(channel.network()) || networkConnections.get(channel.network()).established()) { + missingChannels.add(channel); + } + } + Set missingNetworks = missingChannels.stream() + .map(Channel::network) + .distinct() + .filter((network) -> !networkConnections.containsKey(network)) + .collect(Collectors.toSet()); + + if (missingNetworks.isEmpty()) { + if (!missingChannels.isEmpty()) { + for (Channel missingChannel : missingChannels) { + Network network = missingChannel.network(); + eventBus.post(new GenericMessage(String.format("Trying to join %s on %s...", missingChannel.name(), network))); + try { + channelsBeingJoined.add(missingChannel); + networkConnections.get(network).joinChannel(missingChannel.name()); + } catch (IOException ioe1) { + logger.warn(String.format("Could not join %s on %s!", missingChannel.name(), network.name()), ioe1); + } + } + } else { + synchronized (syncObject) { + try { + syncObject.wait(TimeUnit.MINUTES.toMillis(1)); + } catch (InterruptedException ie1) { + /* ignore. */ + } + } + } + continue; + } + + Map timesForNextConnects = new TreeMap<>(missingNetworks.stream() + .collect(Collectors.toMap(connectionBackoff::getConnectionTime, Function.identity(), (network, ignore) -> network))); + + Optional> firstNetwork = Optional.fromNullable(timesForNextConnects.entrySet().stream().findFirst().orElse(null)); + if (!firstNetwork.isPresent()) { + continue; + } + if (firstNetwork.get().getKey() > System.currentTimeMillis()) { + eventBus.post(new GenericMessage(String.format("Will connect to %2$s at %1$tH:%1$tM...", firstNetwork.get().getKey(), firstNetwork.get().getValue().name()))); + synchronized (syncObject) { + try { + syncObject.wait(firstNetwork.get().getKey() - System.currentTimeMillis()); + } catch (InterruptedException ie1) { + /* ignore. */ + } + } + if (!isRunning()) { + break; + } + if (firstNetwork.get().getKey() > System.currentTimeMillis()) { + continue; + } + } + + connectNetwork(firstNetwork.get().getValue()); + } + } + + @Override + protected void triggerShutdown() { + synchronized (syncObject) { + syncObject.notifyAll(); + } + } + + // + // 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())); + eventBus.post(new GenericMessage(String.format("Connecting to %s on %s...", network.name(), server.hostname()))); + Connection connection = connectionFactory.createConnection(server.hostname(), + server.unencryptedPorts().iterator().next()); + connection.username(RandomNickname.get()).realName(RandomNickname.get()); + networkConnections.put(network, connection); + connection.open(); + } + } + + /** + * 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; + } + networkConnections.remove(network.get()); + if (!connection.established()) { + 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))); } // @@ -144,22 +549,254 @@ public class Core extends AbstractIdleService { /* found network? */ if (!network.isPresent()) { + eventBus.post(new GenericMessage(String.format("Connected to unknown network: %s", connectionEstablished.connection().hostname()))); return; } + connectionBackoff.connectionSuccessful(network.get()); + 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); } } } } /** + * Remove all data stored for a network if the connection is closed. + * + * @param connectionClosed + * The connection closed event + */ + @Subscribe + public void connectionClosed(ConnectionClosed connectionClosed) { + connectionBackoff.connectionFailed(getNetwork(connectionClosed.connection()).get()); + removeConnection(connectionClosed.connection()); + synchronized (syncObject) { + syncObject.notifyAll(); + } + eventBus.post(new GenericMessage(String.format("Connection closed by %s.", connectionClosed.connection().hostname()))); + } + + /** + * Remove all data stored for a network if the connection fails. + * + * @param connectionFailed + * The connection failed event + */ + @Subscribe + public void connectionFailed(ConnectionFailed connectionFailed) { + connectionBackoff.connectionFailed(getNetwork(connectionFailed.connection()).get()); + removeConnection(connectionFailed.connection()); + synchronized (syncObject) { + syncObject.notifyAll(); + } + eventBus.post(new GenericMessage(String.format("Could not connect to %s: %s.", connectionFailed.connection().hostname(), connectionFailed.cause()))); + } + + /** + * Shows a message when a channel was joined by us. + * + * @param channelJoined + * The channel joined event + */ + @Subscribe + public void channelJoined(ChannelJoined channelJoined) { + if (channelJoined.connection().isSource(channelJoined.client())) { + Optional network = getNetwork(channelJoined.connection()); + if (!network.isPresent()) { + return; + } + + Optional channel = getChannel(network.get(), channelJoined.channel()); + if (!channel.isPresent()) { + /* it’s an extra channel. */ + extraChannels.add(new Channel(network.get(), channelJoined.channel())); + logger.info(String.format("Joined extra Channel %s on %s.", channelJoined.channel(), network.get().name())); + return; + } + + channelBanManager.unban(channel.get()); + joinedChannels.add(channel.get()); + channelsBeingJoined.remove(channel.get()); + logger.info(String.format("Joined Channel %s on %s.", channelJoined.channel(), network.get().name())); + } + } + + @Subscribe + public void channelNotJoined(ChannelNotJoined channelNotJoined) { + Optional network = getNetwork(channelNotJoined.connection()); + if (!network.isPresent()) { + return; + } + + Optional channel = getChannel(network.get(), channelNotJoined.channel()); + synchronized (syncObject) { + syncObject.notifyAll(); + } + if (!channel.isPresent()) { + eventBus.post(new GenericMessage(format("Could not join %s but didn’t try to join, either.", channelNotJoined.channel()))); + return; + } + channelsBeingJoined.remove(channel.get()); + + + /* remove all bots for this channel, we might have been kicked. */ + Collection botsToRemove = networkBots.row(network.get()) + .values().stream() + .filter(bot -> bot.channel() + .equalsIgnoreCase(channel.get().name())) + .collect(Collectors.toSet()); + botsToRemove.stream() + .forEach(bot -> networkBots.row(network.get()) + .remove(bot.name())); + + channelBanManager.ban(channel.get()); + if (channelNotJoined.reason() == registeredNicknamesOnly) { + eventBus.post(new GenericMessage( + format("Not trying to join %s anymore.", channel.get()))); + } else if (channelNotJoined.reason() == inviteOnly) { + eventBus.post(new GenericMessage( + format("%s is invite-only, suspending join for a day.", + channel.get()))); + } else if (channelNotJoined.reason() == banned) { + eventBus.post(new GenericMessage( + format("Banned from %s, suspending join for a day.", + channel.get()))); + } else { + eventBus.post(new GenericMessage( + format("Could not join %s: %s", channelNotJoined.channel(), + channelNotJoined.reason()))); + } + } + + /** + * 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()); + } + synchronized (syncObject) { + syncObject.notifyAll(); + } + + eventBus.post(new GenericMessage(String.format("Left Channel %s on %s.", channel.get().name(), channel.get().network().name()))); + } + + return; + } + + 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())) { + Optional channel = getChannel(network.get(), kickedFromChannel.channel()); + if (!channel.isPresent()) { + /* maybe it was an extra channel? */ + channel = getExtraChannel(network.get(), kickedFromChannel.channel()); + if (!channel.isPresent()) { + /* okay, whatever. */ + return; + } + + extraChannels.remove(channel.get()); + } else { + joinedChannels.remove(channel.get()); + } + synchronized (syncObject) { + syncObject.notifyAll(); + } + eventBus.post(new GenericMessage(format( + "Kicked from %s by %s: %s", + kickedFromChannel.channel(), + 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. + * + * @param clientQuit + * The client quit event + */ + @Subscribe + public void clientQuit(ClientQuit clientQuit) { + Optional network = getNetwork(clientQuit.connection()); + if (!network.isPresent()) { + return; + } + + networkBots.remove(network.get(), clientQuit.client().nick().get()); + } + + /** + * 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. * @@ -168,7 +805,7 @@ public class Core extends AbstractIdleService { */ @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; @@ -189,7 +826,8 @@ public class Core extends AbstractIdleService { 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.channel(), + channelMessageReceived.source().nick().get()); networkBots.put(network.get(), channelMessageReceived.source().nick().get(), bot); eventBus.post(new BotAdded(bot)); } else { @@ -199,7 +837,239 @@ public class Core extends AbstractIdleService { /* 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())); + } + + /** + * 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.source(), network.get(), privateNoticeReceived.text()))); + } + + /** + * Starts a DCC download. + * + * @param dccSendReceived + * The DCC SEND event + */ + @Subscribe + public void dccSendReceived(final DccSendReceived dccSendReceived) { + final Optional network = getNetwork(dccSendReceived.connection()); + if (!network.isPresent()) { + return; + } + + 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()) { + 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.removeAll(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(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(final DccAcceptReceived dccAcceptReceived) { + final Optional network = getNetwork(dccAcceptReceived.connection()); + if (!network.isPresent()) { + return; + } + + 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.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.removeAll(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) { + } + } + + /** + * 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) { + + /* 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(); + File file = new File(download.filename()); + file.renameTo(new File(finalDirectory, download.pack().name())); + eventBus.post(new DownloadFinished(download)); + dccReceivers.remove(dccDownloadFinished.dccReceiver()); + downloads.removeAll(download.pack().name()); + } catch (IOException ioe1) { + /* TODO - handle all the errors. */ + logger.warn(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) { + + /* 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.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())); } // @@ -207,6 +1077,28 @@ public class Core extends AbstractIdleService { // /** + * 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. * @@ -225,6 +1117,44 @@ public class Core extends AbstractIdleService { } /** + * Returns the configured 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 configured channel, or {@link Optional#absent()} if no + * configured channel matching the given network and name was found + */ + public Optional getChannel(Network network, String channelName) { + for (Channel channel : channels) { + 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); + } + } + return Optional.absent(); + } + + /** * Parses {@link Pack} information from the given message. * * @param message