com.techcavern.pircbotz.PircBotZ.java Source code

Java tutorial

Introduction

Here is the source code for com.techcavern.pircbotz.PircBotZ.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.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
import java.io.BufferedReader;
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.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import com.techcavern.pircbotz.dcc.DccHandler;
import com.techcavern.pircbotz.exception.IrcException;
import com.techcavern.pircbotz.hooks.ListenerAdapter;
import com.techcavern.pircbotz.hooks.events.*;
import com.techcavern.pircbotz.output.OutputCAP;
import com.techcavern.pircbotz.output.OutputDCC;
import com.techcavern.pircbotz.output.OutputIRC;
import com.techcavern.pircbotz.output.OutputRaw;
import com.techcavern.pircbotz.snapshot.UserChannelDaoSnapshot;

/**
 * PircBotZ 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 PircBotZ 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 PircBotZ.
 * <p>
 * To perform an action when the PircBotZ 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 Originally by:
 * <a href="http://www.jibble.org/">Paul James Mutton</a> for <a href="http://www.jibble.org/pircbot.php">PircBot</a>
 * <a href="http://pircbotx.googlecode.com">and Forked by 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>
 */
@Slf4j
public class PircBotZ implements Comparable<PircBotZ> {
    /**
     * The definitive version number of this release of PircBotX.
     */
    //THIS LINE IS AUTOGENERATED, DO NOT EDIT
    public static final String VERSION = "2.1";
    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<PircBotZ> 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;

    /**
     * Constructs a PircBotX with the provided configuration.
     */
    @SuppressWarnings("unchecked")
    public PircBotZ(Configuration<? extends PircBotZ> configuration) {
        botId = BOT_COUNT.getAndIncrement();
        this.configuration = (Configuration<PircBotZ>) configuration;
        this.userChannelDao = configuration.getBotFactory().createUserChannelDao(this);
        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 #stopBot()} 
     * 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 {
        reconnectStopped = false;
        do
            connect();
        while (configuration.isAutoReconnect() && !reconnectStopped);
    }

    /**
     * Stops the bot from reconnecting constantly to the server 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
     *
     * @param hostname The hostname of the server to connect to.
     * @param port The port number to connect to on the server.
     * @param password The password to use to join the server.
     * @param socketFactory The factory to use for creating sockets, including secure sockets
     *
     * @throws IOException if it was not possible to connect to the server.
     * @throws IrcException if the server would not let us join it.
     * @throws NickAlreadyInUseException if our nick is already in use on the server.
     */
    protected void connect() throws IOException, IrcException {
        synchronized (stateLock) {
            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>();

            // Connect to the server by DNS server
            InetAddress[] serverAddresses = InetAddress.getAllByName(configuration.getServerHostname());
            Exception lastException = null;
            for (InetAddress curAddress : serverAddresses) {
                log.debug("Trying address " + curAddress);
                try {
                    socket = configuration.getSocketFactory().createSocket(curAddress,
                            configuration.getServerPort(), configuration.getLocalAddress(), 0);

                    //No exception, assume successful
                    break;
                } catch (Exception e) {
                    lastException = e;
                    String debugSuffix = serverAddresses.length == 0 ? "no more servers"
                            : "trying to check another address";
                    log.debug("Unable to connect to " + configuration.getServerHostname() + " using the IP address "
                            + curAddress.getHostAddress() + ", " + debugSuffix, e);
                    configuration.getListenerManager()
                            .dispatchEvent(new ConnectAttemptFailedEvent<PircBotZ>(this, curAddress,
                                    configuration.getServerPort(), configuration.getLocalAddress(),
                                    serverAddresses.length));
                }
            }

            //Make sure were connected
            if (socket == null || (socket != null && !socket.isConnected()))
                throw new IOException("Unable to connect to the IRC network " + configuration.getServerHostname()
                        + " (last connection attempt exception attached)", lastException);
            state = State.CONNECTED;
            socket.setSoTimeout(configuration.getSocketTimeout());
            log.info("Connected to server.");

            changeSocket(socket);
        }

        configuration.getListenerManager().dispatchEvent(new SocketConnectEvent<PircBotZ>(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();
    }

    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 (e instanceof SocketException && getState() == State.DISCONNECTED) {
                    log.info("Shutdown has been called, closing InputParser");
                    return;
                } 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;
                }
            }

            //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", e);
            }

            //Do nothing if this thread is being interrupted (meaning shutdown() was run)
            if (Thread.interrupted())
                return;
        }

        //Now that the socket is definatly 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 
     */
    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();
    }

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

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

    public OutputRaw sendRaw() {
        return outputRaw;
    }

    public OutputIRC sendIRC() {
        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{"
                + configuration.getServerHostname() + "}" + " Port{" + configuration.getServerPort() + "}"
                + " Password{" + configuration.getServerPassword() + "}";
    }

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

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

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

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

    /**
     * Calls shutdown allowing reconnect.
     */
    protected void shutdown() {
        shutdown(false);
    }

    /**
     * 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
     * <p/>
     * @param noReconnect Toggle whether to reconnect if enabled. Set to true to
     * 100% shutdown the bot
     */
    protected void shutdown(boolean noReconnect) {
        UserChannelDaoSnapshot daoSnapshot;
        synchronized (stateLock) {
            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());

            try {
                socket.close();
            } catch (Exception e) {
                log.error("Can't close socket", e);
            }

            //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 = userChannelDao.createSnapshot();
            userChannelDao.close();
            inputParser.close();
            dccHandler.close();
        }

        //Dispatch event
        configuration.getListenerManager()
                .dispatchEvent(new DisconnectEvent<PircBotZ>(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(PircBotZ 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<PircBotZ> thisBotRef;

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

        @Override
        public void run() {
            PircBotZ thisBot = thisBotRef.get();
            if (thisBot != null && thisBot.getState() != PircBotZ.State.DISCONNECTED)
                try {
                    thisBot.stopBotReconnect();
                    thisBot.sendIRC().quitServer();
                } finally {
                    if (thisBot.getState() != PircBotZ.State.DISCONNECTED)
                        thisBot.shutdown(true);
                }
        }
    }

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