/*
* Copyright (c) 2006-2009 Chris Smith, Shane Mc Cormack, Gregory Holmes
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.dmdirc.parser.irc;
import com.dmdirc.parser.irc.callbacks.CallbackManager;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Map;
import java.util.Timer;
import java.util.Queue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* IRC Parser.
*
* @author Shane Mc Cormack
*/
public class IRCParser implements Runnable {
/** Max length an outgoing line should be (NOT including \r\n). */
public static final int MAX_LINELENGTH = 510;
/** General Debug Information. */
public static final int DEBUG_INFO = 1;
/** Socket Debug Information. */
public static final int DEBUG_SOCKET = 2;
/** Processing Manager Debug Information. */
public static final int DEBUG_PROCESSOR = 4;
/** List Mode Queue Debug Information. */
public static final int DEBUG_LMQ = 8;
// public static final int DEBUG_SOMETHING = 16; //Next thingy
/** Attempt to update user host all the time, not just on Who/Add/NickChange. */
static final boolean ALWAYS_UPDATECLIENT = true;
/** Byte used to show that a non-boolean mode is a list (b). */
static final byte MODE_LIST = 1;
/** Byte used to show that a non-boolean mode is not a list, and requires a parameter to set (lk). */
static final byte MODE_SET = 2;
/** Byte used to show that a non-boolean mode is not a list, and requires a parameter to unset (k). */
static final byte MODE_UNSET = 4;
/**
* This is what the user wants settings to be.
* Nickname here is *not* always accurate.
* ClientInfo variable tParser.getMyself() should be used for accurate info.
*/
public MyInfo me = new MyInfo();
/** Server Info requested by user. */
public ServerInfo server = new ServerInfo();
/** Timer for server ping. */
private Timer pingTimer = null;
/** Length of time to wait between ping stuff. */
private long pingTimerLength = 10000;
/** Is a ping needed? */
private volatile AtomicBoolean pingNeeded = new AtomicBoolean(false);
/** Time last ping was sent at. */
private long pingTime;
/** Current Server Lag. */
private long serverLag;
/** Last value sent as a ping argument. */
private String lastPingValue = "";
/**
* Count down to next ping.
* The timer fires every 10 seconds, this value is decreased every time the
* timer fires.
* Once it reaches 0, we send a ping, and reset it to 6, this means we ping
* the server every minute.
*
* @see setPingCountDownLength
*/
private byte pingCountDown;
/**
* Amount of times the timer has to fire for inactivity before sending a ping.
*
* @see setPingCountDownLength
*/
private byte pingCountDownLength = 6;
/** Name the server calls itself. */
String sServerName;
/** Network name. This is "" if no network name is provided */
String sNetworkName;
/** This is what we think the nickname should be. */
String sThinkNickname;
/** When using inbuilt pre-001 NickInUse handler, have we tried our AltNick. */
boolean triedAlt;
/** Have we recieved the 001. */
boolean got001;
/** Have we fired post005? */
boolean post005;
/** Has the thread started execution yet, (Prevents run() being called multiple times). */
boolean hasBegan;
/** Hashtable storing known prefix modes (ohv). */
final Map hPrefixModes = new Hashtable();
/**
* Hashtable maping known prefix modes (ohv) to prefixes (@%+) - Both ways.
* Prefix map contains 2 pairs for each mode. (eg @ => o and o => @)
*/
final Map hPrefixMap = new Hashtable();
/** Integer representing the next avaliable integer value of a prefix mode. */
long nNextKeyPrefix = 1;
/** Hashtable storing known user modes (owxis etc). */
final Map hUserModes = new Hashtable();
/** Integer representing the next avaliable integer value of a User mode. */
long nNextKeyUser = 1;
/**
* Hashtable storing known boolean chan modes (cntmi etc).
* Valid Boolean Modes are stored as Hashtable.pub('m',1); where 'm' is the mode and 1 is a numeric value.
* Numeric values are powers of 2. This allows up to 32 modes at present (expandable to 64)
* ChannelInfo/ChannelClientInfo etc provide methods to view the modes in a human way.
*
* Channel modes discovered but not listed in 005 are stored as boolean modes automatically (and a ERROR_WARNING Error is called)
*/
final Map hChanModesBool = new Hashtable();
/** Integer representing the next avaliable integer value of a Boolean mode. */
long nNextKeyCMBool = 1;
/**
* Hashtable storing known non-boolean chan modes (klbeI etc).
* Non Boolean Modes (for Channels) are stored together in this hashtable, the value param
* is used to show the type of variable. (List (1), Param just for set (2), Param for Set and Unset (2+4=6))
*
* see MODE_LIST
* see MODE_SET
* see MODE_UNSET
*/
final Map hChanModesOther = new Hashtable();
/** The last line of input recieved from the server */
String lastLine = "";
/** Should the lastline (where given) be appended to the "data" part of any onErrorInfo call? */
boolean addLastLine = false;
/**
* Channel Prefixes (ie # + etc).
* The "value" for these is always true.
*/
final Map hChanPrefix = new Hashtable();
/** Hashtable storing all known clients based on nickname (in lowercase). */
private final Map hClientList = new Hashtable();
/** Hashtable storing all known channels based on chanel name (inc prefix - in lowercase). */
private final Map hChannelList = new Hashtable();
/** Reference to the ClientInfo object that references ourself. */
private ClientInfo cMyself = new ClientInfo(this, "myself").setFake(true);
/** Hashtable storing all information gathered from 005. */
final Map h005Info = new Hashtable();
/** Ignore List. */
RegexStringList myIgnoreList = new RegexStringList();
/** Reference to the callback Manager. */
CallbackManager myCallbackManager = new CallbackManager(this);
/** Reference to the Processing Manager. */
ProcessingManager myProcessingManager = new ProcessingManager(this);
/** Should we automatically disconnect on fatal errors?. */
private boolean disconnectOnFatal = true;
/** Current Socket State. */
protected SocketState currentSocketState = SocketState.NULL;
/** This is the socket used for reading from/writing to the IRC server. */
private Socket socket;
/** Used for writing to the server. */
private PrintWriter out;
/** Used for reading from the server. */
private BufferedReader in;
/** This is the default TrustManager for SSL Sockets, it trusts all ssl certs. */
private final TrustManager[] trustAllCerts = {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() { return null; }
@Override
public void checkClientTrusted(final X509Certificate[] certs, final String authType) { }
@Override
public void checkServerTrusted(final X509Certificate[] certs, final String authType) { }
},
};
/** Should fake (channel)clients be created for callbacks where they do not exist? */
boolean createFake = false;
/** Should channels automatically request list modes? */
boolean autoListMode = true;
/** Should part/quit/kick callbacks be fired before removing the user internally? */
boolean removeAfterCallback = true;
/** This is the TrustManager used for SSL Sockets. */
private TrustManager[] myTrustManager = trustAllCerts;
/** The KeyManagers used for client certificates for SSL sockets. */
private KeyManager[] myKeyManagers;
/** This is the IP we want to bind to. */
private String bindIP = "";
/**
* Default constructor, ServerInfo and MyInfo need to be added separately (using IRC.me and IRC.server).
*/
public IRCParser() { this(null, null); }
/**
* Constructor with ServerInfo, MyInfo needs to be added separately (using IRC.me).
*
* @param serverDetails Server information.
*/
public IRCParser(final ServerInfo serverDetails) { this(null, serverDetails); }
/**
* Constructor with MyInfo, ServerInfo needs to be added separately (using IRC.server).
*
* @param myDetails Client information.
*/
public IRCParser(final MyInfo myDetails) { this(myDetails, null); }
/**
* Constructor with ServerInfo and MyInfo.
*
* @param serverDetails Server information.
* @param myDetails Client information.
*/
public IRCParser(final MyInfo myDetails, final ServerInfo serverDetails) {
if (myDetails != null) { this.me = myDetails; }
if (serverDetails != null) { this.server = serverDetails; }
resetState();
}
/**
* Get the current Value of bindIP.
*
* @return Value of bindIP ("" for default IP)
*/
public String getBindIP() { return bindIP; }
/**
* Set the current Value of bindIP.
*
* @param newValue New value to set bindIP
*/
public void setBindIP(final String newValue) { bindIP = newValue; }
/**
* Get the current Value of createFake.
*
* @return Value of createFake (true if fake clients will be added for callbacks, else false)
*/
public boolean getCreateFake() { return createFake; }
/**
* Set the current Value of createFake.
*
* @param newValue New value to set createFake
*/
public void setCreateFake(final boolean newValue) { createFake = newValue; }
/**
* Get the current Value of autoListMode.
*
* @return Value of autoListMode (true if channels automatically ask for list modes on join, else false)
*/
public boolean getAutoListMode() { return autoListMode; }
/**
* Set the current Value of autoListMode.
*
* @param newValue New value to set autoListMode
*/
public void setAutoListMode(final boolean newValue) { autoListMode = newValue; }
/**
* Get the current Value of removeAfterCallback.
*
* @return Value of removeAfterCallback (true if kick/part/quit callbacks are fired before internal removal)
*/
public boolean getRemoveAfterCallback() { return removeAfterCallback; }
/**
* Get the current Value of removeAfterCallback.
*
* @param newValue New value to set removeAfterCallback
*/
public void setRemoveAfterCallback(final boolean newValue) { removeAfterCallback = newValue; }
/**
* Get the current Value of addLastLine.
*
* @return Value of addLastLine (true if lastLine info will be automatically
* added to the errorInfo data line). This should be true if lastLine
* isn't handled any other way.
*/
public boolean getAddLastLine() { return addLastLine; }
/**
* Get the current Value of addLastLine.
* This returns "this" and thus can be used in the construction line.
*
* @param newValue New value to set addLastLine
*/
public void setAddLastLine(final boolean newValue) { addLastLine = newValue; }
/**
* Get the current socket State.
*
* @since 0.6.3
* @return Current {@link SocketState}
*/
public SocketState getSocketState() { return currentSocketState; }
/**
* Get a reference to the Processing Manager.
*
* @return Reference to the CallbackManager
*/
public ProcessingManager getProcessingManager() { return myProcessingManager; }
/**
* Get a reference to the CallbackManager.
*
* @return Reference to the CallbackManager
*/
public CallbackManager getCallbackManager() { return myCallbackManager; }
/**
* Get a reference to the default TrustManager for SSL Sockets.
*
* @return a reference to trustAllCerts
*/
public TrustManager[] getDefaultTrustManager() { return trustAllCerts; }
/**
* Get a reference to the current TrustManager for SSL Sockets.
*
* @return a reference to myTrustManager;
*/
public TrustManager[] getTrustManager() { return myTrustManager; }
/**
* Replace the current TrustManager for SSL Sockets with a new one.
*
* @param newTrustManager Replacement TrustManager for SSL Sockets.
*/
public void setTrustManager(final TrustManager[] newTrustManager) { myTrustManager = newTrustManager; }
/**
* Replace the current KeyManagers for SSL Sockets with a new set.
*
* @param newKeyManagers Replacement KeyManagers for SSL Sockets.
*/
public void setKeyManagers(final KeyManager[] newKeyManagers) { myKeyManagers = newKeyManagers; }
/**
* Get a reference to the ignorelist.
*
* @return a reference to the ignorelist
*/
public RegexStringList getIgnoreList() { return myIgnoreList; }
/**
* Replaces the current ignorelist with a new one.
*
* @param ignoreList Replacement ignorelist
*/
public void setIgnoreList(final RegexStringList ignoreList) { myIgnoreList = ignoreList; }
//---------------------------------------------------------------------------
// Start Callbacks
//---------------------------------------------------------------------------
/**
* Callback to all objects implementing the ServerError Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IServerError
* @param message The error message
* @return true if a method was called, false otherwise
*/
protected boolean callServerError(final String message) {
return myCallbackManager.getCallbackType("OnServerError").call(message);
}
/**
* Callback to all objects implementing the DataIn Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IDataIn
* @param data Incomming Line.
* @return true if a method was called, false otherwise
*/
protected boolean callDataIn(final String data) {
return myCallbackManager.getCallbackType("OnDataIn").call(data);
}
/**
* Callback to all objects implementing the DataOut Callback.
*
* @param data Outgoing Data
* @param fromParser True if parser sent the data, false if sent using .sendLine
* @return true if a method was called, false otherwise
* @see com.dmdirc.parser.irc.callbacks.interfaces.IDataOut
*/
protected boolean callDataOut(final String data, final boolean fromParser) {
return myCallbackManager.getCallbackType("OnDataOut").call(data, fromParser);
}
/**
* Callback to all objects implementing the DebugInfo Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IDebugInfo
* @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc)
* @param data Debugging Information as a format string
* @param args Formatting String Options
* @return true if a method was called, false otherwise
*/
protected boolean callDebugInfo(final int level, final String data, final Object... args) {
return callDebugInfo(level, String.format(data, args));
}
/**
* Callback to all objects implementing the DebugInfo Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IDebugInfo
* @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc)
* @param data Debugging Information
* @return true if a method was called, false otherwise
*/
protected boolean callDebugInfo(final int level, final String data) {
return myCallbackManager.getCallbackType("OnDebugInfo").call(level, data);
}
/**
* Callback to all objects implementing the IErrorInfo Interface.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IErrorInfo
* @param errorInfo ParserError object representing the error.
* @return true if a method was called, false otherwise
*/
protected boolean callErrorInfo(final ParserError errorInfo) {
return myCallbackManager.getCallbackType("OnErrorInfo").call(errorInfo);
}
/**
* Callback to all objects implementing the IConnectError Interface.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IConnectError
* @param errorInfo ParserError object representing the error.
* @return true if a method was called, false otherwise
*/
protected boolean callConnectError(final ParserError errorInfo) {
return myCallbackManager.getCallbackType("OnConnectError").call(errorInfo);
}
/**
* Callback to all objects implementing the SocketClosed Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.ISocketClosed
* @return true if a method was called, false otherwise
*/
protected boolean callSocketClosed() {
return myCallbackManager.getCallbackType("OnSocketClosed").call();
}
/**
* Callback to all objects implementing the PingFailed Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IPingFailed
* @return true if a method was called, false otherwise
*/
protected boolean callPingFailed() {
return myCallbackManager.getCallbackType("OnPingFailed").call();
}
/**
* Callback to all objects implementing the PingSent Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSent
* @return true if a method was called, false otherwise
*/
protected boolean callPingSent() {
return myCallbackManager.getCallbackType("OnPingSent").call();
}
/**
* Callback to all objects implementing the PingSuccess Callback.
*
* @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSuccess
* @return true if a method was called, false otherwise
*/
protected boolean callPingSuccess() {
return myCallbackManager.getCallbackType("OnPingSuccess").call();
}
/**
* Callback to all objects implementing the Post005 Callback.
*
* @return true if any callbacks were called.
* @see IPost005
*/
protected synchronized boolean callPost005() {
if (post005) { return false; }
post005 = true;
return getCallbackManager().getCallbackType("OnPost005").call();
}
//---------------------------------------------------------------------------
// End Callbacks
//---------------------------------------------------------------------------
/** Reset internal state (use before connect). */
private void resetState() {
// Reset General State info
triedAlt = false;
got001 = false;
post005 = false;
// Clear the hash tables
hChannelList.clear();
hClientList.clear();
h005Info.clear();
hPrefixModes.clear();
hPrefixMap.clear();
hChanModesOther.clear();
hChanModesBool.clear();
hUserModes.clear();
hChanPrefix.clear();
// Reset the mode indexes
nNextKeyPrefix = 1;
nNextKeyCMBool = 1;
nNextKeyUser = 1;
sServerName = "";
sNetworkName = "";
lastLine = "";
cMyself = new ClientInfo(this, "myself").setFake(true);
if (pingTimer != null) {
pingTimer.cancel();
pingTimer = null;
}
currentSocketState = SocketState.CLOSED;
// Char Mapping
updateCharArrays((byte)4);
}
/**
* Called after other error callbacks.
* CallbackOnErrorInfo automatically calls this *AFTER* any registered callbacks
* for it are called.
*
* @param errorInfo ParserError object representing the error.
* @param called True/False depending on the the success of other callbacks.
*/
public void onPostErrorInfo(final ParserError errorInfo, final boolean called) {
if (errorInfo.isFatal() && disconnectOnFatal) {
disconnect("Fatal Parser Error");
}
}
/**
* Get the current Value of disconnectOnFatal.
*
* @return Value of disconnectOnFatal (true if the parser automatically disconnects on fatal errors, else false)
*/
public boolean getDisconnectOnFatal() { return disconnectOnFatal; }
/**
* Set the current Value of disconnectOnFatal.
*
* @param newValue New value to set disconnectOnFatal
*/
public void setDisconnectOnFatal(final boolean newValue) { disconnectOnFatal = newValue; }
/**
* Connect to IRC.
*
* @throws IOException if the socket can not be connected
* @throws UnknownHostException if the hostname can not be resolved
* @throws NoSuchAlgorithmException if SSL is not available
* @throws KeyManagementException if the trustManager is invalid
*/
private void connect() throws UnknownHostException, IOException, NoSuchAlgorithmException, KeyManagementException {
resetState();
callDebugInfo(DEBUG_SOCKET, "Connecting to " + server.getHost() + ":" + server.getPort());
if (server.getPort() > 65535 || server.getPort() <= 0) {
throw new IOException("Server port ("+server.getPort()+") is invalid.");
}
if (server.getUseSocks()) {
callDebugInfo(DEBUG_SOCKET, "Using Proxy");
if (bindIP != null && !bindIP.isEmpty()) {
callDebugInfo(DEBUG_SOCKET, "IP Binding is not possible when using a proxy.");
}
if (server.getProxyPort() > 65535 || server.getProxyPort() <= 0) {
throw new IOException("Proxy port ("+server.getProxyPort()+") is invalid.");
}
final Proxy.Type proxyType = Proxy.Type.SOCKS;
socket = new Socket(new Proxy(proxyType, new InetSocketAddress(server.getProxyHost(), server.getProxyPort())));
currentSocketState = SocketState.OPEN;
if (server.getProxyUser() != null && !server.getProxyUser().isEmpty()) {
IRCAuthenticator.getIRCAuthenticator().addAuthentication(server);
}
socket.connect(new InetSocketAddress(server.getHost(), server.getPort()));
} else {
callDebugInfo(DEBUG_SOCKET, "Not using Proxy");
if (!server.getSSL()) {
socket = new Socket();
if (bindIP != null && !bindIP.isEmpty()) {
callDebugInfo(DEBUG_SOCKET, "Binding to IP: "+bindIP);
try {
socket.bind(new InetSocketAddress(InetAddress.getByName(bindIP), 0));
} catch (IOException e) {
callDebugInfo(DEBUG_SOCKET, "Binding failed: "+e.getMessage());
}
}
currentSocketState = SocketState.OPEN;
socket.connect(new InetSocketAddress(server.getHost(), server.getPort()));
}
}
if (server.getSSL()) {
callDebugInfo(DEBUG_SOCKET, "Server is SSL.");
if (myTrustManager == null) { myTrustManager = trustAllCerts; }
final SSLContext sc = SSLContext.getInstance("SSL");
sc.init(myKeyManagers, myTrustManager, new java.security.SecureRandom());
final SSLSocketFactory socketFactory = sc.getSocketFactory();
if (server.getUseSocks()) {
socket = socketFactory.createSocket(socket, server.getHost(), server.getPort(), false);
} else {
if (bindIP == null || bindIP.isEmpty()) {
socket = socketFactory.createSocket(server.getHost(), server.getPort());
} else {
callDebugInfo(DEBUG_SOCKET, "Binding to IP: "+bindIP);
try {
socket = socketFactory.createSocket(server.getHost(), server.getPort(), InetAddress.getByName(bindIP), 0);
} catch (UnknownHostException e) {
callDebugInfo(DEBUG_SOCKET, "Bind failed: "+e.getMessage());
socket = socketFactory.createSocket(server.getHost(), server.getPort());
}
}
}
currentSocketState = SocketState.OPEN;
}
callDebugInfo(DEBUG_SOCKET, "\t-> Opening socket output stream PrintWriter");
out = new PrintWriter(socket.getOutputStream(), true);
callDebugInfo(DEBUG_SOCKET, "\t-> Opening socket input stream BufferedReader");
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
callDebugInfo(DEBUG_SOCKET, "\t-> Socket Opened");
}
/**
* Send server connection strings (NICK/USER/PASS).
*/
protected void sendConnectionStrings() {
if (!server.getPassword().isEmpty()) {
sendString("PASS " + server.getPassword());
}
setNickname(me.getNickname());
String localhost;
try {
localhost = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException uhe) {
localhost = "*";
}
sendString("USER " + me.getUsername() + " "+localhost+" "+server.getHost()+" :" + me.getRealname());
}
/**
* Handle an onConnect error.
*
* @param e Exception to handle
*/
private void handleConnectException(final Exception e) {
callDebugInfo(DEBUG_SOCKET, "Error Connecting (" + e.getMessage() + "), Aborted");
final ParserError ei = new ParserError(ParserError.ERROR_ERROR, "Exception with server socket", getLastLine());
ei.setException(e);
callConnectError(ei);
if (currentSocketState != SocketState.CLOSED) {
currentSocketState = SocketState.CLOSED;
callSocketClosed();
}
resetState();
}
/**
* Begin execution.
* Connect to server, and start parsing incomming lines
*/
@Override
public void run() {
callDebugInfo(DEBUG_INFO, "Begin Thread Execution");
if (hasBegan) { return; } else { hasBegan = true; }
try {
connect();
} catch (UnknownHostException e) {
handleConnectException(e);
return;
} catch (IOException e) {
handleConnectException(e);
return;
} catch (NoSuchAlgorithmException e) {
handleConnectException(e);
return;
} catch (KeyManagementException e) {
handleConnectException(e);
return;
}
callDebugInfo(DEBUG_SOCKET, "Socket Connected");
sendConnectionStrings();
while (true) {
try {
lastLine = in.readLine(); // Blocking :/
if (lastLine == null) {
if (currentSocketState != SocketState.CLOSED) {
currentSocketState = SocketState.CLOSED;
callSocketClosed();
}
resetState();
break;
} else {
processLine(lastLine);
}
} catch (IOException e) {
callDebugInfo(DEBUG_SOCKET, "Exception in main loop (" + e.getMessage() + "), Aborted");
if (currentSocketState != SocketState.CLOSED) {
currentSocketState = SocketState.CLOSED;
callSocketClosed();
}
resetState();
break;
}
}
callDebugInfo(DEBUG_INFO, "End Thread Execution");
}
/**
* Get the current local port number.
*
* @return 0 if not connected, else the current local port number
*/
public int getLocalPort() {
if (currentSocketState == SocketState.OPEN) {
return socket.getLocalPort();
} else {
return 0;
}
}
/** Close socket on destroy. */
@Override
protected void finalize() throws Throwable {
try { socket.close(); }
catch (IOException e) {
callDebugInfo(DEBUG_SOCKET, "Could not close socket");
}
super.finalize();
}
/**
* Get the trailing parameter for a line.
* The parameter is everything after the first occurance of " :" ot the last token in the line after a space.
*
* @param line Line to get parameter for
* @return Parameter of the line
*/
public static String getParam(final String line) {
String[] params = null;
params = line.split(" :", 2);
return params[params.length - 1];
}
/**
* Tokenise a line.
* splits by " " up to the first " :" everything after this is a single token
*
* @param line Line to tokenise
* @return Array of tokens
*/
public static String[] tokeniseLine(final String line) {
if (line == null) {
return new String[]{"", }; // Return empty string[]
}
final int lastarg = line.indexOf(" :");
String[] tokens;
if (lastarg > -1) {
final String[] temp = line.substring(0, lastarg).split(" ");
tokens = new String[temp.length + 1];
System.arraycopy(temp, 0, tokens, 0, temp.length);
tokens[temp.length] = line.substring(lastarg + 2);
} else {
tokens = line.split(" ");
}
return tokens;
}
/**
* Get the ClientInfo object for a person.
*
* @param sHost Who can be any valid identifier for a client as long as it contains a nickname (?:)nick(?!ident)(?@host)
* @return ClientInfo Object for the client, or null
*/
public ClientInfo getClientInfo(final String sHost) {
final String sWho = getIRCStringConverter().toLowerCase(ClientInfo.parseHost(sHost));
if (hClientList.containsKey(sWho)) { return hClientList.get(sWho); }
else { return null; }
}
/**
* Get the ClientInfo object for a person, or create a fake client info object.
*
* @param sHost Who can be any valid identifier for a client as long as it contains a nickname (?:)nick(?!ident)(?@host)
* @return ClientInfo Object for the client.
*/
public ClientInfo getClientInfoOrFake(final String sHost) {
final String sWho = getIRCStringConverter().toLowerCase(ClientInfo.parseHost(sHost));
if (hClientList.containsKey(sWho)) { return hClientList.get(sWho); }
else { return new ClientInfo(this, sHost).setFake(true); }
}
/**
* Get the ChannelInfo object for a channel.
*
* @param sWhat This is the name of the channel.
* @return ChannelInfo Object for the channel, or null
*/
public ChannelInfo getChannelInfo(String sWhat) {
synchronized (hChannelList) {
sWhat = getIRCStringConverter().toLowerCase(sWhat);
if (hChannelList.containsKey(sWhat)) { return hChannelList.get(sWhat); } else { return null; }
}
}
/**
* Send a line to the server.
*
* @param line Line to send (\r\n termination is added automatically)
*/
public void sendLine(final String line) { doSendString(line, false); }
/**
* Send a line to the server and add proper line ending.
*
* @param line Line to send (\r\n termination is added automatically)
*/
protected void sendString(final String line) { doSendString(line, true); }
/**
* Send a line to the server and add proper line ending.
*
* @param line Line to send (\r\n termination is added automatically)
* @param fromParser is this line from the parser? (used for callDataOut)
*/
protected void doSendString(final String line, final boolean fromParser) {
if (out == null) { return; }
callDataOut(line, fromParser);
out.printf("%s\r\n", line);
final String[] newLine = tokeniseLine(line);
if (newLine[0].equalsIgnoreCase("away") && newLine.length > 1) {
cMyself.setAwayReason(newLine[newLine.length-1]);
} else if (newLine[0].equalsIgnoreCase("mode") && newLine.length == 3) {
// This makes sure we don't add the same item to the LMQ twice, even if its requested twice,
// as the ircd will only reply once.
final LinkedList foundModes = new LinkedList();
final ChannelInfo channel = getChannelInfo(newLine[1]);
if (channel != null) {
final Queue listModeQueue = channel.getListModeQueue();
for (int i = 0; i < newLine[2].length() ; ++i) {
final Character mode = newLine[2].charAt(i);
callDebugInfo(DEBUG_LMQ, "Intercepted mode request for "+channel+" for mode "+mode);
if (hChanModesOther.containsKey(mode) && hChanModesOther.get(mode) == MODE_LIST) {
if (foundModes.contains(mode)) {
callDebugInfo(DEBUG_LMQ, "Already added to LMQ");
} else {
listModeQueue.offer(mode);
foundModes.offer(mode);
callDebugInfo(DEBUG_LMQ, "Added to LMQ");
}
}
}
}
}
}
/**
* Get the network name given in 005.
*
* @return network name from 005
*/
public String getNetworkName() {
return sNetworkName;
}
/**
* Get the server name given in 001.
*
* @return server name from 001
*/
public String getServerName() {
return sServerName;
}
/**
* Get the last line of input recieved from the server.
*
* @return the last line of input recieved from the server.
*/
public String getLastLine() {
return lastLine;
}
/**
* Process a line and call relevent methods for handling.
*
* @param line IRC Line to process
*/
protected void processLine(final String line) {
callDataIn(line);
final String[] token = tokeniseLine(line);
int nParam;
setPingNeeded(false);
// pingCountDown = pingCountDownLength;
if (token.length < 2) {
return;
}
try {
final String sParam = token[1];
if (token[0].equalsIgnoreCase("PING") || token[1].equalsIgnoreCase("PING")) {
sendString("PONG :" + sParam);
} else if (token[0].equalsIgnoreCase("PONG") || token[1].equalsIgnoreCase("PONG")) {
if (!lastPingValue.isEmpty() && lastPingValue.equals(token[token.length-1])) {
lastPingValue = "";
serverLag = System.currentTimeMillis() - pingTime;
callPingSuccess();
}
} else if (token[0].equalsIgnoreCase("ERROR")) {
final StringBuilder errorMessage = new StringBuilder();
for (int i = 1; i < token.length; ++i) { errorMessage.append(token[i]); }
callServerError(errorMessage.toString());
} else {
if (got001) {
// Freenode sends a random notice in a stupid place, others might do aswell
// These shouldn't cause post005 to be fired, so handle them here.
if (token[0].equalsIgnoreCase("NOTICE")) {
try { myProcessingManager.process("Notice Auth", token); } catch (ProcessorNotFoundException e) { }
return;
}
if (!post005) {
try { nParam = Integer.parseInt(token[1]); } catch (NumberFormatException e) { nParam = -1; }
if (nParam < 0 || nParam > 5) {
callPost005();
}
}
// After 001 we potentially care about everything!
try { myProcessingManager.process(sParam, token); }
catch (ProcessorNotFoundException e) { }
} else {
// Before 001 we don't care about much.
try { nParam = Integer.parseInt(token[1]); } catch (NumberFormatException e) { nParam = -1; }
switch (nParam) {
case 1: // 001 - Welcome to IRC
case 464: // Password Required
case 433: // Nick In Use
try { myProcessingManager.process(sParam, token); } catch (ProcessorNotFoundException e) { }
break;
default: // Unknown - Send to Notice Auth
// Some networks send a CTCP during the auth process, handle it
if (token.length > 3 && !token[3].isEmpty() && token[3].charAt(0) == (char)1 && token[3].charAt(token[3].length()-1) == (char)1) {
try { myProcessingManager.process(sParam, token); } catch (ProcessorNotFoundException e) { }
break;
}
// Some networks may send a NICK message if you nick change before 001
// Eat it up so that it isn't treated as a notice auth.
if (token[0].equalsIgnoreCase("NICK")) { break; }
// Otherwise, send to Notice Auth
try { myProcessingManager.process("Notice Auth", token); } catch (ProcessorNotFoundException e) { }
break;
}
}
}
} catch (Exception e) {
final ParserError ei = new ParserError(ParserError.ERROR_FATAL, "Fatal Exception in Parser.", getLastLine());
ei.setException(e);
callErrorInfo(ei);
}
}
/** The IRCStringConverter for this parser */
private IRCStringConverter stringConverter = null;
/**
* Get the IRCStringConverter used by this parser.
*
* @return the IRCStringConverter used by this parser. (will create a default
* one if none exists already);
*/
public IRCStringConverter getIRCStringConverter() {
if (stringConverter == null) {
stringConverter = new IRCStringConverter((byte)4);
}
return stringConverter;
}
/**
* Update the character arrays.
*
* @param limit Number of post-alphabetical characters to convert
* 0 = ascii encoding
* 3 = strict-rfc1459 encoding
* 4 = rfc1459 encoding
*/
protected void updateCharArrays(final byte limit) {
stringConverter = new IRCStringConverter(limit);
}
/**
* Get the known boolean chanmodes in 005 order.
* Modes are returned in the order that the ircd specifies the modes in 005
* with any newly-found modes (mode being set that wasn't specified in 005)
* being added at the end.
*
* @return All the currently known boolean modes
*/
public String getBoolChanModes005() {
// This code isn't the nicest, as Hashtable's don't lend themselves to being
// ordered.
// Order isn't really important, and this code only takes 3 lines of we
// don't care about it but ordered guarentees that on a specific ircd this
// method will ALWAYs return the same value.
final char[] modes = new char[hChanModesBool.size()];
long nTemp;
double pos;
for (char cTemp : hChanModesBool.keySet()) {
nTemp = hChanModesBool.get(cTemp);
// nTemp should never be less than 0
if (nTemp > 0) {
pos = Math.log(nTemp) / Math.log(2);
modes[(int)pos] = cTemp;
}
/* // Is there an easier way to find out the power of 2 value for a number?
// ie 1024 = 10, 512 = 9 ?
for (int i = 0; i < modes.length; i++) {
if (Math.pow(2, i) == (double) nTemp) {
modes[i] = cTemp;
break;
}
}*/
}
return new String(modes);
}
/**
* Process CHANMODES from 005.
*/
public void parseChanModes() {
final StringBuilder sDefaultModes = new StringBuilder("b,k,l,");
String[] bits = null;
String modeStr;
if (h005Info.containsKey("USERCHANMODES")) {
if (getIRCD(true).equalsIgnoreCase("dancer")) {
sDefaultModes.insert(0, "dqeI");
} else if (getIRCD(true).equalsIgnoreCase("austirc")) {
sDefaultModes.insert(0, "e");
}
modeStr = h005Info.get("USERCHANMODES");
char mode;
for (int i = 0; i < modeStr.length(); ++i) {
mode = modeStr.charAt(i);
if (!hPrefixModes.containsKey(mode) && sDefaultModes.indexOf(Character.toString(mode)) < 0) {
sDefaultModes.append(mode);
}
}
} else {
sDefaultModes.append("imnpstrc");
}
if (h005Info.containsKey("CHANMODES")) {
modeStr = h005Info.get("CHANMODES");
} else {
modeStr = sDefaultModes.toString();
h005Info.put("CHANMODES", modeStr);
}
bits = modeStr.split(",", 5);
if (bits.length < 4) {
modeStr = sDefaultModes.toString();
callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "CHANMODES String not valid. Using default string of \"" + modeStr + "\"", getLastLine()));
h005Info.put("CHANMODES", modeStr);
bits = modeStr.split(",", 5);
}
// resetState
hChanModesOther.clear();
hChanModesBool.clear();
nNextKeyCMBool = 1;
// List modes.
for (int i = 0; i < bits[0].length(); ++i) {
final Character cMode = bits[0].charAt(i);
callDebugInfo(DEBUG_INFO, "Found List Mode: %c", cMode);
if (!hChanModesOther.containsKey(cMode)) { hChanModesOther.put(cMode, MODE_LIST); }
}
// Param for Set and Unset.
final Byte nBoth = MODE_SET + MODE_UNSET;
for (int i = 0; i < bits[1].length(); ++i) {
final Character cMode = bits[1].charAt(i);
callDebugInfo(DEBUG_INFO, "Found Set/Unset Mode: %c", cMode);
if (!hChanModesOther.containsKey(cMode)) { hChanModesOther.put(cMode, nBoth); }
}
// Param just for Set
for (int i = 0; i < bits[2].length(); ++i) {
final Character cMode = bits[2].charAt(i);
callDebugInfo(DEBUG_INFO, "Found Set Only Mode: %c", cMode);
if (!hChanModesOther.containsKey(cMode)) { hChanModesOther.put(cMode, MODE_SET); }
}
// Boolean Mode
for (int i = 0; i < bits[3].length(); ++i) {
final Character cMode = bits[3].charAt(i);
callDebugInfo(DEBUG_INFO, "Found Boolean Mode: %c [%d]", cMode, nNextKeyCMBool);
if (!hChanModesBool.containsKey(cMode)) {
hChanModesBool.put(cMode, nNextKeyCMBool);
nNextKeyCMBool = nNextKeyCMBool * 2;
}
}
}
/**
* Get the known prefixmodes in priority order.
*
* @return All the currently known usermodes
*/
public String getPrefixModes() {
if (h005Info.containsKey("PREFIXSTRING")) {
return h005Info.get("PREFIXSTRING");
} else {
return "";
}
}
/**
* Get the known boolean chanmodes in alphabetical order.
* Modes are returned in alphabetic order
*
* @return All the currently known boolean modes
*/
public String getBoolChanModes() {
final char[] modes = new char[hChanModesBool.size()];
int i = 0;
for (char mode : hChanModesBool.keySet()) {
modes[i++] = mode;
}
// Alphabetically sort the array
Arrays.sort(modes);
return new String(modes);
}
/**
* Get the known List chanmodes.
* Modes are returned in alphabetical order
*
* @return All the currently known List modes
*/
public String getListChanModes() {
return getOtherModeString(MODE_LIST);
}
/**
* Get the known Set-Only chanmodes.
* Modes are returned in alphabetical order
*
* @return All the currently known Set-Only modes
*/
public String getSetOnlyChanModes() {
return getOtherModeString(MODE_SET);
}
/**
* Get the known Set-Unset chanmodes.
* Modes are returned in alphabetical order
*
* @return All the currently known Set-Unset modes
*/
public String getSetUnsetChanModes() {
return getOtherModeString((byte) (MODE_SET + MODE_UNSET));
}
/**
* Get modes from hChanModesOther that have a specific value.
* Modes are returned in alphabetical order
*
* @param nValue Value mode must have to be included
* @return All the currently known Set-Unset modes
*/
protected String getOtherModeString(final byte nValue) {
final char[] modes = new char[hChanModesOther.size()];
Byte nTemp;
int i = 0;
for (char cTemp : hChanModesOther.keySet()) {
nTemp = hChanModesOther.get(cTemp);
if (nTemp == nValue) { modes[i++] = cTemp; }
}
// Alphabetically sort the array
Arrays.sort(modes);
return new String(modes).trim();
}
/**
* Get the known usermodes.
* Modes are returned in the order specified by the ircd.
*
* @return All the currently known usermodes (returns "" if usermodes are unknown)
*/
public String getUserModeString() {
if (h005Info.containsKey("USERMODES")) {
return h005Info.get("USERMODES");
} else {
return "";
}
}
/**
* Process USERMODES from 004.
*/
protected void parseUserModes() {
final String sDefaultModes = "nwdoi";
String modeStr;
if (h005Info.containsKey("USERMODES")) {
modeStr = h005Info.get("USERMODES");
} else {
modeStr = sDefaultModes;
h005Info.put("USERMODES", sDefaultModes);
}
// resetState
hUserModes.clear();
nNextKeyUser = 1;
// Boolean Mode
for (int i = 0; i < modeStr.length(); ++i) {
final Character cMode = modeStr.charAt(i);
callDebugInfo(DEBUG_INFO, "Found User Mode: %c [%d]", cMode, nNextKeyUser);
if (!hUserModes.containsKey(cMode)) {
hUserModes.put(cMode, nNextKeyUser);
nNextKeyUser = nNextKeyUser * 2;
}
}
}
/**
* Process CHANTYPES from 005.
*/
protected void parseChanPrefix() {
final String sDefaultModes = "#&";
String modeStr;
if (h005Info.containsKey("CHANTYPES")) {
modeStr = h005Info.get("CHANTYPES");
} else {
modeStr = sDefaultModes;
h005Info.put("CHANTYPES", sDefaultModes);
}
// resetState
hChanPrefix.clear();
// Boolean Mode
for (int i = 0; i < modeStr.length(); ++i) {
final Character cMode = modeStr.charAt(i);
callDebugInfo(DEBUG_INFO, "Found Chan Prefix: %c", cMode);
if (!hChanPrefix.containsKey(cMode)) { hChanPrefix.put(cMode, true); }
}
}
/**
* Process PREFIX from 005.
*/
public void parsePrefixModes() {
final String sDefaultModes = "(ohv)@%+";
String[] bits;
String modeStr;
if (h005Info.containsKey("PREFIX")) {
modeStr = h005Info.get("PREFIX");
} else {
modeStr = sDefaultModes;
}
if (modeStr.substring(0, 1).equals("(")) {
modeStr = modeStr.substring(1);
} else {
modeStr = sDefaultModes.substring(1);
h005Info.put("PREFIX", sDefaultModes);
}
bits = modeStr.split("\\)", 2);
if (bits.length != 2 || bits[0].length() != bits[1].length()) {
modeStr = sDefaultModes;
callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "PREFIX String not valid. Using default string of \"" + modeStr + "\"", getLastLine()));
h005Info.put("PREFIX", modeStr);
modeStr = modeStr.substring(1);
bits = modeStr.split("\\)", 2);
}
// resetState
hPrefixModes.clear();
hPrefixMap.clear();
nNextKeyPrefix = 1;
for (int i = bits[0].length() - 1; i > -1; --i) {
final Character cMode = bits[0].charAt(i);
final Character cPrefix = bits[1].charAt(i);
callDebugInfo(DEBUG_INFO, "Found Prefix Mode: %c => %c [%d]", cMode, cPrefix, nNextKeyPrefix);
if (!hPrefixModes.containsKey(cMode)) {
hPrefixModes.put(cMode, nNextKeyPrefix);
hPrefixMap.put(cMode, cPrefix);
hPrefixMap.put(cPrefix, cMode);
nNextKeyPrefix = nNextKeyPrefix * 2;
}
}
h005Info.put("PREFIXSTRING", bits[0]);
}
/**
* Check if server is ready.
*
* @return true if 001 has been recieved, false otherwise.
*/
public boolean isReady() { return got001; }
/**
* Join a Channel.
*
* @param sChannelName Name of channel to join
*/
public void joinChannel(final String sChannelName) {
joinChannel(sChannelName, "", true);
}
/**
* Join a Channel.
*
* @param sChannelName Name of channel to join
* @param autoPrefix Automatically prepend the first channel prefix defined
* in 005 if sChannelName is an invalid channel.
* **This only applies to the first channel if given a list**
*/
public void joinChannel(final String sChannelName, final boolean autoPrefix) {
joinChannel(sChannelName, "", autoPrefix);
}
/**
* Join a Channel with a key.
*
* @param sChannelName Name of channel to join
* @param sKey Key to use to try and join the channel
*/
public void joinChannel(final String sChannelName, final String sKey) {
joinChannel(sChannelName, sKey, true);
}
/**
* Join a Channel with a key.
*
* @param sChannelName Name of channel to join
* @param sKey Key to use to try and join the channel
* @param autoPrefix Automatically prepend the first channel prefix defined
* in 005 if sChannelName is an invalid channel.
* **This only applies to the first channel if given a list**
*/
public void joinChannel(final String sChannelName, final String sKey, final boolean autoPrefix) {
final String channelName;
if (isValidChannelName(sChannelName)) {
channelName = sChannelName;
} else {
if (autoPrefix) {
if (h005Info.containsKey("CHANTYPES")) {
final String chantypes = h005Info.get("CHANTYPES");
if (chantypes.isEmpty()) {
channelName = "#" + sChannelName;
} else {
channelName = chantypes.charAt(0) + sChannelName;
}
} else {
return;
}
} else {
return;
}
}
if (sKey.isEmpty()) {
sendString("JOIN " + channelName);
} else {
sendString("JOIN " + channelName + " " + sKey);
}
}
/**
* Leave a Channel.
*
* @param sChannelName Name of channel to part
* @param sReason Reason for leaving (Nothing sent if sReason is "")
*/
public void partChannel(final String sChannelName, final String sReason) {
if (getChannelInfo(sChannelName) == null) { return; }
if (sReason.isEmpty()) {
sendString("PART " + sChannelName);
} else {
sendString("PART " + sChannelName + " :" + sReason);
}
}
/**
* Set Nickname.
*
* @param sNewNickName New nickname wanted.
*/
public void setNickname(final String sNewNickName) {
if (getSocketState() == SocketState.OPEN) {
if (!cMyself.isFake() && cMyself.getNickname().equals(sNewNickName)) {
return;
}
sendString("NICK " + sNewNickName);
} else {
me.setNickname(sNewNickName);
}
sThinkNickname = sNewNickName;
}
/**
* Get the max length a message can be.
*
* @param sType Type of message (ie PRIVMSG)
* @param sTarget Target for message (eg #DMDirc)
* @return Max Length message should be.
*/
public int getMaxLength(final String sType, final String sTarget) {
// If my host is "nick!user@host" and we are sending "#Channel"
// a "PRIVMSG" this will find the length of ":nick!user@host PRIVMSG #channel :"
// and subtract it from the MAX_LINELENGTH. This should be sufficient in most cases.
// Lint = the 2 ":" at the start and end and the 3 separating " "s
int length = 0;
if (sType != null) { length = length + sType.length(); }
if (sTarget != null) { length = length + sTarget.length(); }
return getMaxLength(length);
}
/**
* Get the max length a message can be.
*
* @param nLength Length of stuff. (Ie "PRIVMSG"+"#Channel")
* @return Max Length message should be.
*/
public int getMaxLength(final int nLength) {
final int lineLint = 5;
if (cMyself.isFake()) {
callErrorInfo(new ParserError(ParserError.ERROR_ERROR + ParserError.ERROR_USER, "getMaxLength() called, but I don't know who I am?", getLastLine()));
return MAX_LINELENGTH - nLength - lineLint;
} else {
return MAX_LINELENGTH - cMyself.toString().length() - nLength - lineLint;
}
}
/**
* Get the max number of list modes.
*
* @param mode The mode to know the max number for
* @return The max number of list modes for the given mode.
* - returns 0 if MAXLIST does not contain the mode, unless MAXBANS is
* set, then this is returned instead.
* - returns -1 if:
* - MAXLIST or MAXBANS were not in 005
* - Values for MAXLIST or MAXBANS were invalid (non integer, empty)
*/
public int getMaxListModes(final char mode) {
// MAXLIST=bdeI:50
// MAXLIST=b:60,e:60,I:60
// MAXBANS=30
int result = -2;
callDebugInfo(DEBUG_INFO, "Looking for maxlistmodes for: "+mode);
// Try in MAXLIST
if (h005Info.get("MAXLIST") != null) {
if (h005Info.get("MAXBANS") == null) {
result = 0;
}
final String maxlist = h005Info.get("MAXLIST");
callDebugInfo(DEBUG_INFO, "Found maxlist ("+maxlist+")");
final String[] bits = maxlist.split(",");
for (String bit : bits) {
final String[] parts = bit.split(":", 2);
callDebugInfo(DEBUG_INFO, "Bit: "+bit+" | parts.length = "+parts.length+" ("+parts[0]+" -> "+parts[0].indexOf(mode)+")");
if (parts.length == 2 && parts[0].indexOf(mode) > -1) {
callDebugInfo(DEBUG_INFO, "parts[0] = '"+parts[0]+"' | parts[1] = '"+parts[1]+"'");
try {
result = Integer.parseInt(parts[1]);
break;
} catch (NumberFormatException nfe) { result = -1; }
}
}
}
// If not in max list, try MAXBANS
if (result == -2 && h005Info.get("MAXBANS") != null) {
callDebugInfo(DEBUG_INFO, "Trying max bans");
try {
result = Integer.parseInt(h005Info.get("MAXBANS"));
} catch (NumberFormatException nfe) { result = -1; }
} else if (result == -2 && getIRCD(true).equalsIgnoreCase("weircd")) {
// -_-
result = 50;
} else if (result == -2) {
result = -1;
callDebugInfo(DEBUG_INFO, "Failed");
callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "Unable to discover max list modes.", getLastLine()));
}
callDebugInfo(DEBUG_INFO, "Result: "+result);
return result;
}
/**
* Send a private message to a target.
*
* @param sTarget Target
* @param sMessage Message to send
*/
public void sendMessage(final String sTarget, final String sMessage) {
if (sTarget == null || sMessage == null) { return; }
if (sTarget.isEmpty()/* || sMessage.isEmpty()*/) { return; }
sendString("PRIVMSG " + sTarget + " :" + sMessage);
}
/**
* Send a notice message to a target.
*
* @param sTarget Target
* @param sMessage Message to send
*/
public void sendNotice(final String sTarget, final String sMessage) {
if (sTarget == null || sMessage == null) { return; }
if (sTarget.isEmpty()/* || sMessage.isEmpty()*/) { return; }
sendString("NOTICE " + sTarget + " :" + sMessage);
}
/**
* Send a Action to a target.
*
* @param sTarget Target
* @param sMessage Action to send
*/
public void sendAction(final String sTarget, final String sMessage) {
sendCTCP(sTarget, "ACTION", sMessage);
}
/**
* Send a CTCP to a target.
*
* @param sTarget Target
* @param sType Type of CTCP
* @param sMessage Optional Additional Parameters
*/
public void sendCTCP(final String sTarget, final String sType, final String sMessage) {
if (sTarget == null || sMessage == null) { return; }
if (sTarget.isEmpty() || sType.isEmpty()) { return; }
final char char1 = (char) 1;
sendString("PRIVMSG " + sTarget + " :" + char1 + sType.toUpperCase() + " " + sMessage + char1);
}
/**
* Send a CTCPReply to a target.
*
* @param sTarget Target
* @param sType Type of CTCP
* @param sMessage Optional Additional Parameters
*/
public void sendCTCPReply(final String sTarget, final String sType, final String sMessage) {
if (sTarget == null || sMessage == null) { return; }
if (sTarget.isEmpty() || sType.isEmpty()) { return; }
final char char1 = (char) 1;
sendString("NOTICE " + sTarget + " :" + char1 + sType.toUpperCase() + " " + sMessage + char1);
}
/**
* Quit IRC.
* This method will wait for the server to close the socket.
*
* @param sReason Reason for quitting.
*/
public void quit(final String sReason) {
if (sReason.isEmpty()) {
sendString("QUIT");
} else {
sendString("QUIT :" + sReason);
}
}
/**
* Disconnect from server.
* This method will quit and automatically close the socket without waiting for
* the server.
*
* @param sReason Reason for quitting.
*/
public void disconnect(final String sReason) {
if (currentSocketState == SocketState.OPEN && got001) { quit(sReason); }
try {
if (socket != null) { socket.close(); }
} catch (IOException e) {
/* Do Nothing */
} finally {
if (currentSocketState != SocketState.CLOSED) {
currentSocketState = SocketState.CLOSED;
callSocketClosed();
}
resetState();
}
}
/**
* Check if a channel name is valid.
*
* @param sChannelName Channel name to test
* @return true if name is valid on the current connection, false otherwise.
* - Before channel prefixes are known (005/noMOTD/MOTDEnd), this checks
* that the first character is either #, &, ! or +
* - Assumes that any channel that is already known is valid, even if
* 005 disagrees.
*/
public boolean isValidChannelName(final String sChannelName) {
// Check sChannelName is not empty or null
if (sChannelName == null || sChannelName.isEmpty()) { return false; }
// Check its not ourself (PM recieved before 005)
if (getIRCStringConverter().equalsIgnoreCase(getMyNickname(), sChannelName)) { return false; }
// Check if we are already on this channel
if (getChannelInfo(sChannelName) != null) { return true; }
// Check if we know of any valid chan prefixes
if (hChanPrefix.isEmpty()) {
// We don't. Lets check against RFC2811-Specified channel types
final char first = sChannelName.charAt(0);
return first == '#' || first == '&' || first == '!' || first == '+';
}
// Otherwise return true if:
// Channel equals "0"
// first character of the channel name is a valid channel prefix.
return hChanPrefix.containsKey(sChannelName.charAt(0)) || sChannelName.equals("0");
}
/**
* Check if a given chanmode is user settable.
*
* @param mode Mode to test
* @return true if mode is settable by users, false if servers only
*/
public boolean isUserSettable(final Character mode) {
String validmodes;
if (h005Info.containsKey("USERCHANMODES")) {
validmodes = h005Info.get("USERCHANMODES");
} else {
validmodes = "bklimnpstrc";
}
return validmodes.matches(".*" + mode + ".*");
}
/**
* Get the 005 info.
*
* @return 005Info hashtable.
*/
public Map get005() { return h005Info; }
/**
* Get the name of the ircd.
*
* @param getType if this is false the string from 004 is returned. Else a guess of the type (ircu, hybrid, ircnet)
* @return IRCD Version or Type
*/
public String getIRCD(final boolean getType) {
if (h005Info.containsKey("004IRCD")) {
final String version = h005Info.get("004IRCD");
if (getType) {
// This ilst is vaugly based on http://searchirc.com/ircd-versions,
// but keeping groups of ircd's together (ie hybrid-based, ircu-based)
if (version.matches("(?i).*unreal.*")) { return "unreal"; }
else if (version.matches("(?i).*bahamut.*")) { return "bahamut"; }
else if (version.matches("(?i).*nefarious.*")) { return "nefarious"; }
else if (version.matches("(?i).*asuka.*")) { return "asuka"; }
else if (version.matches("(?i).*snircd.*")) { return "snircd"; }
else if (version.matches("(?i).*beware.*")) { return "bircd"; }
else if (version.matches("(?i).*u2\\.[0-9]+\\.H\\..*")) { return "irchispano"; }
else if (version.matches("(?i).*u2\\.[0-9]+\\..*")) { return "ircu"; }
else if (version.matches("(?i).*ircu.*")) { return "ircu"; }
else if (version.matches("(?i).*plexus.*")) { return "plexus"; }
else if (version.matches("(?i).*hybrid.*oftc.*")) { return "oftc-hybrid"; }
else if (version.matches("(?i).*ircd.hybrid.*")) { return "hybrid7"; }
else if (version.matches("(?i).*hybrid.*")) { return "hybrid"; }
else if (version.matches("(?i).*charybdis.*")) { return "charybdis"; }
else if (version.matches("(?i).*inspircd.*")) { return "inspircd"; }
else if (version.matches("(?i).*ultimateircd.*")) { return "ultimateircd"; }
else if (version.matches("(?i).*critenircd.*")) { return "critenircd"; }
else if (version.matches("(?i).*fqircd.*")) { return "fqircd"; }
else if (version.matches("(?i).*conferenceroom.*")) { return "conferenceroom"; }
else if (version.matches("(?i).*hyperion.*")) { return "hyperion"; }
else if (version.matches("(?i).*dancer.*")) { return "dancer"; }
else if (version.matches("(?i).*austhex.*")) { return "austhex"; }
else if (version.matches("(?i).*austirc.*")) { return "austirc"; }
else if (version.matches("(?i).*ratbox.*")) { return "ratbox"; }
else if (version.matches("(?i).*euircd.*")) { return "euircd"; }
else if (version.matches("(?i).*weircd.*")) { return "weircd"; }
else if (version.matches("(?i).*swiftirc.*")) { return "swiftirc"; }
else {
// Stupid networks/ircds go here...
if (sNetworkName.equalsIgnoreCase("ircnet")) { return "ircnet"; }
else if (sNetworkName.equalsIgnoreCase("starchat")) { return "starchat"; }
else if (sNetworkName.equalsIgnoreCase("bitlbee")) { return "bitlbee"; }
else if (h005Info.containsKey("003IRCD") && h005Info.get("003IRCD").matches("(?i).*bitlbee.*")) { return "bitlbee"; } // Older bitlbee
else { return "generic"; }
}
} else {
return version;
}
} else {
if (getType) { return "generic"; }
else { return ""; }
}
}
/**
* Get the time used for the ping Timer.
*
* @return current time used.
* @see setPingCountDownLength
*/
public long getPingTimerLength() { return pingTimerLength; }
/**
* Set the time used for the ping Timer.
* This will also reset the pingTimer.
*
* @param newValue New value to use.
* @see setPingCountDownLength
*/
public void setPingTimerLength(final long newValue) {
pingTimerLength = newValue;
startPingTimer();
}
/**
* Get the time used for the pingCountdown.
*
* @return current time used.
* @see setPingCountDownLength
*/
public byte getPingCountDownLength() { return pingCountDownLength; }
/**
* Set the time used for the ping countdown.
* The pingTimer fires every pingTimerLength/1000 seconds, whenever a line of data
* is received, the "waiting for ping" flag is set to false, if the line is
* a "PONG", then onPingSuccess is also called.
*
* When waiting for a ping reply, onPingFailed() is called every time the
* timer is fired.
*
* When not waiting for a ping reply, the pingCountDown is decreased by 1
* every time the timer fires, when it reaches 0 is is reset to
* pingCountDownLength and a PING is sent to the server.
*
* To ping the server after 30 seconds of inactivity you could use:
* pingTimerLength = 5000, pingCountDown = 6
* or
* pingTimerLength = 10000, pingCountDown = 3
*
* @param newValue New value to use.
* @see pingCountDown
* @see pingTimerLength
* @see pingTimerTask
*/
public void setPingCountDownLength(final byte newValue) {
pingCountDownLength = newValue;
}
/**
* Start the pingTimer.
*/
protected void startPingTimer() {
setPingNeeded(false);
if (pingTimer != null) { pingTimer.cancel(); }
pingTimer = new Timer("IRCParser pingTimer");
pingTimer.schedule(new PingTimer(this, pingTimer), 0, pingTimerLength);
pingCountDown = 1;
}
/**
* This is called when the ping Timer has been executed.
* As the timer is restarted on every incomming message, this will only be
* called when there has been no incomming line for 10 seconds.
*
* @param timer The timer that called this.
*/
protected void pingTimerTask(final Timer timer) {
if (pingTimer == null || !pingTimer.equals(timer)) { return; }
if (getPingNeeded()) {
if (!callPingFailed()) {
pingTimer.cancel();
disconnect("Server not responding.");
}
} else {
--pingCountDown;
if (pingCountDown < 1) {
pingTime = System.currentTimeMillis();
setPingNeeded(true);
pingCountDown = pingCountDownLength;
callPingSent();
lastPingValue = String.valueOf(System.currentTimeMillis());
sendLine("PING " + lastPingValue);
}
}
}
/**
* Get the current server lag.
*
* @return Last time between sending a PING and recieving a PONG
*/
public long getServerLag() {
return serverLag;
}
/**
* Get the current server lag.
*
* @param actualTime if True the value returned will be the actual time the ping was sent
* else it will be the amount of time sinse the last ping was sent.
* @return Time last ping was sent
*/
public long getPingTime(final boolean actualTime) {
if (actualTime) { return pingTime; }
else { return System.currentTimeMillis() - pingTime; }
}
/**
* Set if a ping is needed or not.
*
* @param newStatus new value to set pingNeeded to.
*/
private void setPingNeeded(final boolean newStatus) {
pingNeeded.set(newStatus);
}
/**
* Get if a ping is needed or not.
*
* @return value of pingNeeded.
*/
private boolean getPingNeeded() {
return pingNeeded.get();
}
/**
* Get a reference to the cMyself object.
*
* @return cMyself reference
*/
public ClientInfo getMyself() { return cMyself; }
/**
* Get the current nickname.
* If after 001 this returns the exact same as getMyself.getNickname();
* Before 001 it returns the nickname that the parser Thinks it has.
*
* @return Current nickname.
*/
public String getMyNickname() {
if (cMyself.isFake()) {
return sThinkNickname;
} else {
return cMyself.getNickname();
}
}
/**
* Get the current username (Specified in MyInfo on construction).
* Get the username given in MyInfo
*
* @return My username.
*/
public String getMyUsername() {
return me.getUsername();
}
/**
* Add a client to the ClientList.
*
* @param client Client to add
*/
public void addClient(final ClientInfo client) {
hClientList.put(getIRCStringConverter().toLowerCase(client.getNickname()),client);
}
/**
* Remove a client from the ClientList.
* This WILL NOT allow cMyself to be removed from the list.
*
* @param client Client to remove
*/
public void removeClient(final ClientInfo client) {
if (client != cMyself) {
forceRemoveClient(client);
}
}
/**
* Remove a client from the ClientList.
. * This WILL allow cMyself to be removed from the list
*
* @param client Client to remove
*/
protected void forceRemoveClient(final ClientInfo client) {
hClientList.remove(getIRCStringConverter().toLowerCase(client.getNickname()));
}
/**
* Get the number of known clients.
*
* @return Count of known clients
*/
public int knownClients() {
return hClientList.size();
}
/**
* Get the known clients as a collection.
*
* @return Known clients as a collection
*/
public Collection getClients() {
return hClientList.values();
}
/**
* Clear the client list.
*/
public void clearClients() {
hClientList.clear();
addClient(getMyself());
}
/**
* Add a channel to the ChannelList.
*
* @param channel Channel to add
*/
public void addChannel(final ChannelInfo channel) {
synchronized (hChannelList) {
hChannelList.put(getIRCStringConverter().toLowerCase(channel.getName()), channel);
}
}
/**
* Remove a channel from the ChannelList.
*
* @param channel Channel to remove
*/
public void removeChannel(final ChannelInfo channel) {
synchronized (hChannelList) {
hChannelList.remove(getIRCStringConverter().toLowerCase(channel.getName()));
}
}
/**
* Get the number of known channel.
*
* @return Count of known channel
*/
public int knownChannels() {
synchronized (hChannelList) {
return hChannelList.size();
}
}
/**
* Get the known channels as a collection.
*
* @return Known channels as a collection
*/
public Collection getChannels() {
synchronized (hChannelList) {
return hChannelList.values();
}
}
/**
* Clear the channel list.
*/
public void clearChannels() {
synchronized (hChannelList) {
hChannelList.clear();
}
}
}