/*
Copyright Paul James Mutton, 2001-2009, http://www.jibble.org/
This file is part of PircBot.
This software is dual-licensed, allowing you to choose between the GNU
General Public License (GPL) and the www.jibble.org Commercial License.
Since the GPL may be too restrictive for use in a proprietary application,
a commercial license is also provided. Full license information can be
found at http://www.jibble.org/licenses/
*/
package lib.pircbot;
import face.FaceManager;
import gui.forms.GUIMain;
import irc.message.MessageHandler;
import util.Constants;
import util.Utils;
import util.settings.Settings;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* PircBot 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 PircBot 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 PircBot.
* <p>
* To perform an action when the PircBot receives a normal message from the IRC
* server, you would override the onMessage method defined in the PircBot
* class. All on<i>XYZ</i> methods in the PircBot class are automatically called
* when the event <i>XYZ</i> happens, so you would override these if you wish
* to do something when it does happen.
* <p>
* Some event methods, such as onPing, should only really perform a specific
* function (i.e. respond to a PING from the server). For your convenience, such
* methods are already correctly implemented in the PircBot and should not
* normally need to be overridden. Please read the full documentation for each
* method to see which ones are already implemented by the PircBot class.
* <p>
* Please visit the PircBot homepage at
* <a href="http://www.jibble.org/pircbot.php">http://www.jibble.org/pircbot.php</a>
* for full revision history, a beginners guide to creating your first PircBot
* and a list of some existing Java IRC bots and clients that use the PircBot
* framework.
*
* @author Paul James Mutton,
* <a href="http://www.jibble.org/">http://www.jibble.org/</a>
* @version 1.5.0 (Build time: Mon Dec 14 20:07:17 2009)
*/
public class PircBot {
private PircBotConnection connection;
public ChannelManager getChannelManager() {
return Settings.channelManager;
}
private MessageHandler handler;
public MessageHandler getMessageHandler() {
return handler;
}
/**
* Constructs a PircBot with the default settings. Your own constructors
* in classes which extend the PircBot abstract class should be responsible
* for changing the default settings if required.
*/
public PircBot(MessageHandler messageHandler) {
handler = messageHandler;
connection = new PircBotConnection(this, PircBotConnection.ConnectionType.NORMAL);
}
/**
* Attempt to connect to the specified IRC server using the supplied
* password.
* The onConnect method is called upon success.
*
* @param hostname The hostname of the server to connect to.
* @param port The port number to connect to on the server.
*/
public boolean connect() {
if (connection.connect()) {
getMessageHandler().onConnect();
return true;
}
return false;
}
/**
* This method disconnects from the server cleanly by calling the
* quitServer() method. Providing the PircBot was connected to an
* IRC server, the onDisconnect() will be called as soon as the
* disconnection is made by the server.
*
* @see #quitServer() quitServer
* @see #quitServer(String) quitServer
*/
public void disconnect() {
quitServer();
}
/**
* Joins a channel.
*
* @param channel The name of the channel to join (eg "#cs").
*/
public void joinChannel(String channel) {
sendRawLine("JOIN " + channel);
getChannelManager().addChannel(new Channel(channel));
}
/**
* Parts a channel.
*
* @param channel The name of the channel to leave.
*/
public void partChannel(String channel) {
sendRawLine("PART " + channel);
getChannelManager().removeChannel(channel);
}
/**
* Quits from the IRC server.
* Providing we are actually connected to an IRC server, the
* onDisconnect() method will be called as soon as the IRC server
* disconnects us.
*/
public void quitServer() {
quitServer("");
}
/**
* Quits from the IRC server with a reason.
* Providing we are actually connected to an IRC server, the
* onDisconnect() method will be called as soon as the IRC server
* disconnects us.
*
* @param reason The reason for quitting the server.
*/
public void quitServer(String reason) {
sendRawLine("QUIT :" + reason);
}
/**
* Sends a raw line to the IRC server as soon as possible, bypassing the
* outgoing message queue.
*
* @param line The raw line to send to the IRC server.
*/
public void sendRawLine(String line) {
if (isConnected()) {
connection.getOutputThread().sendRawLine(line);
}
}
public boolean isConnected() {
return connection.isConnected();
}
public PircBotConnection getConnection() {
return connection;
}
/**
* Sends a message to a channel or a private message to a user. These
* messages are added to the outgoing message queue and sent at the
* earliest possible opportunity.
* <p>
* Some examples: -
* <pre> // Send the message "Hello!" to the channel #cs.
* sendMessage("#cs", "Hello!");
* <p>
* // Send a private message to Paul that says "Hi".
* sendMessage("Paul", "Hi");</pre>
* <p>
* You may optionally apply colours, boldness, underlining, etc to
* the message by using the <code>Colors</code> class.
*
* @param target The name of the channel or user nick to send to.
* @param message The message to send.
*/
public void sendMessage(String target, String message) {
if (message.startsWith("/w")) {
String[] split = message.split(" ", 3);
sendWhisper(split[1], split[2]);
getMessageHandler().onWhisper(getNick(), split[1], split[2]);
} else {
sendRawMessage(target, message);
if (message.startsWith("/me")) {
getMessageHandler().onAction(getNick(), target, message.substring(4));
} else {
getMessageHandler().onMessage(target, getNick(), message);
}
}
}
public void sendWhisper(String target, String message) {
sendRawWhisper("/w " + target + " " + message);
}
public void sendRawWhisper(String raw) {
if (isConnected())
connection.getOutQueue().add("PRIVMSG #jtv :" + raw);
else log("Whisper not connected!");
}
/**
* Sends a message that does not show up in the main GUI.
*
* @param channel The channel to send to.
* @param message The message to send.
*/
public void sendRawMessage(String channel, String message) {
if (isConnected())
connection.getOutQueue().add("PRIVMSG " + channel + " :" + message);
}
/**
* Adds a line to the log. This log is currently output to the standard
* output and is in the correct format for use by tools such as pisg, the
* Perl IRC Statistics Generator. You may override this method if you wish
* to do something else with log entries.
* Each line in the log begins with a number which
* represents the logging time (as the number of milliseconds since the
* epoch). This timestamp and the following log entry are separated by
* a single space character, " ". Outgoing messages are distinguishable
* by a log entry that has ">>>" immediately following the space character
* after the timestamp. DCC events use "+++" and warnings about unhandled
* Exceptions and Errors use "###".
* <p>
* This implementation of the method will only cause log entries to be
* output if the PircBot has had its verbose mode turned on by calling
* setVerbose(true);
*
* @param line The line to add to the log.
*/
public void log(String line) {
if (_verbose) System.out.println(System.currentTimeMillis() + " " + line);
}
/**
* This method handles events when any line of text arrives from the server,
* then calling the appropriate method in the PircBot.
*
* @param line The raw line of text from the server.
*/
public void handleLine(String line) {
String sourceNick = "";
String sourceLogin = "";
String sourceHostname = "";
StringTokenizer tokenizer = new StringTokenizer(line);
String tags = null;
String content = null;
if (line.startsWith("@")) {
tags = tokenizer.nextToken();
if (line.contains("USERSTATE")) {
parseUserstate(line);
return;
} else {
content = line.substring(line.indexOf(" :", line.indexOf(" :") + 2) + 2);
}
} else {
content = line.substring(line.indexOf(" :") + 2);
}
String senderInfo = tokenizer.nextToken();
String command = tokenizer.nextToken();
String target = null;
if (checkCommand(command, tags, line, content, tokenizer, senderInfo)) return;
int exclamation = senderInfo.indexOf("!");
int at = senderInfo.indexOf("@");
if (senderInfo.startsWith(":")) {
if (exclamation > 0 && at > 0 && exclamation < at) {
sourceNick = senderInfo.substring(1, exclamation);
sourceLogin = senderInfo.substring(exclamation + 1, at);
sourceHostname = senderInfo.substring(at + 1);
} else {
if (tokenizer.hasMoreTokens()) {
int code = -1;
try {
code = Integer.parseInt(command);
} catch (NumberFormatException ignored) {
}
if (code != -1) {
String response = line.substring(line.indexOf(command, senderInfo.length()) + 4, line.length());
processServerResponse(code, response);
// Return from the method.
return;
} else {
// This is not a server response.
// It must be a nick without login and hostname.
// (or maybe a NOTICE or suchlike from the server)
sourceNick = senderInfo;
target = command;
}
} else {
// We don't know what this line means.
onUnknown(line);
// Return from the method;
return;
}
}
}
if (sourceNick.startsWith(":")) {
sourceNick = sourceNick.substring(1);
}
command = command.toUpperCase();
if (target == null) {
target = tokenizer.nextToken();
}
if (target.startsWith(":")) {
target = target.substring(1);
}
String _channelPrefixes = "#&+!";
parseTags(tags, sourceNick, target);
// Check for CTCP requests.
if ("PRIVMSG".equals(command) && line.indexOf(":\u0001") > 0 && line.endsWith("\u0001")) {
String request = line.substring(line.indexOf(":\u0001") + 2, line.length() - 1);
if (request.startsWith("ACTION ")) {
// ACTION request
getMessageHandler().onAction(sourceNick, target, request.substring(7));
}
} else if (command.equals("PRIVMSG") && _channelPrefixes.indexOf(target.charAt(0)) >= 0) {
//catch the subscriber message
if (sourceNick.equalsIgnoreCase("twitchnotify"))
{
if (line.contains("resubscribed"))
{
getMessageHandler().onJTVMessage(target, content, null);
} else if (line.contains("subscribed"))
{
//we dont want to get the hosted sub messages, Botnak should be in that chat for that
String user = content.split(" ")[0];
getChannelManager().handleSubscriber(target, user);
getMessageHandler().onNewSubscriber(target, content, user);
return;
}
}
//This message is a cheer message!
else if (tags != null && tags.contains("bits="))
{
HashMap<String, String> tagsMap = Utils.parseTagsToMap(tags);
if (!tagsMap.isEmpty())
{
int bitAmount = Integer.parseInt(tagsMap.get("bits"));
getMessageHandler().onCheer(target, sourceNick, bitAmount, content);
}
return;
} else
// This is a normal message to a channel.
getMessageHandler().onMessage(target, sourceNick, content);
} else if ("PRIVMSG".equals(command)) {
if (sourceNick.equals("jtv")) {
if (line.contains("now hosting you")) {
getMessageHandler().onBeingHosted(content);//KEEP THIS
}
}
// This is a private message to us.
getMessageHandler().onPrivateMessage(sourceNick, sourceLogin, sourceHostname, content);
} else {
// If we reach this point, then we've found something that the PircBot
// Doesn't currently deal with.
onUnknown(line);
}
}
private boolean checkCommand(String command, String tags, String line, String content, StringTokenizer tokenizer, String senderInfo) {
String target;
HashMap<String, String> tagsMap = Utils.parseTagsToMap(tags);
switch (command)
{
case "CLEARCHAT":
target = tokenizer.nextToken();
if (tagsMap.isEmpty())
getMessageHandler().onClearChat(target);
else if (!tagsMap.containsKey("ban-duration"))
getMessageHandler().onUserPermaBanned(target, content, tagsMap.get("ban-reason"));
else
getMessageHandler().onUserTimedOut(target, content, Integer.parseInt(tagsMap.get("ban-duration")), tagsMap.get("ban-reason"));
return true;
case "HOSTTARGET":
target = tokenizer.nextToken();
String[] split = content.split(" ");
getMessageHandler().onHosting(target.substring(1), split[0], split[1]);
return true;
case "ROOMSTATE":
target = tokenizer.nextToken();
getMessageHandler().onRoomstate(target, tags);
parseTags(tags, null, target);
return true;
case "NOTICE":
if (tags.contains("room_mods"))
{
target = tokenizer.nextToken();
buildMods(target, content);
return true;
} else if (!tags.contains("host_on") && !tags.contains("host_off"))
{//handled above
target = tokenizer.nextToken();
getMessageHandler().onJTVMessage(target.substring(1), content, tags);
return true;
}
break;
case "WHISPER":
target = tokenizer.nextToken();
String nick = senderInfo.substring(1, senderInfo.indexOf('!'));
parseTags(tags, nick, null);
getMessageHandler().onWhisper(nick, target, content);
return true;
case "RECONNECT"://We need to reconnect to this server
GUIMain.logCurrent("Detected a RECONNECT command, currently reconnecting the connection for: " + _nick + "!");
getConnection().dispose();
Settings.accountManager.createReconnectThread(getConnection());
return true;
case "USERNOTICE": //User has resubscribed to this channel (for X months)
target = tokenizer.nextToken(); // TODO update to support upcoming subscriber change
String user = tagsMap.get("login");
parseTags(line, user, target);
getMessageHandler().onResubscribe(target, user, tagsMap.get("system-msg"));
//Only send their message if there is one
if (content != null && line.indexOf(" :", line.indexOf(" :") + 2) > -1)
getMessageHandler().onMessage(target, user, content);
return true;
default:
return false;
}
return false;
}
private void parseUserstate(String line) {
String[] parts = line.split(" ");
String tags = parts[0];
String channel = parts[3];
parseTags(tags, getNick(), channel);
}
private void parseTags(String line, String user, String channel) {
HashMap<String, String> tags = Utils.parseTagsToMap(line);
if (!tags.isEmpty())
{
Set<Map.Entry<String, String>> entries = tags.entrySet();
for (Map.Entry<String, String> tag : entries)
{
switch (tag.getKey())
{
case "color":
handleColor(tag.getValue(), user);
break;
case "display-name":
handleDisplayName(tag.getValue(), user);
break;
case "emotes":
handleEmotes(tag.getValue(), user);
break;
case "subscriber":
if ("1".equals(tag.getValue()))
{
handleSpecial(channel, tag.getKey(), user);
}
break;
case "turbo":
if ("1".equals(tag.getValue()))
{
handleSpecial(null, tag.getKey(), user);
}
break;
case "user-type":
handleSpecial(channel, tag.getValue(), user);
break;
case "r9k":
if ("1".equals(tag.getValue()))
{
getMessageHandler().onJTVMessage(channel, "This room is in r9k mode.", tag.getKey());
}
break;
case "slow":
if (!"0".equals(tag.getValue()))
{
getMessageHandler().onJTVMessage(channel,
"This room is in slow mode. You may send messages every " + tag.getValue() + " seconds.", tag.getKey());
}
break;
case "subs-only":
if ("1".equals(tag.getValue()))
{
getMessageHandler().onJTVMessage(channel, "This room is in subscribers-only mode.", tag.getKey());
}
break;
case "emote-sets":
FaceManager.handleEmoteSet(tag.getValue());
break;
case "badges": // Although user-type handles most of this, we need it for bits status
String badges = tag.getValue();
// Bit donor
if (badges.contains("bits"))
{
Matcher m = Pattern.compile("bits/(\\d+)").matcher(badges);
if (m.find())
getChannelManager().getChannel(channel).setCheer(user, Integer.parseInt(m.group(1)));
}
// Prime
if (badges.contains("premium"))
getChannelManager().getUser(user, true).setPrime(true);
// Verified
if (badges.contains("partner"))
getChannelManager().getUser(user, true).setVerified(true);
break;
case "bits":
//This message contains a cheer!
//This is handled above, don't worry.
break;
default:
break;
}
}
}
}
private void buildMods(String channel, String line) {
if (!line.equals("")) {
String init = line.substring(line.indexOf(":") + 1);
String[] upMods = init.replaceAll(" ", "").split(",");
getChannelManager().getChannel(channel).addMods(upMods);
}
}
/**
* Determines if a user is admin, staff, turbo, or subscriber and sets their prefix accordingly.
*
* @param channel The channel it's for
* @param type The user type
* @param user The user
*/
public void handleSpecial(String channel, String type, String user) {
if (user != null) {
Channel c = getChannelManager().getChannel(channel);
switch (type) {
case "mod":
if (c != null) c.addMods(user);
break;
case "subscriber":
if (c != null) c.addSubscriber(user);
break;
case "turbo":
getChannelManager().getUser(user, true).setTurbo(true);
break;
case "admin":
getChannelManager().getUser(user, true).setAdmin(true);
break;
case "global_mod":
getChannelManager().getUser(user, true).setGlobalMod(true);
break;
case "staff":
getChannelManager().getUser(user, true).setStaff(true);
break;
default:
break;
}
}
}
/**
* This method is called by the PircBot when a numeric response
* is received from the IRC server. We use this method to
* allow PircBot to process various responses from the server
* before then passing them on to the onServerResponse method.
* <p>
* Note that this method is private and should not appear in any
* of the javadoc generated documenation.
*
* @param code The three-digit numerical code for the response.
* @param response The full response from the IRC server.
*/
public void processServerResponse(int code, String response) {
if (code == 366) {//"END OF NAMES"
int channelEndIndex = response.indexOf(" :");
String channel = response.substring(response.lastIndexOf(' ', channelEndIndex - 1) + 1, channelEndIndex);
sendRawMessage(channel, ".mods");//start building mod list
}
}
/**
* This method is called whenever we receive a line from the server that
* the PircBot has not been programmed to recognise.
* <p>
* The implementation of this method in the PircBot abstract class
* performs no actions and may be overridden as required.
*
* @param line The raw line that was received from the server.
*/
protected void onUnknown(String line) {
// And then there were none :)
}
/**
* Sets the verbose mode. If verbose mode is set to true, then log entries
* will be printed to the standard output. The default value is false and
* will result in no output. For general development, we strongly recommend
* setting the verbose mode to true.
*
* @param verbose true if verbose mode is to be used. Default is false.
*/
public void setVerbose(boolean verbose) {
_verbose = verbose;
}
/**
* Sets the internal nick of the bot. This is only to be called by the
* PircBot class in response to notification of nick changes that apply
* to us.
*
* @param nick The new nick.
*/
public void setNick(String nick) {
_nick = nick;
if (connection != null)
connection.setName(_nick);
}
public void setPassword(String password) {
_password = password;
}
/**
* Sets the internal version of the Bot. This should be set before joining
* any servers.
*
* @param version The new version of the Bot.
*/
public void setVersion(String version) {
_version = version;
}
/**
* 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.
* <p>
* The nick returned by this method is maintained only by the PircBot
* class and is guaranteed to be correct in the context of the IRC server.
*
* @return The current nick of the bot.
* @since PircBot 1.0.0
*/
public String getNick() {
return _nick;
}
/**
* Gets the internal version of the PircBot.
*
* @return The version of the PircBot.
*/
public String getVersion() {
return _version;
}
/**
* Sets the number of milliseconds to delay between consecutive
* messages when there are multiple messages waiting in the
* outgoing message queue. This has a default value of 1000ms.
* It is a good idea to stick to this default value, as it will
* prevent your bot from spamming servers and facing the subsequent
* wrath! However, if you do need to change this delay value (<b>not
* recommended</b>), then this is the method to use.
*
* @param delay The number of milliseconds between each outgoing message.
*/
public final void setMessageDelay(long delay) {
if (delay < 0) {
throw new IllegalArgumentException("Cannot have a negative time.");
}
_messageDelay = delay;
}
/**
* Returns the number of milliseconds that will be used to separate
* consecutive messages to the server from the outgoing message queue.
*
* @return Number of milliseconds.
*/
public final long getMessageDelay() {
return _messageDelay;
}
/**
* Returns the last password that we used when connecting to an IRC server.
* This does not imply that the connection attempt to the server was
* successful (we suggest you look at the onConnect method).
* A value of null is returned if the PircBot has never tried to connect
* to a server using a password.
*
* @return The last password that we used when connecting to an IRC server.
* Returns null if we have not previously connected using a password.
* @since PircBot 0.9.9
*/
public final String getPassword() {
return _password;
}
/**
* Returns true if and only if the object being compared is the exact
* same instance as this PircBot. This may be useful if you are writing
* a multiple server IRC bot that uses more than one instance of PircBot.
*
* @return true if and only if Object o is a PircBot and equal to this.
* @since PircBot 0.9.9
*/
public boolean equals(Object o) {
if (o instanceof PircBot) {
PircBot other = (PircBot) o;
return this.getNick().equals(other.getNick()) && this.getPassword().equals(other.getPassword());
}
return false;
}
/**
* Returns a String representation of this object.
* You may find this useful for debugging purposes, particularly
* if you are using more than one PircBot instance to achieve
* multiple server connectivity. The format of
* this String may change between different versions of PircBot
* but is currently something of the form
* <code>
* Version{PircBot x.y.z Java IRC Bot - www.jibble.org}
* Connected{true}
* Server{irc.dal.net}
* Port{6667}
* Password{}
* </code>
*
* @return a String representation of this object.
* @since PircBot 0.9.10
*/
public String toString() {
return "Version{" + _version + "}" +
" Connected{" + connection.isConnected() + "}" +
" Server{" + connection.getServer() + "}" +
" Port{" + connection.getPort() + "}" +
" Password{" + _password + "}";
}
/**
* Returns an array of all channels that we are in. Note that if you
* call this method immediately after joining a new channel, the new
* channel may not appear in this array as it is not possible to tell
* if the join was successful until a response is received from the
* IRC server.
*
* @return A String array containing the names of all channels that we
* are in.
* @since PircBot 1.0.0
*/
public final String[] getChannels() {
return getChannelManager().getChannelNames();
}
public void dispose() {
if (connection != null) connection.dispose();
}
public void handleColor(String color, String user) {
if (color != null) {
Color c;
try {
c = Color.decode(color);
} catch (Exception ignored) {
return;
}
getChannelManager().getUser(user, true).setColor(c);
}
}
public void handleDisplayName(String name, String user) {
if (name != null) {
getChannelManager().getUser(user, true).setDisplayName(name.replaceAll("\\\\s", " ").trim());
}
}
public void handleEmotes(String numbers, String user) {
try {
String[] parts = numbers.split("/");
User u = getChannelManager().getUser(user, true);
for (String emote : parts) {
String emoteID = emote.split(":")[0];
try {
int id = Integer.parseInt(emoteID);
u.addEmote(id);
} catch (Exception e) {
GUIMain.log("Cannot parse emote ID given by IRCv3 tags!");
}
}
} catch (Exception ignored) {
}
}
private String _password = null;
// Outgoing message stuff.
private long _messageDelay = 1000;
// Default settings for the PircBot.
private boolean _verbose = false;
private String _nick = null;
private String _version = "Botnak " + Constants.VERSION;
}