org.pircbotx.InputParser.java Source code

Java tutorial

Introduction

Here is the source code for org.pircbotx.InputParser.java

Source

/**
 * Copyright (C) 2010-2014 Leon Blakey <lord.quackstar at gmail.com>
 *
 * This file is part of PircBotX.
 *
 * PircBotX 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.
 *
 * PircBotX 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
 * PircBotX. If not, see <http://www.gnu.org/licenses/>.
 */
package org.pircbotx;

import org.pircbotx.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.LinkedListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.PeekingIterator;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
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 org.pircbotx.ReplyConstants.*;
import org.pircbotx.cap.CapHandler;
import org.pircbotx.cap.TLSCapHandler;
import org.pircbotx.exception.IrcException;
import org.pircbotx.hooks.events.ActionEvent;
import org.pircbotx.hooks.events.BanListEvent;
import org.pircbotx.hooks.events.ChannelInfoEvent;
import org.pircbotx.hooks.events.ConnectEvent;
import org.pircbotx.hooks.events.FingerEvent;
import org.pircbotx.hooks.events.HalfOpEvent;
import org.pircbotx.hooks.events.InviteEvent;
import org.pircbotx.hooks.events.JoinEvent;
import org.pircbotx.hooks.events.KickEvent;
import org.pircbotx.hooks.events.MessageEvent;
import org.pircbotx.hooks.events.ModeEvent;
import org.pircbotx.hooks.events.MotdEvent;
import org.pircbotx.hooks.events.NickAlreadyInUseEvent;
import org.pircbotx.hooks.events.NickChangeEvent;
import org.pircbotx.hooks.events.NoticeEvent;
import org.pircbotx.hooks.events.OpEvent;
import org.pircbotx.hooks.events.OwnerEvent;
import org.pircbotx.hooks.events.PartEvent;
import org.pircbotx.hooks.events.PingEvent;
import org.pircbotx.hooks.events.PrivateMessageEvent;
import org.pircbotx.hooks.events.QuitEvent;
import org.pircbotx.hooks.events.RemoveChannelBanEvent;
import org.pircbotx.hooks.events.RemoveChannelKeyEvent;
import org.pircbotx.hooks.events.RemoveChannelLimitEvent;
import org.pircbotx.hooks.events.RemoveInviteOnlyEvent;
import org.pircbotx.hooks.events.RemoveModeratedEvent;
import org.pircbotx.hooks.events.RemoveNoExternalMessagesEvent;
import org.pircbotx.hooks.events.RemovePrivateEvent;
import org.pircbotx.hooks.events.RemoveSecretEvent;
import org.pircbotx.hooks.events.RemoveTopicProtectionEvent;
import org.pircbotx.hooks.events.ServerPingEvent;
import org.pircbotx.hooks.events.ServerResponseEvent;
import org.pircbotx.hooks.events.SetChannelBanEvent;
import org.pircbotx.hooks.events.SetChannelKeyEvent;
import org.pircbotx.hooks.events.SetChannelLimitEvent;
import org.pircbotx.hooks.events.SetInviteOnlyEvent;
import org.pircbotx.hooks.events.SetModeratedEvent;
import org.pircbotx.hooks.events.SetNoExternalMessagesEvent;
import org.pircbotx.hooks.events.SetPrivateEvent;
import org.pircbotx.hooks.events.SetSecretEvent;
import org.pircbotx.hooks.events.SetTopicProtectionEvent;
import org.pircbotx.hooks.events.SuperOpEvent;
import org.pircbotx.hooks.events.TimeEvent;
import org.pircbotx.hooks.events.TopicEvent;
import org.pircbotx.hooks.events.UnknownEvent;
import org.pircbotx.hooks.events.UserListEvent;
import org.pircbotx.hooks.events.UserModeEvent;
import org.pircbotx.hooks.events.VersionEvent;
import org.pircbotx.hooks.events.VoiceEvent;
import org.pircbotx.hooks.events.WhoisEvent;
import org.pircbotx.snapshot.ChannelSnapshot;
import org.pircbotx.snapshot.UserChannelDaoSnapshot;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

/**
 * Parse received input from IRC server.
 *
 * @author Leon Blakey
 */
@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(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, UserHostmask recipientHostmask, User recipientUser, boolean adding) {
                        Utils.dispatchEvent(bot, new OpEvent(bot, channel, sourceHostmask, sourceUser,
                                recipientHostmask, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('v', UserLevel.VOICE) {
                    @Override
                    public void dispatchEvent(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, UserHostmask recipientHostmask, User recipientUser, boolean adding) {
                        Utils.dispatchEvent(bot, new VoiceEvent(bot, channel, sourceHostmask, sourceUser,
                                recipientHostmask, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('h', UserLevel.HALFOP) {
                    @Override
                    public void dispatchEvent(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, UserHostmask recipientHostmask, User recipientUser, boolean adding) {
                        Utils.dispatchEvent(bot, new HalfOpEvent(bot, channel, sourceHostmask, sourceUser,
                                recipientHostmask, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('a', UserLevel.SUPEROP) {
                    @Override
                    public void dispatchEvent(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, UserHostmask recipientHostmask, User recipientUser, boolean adding) {
                        Utils.dispatchEvent(bot, new SuperOpEvent(bot, channel, sourceHostmask, sourceUser,
                                recipientHostmask, recipientUser, adding));
                    }
                }).add(new OpChannelModeHandler('q', UserLevel.OWNER) {
                    @Override
                    public void dispatchEvent(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, UserHostmask recipientHostmask, User recipientUser, boolean adding) {
                        Utils.dispatchEvent(bot, new OwnerEvent(bot, channel, sourceHostmask, sourceUser,
                                recipientHostmask, recipientUser, adding));
                    }
                }).add(new ChannelModeHandler('k') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            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(bot, channel, sourceHostmask, sourceUser, key));
                        } else {
                            String key = params.hasNext() ? params.next() : null;
                            channel.setChannelKey(null);
                            if (dispatchEvent)
                                Utils.dispatchEvent(bot,
                                        new RemoveChannelKeyEvent(bot, channel, sourceHostmask, sourceUser, key));
                        }
                    }
                }).add(new ChannelModeHandler('l') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            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(bot, channel, sourceHostmask, sourceUser, limit));
                        } else {
                            channel.setChannelLimit(-1);
                            if (dispatchEvent)
                                Utils.dispatchEvent(bot,
                                        new RemoveChannelLimitEvent(bot, channel, sourceHostmask, sourceUser));
                        }
                    }
                }).add(new ChannelModeHandler('b') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, PeekingIterator<String> params, boolean adding,
                            boolean dispatchEvent) {
                        if (dispatchEvent) {
                            UserHostmask banHostmask = bot.getConfiguration().getBotFactory()
                                    .createUserHostmask(bot, params.next());
                            if (adding)
                                Utils.dispatchEvent(bot, new SetChannelBanEvent(bot, channel, sourceHostmask,
                                        sourceUser, banHostmask));
                            else
                                Utils.dispatchEvent(bot, new RemoveChannelBanEvent(bot, channel, sourceHostmask,
                                        sourceUser, banHostmask));
                        }
                    }
                }).add(new ChannelModeHandler('t') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, PeekingIterator<String> params, boolean adding,
                            boolean dispatchEvent) {
                        channel.setTopicProtection(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetTopicProtectionEvent(bot, channel, sourceHostmask, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveTopicProtectionEvent(bot, channel, sourceHostmask, sourceUser));
                    }
                }).add(new ChannelModeHandler('n') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, PeekingIterator<String> params, boolean adding,
                            boolean dispatchEvent) {
                        channel.setNoExternalMessages(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetNoExternalMessagesEvent(bot, channel, sourceHostmask, sourceUser));
                            else
                                Utils.dispatchEvent(bot, new RemoveNoExternalMessagesEvent(bot, channel,
                                        sourceHostmask, sourceUser));
                    }
                }).add(new ChannelModeHandler('i') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, PeekingIterator<String> params, boolean adding,
                            boolean dispatchEvent) {
                        channel.setInviteOnly(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetInviteOnlyEvent(bot, channel, sourceHostmask, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveInviteOnlyEvent(bot, channel, sourceHostmask, sourceUser));
                    }
                }).add(new ChannelModeHandler('m') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, PeekingIterator<String> params, boolean adding,
                            boolean dispatchEvent) {
                        channel.setModerated(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetModeratedEvent(bot, channel, sourceHostmask, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveModeratedEvent(bot, channel, sourceHostmask, sourceUser));
                    }
                }).add(new ChannelModeHandler('p') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, PeekingIterator<String> params, boolean adding,
                            boolean dispatchEvent) {
                        channel.setChannelPrivate(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetPrivateEvent(bot, channel, sourceHostmask, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemovePrivateEvent(bot, channel, sourceHostmask, sourceUser));
                    }
                }).add(new ChannelModeHandler('s') {
                    @Override
                    public void handleMode(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                            User sourceUser, PeekingIterator<String> params, boolean adding,
                            boolean dispatchEvent) {
                        channel.setSecret(adding);
                        if (dispatchEvent)
                            if (adding)
                                Utils.dispatchEvent(bot,
                                        new SetSecretEvent(bot, channel, sourceHostmask, sourceUser));
                            else
                                Utils.dispatchEvent(bot,
                                        new RemoveSecretEvent(bot, channel, sourceHostmask, sourceUser));
                    }
                }).build();
    }
    protected final Configuration configuration;
    protected final PircBotX bot;
    protected final List<CapHandler> capHandlersFinished = Lists.newArrayList();
    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> whoisBuilder = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
    protected StringBuilder motdBuilder;
    @Getter
    protected boolean channelListRunning = false;
    protected ImmutableList.Builder<ChannelListEntry> channelListBuilder;
    protected int nickSuffix = 0;
    protected final Multimap<Channel, BanListEvent.Entry> banListBuilder = LinkedListMultimap.create();

    public InputParser(PircBotX 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 rawLine The raw line of text from the server.
     */
    public void handleLine(@NonNull String rawLine) throws IOException, IrcException {
        String line = CharMatcher.WHITESPACE.trimFrom(rawLine);
        log.info(INPUT_MARKER, line);

        // Parse out v3Tags before
        ImmutableMap.Builder<String, String> tags = ImmutableMap.builder();
        if (line.startsWith("@")) {
            //This message has IRCv3 tags
            String v3Tags = line.substring(1, line.indexOf(" "));
            line = line.substring(line.indexOf(" ") + 1);

            StringTokenizer tokenizer = new StringTokenizer(v3Tags);

            while (tokenizer.hasMoreTokens()) {
                String tag = tokenizer.nextToken(";");
                if (tag.contains("=")) {
                    String[] parts = tag.split("=");
                    tags.put(parts[0], (parts.length == 2 ? parts[1] : null));
                } else {
                    tags.put(tag, null);
                }
            }
        }

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

        String sourceRaw = "";
        if (parsedLine.get(0).charAt(0) == ':')
            sourceRaw = 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(bot, parsedLine.get(0)));
            return;
        } else if (command.startsWith("ERROR")) {
            //Server is shutting us down
            bot.close();
            return;
        }

        String target = (parsedLine.isEmpty()) ? "" : parsedLine.get(0);
        if (target.startsWith(":"))
            target = target.substring(1);

        //Make sure this is a valid IRC line
        if (!sourceRaw.startsWith(":")) {
            // We don't know what this line means.
            configuration.getListenerManager().dispatchEvent(new UnknownEvent(bot, line));
            if (!bot.loggedIn)
                //Pass to CapHandlers, could be important
                for (CapHandler curCapHandler : configuration.getCapHandlers())
                    if (curCapHandler.handleUnknown(bot, line))
                        addCapHandlerFinished(curCapHandler);
            // Return from the method;
            return;
        }

        //if user build source hostmask or call server parsing method
        UserHostmask source;
        if (StringUtils.containsAny(target, '!', '@'))
            source = bot.getConfiguration().getBotFactory().createUserHostmask(bot, target);
        else {
            //Must be a backend code 
            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 v2 FROM PIRCBOT: Assume no nick
                source = bot.getConfiguration().getBotFactory().createUserHostmask(bot, sourceRaw.substring(1));
        }
        if (!bot.loggedIn)
            processConnect(line, command, target, parsedLine);

        //Must be from user
        processCommand(target, source, command, line, parsedLine, tags.build());
    }

    /**
     * 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.onLoggedIn(parsedLine.get(0));
            log.debug("Logged onto server.");

            configuration.getListenerManager().dispatchEvent(new ConnectEvent(bot));

            //Handle automatic on connect stuff
            if (configuration.getNickservPassword() != null)
                bot.sendIRC().identify(configuration.getNickservPassword());
            ImmutableMap<String, String> autoConnectChannels = bot.reconnectChannels();
            if (autoConnectChannels == null)
                if (configuration.isNickservDelayJoin())
                    autoConnectChannels = ImmutableMap.of();
                else
                    autoConnectChannels = configuration.getAutoJoinChannels();
            for (Map.Entry<String, String> channelEntry : autoConnectChannels.entrySet())
                bot.sendIRC().joinChannel(channelEntry.getKey(), channelEntry.getValue());
        } 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 (configuration.isCapEnabled() && code.equals("410") && parsedLine.get(1).contains("CAP")) {
            //EXAMPLE: 410 :Invalid CAP command
            //Ignore, Twitch.tv uses this code for some reason
            log.warn("Ignoring invalid command error, server does not support CAP negotiation");
        } else if ((code.startsWith("5") || code.startsWith("4")) && !code.equals("433"))
            //Ignore 433 NickAlreadyInUse, handled later
            throw new IrcException(IrcException.Reason.CannotLogin, "Received error: " + rawLine);
        else if (code.equals("670")) {
            //Server is saying that we can upgrade to TLS
            log.debug("Upgrading to TLS connection");
            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())
                if (curCapHandler.handleUnknown(bot, rawLine))
                    addCapHandlerFinished(curCapHandler);
        } else if (code.equals("CAP") && configuration.isCapEnabled()) {
            //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")) {
                log.debug("Starting Cap Handlers {}", getCapHandlersRemaining());
                for (CapHandler curCapHandler : getCapHandlersRemaining()) {
                    if (curCapHandler.handleLS(bot, capParams))
                        addCapHandlerFinished(curCapHandler);
                }
            } else if (capCommand.equals("ACK")) {
                //Server is enabling a capability, store that
                bot.getEnabledCapabilities().addAll(capParams);

                for (CapHandler curCapHandler : getCapHandlersRemaining())
                    if (curCapHandler.handleACK(bot, capParams))
                        addCapHandlerFinished(curCapHandler);
            } else if (capCommand.equals("NAK")) {
                for (CapHandler curCapHandler : getCapHandlersRemaining())
                    if (curCapHandler.handleNAK(bot, capParams))
                        addCapHandlerFinished(curCapHandler);
            } else {
                //Maybe the CapHandlers know how to use it
                for (CapHandler curCapHandler : getCapHandlersRemaining())
                    if (curCapHandler.handleUnknown(bot, rawLine))
                        addCapHandlerFinished(curCapHandler);
            }
        } else
            //Pass to CapHandlers, could be important
            for (CapHandler curCapHandler : getCapHandlersRemaining())
                if (curCapHandler.handleUnknown(bot, rawLine))
                    addCapHandlerFinished(curCapHandler);
    }

    protected List<CapHandler> getCapHandlersRemaining() {
        List<CapHandler> remaining = Lists.newArrayList(configuration.getCapHandlers());
        remaining.removeAll(capHandlersFinished);
        return remaining;
    }

    protected void addCapHandlerFinished(CapHandler capHandler) {
        log.debug("Cap Handler finished " + capHandler);
        capHandlersFinished.add(capHandler);
        if (!capEndSent && capHandlersFinished.size() == configuration.getCapHandlers().size()) {
            capEndSent = true;
            bot.sendCAP().end();
            bot.enabledCapabilities = Collections.unmodifiableList(bot.enabledCapabilities);
        }
    }

    public void processCommand(String target, UserHostmask source, String command, String line,
            List<String> parsedLine, ImmutableMap<String, String> tags) throws IOException {
        //If the channel matches a prefix, then its a channel
        Channel channel = (target.length() != 0 && bot.getUserChannelDao().containsChannel(target))
                ? bot.getUserChannelDao().getChannel(target)
                : null;
        String message = parsedLine.size() >= 2 ? parsedLine.get(1) : "";
        //Try to load the source user if it exists
        User sourceUser = bot.getUserChannelDao().containsUser(source) ? bot.getUserChannelDao().getUser(source)
                : null;

        // Check for CTCP requests.
        if (command.equals("PRIVMSG") && message.startsWith("\u0001") && message.endsWith("\u0001")) {
            sourceUser = createUserIfNull(sourceUser, source);
            String request = message.substring(1, message.length() - 1);
            if (request.equals("VERSION"))
                // VERSION request
                configuration.getListenerManager()
                        .dispatchEvent(new VersionEvent(bot, source, sourceUser, channel));
            else if (request.startsWith("ACTION "))
                // ACTION request
                configuration.getListenerManager().dispatchEvent(
                        new ActionEvent(bot, source, sourceUser, channel, target, request.substring(7)));
            else if (request.startsWith("PING "))
                // PING request
                configuration.getListenerManager()
                        .dispatchEvent(new PingEvent(bot, source, sourceUser, channel, request.substring(5)));
            else if (request.equals("TIME"))
                // TIME request
                configuration.getListenerManager().dispatchEvent(new TimeEvent(bot, channel, source, sourceUser));
            else if (request.equals("FINGER"))
                // FINGER request
                configuration.getListenerManager().dispatchEvent(new FingerEvent(bot, source, sourceUser, channel));
            else if (request.startsWith("DCC ")) {
                // This is a DCC request.
                boolean success = bot.getDccHandler().processDcc(source, sourceUser, request);
                if (!success)
                    // The DccManager didn't know what to do with the line.
                    configuration.getListenerManager().dispatchEvent(new UnknownEvent(bot, line));
            } else
                // An unknown CTCP message - ignore it.
                configuration.getListenerManager().dispatchEvent(new UnknownEvent(bot, line));
        } else if (command.equals("PRIVMSG") && channel != null) {
            // This is a normal message to a channel.
            sourceUser = createUserIfNull(sourceUser, source);
            configuration.getListenerManager()
                    .dispatchEvent(new MessageEvent(bot, channel, target, source, sourceUser, message, tags));
        } else if (command.equals("PRIVMSG")) {
            // This is a private message to us.
            //Add to private message
            sourceUser = createUserIfNull(sourceUser, source);
            bot.getUserChannelDao().addUserToPrivate(sourceUser);
            configuration.getListenerManager()
                    .dispatchEvent(new PrivateMessageEvent(bot, source, sourceUser, message));
        } else if (command.equals("JOIN")) {
            // Someone is joining a channel.
            if (source.getNick().equalsIgnoreCase(bot.getNick())) {
                //Its us, get channel info
                channel = bot.getUserChannelDao().createChannel(target);
                bot.sendRaw().rawLine("WHO " + target);
                bot.sendRaw().rawLine("MODE " + target);
            }
            //Create user if it doesn't exist already
            sourceUser = createUserIfNull(sourceUser, source);

            bot.getUserChannelDao().addUserToChannel(sourceUser, channel);
            configuration.getListenerManager().dispatchEvent(new JoinEvent(bot, channel, source, sourceUser));
        } else if (command.equals("PART")) {
            // Someone is parting from a channel.
            UserChannelDaoSnapshot daoSnapshot;
            ChannelSnapshot channelSnapshot;
            UserSnapshot sourceSnapshot;
            if (configuration.isSnapshotsEnabled()) {
                daoSnapshot = bot.getUserChannelDao().createSnapshot();
                channelSnapshot = daoSnapshot.getChannel(channel.getName());
                sourceSnapshot = daoSnapshot.getUser(source);
            } else {
                daoSnapshot = null;
                channelSnapshot = null;
                sourceSnapshot = null;
            }

            if (source.getNick().equalsIgnoreCase(bot.getNick()))
                //We parted the channel
                bot.getUserChannelDao().removeChannel(channel);
            else
                //Just remove the user from memory
                bot.getUserChannelDao().removeUserFromChannel(sourceUser, channel);
            configuration.getListenerManager().dispatchEvent(
                    new PartEvent(bot, daoSnapshot, channelSnapshot, source, sourceSnapshot, message));
        } else if (command.equals("NICK")) {
            // Somebody is changing their nick.
            sourceUser = createUserIfNull(sourceUser, source);
            String newNick = target;
            bot.getUserChannelDao().renameUser(sourceUser, newNick);
            if (source.getNick().equals(bot.getNick()))
                // Update our nick if it was us that changed nick.
                bot.setNick(newNick);
            configuration.getListenerManager()
                    .dispatchEvent(new NickChangeEvent(bot, source.getNick(), newNick, source, sourceUser));
        } else if (command.equals("NOTICE")) {
            // Someone is sending a notice.
            configuration.getListenerManager()
                    .dispatchEvent(new NoticeEvent(bot, source, sourceUser, channel, target, message));
        } else if (command.equals("QUIT")) {
            UserChannelDaoSnapshot daoSnapshot;
            UserSnapshot sourceSnapshot;
            if (configuration.isSnapshotsEnabled()) {
                daoSnapshot = bot.getUserChannelDao().createSnapshot();
                sourceSnapshot = daoSnapshot.getUser(sourceUser.getNick());
            } else {
                daoSnapshot = null;
                sourceSnapshot = null;
            }
            //A real target is missing, so index is off
            String reason = target;
            // Someone has quit from the IRC server.
            if (!source.getNick().equals(bot.getNick()))
                //Someone else
                bot.getUserChannelDao().removeUser(sourceUser);
            configuration.getListenerManager()
                    .dispatchEvent(new QuitEvent(bot, daoSnapshot, source, sourceSnapshot, reason));
        } else if (command.equals("KICK")) {
            // Somebody has been kicked from a channel.
            UserHostmask recipientHostmask = bot.getConfiguration().getBotFactory().createUserHostmask(bot,
                    message);
            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(bot, channel, source, sourceUser,
                    recipientHostmask, 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);
            //TODO: ummm... what does this do?
            //Handle situations where source doesn't have a full username (IE server setting user mode on connect)
            //User sourceModeUser = sourceUser;
            //if (sourceModeUser == null)
            //   sourceModeUser = bot.getUserChannelDao().getUser(source);
            processMode(source, sourceUser, 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(source);
            channel.setTopicTimestamp(currentTime);

            configuration.getListenerManager()
                    .dispatchEvent(new TopicEvent(bot, channel, oldTopic, message, source, currentTime, true));
        } else if (command.equals("INVITE")) {
            // Somebody is inviting somebody else into a channel.
            configuration.getListenerManager().dispatchEvent(new InviteEvent(bot, source, sourceUser, message));
        } else if (command.equals("AWAY"))
            //IRCv3 AWAY notify
            if (parsedLine.isEmpty())
                sourceUser.setAwayMessage("");
            else
                sourceUser.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(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.
     */
    public void processServerResponse(int code, String rawResponse, List<String> parsedResponseOrig) {
        ImmutableList<String> parsedResponse = ImmutableList.copyOf(parsedResponseOrig);
        //Parsed response format: Everything after code
        if (code == 433) {
            //EXAMPLE: * AnAlreadyUsedName :Nickname already in use
            //Nickname in use, rename
            String usedNick = parsedResponseOrig.get(1);
            boolean autoNickChange = configuration.isAutoNickChange();
            String autoNewNick = null;
            if (autoNickChange) {
                nickSuffix++;
                autoNewNick = configuration.getName() + nickSuffix;
                bot.sendIRC().changeNick(autoNewNick);
                bot.setNick(autoNewNick);
                bot.getUserChannelDao().renameUser(bot.getUserChannelDao().getUser(usedNick), autoNewNick);
            }
            configuration.getListenerManager()
                    .dispatchEvent(new NickAlreadyInUseEvent(bot, usedNick, autoNewNick, autoNickChange));
        } else 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(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));
            UserHostmask setBy = configuration.getBotFactory().createUserHostmask(bot, parsedResponse.get(2));
            long date = Utils.tryParseLong(parsedResponse.get(3), -1);

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

            configuration.getListenerManager()
                    .dispatchEvent(new TopicEvent(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
            UserHostmask curUserHostmask = bot.getConfiguration().getBotFactory().createUserHostmask(bot, null,
                    parsedResponse.get(5), parsedResponse.get(2), parsedResponse.get(3));
            User curUser = (bot.getUserChannelDao().containsUser(curUserHostmask))
                    ? bot.getUserChannelDao().getUser(curUserHostmask)
                    : bot.getUserChannelDao().createUser(curUserHostmask);
            curUser.setServer(parsedResponse.get(4));
            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(bot, channel, bot.getUserChannelDao().getUsers(channel), true));
        } 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(bot, channel, null, 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(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 builder = WhoisEvent.builder();
            builder.nick(whoisNick);
            builder.login(parsedResponse.get(2));
            builder.hostname(parsedResponse.get(3));
            builder.realname(parsedResponse.get(5));
            whoisBuilder.put(whoisNick, builder);
        } else if (code == RPL_AWAY) {
            //Example: 301 PircBotXUser TheLQ_ :I'm away, sorry
            //Can be sent during whois
            String nick = parsedResponse.get(1);
            String awayMessage = parsedResponse.get(2);
            if (bot.getUserChannelDao().containsUser(nick))
                bot.getUserChannelDao().getUser(nick).setAwayMessage(awayMessage);
            if (whoisBuilder.containsKey(nick))
                whoisBuilder.get(nick).awayMessage(awayMessage);
        } 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).channels(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).server(parsedResponse.get(2));
            whoisBuilder.get(whoisNick).serverInfo(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).idleSeconds(Long.parseLong(parsedResponse.get(2)));
            whoisBuilder.get(whoisNick).signOnTime(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)).registeredAs(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)).registeredAs("");
        else if (code == RPL_ENDOFWHOIS) {
            //End of whois
            //318 TheLQ Plazma :End of /WHOIS list.
            String whoisNick = parsedResponse.get(1);
            WhoisEvent.Builder builder;
            if (whoisBuilder.containsKey(whoisNick)) {
                builder = whoisBuilder.get(whoisNick);
                builder.exists(true);
            } else {
                builder = WhoisEvent.builder();
                builder.nick(whoisNick);
                builder.exists(false);
            }
            configuration.getListenerManager().dispatchEvent(builder.generateEvent(bot));
            whoisBuilder.remove(whoisNick);
        } else if (code == 367) {
            //Ban list entry
            //367 TheLQ #aChannel *!*@test1.host TheLQ!~quackstar@some.host 1415143822
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));

            UserHostmask recipient = bot.getConfiguration().getBotFactory().createUserHostmask(bot,
                    parsedResponse.get(2));
            UserHostmask source = bot.getConfiguration().getBotFactory().createUserHostmask(bot,
                    parsedResponse.get(3));
            long time = Long.parseLong(parsedResponse.get(4));
            banListBuilder.put(channel, new BanListEvent.Entry(recipient, source, time));
            log.debug("Adding entry");
        } else if (code == 368) {
            //Ban list is finished
            //368 TheLQ #aChannel :End of Channel Ban List
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));
            ImmutableList<BanListEvent.Entry> entries = ImmutableList.copyOf(banListBuilder.removeAll(channel));
            log.debug("Dispatching event");
            configuration.getListenerManager().dispatchEvent(new BanListEvent(bot, channel, entries));
        } else if (code == 353) {
            //NAMES response
            //353 PircBotXUser = #aChannel :aUser1 aUser2
            for (String curUser : StringUtils.split(parsedResponse.get(3))) {
                //Siphon off any levels this user has
                String nick = curUser;
                List<UserLevel> levels = Lists.newArrayList();
                UserLevel parsedLevel;
                while ((parsedLevel = UserLevel.fromSymbol(nick.charAt(0))) != null) {
                    nick = nick.substring(1);
                    levels.add(parsedLevel);
                }

                User user;
                if (!bot.getUserChannelDao().containsUser(nick))
                    //Create user with nick only
                    user = bot.getUserChannelDao().createUser(new UserHostmask(bot, nick));
                else
                    user = bot.getUserChannelDao().getUser(nick);
                Channel chan = bot.getUserChannelDao().getChannel(parsedResponse.get(2));
                bot.getUserChannelDao().addUserToChannel(user, chan);

                //Now that the user is created, add them to the appropiate levels
                for (UserLevel curLevel : levels) {
                    bot.getUserChannelDao().addUserToLevel(curLevel, user, chan);
                }
            }
        } else if (code == 366) {
            //NAMES response finished
            //366 PircBotXUser #aChannel :End of /NAMES list.
            Channel channel = bot.getUserChannelDao().getChannel(parsedResponse.get(1));
            configuration.getListenerManager().dispatchEvent(
                    new UserListEvent(bot, channel, bot.getUserChannelDao().getUsers(channel), false));
        }
        configuration.getListenerManager()
                .dispatchEvent(new ServerResponseEvent(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 mode The mode that has been set.
     */
    public void processMode(UserHostmask userHostmask, 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, userHostmask, user, params, adding, true);
                }
            }
            configuration.getListenerManager()
                    .dispatchEvent(new ModeEvent(bot, channel, userHostmask, user, mode, modeParsed));
        } else {
            // The mode of a user is being changed.
            UserHostmask targetHostmask = bot.getConfiguration().getBotFactory().createUserHostmask(bot, target);
            User targetUser = bot.getUserChannelDao().getUser(target);
            configuration.getListenerManager()
                    .dispatchEvent(new UserModeEvent(bot, userHostmask, user, targetHostmask, targetUser, mode));
        }
    }

    public void processUserStatus(Channel chan, User user, String prefix) {
        for (char prefixChar : prefix.toCharArray()) {
            UserLevel level = UserLevel.fromSymbol(prefixChar);
            if (level != null)
                bot.getUserChannelDao().addUserToLevel(level, user, chan);
        }
        //Assume here (H) if there is no G
        user.setAwayMessage(prefix.contains("G") ? "" : null);
        user.setIrcop(prefix.contains("*"));
    }

    public User createUserIfNull(User otherUser, @NonNull UserHostmask hostmask) {
        if (otherUser != null) {
            //We could have fresh user data
            otherUser.updateHostmask(hostmask);
            return otherUser;
        } else if (bot.getUserChannelDao().containsUser(hostmask))
            throw new RuntimeException("User wasn't fetched but user exists in DAO. Please report this bug");
        return bot.getUserChannelDao().createUser(hostmask);
    }

    /**
     * Clear out builders.
     */
    public void close() {
        capEndSent = false;
        capHandlersFinished.clear();
        capHandlersFinished.addAll(configuration.getCapHandlers());
        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(PircBotX bot, Channel channel, UserHostmask sourceHostmask, User sourceUser,
                PeekingIterator<String> params, boolean adding, boolean dispatchEvent) {
            String recipient = params.next();
            UserHostmask recipientHostmask = bot.getConfiguration().getBotFactory().createUserHostmask(bot,
                    recipient);
            User recipientUser = null;
            if (bot.getUserChannelDao().containsUser(recipient)) {
                recipientUser = bot.getUserChannelDao().getUser(recipient);
                if (adding)
                    bot.getUserChannelDao().addUserToLevel(level, recipientUser, channel);
                else
                    bot.getUserChannelDao().removeUserFromLevel(level, recipientUser, channel);
            }

            if (dispatchEvent)
                dispatchEvent(bot, channel, sourceHostmask, sourceUser, recipientHostmask, recipientUser, adding);
        }

        public abstract void dispatchEvent(PircBotX bot, Channel channel, UserHostmask sourceHostmask,
                User sourceUser, UserHostmask recipientHostmask, User recipientUser, boolean adding);
    }
}