org.pircbotx.PircBotX Maven / Gradle / Ivy
// Generated by delombok at Sun Jan 24 05:06:42 EST 2016
/**
* Copyright (C) 2010-2014 Leon Blakey
*
* 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 .
*/
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.io.Writer;
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.NonNull;
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.
*
* 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.
*
* 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.
*
* 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:
* Paul James Mutton for PircBot
*
* Forked and Maintained by Leon Blakey in PircBotX
*/
public class PircBotX implements Comparable, Closeable {
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PircBotX.class);
//THIS LINE IS AUTOGENERATED, DO NOT EDIT
/**
* The definitive version number of this release of PircBotX.
*/
public static final String VERSION = "2.1";
protected static final AtomicInteger BOT_COUNT = new AtomicInteger();
/**
* Unique number for this bot
*/
protected final int botId;
//Utility objects
/**
* Configuration used for this bot
*/
protected final Configuration configuration;
protected final InputParser inputParser;
/**
* User-Channel mapper
*/
protected final UserChannelDao userChannelDao;
protected final DccHandler dccHandler;
protected final ServerInfo serverInfo;
//Connection stuff.
protected Socket socket;
protected BufferedReader inputReader;
protected Writer outputWriter;
protected final OutputRaw outputRaw;
protected final OutputIRC outputIRC;
protected final OutputCAP outputCAP;
protected final OutputDCC outputDCC;
/**
* Enabled CAP features
*/
protected List enabledCapabilities = new ArrayList();
protected String nick;
protected boolean loggedIn = false;
protected Thread shutdownHook;
protected volatile boolean reconnectStopped = false;
protected ImmutableMap reconnectChannels;
private State state = State.INIT;
protected final Object stateLock = new Object();
protected Exception disconnectException;
protected String serverHostname;
protected int serverPort;
/**
*/
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) {
if (configuration == null) {
throw new java.lang.NullPointerException("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 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 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();
//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 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().onEvent(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 (processNextLine()) {
//see processNextLine
}
//Now that the socket is definitely closed call event, log, and kill the OutputThread
shutdown();
}
/**
* @return true to continue, false to end
*/
protected boolean processNextLine() {
//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...
return true;
} catch (Exception e) {
if (Thread.interrupted()) {
log.error("--- PircBotX interrupted during read, aborting reconnect loop and shutting down ---");
stopBotReconnect();
return false;
} else if (socket.isClosed()) {
log.info("Socket is closed, stopping read loop and shutting down");
return false;
} else {
disconnectException = e;
//Something is wrong. Assume its bad and begin disconnect
String debug = "Exception encountered when reading next line from server";
log.error(debug, e);
Utils.dispatchEvent(this, new ExceptionEvent(this, e, debug));
line = null;
}
}
if (Thread.interrupted()) {
log.error("--- PircBotX interrupted during read, aborting reconnect loop and shutting down ---");
stopBotReconnect();
return false;
}
//End the loop if the line is null
if (line == null) return false;
//Start acting the line
try {
inputParser.handleLine(line);
} catch (Exception e) {
//Exception in client code. Just log and continue
String debug = "Exception encountered when parsing line " + line;
log.error(debug, e);
Utils.dispatchEvent(this, new ExceptionEvent(this, e, debug));
}
if (Thread.interrupted()) {
log.error("--- PircBotX interrupted during parsing, aborting reconnect loop and shutting down ---");
stopBotReconnect();
return false;
}
return true;
}
/**
* 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 lineParts = Utils.tokenizeLine(line);
getConfiguration().getListenerManager().onEvent(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.
*/
public boolean isConnected() {
synchronized (this.stateLock) {
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
* Version{PircBotX x.y.z Java IRC Bot - www.jibble.org}
* Connected{true}
* Server{irc.dal.net}
* Port{6667}
* Password{}
*
*
* @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() + "}";
}
/**
* 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 reconnectChannels() {
ImmutableMap 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
*/
protected 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 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().onEvent(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
*/
public State getState() {
synchronized (this.stateLock) {
return state;
}
}
protected static class BotShutdownHook extends Thread {
protected final WeakReference thisBotRef;
public BotShutdownHook(PircBotX bot) {
this.thisBotRef = new WeakReference(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;
}
@java.lang.Override
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public boolean equals(final java.lang.Object o) {
if (o == this) return true;
if (!(o instanceof PircBotX)) return false;
final PircBotX other = (PircBotX)o;
if (!other.canEqual((java.lang.Object)this)) return false;
if (this.getBotId() != other.getBotId()) return false;
return true;
}
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
protected boolean canEqual(final java.lang.Object other) {
return other instanceof PircBotX;
}
@java.lang.Override
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public int hashCode() {
final int PRIME = 59;
int result = 1;
result = result * PRIME + this.getBotId();
return result;
}
/**
* Unique number for this bot
*/
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public int getBotId() {
return this.botId;
}
/**
* Configuration used for this bot
*/
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public Configuration getConfiguration() {
return this.configuration;
}
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public InputParser getInputParser() {
return this.inputParser;
}
/**
* User-Channel mapper
*/
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public UserChannelDao getUserChannelDao() {
return this.userChannelDao;
}
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public DccHandler getDccHandler() {
return this.dccHandler;
}
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
protected Socket getSocket() {
return this.socket;
}
/**
* Enabled CAP features
*/
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public List getEnabledCapabilities() {
return this.enabledCapabilities;
}
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public String getServerHostname() {
return this.serverHostname;
}
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public int getServerPort() {
return this.serverPort;
}
/**
*/
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
public boolean isNickservIdentified() {
return this.nickservIdentified;
}
/**
*/
@java.lang.SuppressWarnings("all")
@javax.annotation.Generated("lombok")
protected void setNickservIdentified(final boolean nickservIdentified) {
this.nickservIdentified = nickservIdentified;
}
}