com.techcavern.pircbotz.InputParser.java Source code

Java tutorial

Introduction

Here is the source code for com.techcavern.pircbotz.InputParser.java

Source

/**
 * Copyright (C) 2014 Julian Zhou <jzhou at techcavern.com>
 *
 * This file is part of PircBotZ.
 *
 * PircBotZ is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PircBotZ is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PircBotZ. If not, see <http://www.gnu.org/licenses/>.
 */
package com.techcavern.pircbotz;

import com.techcavern.pircbotz.snapshot.UserSnapshot;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.collect.PeekingIterator;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import static com.techcavern.pircbotz.ReplyConstants.*;
import com.techcavern.pircbotz.cap.CapHandler;
import com.techcavern.pircbotz.cap.TLSCapHandler;
import com.techcavern.pircbotz.exception.IrcException;
import com.techcavern.pircbotz.hooks.events.ActionEvent;
import com.techcavern.pircbotz.hooks.events.ChannelInfoEvent;
import com.techcavern.pircbotz.hooks.events.ConnectEvent;
import com.techcavern.pircbotz.hooks.events.FingerEvent;
import com.techcavern.pircbotz.hooks.events.HalfOpEvent;
import com.techcavern.pircbotz.hooks.events.InviteEvent;
import com.techcavern.pircbotz.hooks.events.JoinEvent;
import com.techcavern.pircbotz.hooks.events.KickEvent;
import com.techcavern.pircbotz.hooks.events.MessageEvent;
import com.techcavern.pircbotz.hooks.events.ModeEvent;
import com.techcavern.pircbotz.hooks.events.MotdEvent;
import com.techcavern.pircbotz.hooks.events.NickAlreadyInUseEvent;
import com.techcavern.pircbotz.hooks.events.NickChangeEvent;
import com.techcavern.pircbotz.hooks.events.NoticeEvent;
import com.techcavern.pircbotz.hooks.events.OpEvent;
import com.techcavern.pircbotz.hooks.events.OwnerEvent;
import com.techcavern.pircbotz.hooks.events.PartEvent;
import com.techcavern.pircbotz.hooks.events.PingEvent;
import com.techcavern.pircbotz.hooks.events.PrivateMessageEvent;
import com.techcavern.pircbotz.hooks.events.QuitEvent;
import com.techcavern.pircbotz.hooks.events.RemoveChannelBanEvent;
import com.techcavern.pircbotz.hooks.events.RemoveChannelKeyEvent;
import com.techcavern.pircbotz.hooks.events.RemoveChannelLimitEvent;
import com.techcavern.pircbotz.hooks.events.RemoveInviteOnlyEvent;
import com.techcavern.pircbotz.hooks.events.RemoveModeratedEvent;
import com.techcavern.pircbotz.hooks.events.RemoveNoExternalMessagesEvent;
import com.techcavern.pircbotz.hooks.events.RemovePrivateEvent;
import com.techcavern.pircbotz.hooks.events.RemoveSecretEvent;
import com.techcavern.pircbotz.hooks.events.RemoveTopicProtectionEvent;
import com.techcavern.pircbotz.hooks.events.ServerPingEvent;
import com.techcavern.pircbotz.hooks.events.ServerResponseEvent;
import com.techcavern.pircbotz.hooks.events.SetChannelBanEvent;
import com.techcavern.pircbotz.hooks.events.SetChannelKeyEvent;
import com.techcavern.pircbotz.hooks.events.SetChannelLimitEvent;
import com.techcavern.pircbotz.hooks.events.SetInviteOnlyEvent;
import com.techcavern.pircbotz.hooks.events.SetModeratedEvent;
import com.techcavern.pircbotz.hooks.events.SetNoExternalMessagesEvent;
import com.techcavern.pircbotz.hooks.events.SetPrivateEvent;
import com.techcavern.pircbotz.hooks.events.SetSecretEvent;
import com.techcavern.pircbotz.hooks.events.SetTopicProtectionEvent;
import com.techcavern.pircbotz.hooks.events.SuperOpEvent;
import com.techcavern.pircbotz.hooks.events.TimeEvent;
import com.techcavern.pircbotz.hooks.events.TopicEvent;
import com.techcavern.pircbotz.hooks.events.UnknownEvent;
import com.techcavern.pircbotz.hooks.events.UserListEvent;
import com.techcavern.pircbotz.hooks.events.UserModeEvent;
import com.techcavern.pircbotz.hooks.events.VersionEvent;
import com.techcavern.pircbotz.hooks.events.VoiceEvent;
import com.techcavern.pircbotz.hooks.events.WhoisEvent;
import com.techcavern.pircbotz.snapshot.ChannelSnapshot;
import com.techcavern.pircbotz.snapshot.UserChannelDaoSnapshot;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

/**
 * Parse received input from IRC server.
 * @author Originally by:
 * <a href="http://pircbotx.googlecode.com">Leon Blakey <lord.quackstar at gmail.com> in PircBotX</a>
 * <p>Forked and Maintained by Julian Zhou <jzhou at techcavern.com> in <a href="https://github.com/TechCavern/PircBotZ">PircBotZ</a> */
@RequiredArgsConstructor
@Slf4j
public class InputParser implements Closeable {
    public static final Marker INPUT_MARKER = MarkerFactory.getMarker("pircbotx.input");
    /**
     * Codes that say we are connected: Initial connection (001-4), user stats (251-5), or MOTD (375-6).
     */
    protected static final ImmutableList<String> CONNECT_CODES = ImmutableList.of("001", "002", "003", "004", "005",
            "251", "252", "253", "254", "255", "375", "376");
    protected static final ImmutableList<ChannelModeHandler> DEFAULT_CHANNEL_MODE_HANDLERS;

    static {
        DEFAULT_CHANNEL_MODE_HANDLERS = ImmutableList.<ChannelModeHandler>builder()
                .add(new OpChannelModeHandler('o', UserLevel.OP) {
                    @Override
                    public void dispatchEvent(PircBotZ bot, Channel channel, User sourceUser, User recipientUser,
                            boolean adding) {
                        Utils.dispatchEvent(bot,
                                new OpEvent<PircBotZ>(bot, channel, sourceUser, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('v', UserLevel.VOICE) {
                    @Override
                    public void dispatchEvent(PircBotZ bot, Channel channel, User sourceUser, User recipientUser,
                            boolean adding) {
                        Utils.dispatchEvent(bot,
                                new VoiceEvent<PircBotZ>(bot, channel, sourceUser, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('h', UserLevel.HALFOP) {
                    @Override
                    public void dispatchEvent(PircBotZ bot, Channel channel, User sourceUser, User recipientUser,
                            boolean adding) {
                        Utils.dispatchEvent(bot,
                                new HalfOpEvent<PircBotZ>(bot, channel, sourceUser, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('a', UserLevel.SUPEROP) {
                    @Override
                    public void dispatchEvent(PircBotZ bot, Channel channel, User sourceUser, User recipientUser,
                            boolean adding) {
                        Utils.dispatchEvent(bot,
                                new SuperOpEvent<PircBotZ>(bot, channel, sourceUser, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('q', UserLevel.OWNER) {
                    @Override
                    public void dispatchEvent(PircBotZ bot, Channel channel, User sourceUser, User recipientUser,
                            boolean adding) {
                        Utils.dispatchEvent(bot,
                                new OwnerEvent<PircBotZ>(bot, channel, sourceUser, recipientUser, adding));
                    }
                }).add(new ChannelModeHandler('k') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        if (adding) {
                            String key = params.next();
                            channel.setChannelKey(key);
                            if (dispatchEvent)
                                Utils.dispatchEvent(bot,
                                        new SetChannelKeyEvent<PircBotZ>(bot, channel, sourceUser, key));
                        } else {
                            String key = params.hasNext() ? params.next() : null;
                            channel.setChannelKey(null);
                            if (dispatchEvent)
                                Utils.dispatchEvent(bot,
                                        new RemoveChannelKeyEvent<PircBotZ>(bot, channel, sourceUser, key));
                        }
                    }
                }).add(new ChannelModeHandler('l') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        if (adding) {
                            int limit = Integer.parseInt(params.next());
                            channel.setChannelLimit(limit);
                            if (dispatchEvent)
                                Utils.dispatchEvent(bot,
                                        new SetChannelLimitEvent<PircBotZ>(bot, channel, sourceUser, limit));
                        } else {
                            channel.setChannelLimit(-1);
                            if (dispatchEvent)
                                Utils.dispatchEvent(bot,
                                        new RemoveChannelLimitEvent<PircBotZ>(bot, channel, sourceUser));
                        }
                    }
                }).add(new ChannelModeHandler('b') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetChannelBanEvent<PircBotZ>(bot, channel, sourceUser, params.next()));
                            else
                                Utils.dispatchEvent(bot, new RemoveChannelBanEvent<PircBotZ>(bot, channel,
                                        sourceUser, params.next()));
                    }
                }).add(new ChannelModeHandler('t') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        channel.setTopicProtection(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetTopicProtectionEvent<PircBotZ>(bot, channel, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveTopicProtectionEvent<PircBotZ>(bot, channel, sourceUser));
                    }
                }).add(new ChannelModeHandler('n') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        channel.setNoExternalMessages(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetNoExternalMessagesEvent<PircBotZ>(bot, channel, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveNoExternalMessagesEvent<PircBotZ>(bot, channel, sourceUser));
                    }
                }).add(new ChannelModeHandler('i') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        channel.setInviteOnly(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetInviteOnlyEvent<PircBotZ>(bot, channel, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveInviteOnlyEvent<PircBotZ>(bot, channel, sourceUser));
                    }
                }).add(new ChannelModeHandler('m') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        channel.setModerated(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot, new SetModeratedEvent<PircBotZ>(bot, channel, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveModeratedEvent<PircBotZ>(bot, channel, sourceUser));
                    }
                }).add(new ChannelModeHandler('p') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        channel.setChannelPrivate(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot, new SetPrivateEvent<PircBotZ>(bot, channel, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemovePrivateEvent<PircBotZ>(bot, channel, sourceUser));
                    }
                }).add(new ChannelModeHandler('s') {
                    @Override
                    public void handleMode(PircBotZ bot, Channel channel, User sourceUser,
                            PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
                        channel.setSecret(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot, new SetSecretEvent<PircBotZ>(bot, channel, sourceUser));
                            else
                                Utils.dispatchEvent(bot, new RemoveSecretEvent<PircBotZ>(bot, channel, sourceUser));
                    }
                }).build();
    }
    protected final Configuration<PircBotZ> configuration;
    protected final PircBotZ bot;
    protected final List<CapHandler> capHandlersFinished = new ArrayList<CapHandler>();
    protected boolean capEndSent = false;
    protected BufferedReader inputReader;
    //Builders
    /**
     * Map to keep active WhoisEvents. Must be a treemap to be case insensitive
     */
    protected final Map<String, WhoisEvent.Builder<PircBotZ>> whoisBuilder = Maps
            .newTreeMap(String.CASE_INSENSITIVE_ORDER);
    protected StringBuilder motdBuilder;
    @Getter
    protected boolean channelListRunning = false;
    protected ImmutableList.Builder<ChannelListEntry> channelListBuilder;
    protected int nickSuffix = 0;

    public InputParser(PircBotZ bot) {
        this.bot = bot;
        this.configuration = bot.getConfiguration();
    }

    /**
     * This method handles events when any line of text arrives from the server,
     * then dispatching the appropriate event.
     *
     * @param line The raw line of text from the server.
     */
    public void handleLine(@NonNull String line) throws IOException, IrcException {
        log.info(INPUT_MARKER, line);

        List<String> parsedLine = Utils.tokenizeLine(line);

        String senderInfo = "";
        if (parsedLine.get(0).charAt(0) == ':')
            senderInfo = parsedLine.remove(0);

        String command = parsedLine.remove(0).toUpperCase(configuration.getLocale());

        // Check for server pings.
        if (command.equals("PING")) {
            // Respond to the ping and return immediately.
            configuration.getListenerManager().dispatchEvent(new ServerPingEvent<PircBotZ>(bot, parsedLine.get(0)));
            return;
        } else if (command.startsWith("ERROR")) {
            //Server is shutting us down
            bot.shutdown(true);
            return;
        }

        String sourceNick;
        String sourceLogin = "";
        String sourceHostname = "";
        String target = !parsedLine.isEmpty() ? parsedLine.get(0) : "";

        if (target.startsWith(":"))
            target = target.substring(1);

        int exclamation = senderInfo.indexOf('!');
        int at = senderInfo.indexOf('@');
        if (senderInfo.startsWith(":"))
            if (exclamation > 0 && at > 0 && exclamation < at) {
                sourceNick = senderInfo.substring(1, exclamation);
                sourceLogin = senderInfo.substring(exclamation + 1, at);
                sourceHostname = senderInfo.substring(at + 1);
            } else {
                int code = Utils.tryParseInt(command, -1);
                if (code != -1) {
                    if (!bot.loggedIn)
                        processConnect(line, command, target, parsedLine);
                    processServerResponse(code, line, parsedLine);
                    // Return from the method.
                    return;
                } else
                    // This is not a server response.
                    // It must be a nick without login and hostname.
                    // (or maybe a NOTICE or suchlike from the server)
                    //WARNING: Changed from origional PircBot. Instead of command as target, use channel/user (setup later)
                    sourceNick = senderInfo;
            }
        else {
            // We don't know what this line means.
            configuration.getListenerManager().dispatchEvent(new UnknownEvent<PircBotZ>(bot, line));
            if (!bot.loggedIn)
                //Pass to CapHandlers, could be important
                for (CapHandler curCapHandler : configuration.getCapHandlers())
                    if (curCapHandler.handleUnknown(bot, line))
                        capHandlersFinished.add(curCapHandler);
            // Return from the method;
            return;
        }

        if (sourceNick.startsWith(":"))
            sourceNick = sourceNick.substring(1);

        if (!bot.loggedIn)
            processConnect(line, command, target, parsedLine);
        processCommand(target, sourceNick, sourceLogin, sourceHostname, command, line, parsedLine);
    }

    /**
     * Process any lines relevant to connect. Only called before bot is logged into the server
     * @param rawLine Raw, unprocessed line from the server
     * @param code 
     * @param target
     * @param parsedLine Processed line
     * @throws IrcException If the server rejects the bot (nick already in use or a 4** or 5** code
     * @throws IOException If an error occurs during upgrading to SSL
     */
    public void processConnect(String rawLine, String code, String target, List<String> parsedLine)
            throws IrcException, IOException {
        if (CONNECT_CODES.contains(code)) {
            // We're connected to the server.
            bot.loggedIn(configuration.getName() + (nickSuffix == 0 ? "" : nickSuffix));
            log.debug("Logged onto server.");

            configuration.getListenerManager().dispatchEvent(new ConnectEvent<PircBotZ>(bot));

            //Handle automatic on connect stuff
            if (configuration.getNickservPassword() != null)
                bot.sendIRC().identify(configuration.getNickservPassword());
            ImmutableMap<String, String> autoConnectChannels = bot.reconnectChannels();
            if (autoConnectChannels == null)
                autoConnectChannels = configuration.getAutoJoinChannels();
            for (Map.Entry<String, String> channelEntry : autoConnectChannels.entrySet())
                bot.sendIRC().joinChannel(channelEntry.getKey(), channelEntry.getValue());
        } else if (code.equals("433")) {
            //EXAMPLE: * AnAlreadyUsedName :Nickname already in use
            //Nickname in use, rename
            String usedNick = parsedLine.get(1);
            boolean autoNickChange = configuration.isAutoNickChange();
            String autoNewNick = null;
            if (autoNickChange) {
                nickSuffix++;
                bot.sendIRC().changeNick(autoNewNick = configuration.getName() + nickSuffix);
            }
            configuration.getListenerManager()
                    .dispatchEvent(new NickAlreadyInUseEvent<PircBotZ>(bot, usedNick, autoNewNick, autoNickChange));
        } else if (code.equals("439")) {
            //EXAMPLE: PircBotX: Target change too fast. Please wait 104 seconds
            // No action required.
            //TODO: Should we delay joining channels here or something?
            log.warn("Ignoring too fast error");
        } else if (configuration.isCapEnabled() && code.equals("421") && parsedLine.get(1).equals("CAP")) {
            //EXAMPLE: 421 you CAP :Unknown command
            log.warn("Ignoring unknown command error, server does not support CAP negotiation");
        } else if (configuration.isCapEnabled() && code.equals("451") && target.equals("CAP")) {
            //EXAMPLE: 451 CAP :You have not registered
            //Ignore, this is from servers that don't support CAP
            log.warn("Ignoring not registered error, server does not support CAP negotiation");
        } else if (code.startsWith("5") || code.startsWith("4"))
            throw new IrcException(IrcException.Reason.CannotLogin, "Received error: " + rawLine);
        else if (code.equals("670")) {
            //Server is saying that we can upgrade to TLS
            SSLSocketFactory sslSocketFactory = ((SSLSocketFactory) SSLSocketFactory.getDefault());
            for (CapHandler curCapHandler : configuration.getCapHandlers())
                if (curCapHandler instanceof TLSCapHandler)
                    sslSocketFactory = ((TLSCapHandler) curCapHandler).getSslSocketFactory();
            SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(bot.getSocket(),
                    bot.getLocalAddress().getHostAddress(), bot.getSocket().getPort(), true);
            sslSocket.startHandshake();
            bot.changeSocket(sslSocket);
            //Notify CAP Handlers
            for (CapHandler curCapHandler : configuration.getCapHandlers())
                curCapHandler.handleUnknown(bot, rawLine);
        } else if (code.equals("CAP")) {
            //Handle CAP Code; remove extra from params
            String capCommand = parsedLine.get(1);
            ImmutableList<String> capParams = ImmutableList.copyOf(StringUtils.split(parsedLine.get(2)));
            if (capCommand.equals("LS"))
                for (CapHandler curCapHandler : configuration.getCapHandlers()) {
                    log.debug("Executing cap handler " + curCapHandler);
                    if (curCapHandler.handleLS(bot, capParams)) {
                        log.debug("Cap handler " + curCapHandler + " finished");
                        capHandlersFinished.add(curCapHandler);
                    }
                }
            else if (capCommand.equals("ACK")) {
                //Server is enabling a capability, store that
                bot.getEnabledCapabilities().addAll(capParams);

                for (CapHandler curCapHandler : configuration.getCapHandlers())
                    if (curCapHandler.handleACK(bot, capParams)) {
                        log.trace("Removing cap handler " + curCapHandler);
                        capHandlersFinished.add(curCapHandler);
                    }
            } else if (capCommand.equals("NAK")) {
                for (CapHandler curCapHandler : configuration.getCapHandlers())
                    if (curCapHandler.handleNAK(bot, capParams))
                        capHandlersFinished.add(curCapHandler);
            } else
                //Maybe the CapHandlers know how to use it
                for (CapHandler curCapHandler : configuration.getCapHandlers())
                    if (curCapHandler.handleUnknown(bot, rawLine))
                        capHandlersFinished.add(curCapHandler);
        } else
            //Pass to CapHandlers, could be important
            for (CapHandler curCapHandler : configuration.getCapHandlers())
                if (curCapHandler.handleUnknown(bot, rawLine))
                    capHandlersFinished.add(curCapHandler);

        //Send CAP END if all CapHandlers are finished
        if (configuration.isCapEnabled() && !capEndSent
                && capHandlersFinished.containsAll(configuration.getCapHandlers())) {
            capEndSent = true;
            bot.sendCAP().end();
            bot.enabledCapabilities = Collections.unmodifiableList(bot.enabledCapabilities);
        }
    }

    public void processCommand(String target, String sourceNick, String sourceLogin, String sourceHostname,
            String command, String line, List<String> parsedLine) throws IOException {
        User source = bot.getUserChannelDao().getUser(sourceNick, sourceHostname, sourceLogin);
        //If the channel matches a prefix, then its a channel
        Channel channel = (target.length() != 0 && configuration.getChannelPrefixes().indexOf(target.charAt(0)) >= 0
                && bot.getUserChannelDao().channelExists(target)) ? bot.getUserChannelDao().getChannel(target)
                        : null;
        String message = parsedLine.size() >= 2 ? parsedLine.get(1) : "";

        // Check for CTCP requests.
        if (command.equals("PRIVMSG") && message.startsWith("\u0001") && message.endsWith("\u0001")) {
            String request = message.substring(1, message.length() - 1);
            if (request.equals("VERSION"))
                // VERSION request
                configuration.getListenerManager().dispatchEvent(new VersionEvent<PircBotZ>(bot, source, channel));
            else if (request.startsWith("ACTION "))
                // ACTION request
                configuration.getListenerManager()
                        .dispatchEvent(new ActionEvent<PircBotZ>(bot, source, channel, request.substring(7)));
            else if (request.startsWith("PING "))
                // PING request
                configuration.getListenerManager()
                        .dispatchEvent(new PingEvent<PircBotZ>(bot, source, channel, request.substring(5)));
            else if (request.equals("TIME"))
                // TIME request
                configuration.getListenerManager().dispatchEvent(new TimeEvent<PircBotZ>(bot, channel, source));
            else if (request.equals("FINGER"))
                // FINGER request
                configuration.getListenerManager().dispatchEvent(new FingerEvent<PircBotZ>(bot, source, channel));
            else if (request.startsWith("DCC ")) {
                // This is a DCC request.
                boolean success = bot.getDccHandler().processDcc(source, request);
                if (!success)
                    // The DccManager didn't know what to do with the line.
                    configuration.getListenerManager().dispatchEvent(new UnknownEvent<PircBotZ>(bot, line));
            } else
                // An unknown CTCP message - ignore it.
                configuration.getListenerManager().dispatchEvent(new UnknownEvent<PircBotZ>(bot, line));
        } else if (command.equals("PRIVMSG") && channel != null)
            // This is a normal message to a channel.
            configuration.getListenerManager()
                    .dispatchEvent(new MessageEvent<PircBotZ>(bot, channel, source, message));
        else if (command.equals("PRIVMSG")) {
            // This is a private message to us.
            //Add to private message
            bot.getUserChannelDao().addUserToPrivate(source);
            configuration.getListenerManager()
                    .dispatchEvent(new PrivateMessageEvent<PircBotZ>(bot, source, message));
        } else if (command.equals("JOIN")) {
            // Someone is joining a channel.
            if (sourceNick.equalsIgnoreCase(bot.getNick())) {
                //Its us, get channel info
                channel = bot.getUserChannelDao().createChannel(target);
                bot.sendRaw().rawLine("WHO " + target);
                bot.sendRaw().rawLine("MODE " + target);
            }
            bot.getUserChannelDao().addUserToChannel(source, channel);
            configuration.getListenerManager().dispatchEvent(new JoinEvent<PircBotZ>(bot, channel, source));
        } else if (command.equals("PART")) {
            // Someone is parting from a channel.
            UserChannelDaoSnapshot daoSnapshot = bot.getUserChannelDao().createSnapshot();
            ChannelSnapshot channelSnapshot = daoSnapshot.getChannel(channel.getName());
            UserSnapshot sourceSnapshot = daoSnapshot.getUser(source.getNick(), source.getHostmask(),
                    source.getLogin());
            if (sourceNick.equals(bot.getNick()))
                //We parted the channel
                bot.getUserChannelDao().removeChannel(channel);
            else
                //Just remove the user from memory
                bot.getUserChannelDao().removeUserFromChannel(source, channel);
            configuration.getListenerManager().dispatchEvent(
                    new PartEvent<PircBotZ>(bot, daoSnapshot, channelSnapshot, sourceSnapshot, message));
        } else if (command.equals("NICK")) {
            // Somebody is changing their nick.
            String newNick = target;
            bot.getUserChannelDao().renameUser(source, newNick);
            if (sourceNick.equals(bot.getNick()))
                // Update our nick if it was us that changed nick.
                bot.setNick(newNick);
            configuration.getListenerManager()
                    .dispatchEvent(new NickChangeEvent<PircBotZ>(bot, sourceNick, newNick, source));
        } else if (command.equals("NOTICE"))
            // Someone is sending a notice.
            configuration.getListenerManager()
                    .dispatchEvent(new NoticeEvent<PircBotZ>(bot, source, channel, message));
        else if (command.equals("QUIT")) {
            UserChannelDaoSnapshot daoSnapshot = bot.getUserChannelDao().createSnapshot();
            UserSnapshot sourceSnapshot = daoSnapshot.getUser(source.getNick(), source.getHostmask(),
                    source.getLogin());
            //A real target is missing, so index is off
            String reason = target;
            // Someone has quit from the IRC server.
            if (!sourceNick.equals(bot.getNick()))
                //Someone else
                bot.getUserChannelDao().removeUser(source);
            configuration.getListenerManager()
                    .dispatchEvent(new QuitEvent<PircBotZ>(bot, daoSnapshot, sourceSnapshot, reason));
        } else if (command.equals("KICK")) {
            // Somebody has been kicked from a channel.
            User recipient = bot.getUserChannelDao().getUser(message);
            if (recipient.getNick().equals(bot.getNick()))
                //We were just kicked
                bot.getUserChannelDao().removeChannel(channel);
            else
                //Someone else
                bot.getUserChannelDao().removeUserFromChannel(recipient, channel);
            configuration.getListenerManager()
                    .dispatchEvent(new KickEvent<PircBotZ>(bot, channel, source, recipient, parsedLine.get(2)));
        } else if (command.equals("MODE")) {
            // Somebody is changing the mode on a channel or user (Use long form since mode isn't after a : )
            String mode = line.substring(line.indexOf(target, 2) + target.length() + 1);
            if (mode.startsWith(":"))
                mode = mode.substring(1);
            processMode(source, target, mode);
        } else if (command.equals("TOPIC")) {
            // Someone is changing the topic.
            long currentTime = System.currentTimeMillis();
            String oldTopic = channel.getTopic();
            channel.setTopic(message);
            channel.setTopicSetter(sourceNick);
            channel.setTopicTimestamp(currentTime);

            configuration.getListenerManager().dispatchEvent(
                    new TopicEvent<PircBotZ>(bot, channel, oldTopic, message, source, currentTime, true));
        } else if (command.equals("INVITE")) {
            // Somebody is inviting somebody else into a channel.
            //Use line method instead of channel since channel is wrong
            configuration.getListenerManager().dispatchEvent(new InviteEvent<PircBotZ>(bot, sourceNick, message));
            if (bot.getUserChannelDao().getChannels(source).isEmpty())
                bot.getUserChannelDao().removeUser(source);
        } else if (command.equals("AWAY"))
            //IRCv3 AWAY notify
            if (parsedLine.isEmpty())
                source.setAwayMessage("");
            else
                source.setAwayMessage(parsedLine.get(0));
        else
            // If we reach this point, then we've found something that the PircBotX
            // Doesn't currently deal with.
            configuration.getListenerManager().dispatchEvent(new UnknownEvent<PircBotZ>(bot, line));
    }

    /**
     * This method is called by the PircBotX when a numeric response
     * is received from the IRC server. We use this method to
     * allow PircBotX to process various responses from the server
     * before then passing them on to the onServerResponse method.
     * <p>
     * Note that this method is private and should not appear in any
     * of the javadoc generated documentation.
     *
     * @param code The three-digit numerical code for the response.
     * @param response The full response from the IRC server.
     */
    public void processServerResponse(int code, String rawResponse, List<String> parsedResponseOrig) {
        ImmutableList<String> parsedResponse = ImmutableList.copyOf(parsedResponseOrig);
        //Parsed response format: Everything after code
        //eg: Response 321 Channel :Users Name gives us [Channel, Users Name]
        if (code == RPL_LISTSTART) {
            //EXAMPLE: 321 Channel :Users Name (actual text)
            //A channel list is about to be sent
            channelListBuilder = ImmutableList.builder();
            channelListRunning = true;
        } else if (code == RPL_LIST) {
            //This is part of a full channel listing as part of /LIST
            //EXAMPLE: 322 lordquackstar #xomb 12 :xomb exokernel project @ www.xomb.org
            String channel = parsedResponse.get(1);
            int userCount = Utils.tryParseInt(parsedResponse.get(2), -1);
            String topic = parsedResponse.get(3);
            channelListBuilder.add(new ChannelListEntry(channel, userCount, topic));
        } else if (code == RPL_LISTEND) {
            //EXAMPLE: 323 :End of /LIST
            //End of channel list, dispatch event
            configuration.getListenerManager()
                    .dispatchEvent(new ChannelInfoEvent<PircBotZ>(bot, channelListBuilder.build()));
            channelListBuilder = null;
            channelListRunning = false;
        } else if (code == RPL_TOPIC) {
            //EXAMPLE: 332 PircBotX #aChannel :I'm some random topic
            //This is topic about a channel we've just joined. From /JOIN or /TOPIC
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));
            String topic = parsedResponse.get(2);

            channel.setTopic(topic);
        } else if (code == RPL_TOPICINFO) {
            //EXAMPLE: 333 PircBotX #aChannel ISetTopic 1564842512
            //This is information on the topic of the channel we've just joined. From /JOIN or /TOPIC
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));
            User setBy = bot.getUserChannelDao().getUser(parsedResponse.get(2));
            long date = Utils.tryParseLong(parsedResponse.get(3), -1);

            channel.setTopicTimestamp(date * 1000);
            channel.setTopicSetter(setBy.getNick());

            configuration.getListenerManager().dispatchEvent(
                    new TopicEvent<PircBotZ>(bot, channel, null, channel.getTopic(), setBy, date, false));
        } else if (code == RPL_WHOREPLY) {
            //EXAMPLE: 352 PircBotX #aChannel ~someName 74.56.56.56.my.Hostmask wolfe.freenode.net someNick H :0 Full Name
            //Part of a WHO reply on information on individual users
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));

            //Setup user
            User curUser = bot.getUserChannelDao().createUser(parsedResponse.get(5));
            curUser.setLogin(parsedResponse.get(2));
            curUser.setHostmask(parsedResponse.get(3));
            curUser.setServer(parsedResponse.get(4));
            curUser.setNick(parsedResponse.get(5));
            processUserStatus(channel, curUser, parsedResponse.get(6));
            //Extra parsing needed since tokenizer stopped at :
            String rawEnding = parsedResponse.get(7);
            int rawEndingSpaceIndex = rawEnding.indexOf(' ');
            if (rawEndingSpaceIndex == -1) {
                //parsedResponse data is trimmed, so if the index == -1, then there was no real name given and the space separating hops from real name was trimmed.
                curUser.setHops(Integer.parseInt(rawEnding));
                curUser.setRealName("");
            } else {
                //parsedResponse data contains a real name
                curUser.setHops(Integer.parseInt(rawEnding.substring(0, rawEndingSpaceIndex)));
                curUser.setRealName(rawEnding.substring(rawEndingSpaceIndex + 1));
            }

            //Associate with channel
            bot.getUserChannelDao().addUserToChannel(curUser, channel);
        } else if (code == RPL_ENDOFWHO) {
            //EXAMPLE: 315 PircBotX #aChannel :End of /WHO list
            //End of the WHO reply
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));
            configuration.getListenerManager().dispatchEvent(
                    new UserListEvent<PircBotZ>(bot, channel, bot.getUserChannelDao().getUsers(channel)));
        } else if (code == RPL_CHANNELMODEIS) {
            //EXAMPLE: 324 PircBotX #aChannel +cnt
            //Full channel mode (In response to MODE <channel>)
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));
            ImmutableList<String> modeParsed = parsedResponse.subList(2, parsedResponse.size());
            String mode = StringUtils.join(modeParsed, ' ');

            channel.setMode(mode, modeParsed);
            configuration.getListenerManager()
                    .dispatchEvent(new ModeEvent<PircBotZ>(bot, channel, null, mode, modeParsed));
        } else if (code == 329) {
            //EXAMPLE: 329 lordquackstar #botters 1199140245
            //Tells when channel was created. From /JOIN
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));
            int createDate = Utils.tryParseInt(parsedResponse.get(2), -1);

            //Set in channel
            channel.setCreateTimestamp(createDate);
        } else if (code == RPL_MOTDSTART)
            //Example: 375 PircBotX :- wolfe.freenode.net Message of the Day -
            //Motd is starting, reset the StringBuilder
            motdBuilder = new StringBuilder();
        else if (code == RPL_MOTD)
            //Example: 372 PircBotX :- Welcome to wolfe.freenode.net in Manchester, England, Uk!  Thanks to
            //This is part of the MOTD, add a new line
            motdBuilder.append(CharMatcher.WHITESPACE.trimFrom(parsedResponse.get(1).substring(1))).append("\n");
        else if (code == RPL_ENDOFMOTD) {
            //Example: PircBotX :End of /MOTD command.
            //End of MOTD, clean it and dispatch MotdEvent
            ServerInfo serverInfo = bot.getServerInfo();
            serverInfo.setMotd(motdBuilder.toString().trim());
            motdBuilder = null;
            configuration.getListenerManager().dispatchEvent(new MotdEvent<PircBotZ>(bot, serverInfo.getMotd()));
        } else if (code == 4 || code == 5) {
            //Example: 004 PircBotX sendak.freenode.net ircd-seven-1.1.3 DOQRSZaghilopswz CFILMPQbcefgijklmnopqrstvz bkloveqjfI
            //Server info line, remove ending comment and let ServerInfo class parse it
            int endCommentIndex = rawResponse.lastIndexOf(" :");
            if (endCommentIndex > 1) {
                String endComment = rawResponse.substring(endCommentIndex + 2);
                int lastIndex = parsedResponseOrig.size() - 1;
                if (endComment.equals(parsedResponseOrig.get(lastIndex)))
                    parsedResponseOrig.remove(lastIndex);
            }
            bot.getServerInfo().parse(code, parsedResponseOrig);
        } else if (code == RPL_WHOISUSER) {
            //Example: 311 TheLQ Plazma ~Plazma freenode/staff/plazma * :Plazma Rooolz!
            //New whois is starting
            String whoisNick = parsedResponse.get(1);

            WhoisEvent.Builder<PircBotZ> builder = new WhoisEvent.Builder<PircBotZ>();
            builder.setNick(whoisNick);
            builder.setLogin(parsedResponse.get(2));
            builder.setHostname(parsedResponse.get(3));
            builder.setRealname(parsedResponse.get(5));
            whoisBuilder.put(whoisNick, builder);
        } else if (code == RPL_AWAY)
            //Example: 301 PircBotXUser TheLQ_ :I'm away, sorry
            bot.getUserChannelDao().getUser(parsedResponse.get(1)).setAwayMessage(parsedResponse.get(2));
        else if (code == RPL_WHOISCHANNELS) {
            //Example: 319 TheLQ Plazma :+#freenode
            //Channel list from whois. Re-tokenize since they're after the :
            String whoisNick = parsedResponse.get(1);
            ImmutableList<String> parsedChannels = ImmutableList.copyOf(Utils.tokenizeLine(parsedResponse.get(2)));

            whoisBuilder.get(whoisNick).setChannels(parsedChannels);
        } else if (code == RPL_WHOISSERVER) {
            //Server info from whois
            //312 TheLQ Plazma leguin.freenode.net :Ume?, SE, EU
            String whoisNick = parsedResponse.get(1);

            whoisBuilder.get(whoisNick).setServer(parsedResponse.get(2));
            whoisBuilder.get(whoisNick).setServerInfo(parsedResponse.get(3));
        } else if (code == RPL_WHOISIDLE) {
            //Idle time from whois
            //317 TheLQ md_5 6077 1347373349 :seconds idle, signon time
            String whoisNick = parsedResponse.get(1);

            whoisBuilder.get(whoisNick).setIdleSeconds(Long.parseLong(parsedResponse.get(2)));
            whoisBuilder.get(whoisNick).setSignOnTime(Long.parseLong(parsedResponse.get(3)));
        } else if (code == 330) {
            //RPL_WHOISACCOUNT: Extra Whois info
            //330 TheLQ Utoxin Utoxin :is logged in as
            //Make sure we set registered as to the nick, not to the note after the colon
            String registeredNick = "";
            if (!rawResponse.endsWith(":" + parsedResponse.get(2)))
                registeredNick = parsedResponse.get(2);
            whoisBuilder.get(parsedResponse.get(1)).setRegisteredAs(registeredNick);
        } else if (code == 307) {
            //If shown, tells us that the user is registered with nickserv
            //307 TheLQ TheLQ-PircBotX :has identified for this nick
            whoisBuilder.get(parsedResponse.get(1)).setRegisteredAs("");
        } else if (code == RPL_ENDOFWHOIS) {
            //End of whois
            //318 TheLQ Plazma :End of /WHOIS list.
            String whoisNick = parsedResponse.get(1);
            try {
                configuration.getListenerManager().dispatchEvent(whoisBuilder.get(whoisNick).generateEvent(bot));
                whoisBuilder.remove(whoisNick);
            } catch (NullPointerException e) {
                WhoisEvent.Builder<PircBotZ> builder = new WhoisEvent.Builder<PircBotZ>();
                configuration.getListenerManager().dispatchEvent(builder.generateEvent(bot));
            }
        }
        configuration.getListenerManager()
                .dispatchEvent(new ServerResponseEvent<PircBotZ>(bot, code, rawResponse, parsedResponse));
    }

    /**
     * Called when the mode of a channel is set. We process this in
     * order to call the appropriate onOp, onDeop, etc method before
     * finally calling the override-able onMode method.
     * <p>
     * Note that this method is private and is not intended to appear
     * in the javadoc generated documentation.
     *
     * @param target The channel or nick that the mode operation applies to.
     * @param sourceNick The nick of the user that set the mode.
     * @param sourceLogin The login of the user that set the mode.
     * @param sourceHostname The hostname of the user that set the mode.
     * @param mode The mode that has been set.
     */
    public void processMode(User user, String target, String mode) {
        if (configuration.getChannelPrefixes().indexOf(target.charAt(0)) >= 0) {
            // The mode of a channel is being changed.
            Channel channel = bot.getUserChannelDao().getChannel(target);
            channel.parseMode(mode);
            ImmutableList<String> modeParsed = ImmutableList.copyOf(StringUtils.split(mode, ' '));
            PeekingIterator<String> params = Iterators.peekingIterator(modeParsed.iterator());

            //Process modes letter by letter, grabbing paramaters as needed
            boolean adding = true;
            String modeLetters = params.next();
            for (int i = 0; i < modeLetters.length(); i++) {
                char curModeChar = modeLetters.charAt(i);
                if (curModeChar == '+')
                    adding = true;
                else if (curModeChar == '-')
                    adding = false;
                else {
                    ChannelModeHandler modeHandler = configuration.getChannelModeHandlers().get(curModeChar);
                    if (modeHandler != null)
                        modeHandler.handleMode(bot, channel, user, params, adding, true);
                }
            }
            configuration.getListenerManager()
                    .dispatchEvent(new ModeEvent<PircBotZ>(bot, channel, user, mode, modeParsed));
        } else
            // The mode of a user is being changed.
            configuration.getListenerManager().dispatchEvent(
                    new UserModeEvent<PircBotZ>(bot, user, bot.getUserChannelDao().getUser(target), mode));
    }

    public void processUserStatus(Channel chan, User user, String prefix) {
        if (prefix.contains("@"))
            bot.getUserChannelDao().addUserToLevel(UserLevel.OP, user, chan);
        if (prefix.contains("+"))
            bot.getUserChannelDao().addUserToLevel(UserLevel.VOICE, user, chan);
        if (prefix.contains("%"))
            bot.getUserChannelDao().addUserToLevel(UserLevel.HALFOP, user, chan);
        if (prefix.contains("~"))
            bot.getUserChannelDao().addUserToLevel(UserLevel.OWNER, user, chan);
        if (prefix.contains("&"))
            bot.getUserChannelDao().addUserToLevel(UserLevel.SUPEROP, user, chan);
        //Assume here (H) if there is no G
        user.setAwayMessage(prefix.contains("G") ? "" : null);
        user.setIrcop(prefix.contains("*"));
    }

    /**
     * Clear out builders.
     */
    public void close() {
        capEndSent = false;
        capHandlersFinished.clear();
        whoisBuilder.clear();
        motdBuilder = null;
        channelListRunning = false;
        channelListBuilder = null;
    }

    protected static abstract class OpChannelModeHandler extends ChannelModeHandler {
        protected final UserLevel level;

        public OpChannelModeHandler(char mode, UserLevel level) {
            super(mode);
            this.level = level;
        }

        @Override
        public void handleMode(PircBotZ bot, Channel channel, User sourceUser, PeekingIterator<String> params,
                boolean adding, boolean dispatchEvent) {
            User recipient = bot.getUserChannelDao().getUser(params.next());
            if (adding)
                bot.getUserChannelDao().addUserToLevel(level, recipient, channel);
            else
                bot.getUserChannelDao().removeUserFromLevel(level, recipient, channel);

            if (dispatchEvent)
                dispatchEvent(bot, channel, sourceUser, recipient, adding);
        }

        public abstract void dispatchEvent(PircBotZ bot, Channel channel, User sourceUser, User recipientUser,
                boolean adding);
    }
}