/**
* Copyright (C) 2013 Alexander Szczuczko
*
* This file may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
package ca.szc.keratin.bot;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.pmw.tinylog.Logger;
import ca.szc.keratin.bot.DelegateConnection.ConnectionRunnable;
import ca.szc.keratin.bot.annotation.AssignedBot;
import ca.szc.keratin.bot.annotation.HandlerContainerDetector;
import ca.szc.keratin.bot.handlers.ConnectionPreamble;
import ca.szc.keratin.bot.handlers.ManageChannels;
import ca.szc.keratin.core.net.IrcConnection;
import ca.szc.keratin.core.net.IrcConnection.SslMode;
import ca.szc.keratin.core.net.io.OutputQueue;
import ca.szc.keratin.core.net.mbassador.MBassadorWrapper;
import ca.szc.keratin.core.net.message.InvalidMessageParamException;
import ca.szc.keratin.core.net.message.IrcMessage;
import ca.szc.keratin.core.net.message.SendMessage;
import ca.szc.keratin.core.net.util.InvalidPortException;
/**
* A class for supporting IRC bots.
*/
public class KeratinBot
{
private String user;
private String nick;
private String realName;
private String serverAddress;
private int serverPort;
private SslMode sslMode;
private Map<String, Channel> channels;
private boolean connectionActive;
private OutputQueue outputQueue;
private IrcConnection connection;
private DelegateConnection delegateConn;
private MBassadorWrapper connectionBus;
/**
* Make a KeratinBot with no fields predefined. Must have fields set before calling connect().
*/
public KeratinBot()
{
connectionActive = false;
}
/**
* Make a KeratinBot with all fields needed for a connection predefined.
*
* @param user IRC username
* @param nick IRC nick. This is the unique ID of a client on IRC
* @param realName IRC full/real name
* @param serverAddress The address of the server to connect to when connect() is called.
* @param serverPort The port on the server to connect to when connect() is called.
* @param sslMode {@link SslMode} value
* @param initialChannels The channels to connect to initially, can be empty, but not null.
*/
public KeratinBot( String user, String nick, String realName, String serverAddress, int serverPort,
SslMode sslMode, Channel[] initialChannels )
{
this();
setUser( user );
setNick( nick );
setRealName( realName );
setServerAddress( serverAddress );
setServerPort( serverPort );
setSslMode( sslMode );
for ( Channel channel : initialChannels )
{
addChannel( channel.getName(), channel.getKey() );
}
}
/**
* Perform the connection. All fields must be defined before calling this, if using the blank constructor.
*/
public void connect()
{
IrcConnection conn = null;
try
{
conn = new IrcConnection( serverAddress, serverPort, sslMode );
}
catch ( UnknownHostException | InvalidPortException e )
{
Logger.error( e, "Could not make IrcConnection" );
}
connectionBus = conn.getEventBus();
connectionBus.subscribe( new ConnectionPreamble( user, nick, realName ) );
connectionBus.subscribe( new ManageChannels( this, channels ) );
for ( Class<?> handlerContainer : HandlerContainerDetector.getContainers() )
{
try
{
// Create an instance of the annotated class
Object listener = handlerContainer.getConstructor().newInstance();
// Set @AssignedBot annotated fields in the listener instance to a reference to this KeratinBot.
for ( Field field : handlerContainer.getDeclaredFields() )
{
if ( field.getType().equals( this.getClass() ) )
{
if ( field.getAnnotation( AssignedBot.class ) != null )
{
field.setAccessible( true );
field.set( listener, this );
}
}
}
// Subscribe the instance, with references injected to the message bus
connectionBus.subscribe( listener );
}
catch ( InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e )
{
Logger.error( e, "Could not create instance of handler container '{0}'", handlerContainer );
}
}
for ( Channel channel : channels.values() )
{
connectionBus.subscribe( channel );
}
delegateConn = new DelegateConnection( serverAddress, serverPort, sslMode, user, nick + "-del", realName );
connection = conn;
conn.connect();
outputQueue = conn.getOutputQueue();
connectionActive = true;
}
/**
* End the connection.
*/
public void disconnect()
{
connectionActive = false;
connection.disconnect();
connection = null;
outputQueue = null;
}
/**
* Get the current nick ID of the bot on the server.
*
* @return Nick string
*/
public String getNick()
{
return nick;
}
/**
* Get the full/real name of the bot on the server.
*
* @return Real name string, can contain spaces.
*/
public String getRealName()
{
return realName;
}
/**
* Get the address of the server the bot is connected to.
*
* @return Server address, can be a domain name or IP address.
*/
public String getServerAddress()
{
return serverAddress;
}
/**
* Get the port of the server the bot is connected to.
*
* @return Port number
*/
public int getServerPort()
{
return serverPort;
}
/**
* Get the user name of the bot on the server.
*
* @return User string
*/
public String getUser()
{
return user;
}
/**
* Get the SSL socket use status/mode.
*
* @return {@link SslMode} value
*/
public SslMode getSslMode()
{
return sslMode;
}
/**
* Set the stored nick value, and send an update message to the server if connected.
*
* @param nick A spaceless string, must start with an alphabetic character
*/
public void setNick( String nick )
{
if ( connectionActive )
{
try
{
outputQueue.nick( nick );
}
catch ( InvalidMessageParamException e )
{
Logger.error( e, "Error creating IRC message" );
}
}
this.nick = nick;
}
/**
* Set the full/real name of the bot on the server. No effect after connect() has been called.
*
* @param realName Real name string, can contain spaces.
*/
public void setRealName( String realName )
{
this.realName = realName;
}
/**
* Set the address of the server the bot going to connect to. No effect after connect() has been called.
*
* @param serverAddress Server address, can be a domain name or IP address.
*/
public void setServerAddress( String serverAddress )
{
this.serverAddress = serverAddress;
}
/**
* Set the port of the server the bot going to connect to. No effect after connect() has been called.
*
* @param serverAddress Server port, has to be a valid port number.
*/
public void setServerPort( int serverPort )
{
this.serverPort = serverPort;
}
/**
* Set if and how SSL sockets are going to be used. No effect after connect() has been called.
*
* @param sslMode {@link SslMode} value
*/
public void setSslMode( SslMode sslMode )
{
this.sslMode = sslMode;
}
/**
* Set the user name the bot will use on the server. No effect after connect() has been called.
*
* @param user User string
*/
public void setUser( String user )
{
this.user = user;
}
/**
* Get the set of current channels the bot is joined to.
*
* @return Set of channel strings
*/
public Collection<Channel> getChannels()
{
return channels.values();
}
/**
* Get the Channel stored under a channel name.
*
* @param name The name of the channel to get. Includes the # prefix.
* @return The corresponding Channel, if it exists, else null
*/
public Channel getChannel( String name )
{
return channels.get( name );
}
/**
* Add a channel to the list of current channels the bot is joined to. Send an update message to the server
* immediately if connected. If a channel was added previously with a key, the key will be looked up automatically
* when being re-added.
*
* @param name Channel's name
*/
public void addChannel( String name )
{
addChannel( name, null );
}
/**
* Add a channel to the list of current channels the bot is joined to. Send an update message to the server
* immediately if connected. If a channel was added previously with a key, and the key given is null, the key will
* be looked up automatically when being re-added.
*
* @param name Channel's name
* @param key Channel's key if it exists, otherwise null
*/
public void addChannel( String name, String key )
{
if ( channels == null )
{
channels = new HashMap<String, Channel>();
}
if ( key == null )
{
for ( Entry<String, Channel> channelEntry : channels.entrySet() )
{
String channelName = channelEntry.getKey();
Channel channel = channelEntry.getValue();
String channelKey = channel.getKey();
if ( channelKey != null && channelName.equals( name ) )
{
key = channelEntry.getKey();
break;
}
}
}
addChannel( new Channel( name, key ) );
}
/**
* Add a channel to the list of current channels the bot is joined to. Send an update message to the server if
* connected.
*
* @param channel The channel to add
*/
public void addChannel( Channel channel )
{
if ( channels == null )
{
channels = new HashMap<String, Channel>();
}
if ( connectionActive )
{
try
{
if ( channel.getKey() == null )
outputQueue.join( channel.getName() );
else
outputQueue.join( channel.getName(), channel.getKey() );
}
catch ( InvalidMessageParamException e )
{
Logger.error( e, "Error creating IRC message" );
}
connectionBus.subscribe( channel );
}
this.channels.put( channel.getName(), channel );
}
/**
* Remove a channel from the list of current channels the bot is joined to. Send an update message to the server if
* connected.
*
* @param channel Channel to part from
*/
public void remChannel( Channel channel )
{
remChannel( channel.getName() );
}
/**
* Remove a channel from the list of current channels the bot is joined to. Send an update message to the server if
* connected.
*
* @param name Name of the channel to part from
*/
public void remChannel( String name )
{
if ( connectionActive )
{
try
{
outputQueue.part( name );
}
catch ( InvalidMessageParamException e )
{
Logger.error( e, "Error creating IRC message" );
}
}
channels.remove( name );
}
/**
* Op a single nick in a channel. Probably will fail if the bot is not an operator in the given channel.
*
* @param channelName The channel to op the nick in
* @param nick The nick to op in the channel
*/
public void opNick( String channelName, String nick )
{
try
{
outputQueue.mode( channelName, "+o", nick );
}
catch ( InvalidMessageParamException e )
{
Logger.error( e, "Error creating IRC message" );
}
}
/**
* Op one or more nicks in a single channel. Probably will fail if the bot is not an operator in the given channel.
*
* @param channelName The channel to op the nicks in
* @param nicks The nicks to op in the channel
*/
public void opNicks( String channelName, Collection<String> nicks )
{
final int bufferSize = 4;
// Op in groups of size bufferSize
Iterator<String> iterator = nicks.iterator();
while ( iterator.hasNext() )
{
List<String> nickBuffer = new LinkedList<String>();
StringBuilder modeBuffer = new StringBuilder();
modeBuffer.append( "+" );
for ( int i = 0; iterator.hasNext() && i < bufferSize; )
{
String nick = iterator.next();
nickBuffer.add( nick );
modeBuffer.append( "o" );
i++;
}
if ( nickBuffer.size() > 0 )
{
String[] nickArray = new String[nickBuffer.size()];
nickBuffer.toArray( nickArray );
try
{
outputQueue.mode( channelName, modeBuffer.toString(), nickArray );
}
catch ( InvalidMessageParamException e )
{
Logger.error( e, "Error creating IRC message" );
}
}
}
}
/**
* Use a separate connection to send a PRIVMSG as a different nick to a certain channel.
*
* @param nick The nick to use
* @param channel The name of the channel. The bot must be in that channel.
* @param text The text to send. Send multiple PRIVMSGs by including \n characters.
*/
public void sendPrivmsgAs( final String nick, final String channel, final String text )
{
if ( channel.startsWith( "#" ) )
{
sendPrivmsgAs( nick, getChannel( channel ), text );
}
else
{
delegateConn.offer( new ConnectionRunnable()
{
@Override
public void run( IrcConnection conn )
{
Logger.trace( "sendPrivmsgAs runnable running" );
OutputQueue outputQueue = conn.getOutputQueue();
try
{
// Send as one block or not at all (an Exception will stop offer from being called)
List<IrcMessage> messageList = new LinkedList<IrcMessage>();
messageList.add( SendMessage.nick( nick ) );
messageList.add( SendMessage.privmsg( channel, text ) );
for ( IrcMessage message : messageList )
outputQueue.offer( message );
}
catch ( InvalidMessageParamException e )
{
Logger.error( e, "Error creating IRC messages" );
}
}
} );
}
}
/**
* Use a separate connection to send a PRIVMSG as a different nick to a certain channel.
*
* @param nick The nick to use
* @param channel The channel to use
* @param text The text to send. Send multiple PRIVMSGs by including \n characters.
*/
public void sendPrivmsgAs( final String nick, final Channel channel, final String text )
{
delegateConn.offer( new ConnectionRunnable()
{
@Override
public void run( IrcConnection conn )
{
Logger.trace( "sendPrivmsgAs runnable running" );
OutputQueue outputQueue = conn.getOutputQueue();
try
{
// Send as one block or not at all (an Exception will stop offer from being called)
List<IrcMessage> messageList = new LinkedList<IrcMessage>();
messageList.add( SendMessage.nick( nick ) );
String channelName = channel.getName();
if ( channel.getKey() == null )
messageList.add( SendMessage.join( channelName ) );
else
messageList.add( SendMessage.join( channelName, channel.getKey() ) );
messageList.add( SendMessage.privmsg( channelName, text ) );
messageList.add( SendMessage.part( channelName ) );
for ( IrcMessage message : messageList )
outputQueue.offer( message );
}
catch ( InvalidMessageParamException e )
{
Logger.error( e, "Error creating IRC messages" );
}
}
} );
}
}