org.pircbotx.PircBotX.java Source code

Java tutorial

Introduction

Here is the source code for org.pircbotx.PircBotX.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 com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.primitives.Ints;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStreamWriter;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pircbotx.dcc.DccHandler;
import org.pircbotx.exception.IrcException;
import org.pircbotx.hooks.ListenerAdapter;
import org.pircbotx.hooks.events.*;
import org.pircbotx.output.OutputCAP;
import org.pircbotx.output.OutputDCC;
import org.pircbotx.output.OutputIRC;
import org.pircbotx.output.OutputRaw;
import org.pircbotx.snapshot.UserChannelDaoSnapshot;

/**
 * PircBotX is a Java framework for writing IRC bots quickly and easily.
 * <p>
 * It provides an event-driven architecture to handle common IRC events, flood
 * protection, DCC support, ident support, and more. The comprehensive logfile
 * format is suitable for use with pisg to generate channel statistics.
 * <p>
 * Methods of the PircBotX class can be called to send events to the IRC server
 * that it connects to. For example, calling the sendMessage method will send a
 * message to a channel or user on the IRC server. Multiple servers can be
 * supported using multiple instances of PircBotX.
 * <p>
 * To perform an action when the PircBotX receives a normal message from the IRC
 * server, you would listen for the MessageEvent in your listener (see
 * {@link ListenerAdapter}). Many other events are dispatched as well for other
 * incoming lines
 *
 * @author Origionally by:
 * <a href="http://www.jibble.org/">Paul James Mutton</a> for <a
 * href="http://www.jibble.org/pircbot.php">PircBot</a>
 * <p>
 * Forked and Maintained by Leon Blakey in <a
 * href="http://pircbotx.googlecode.com">PircBotX</a>
 */
@Slf4j
@EqualsAndHashCode(of = "botId")
public class PircBotX implements Comparable<PircBotX>, Closeable {
    /**
     * The definitive version number of this release of PircBotX.
     */
    //THIS LINE IS AUTOGENERATED, DO NOT EDIT
    public static final String VERSION = "2.1-SNAPSHOT";
    protected static final AtomicInteger BOT_COUNT = new AtomicInteger();
    /**
     * Unique number for this bot
     */
    @Getter
    protected final int botId;
    //Utility objects
    /**
     * Configuration used for this bot
     */
    @Getter
    protected final Configuration configuration;
    @Getter
    protected final InputParser inputParser;
    /**
     * User-Channel mapper
     */
    @Getter
    protected final UserChannelDao<User, Channel> userChannelDao;
    @Getter
    protected final DccHandler dccHandler;
    protected final ServerInfo serverInfo;
    //Connection stuff.
    @Getter(AccessLevel.PROTECTED)
    protected Socket socket;
    protected BufferedReader inputReader;
    protected OutputStreamWriter outputWriter;
    protected final OutputRaw outputRaw;
    protected final OutputIRC outputIRC;
    protected final OutputCAP outputCAP;
    protected final OutputDCC outputDCC;
    /**
     * Enabled CAP features
     */
    @Getter
    protected List<String> enabledCapabilities = new ArrayList<String>();
    protected String nick;
    protected boolean loggedIn = false;
    protected Thread shutdownHook;
    protected volatile boolean reconnectStopped = false;
    protected ImmutableMap<String, String> reconnectChannels;
    private State state = State.INIT;
    protected final Object stateLock = new Object();
    protected Exception disconnectException;
    @Getter
    protected String serverHostname;
    @Getter
    protected int serverPort;
    /**
     *
     */
    @Getter
    @Setter(AccessLevel.PROTECTED)
    protected boolean nickservIdentified = false;
    private int connectAttempts = 0;
    private int connectAttemptTotal = 0;

    /**
     * Constructs a PircBotX with the provided configuration.
     *
     * @param configuration Fully built Configuration
     */
    @SuppressWarnings("unchecked")
    public PircBotX(@NonNull Configuration configuration) {
        botId = BOT_COUNT.getAndIncrement();
        this.configuration = configuration;
        this.nick = configuration.getName();

        //Pre-insert an initial User representing the bot itself
        this.userChannelDao = configuration.getBotFactory().createUserChannelDao(this);
        UserHostmask botHostmask = configuration.getBotFactory().createUserHostmask(this, null,
                configuration.getName(), configuration.getLogin(), null);
        getUserChannelDao().createUser(botHostmask);

        this.serverInfo = configuration.getBotFactory().createServerInfo(this);
        this.outputRaw = configuration.getBotFactory().createOutputRaw(this);
        this.outputIRC = configuration.getBotFactory().createOutputIRC(this);
        this.outputCAP = configuration.getBotFactory().createOutputCAP(this);
        this.outputDCC = configuration.getBotFactory().createOutputDCC(this);
        this.dccHandler = configuration.getBotFactory().createDccHandler(this);
        this.inputParser = configuration.getBotFactory().createInputParser(this);
    }

    /**
     * Start the bot by connecting to the server. If
     * {@link Configuration#isAutoReconnect()} is true this will continuously
     * reconnect to the server until {@link #stopBotReconnect() } is called or
     * an exception is thrown from connecting
     *
     * @throws IOException if it was not possible to connect to the server.
     * @throws IrcException
     */
    public void startBot() throws IOException, IrcException {
        //Begin magic
        reconnectStopped = false;
        do {
            //Try to connect to the server, grabbing any exceptions
            LinkedHashMap<InetSocketAddress, Exception> connectExceptions = Maps.newLinkedHashMap();
            try {
                connectAttemptTotal++;
                connectAttempts++;
                connectExceptions.putAll(connect());
            } catch (Exception e) {
                //Initial connect exceptions are returned in the map, this is a more serious error
                log.error("Exception encountered during connect", e);
                connectExceptions.put(new InetSocketAddress(serverHostname, serverPort), e);

                if (!configuration.isAutoReconnect())
                    throw new RuntimeException("Exception encountered during connect", e);
            } finally {
                if (!connectExceptions.isEmpty())
                    Utils.dispatchEvent(this,
                            new ConnectAttemptFailedEvent(this,
                                    configuration.getAutoReconnectAttempts() - connectAttempts,
                                    ImmutableMap.copyOf(connectExceptions)));

                //Cleanup if not already called
                synchronized (stateLock) {
                    if (state != State.DISCONNECTED)
                        shutdown();
                }
            }

            //No longer connected to the server
            if (!configuration.isAutoReconnect())
                return;
            if (reconnectStopped) {
                log.debug("stopBotReconnect() called, exiting reconnect loop");
                return;
            }
            if (connectAttempts == configuration.getAutoReconnectAttempts()) {
                throw new IOException("Failed to connect to IRC server(s) after " + connectAttempts + " attempts");
            }

            //Optionally pause between attempts, useful if network is temporarily down
            if (configuration.getAutoReconnectDelay() > 0)
                try {
                    log.debug("Pausing for {} milliseconds before connecting again",
                            configuration.getAutoReconnectDelay());
                    Thread.sleep(configuration.getAutoReconnectDelay());
                } catch (InterruptedException e) {
                    throw new RuntimeException("Interrupted while pausing before the next connect attempt", e);
                }
        } while (connectAttempts < configuration.getAutoReconnectAttempts());
    }

    /**
     * Do not try connecting again in the future.
     */
    public void stopBotReconnect() {
        reconnectStopped = true;
    }

    /**
     * Attempt to connect to the specified IRC server using the supplied port
     * number, password, and socketFactory. On success a {@link ConnectEvent}
     * will be dispatched
     *
     * @throws IOException if it was not possible to connect to the server.
     * @throws IrcException if the server would not let us join it.
     */
    protected ImmutableMap<InetSocketAddress, Exception> connect() throws IOException, IrcException {
        synchronized (stateLock) {
            //Server id
            Utils.addBotToMDC(this);
            if (isConnected())
                throw new IrcException(IrcException.Reason.AlreadyConnected,
                        "Must disconnect from server before connecting again");
            if (getState() == State.CONNECTED)
                throw new RuntimeException(
                        "Bot is not connected but state is State.CONNECTED. This shouldn't happen");
            if (configuration.isIdentServerEnabled() && IdentServer.getServer() == null)
                throw new RuntimeException("UseIdentServer is enabled but no IdentServer has been started");

            //Reset capabilities
            enabledCapabilities = new ArrayList<String>();

            //Pre-insert an initial User representing the bot itself
            getUserChannelDao().close();
            UserHostmask botHostmask = configuration.getBotFactory().createUserHostmask(this, null,
                    configuration.getName(), configuration.getLogin(), null);
            getUserChannelDao().createUser(botHostmask);

            //On each server the user gives us, try to connect to all the IP addresses
            ImmutableMap.Builder<InetSocketAddress, Exception> connectExceptions = ImmutableMap.builder();
            int serverEntryCounter = 0;
            ServerEntryLoop: for (Configuration.ServerEntry curServerEntry : configuration.getServers()) {
                serverEntryCounter++;
                serverHostname = curServerEntry.getHostname();
                //Hostname and port
                Utils.addBotToMDC(this);
                log.info("---Starting Connect attempt {}/{}", connectAttempts,
                        configuration.getAutoReconnectAttempts() + "---");

                int serverAddressCounter = 0;
                InetAddress[] serverAddresses = InetAddress.getAllByName(serverHostname);
                for (InetAddress curAddress : serverAddresses) {
                    serverAddressCounter++;
                    String debug = Utils.format("[{}/{} address left from {}, {}/{} hostnames left] ",
                            String.valueOf(serverAddresses.length - serverAddressCounter),
                            String.valueOf(serverAddresses.length), serverHostname,
                            String.valueOf(configuration.getServers().size() - serverEntryCounter),
                            String.valueOf(configuration.getServers().size()));
                    log.debug("{}Atempting to connect to {} on port {}", debug, curAddress,
                            curServerEntry.getPort());
                    try {
                        socket = configuration.getSocketFactory().createSocket(curAddress, curServerEntry.getPort(),
                                configuration.getLocalAddress(), 0);

                        //No exception, assume successful
                        serverPort = curServerEntry.getPort();
                        break ServerEntryLoop;
                    } catch (Exception e) {
                        connectExceptions.put(new InetSocketAddress(curAddress, curServerEntry.getPort()), e);
                        log.warn("{}Failed to connect to {} on port {}", debug, curAddress,
                                curServerEntry.getPort(), e);
                    }
                }
            }

            //Make sure were connected
            if (socket == null || (socket != null && !socket.isConnected())) {
                return connectExceptions.build();
            }
            state = State.CONNECTED;
            socket.setSoTimeout(configuration.getSocketTimeout());
            log.info("Connected to server.");

            changeSocket(socket);
        }

        configuration.getListenerManager().dispatchEvent(new SocketConnectEvent(this));

        if (configuration.isIdentServerEnabled())
            IdentServer.getServer().addIdentEntry(socket.getInetAddress(), socket.getPort(), socket.getLocalPort(),
                    configuration.getLogin());

        if (configuration.isCapEnabled())
            // Attempt to initiate a CAP transaction.
            sendCAP().getSupported();

        // Attempt to join the server.
        if (configuration.isWebIrcEnabled())
            sendRaw().rawLineNow("WEBIRC " + configuration.getWebIrcPassword() + " "
                    + configuration.getWebIrcUsername() + " " + configuration.getWebIrcHostname() + " "
                    + configuration.getWebIrcAddress().getHostAddress());
        if (StringUtils.isNotBlank(configuration.getServerPassword()))
            sendRaw().rawLineNow("PASS " + configuration.getServerPassword());

        sendRaw().rawLineNow("NICK " + configuration.getName());
        sendRaw().rawLineNow("USER " + configuration.getLogin() + " 8 * :" + configuration.getRealName());

        //Start input to start accepting lines
        startLineProcessing();

        return ImmutableMap.of();
    }

    protected void changeSocket(Socket socket) throws IOException {
        this.socket = socket;
        this.inputReader = new BufferedReader(
                new InputStreamReader(socket.getInputStream(), configuration.getEncoding()));
        this.outputWriter = new OutputStreamWriter(socket.getOutputStream(), configuration.getEncoding());
    }

    protected void startLineProcessing() {
        while (true) {
            //Get line from the server
            String line;
            try {
                line = inputReader.readLine();
            } catch (InterruptedIOException iioe) {
                // This will happen if we haven't received anything from the server for a while.
                // So we shall send it a ping to check that we are still connected.
                sendRaw().rawLine("PING " + (System.currentTimeMillis() / 1000));
                // Now we go back to listening for stuff from the server...
                continue;
            } catch (Exception e) {
                if (Thread.interrupted()) {
                    System.out.println(
                            "--- PircBotX interrupted during read, aborting reconnect loop and shutting down ---");
                    stopBotReconnect();
                    break;
                } else if (socket.isClosed()) {
                    log.info("Socket is closed, stopping read loop and shutting down");
                    break;
                } else {
                    disconnectException = e;
                    //Something is wrong. Assume its bad and begin disconnect
                    log.error("Exception encountered when reading next line from server", e);
                    line = null;
                }
            }

            if (Thread.interrupted()) {
                System.out.println(
                        "--- PircBotX interrupted during read, aborting reconnect loop and shutting down ---");
                stopBotReconnect();
                break;
            }

            //End the loop if the line is null
            if (line == null)
                break;

            //Start acting the line
            try {
                inputParser.handleLine(line);
            } catch (Exception e) {
                //Exception in client code. Just log and continue
                log.error("Exception encountered when parsing line " + line, e);
            }

            if (Thread.interrupted()) {
                System.out.println(
                        "--- PircBotX interrupted during parsing, aborting reconnect loop and shutting down ---");
                stopBotReconnect();
                break;
            }
        }

        //Now that the socket is definitely closed call event, log, and kill the OutputThread
        shutdown();
    }

    /**
     * Actually sends the raw line to the server. This method is NOT
     * SYNCHRONIZED since it's only called from methods that handle locking
     *
     * @param line
     * @throws java.io.IOException
     */
    protected void sendRawLineToServer(String line) throws IOException {
        if (line.length() > configuration.getMaxLineLength() - 2)
            line = line.substring(0, configuration.getMaxLineLength() - 2);
        outputWriter.write(line + "\r\n");
        outputWriter.flush();

        List<String> lineParts = Utils.tokenizeLine(line);
        getConfiguration().getListenerManager().dispatchEvent(new OutputEvent(this, line, lineParts));
    }

    protected void onLoggedIn(String nick) {
        this.loggedIn = true;
        setNick(nick);

        //Were probably connected to the server at this point
        this.connectAttempts = 0;

        if (configuration.isShutdownHookEnabled())
            Runtime.getRuntime().addShutdownHook(shutdownHook = new PircBotX.BotShutdownHook(this));
    }

    public OutputRaw sendRaw() {
        return outputRaw;
    }

    public OutputIRC sendIRC() {
        return outputIRC;
    }

    public OutputIRC send() {
        return outputIRC;
    }

    public OutputCAP sendCAP() {
        return outputCAP;
    }

    public OutputDCC sendDCC() {
        return outputDCC;
    }

    /**
     * Sets the internal nick of the bot. This is only to be called by the
     * PircBotX class in response to notification of nick changes that apply to
     * us.
     *
     * @param nick The new nick.
     */
    protected void setNick(String nick) {
        this.nick = nick;
    }

    /**
     * Returns the current nick of the bot. Note that if you have just changed
     * your nick, this method will still return the old nick until confirmation
     * of the nick change is received from the server.
     *
     * @since PircBot 1.0.0
     *
     * @return The current nick of the bot.
     */
    public String getNick() {
        return nick;
    }

    /**
     * Returns whether or not the PircBotX is currently connected to a server.
     * The result of this method should only act as a rough guide, as the result
     * may not be valid by the time you act upon it.
     *
     * @return True if and only if the PircBotX is currently connected to a
     * server.
     */
    @Synchronized("stateLock")
    public boolean isConnected() {
        return socket != null && !socket.isClosed();
    }

    /**
     * Returns a String representation of this object. You may find this useful
     * for debugging purposes, particularly if you are using more than one
     * PircBotX instance to achieve multiple server connectivity. The format of
     * this String may change between different versions of PircBotX but is
     * currently something of the form     <code>
     *   Version{PircBotX x.y.z Java IRC Bot - www.jibble.org}
     *   Connected{true}
     *   Server{irc.dal.net}
     *   Port{6667}
     *   Password{}
     * </code>
     *
     * @since PircBot 0.9.10
     *
     * @return a String representation of this object.
     */
    @Override
    public String toString() {
        return "Version{" + configuration.getVersion() + "}" + " Connected{" + isConnected() + "}" + " Server{"
                + getServerHostname() + "}" + " Port{" + getServerPort() + "}" + " Password{"
                + configuration.getServerPassword() + "}";
    }

    /**
     * Gets the bots own user object.
     *
     * @return The user object representing this bot
     * @see UserChannelDao#getUserBot()
     */
    public User getUserBot() {
        return userChannelDao.getUser(getNick());
    }

    /**
     * @return the serverInfo
     */
    public ServerInfo getServerInfo() {
        return serverInfo;
    }

    public InetAddress getLocalAddress() {
        return socket.getLocalAddress();
    }

    public int getConnectionId() {
        return connectAttemptTotal;
    }

    /**
     * Get the auto reconnect channels and clear local copy
     *
     * @return
     */
    protected ImmutableMap<String, String> reconnectChannels() {
        ImmutableMap<String, String> reconnectChannelsLocal = reconnectChannels;
        reconnectChannels = null;
        return reconnectChannelsLocal;
    }

    /**
     * If for some reason you absolutely need to stop PircBotX now instead of
     * gracefully closing with {@link OutputIRC#quitServer() }, this will close
     * the socket which causes read loop to terminate which will shutdown
     * PircBotX shortly.
     *
     * @see OutputIRC#quitServer()
     */
    public void close() {
        try {
            socket.close();
        } catch (Exception e) {
            log.error("Can't close socket", e);
        }
    }

    /**
     * Fully shutdown the bot and all internal resources. This will close the
     * connections to the server, kill background threads, clear server specific
     * state, and dispatch a DisconnectedEvent
     */
    private void shutdown() {
        UserChannelDaoSnapshot daoSnapshot;
        synchronized (stateLock) {
            log.debug("---PircBotX shutdown started---");
            if (state == State.DISCONNECTED)
                throw new RuntimeException("Cannot call shutdown twice");
            state = State.DISCONNECTED;

            if (configuration.isIdentServerEnabled())
                IdentServer.getServer().removeIdentEntry(socket.getInetAddress(), socket.getPort(),
                        socket.getLocalPort(), configuration.getLogin());

            //Close the socket from here and let the threads die
            if (socket != null && !socket.isClosed())
                try {
                    socket.close();
                } catch (Exception e) {
                    log.error("Cannot close socket", e);
                }

            //Cache channels for possible next reconnect
            ImmutableMap.Builder<String, String> reconnectChannelsBuilder = ImmutableMap.builder();
            for (Channel curChannel : userChannelDao.getAllChannels()) {
                String key = (curChannel.getChannelKey() == null) ? "" : curChannel.getChannelKey();
                reconnectChannelsBuilder.put(curChannel.getName(), key);
            }
            reconnectChannels = reconnectChannelsBuilder.build();

            //Clear relevant variables of information
            loggedIn = false;
            daoSnapshot = (configuration.isSnapshotsEnabled()) ? userChannelDao.createSnapshot() : null;
            userChannelDao.close();
            inputParser.close();
            dccHandler.close();
        }

        //Dispatch event
        configuration.getListenerManager()
                .dispatchEvent(new DisconnectEvent(this, daoSnapshot, disconnectException));
        disconnectException = null;
        log.debug("Disconnected.");

        //Shutdown listener manager
        configuration.getListenerManager().shutdown(this);
    }

    /**
     * Compare {@link #getBotId() bot id's}. This is useful for sorting lists of
     * Channel objects.
     *
     * @param other Other channel to compare to
     * @return the result of calling compareToIgnoreCase on channel names.
     */
    public int compareTo(PircBotX other) {
        return Ints.compare(getBotId(), other.getBotId());
    }

    /**
     * @return the state
     */
    @Synchronized("stateLock")
    public State getState() {
        return state;
    }

    protected static class BotShutdownHook extends Thread {
        protected final WeakReference<PircBotX> thisBotRef;

        public BotShutdownHook(PircBotX bot) {
            this.thisBotRef = new WeakReference<PircBotX>(bot);
            setName("bot" + BOT_COUNT + "-shutdownhook");
        }

        @Override
        public void run() {
            PircBotX thisBot = thisBotRef.get();
            if (thisBot != null && thisBot.getState() != PircBotX.State.DISCONNECTED) {
                thisBot.stopBotReconnect();
                thisBot.sendIRC().quitServer();
                try {
                    if (thisBot.isConnected())
                        thisBot.socket.close();
                } catch (IOException ex) {
                    log.debug("Unabloe to forcibly close socket", ex);
                }
            }
        }
    }

    public static enum State {
        INIT, CONNECTED, DISCONNECTED
    }
}