Commit 74e97468 authored by Adam Gausmann's avatar Adam Gausmann

Merge branch 'multi' into 'master'

Support for multiple connections

Resolves #5 

Adds support for a single bot to maintain connection to multiple IRC servers. 

~~This seems to be stable and ready to merge, but a lot of documentation is missing. I'll add that before merging.~~ Ready to merge! I've also added some more features like a >version command.

See merge request !14
parents 8387e88e 2632fe29
Pipeline #5065263 passed with stages
in 35 seconds
......@@ -5,7 +5,7 @@
<parent>
<artifactId>samurai</artifactId>
<groupId>ninja.nonemu</groupId>
<version>1.0.2</version>
<version>1.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
......@@ -15,7 +15,7 @@
<dependency>
<groupId>ninja.nonemu</groupId>
<artifactId>ircninja</artifactId>
<version>1.0</version>
<version>1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
......
......@@ -28,4 +28,6 @@ public interface Bot {
default void quit() {
quit(0);
}
String getVersion();
}
......@@ -6,25 +6,13 @@ import ninja.nonemu.samurai.event.Event;
* Dispatched by the connection manager when the bot has successfully connected to a server.
*/
public class BotConnectEvent extends Event {
private final String host;
private final int port;
private final String nickname;
private final Connection connection;
public BotConnectEvent(String host, int port, String nickname) {
this.host = host;
this.port = port;
this.nickname = nickname;
public BotConnectEvent(Connection connection) {
this.connection = connection;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getNickname() {
return nickname;
public Connection getConnection() {
return connection;
}
}
......@@ -6,24 +6,19 @@ import ninja.nonemu.samurai.event.Event;
* Dispatched by the connection manager when the bot disconnects from a server.
*/
public class BotDisconnectEvent extends Event {
private final String host;
private final int port;
private final Connection connection;
private final String comment;
public BotDisconnectEvent(String host, int port) {
this.host = host;
this.port = port;
public BotDisconnectEvent(Connection connection, String comment) {
this.connection = connection;
this.comment = comment;
}
public String getHost() {
return host;
public Connection getConnection() {
return connection;
}
public int getPort() {
return port;
}
@Override
public boolean isCancelled() {
return false;
public String getComment() {
return comment;
}
}
package ninja.nonemu.samurai.connection;
import java.io.IOException;
import ninja.nonemu.irc.ChatRecipient;
public interface Channel extends ChatRecipient {
String getName();
ninja.nonemu.irc.Channel moreInfo() throws IOException;
Connection getConnection();
}
package ninja.nonemu.samurai.connection;
import java.io.IOException;
import ninja.nonemu.irc.Message;
public interface Connection {
String getName();
/**
* Sends a custom message to the server.
* @param message The message to send.
*/
void sendMessage(Message message);
/**
* Checks whether a connection exists by checking if there is a socket and it says it is connected.
* @return true if there is a connection; false otherwise.
*/
boolean isConnected();
/**
* Connects to the network using the details provided during construction.
*
* <br>This may send any/all of the following: PASS, NICK, and USER messages.
*
* @throws IOException if an IO error occurs while connecting.
*/
void connect() throws IOException;
/**
* Disconnects from any connected network.
*
* @param comment The comment to leave when disconnecting.
* @throws IOException if an IO error occurs while disconnecting.
*/
void disconnect(String comment) throws IOException;
/**
* Disconnects from any connected network without providing a comment.
*
* @throws IOException if an IO error occurs while disconnecting.
*/
void disconnect() throws IOException;
/**
* Attempts to set a new nickname for the client.
*
* @param nickname The new nickname.
*/
void setNickname(String nickname);
/**
* Attempts to join a channel.
*
* @param channel The channel to join.
*/
void joinChannel(String channel);
/**
* Attempts to leave a channel, leaving a comment behind.
* @param channel The channel to leave.
* @param comment The comment/reason for leaving.
*/
void leaveChannel(String channel, String comment);
/**
* Attempts to leave a channel with no comment.
* @param channel The channel to leave.
*/
void leaveChannel(String channel);
/**
* Sends a chat message to a channel or user connected to the network.
* @param target The entity receiving the message.
* @param message The message to send.
*/
void sendChat(String target, String message);
Channel getChannel(String channel);
User getUser(String userId);
User getUser(UserMask userMask);
/**
* Gets the currently-configured host that the client is connecting to.
*/
String getHost();
/**
* Gets the currently-configured port that the client is connecting to.
*/
int getPort();
/**
* Gets the currently-configured nickname of the client.
*/
String getNickname();
}
package ninja.nonemu.samurai.connection;
import java.io.IOException;
import ninja.nonemu.irc.Message;
import ninja.nonemu.irc.ConnectionBuilder;
/**
* Manages the connection to the IRC network and is responsible for processing messages sent and received.
* Manages connections to IRC networks.
* Since multiple simultaneous connections are allowed, this class is provided to allow plugins to add and remove
* connections on their own.
*
* @see Connection
*/
public interface ConnectionManager {
/**
* Sends a custom message to the server.
* @param message The message to send.
* Gets a list of all the connections that are currently registered. They may or may not be connected.
*/
void sendMessage(Message message);
Connection[] getConnections();
/**
* Checks whether a connection exists by checking if there is a socket and it says it is connected.
* @return true if there is a connection; false otherwise.
* Gets a connection given its name.
* @param name The name of the connection that was given at registration (case insensitive).
* @return The connection found, or null if there is no connection with that name.
*/
boolean isConnected();
Connection getConnection(String name);
/**
* Connects to the network using the details provided during construction.
*
* <br>This may send any/all of the following: PASS, NICK, and USER messages.
*
* @throws IOException if an IO error occurs while connecting.
* Builds and registers a connection to an IRC server. This does NOT automatically connect to the server.
* Since no name is provided, one is generated from the connection's hostname.
* @param builder A builder that will be responsible for building the connection.
* @return The created connection.
*/
void connect() throws IOException;
Connection addConnection(ConnectionBuilder builder);
/**
* Disconnects from any connected network.
*
* @param comment The comment to leave when disconnecting.
* @throws IOException if an IO error occurs while disconnecting.
* Builds and registers a connection to an IRC server. This does NOT automatically connect to the server.
* @param name The name of the new connection (case insensitive).
* @param builder A builder that will be responsible for building the connection.
* @return The created connection.
*/
void disconnect(String comment) throws IOException;
Connection addConnection(String name, ConnectionBuilder builder);
/**
* Disconnects from any connected network without providing a comment.
*
* @throws IOException if an IO error occurs while disconnecting.
* Removes a connection if it has been registered.
* @param connection The connection to remove.
*/
void disconnect() throws IOException;
default void removeConnection(Connection connection) {
removeConnection(connection.getName());
}
/**
* Attempts to set a new nickname for the client.
*
* @param nickname The new nickname.
* Removes a connection if it has been registered.
* @param name The name of the connection to remove (case insensitive).
*/
void setNickname(String nickname);
/**
* Attempts to join a channel.
*
* @param channel The channel to join.
*/
void joinChannel(String channel);
/**
* Attempts to leave a channel, leaving a comment behind.
* @param channel The channel to leave.
* @param comment The comment/reason for leaving.
*/
void leaveChannel(String channel, String comment);
/**
* Attempts to leave a channel with no comment.
* @param channel The channel to leave.
*/
void leaveChannel(String channel);
/**
* Sends a chat message to a channel or user connected to the network.
* @param target The entity receiving the message.
* @param message The message to send.
*/
void sendChat(String target, String message);
Channel getChannel(String channel);
User getUser(String userId);
User getUser(UserMask userMask);
/**
* Gets the currently-configured host that the client is connecting to.
*/
String getHost();
/**
* Gets the currently-configured port that the client is connecting to.
*/
int getPort();
/**
* Gets the currently-configured nickname of the client.
*/
String getNickname();
void removeConnection(String name);
}
......@@ -17,4 +17,6 @@ public interface User extends ChatSender, ChatRecipient, CommandSender {
String getUsername();
String getHostname();
Connection getConnection();
}
......@@ -7,6 +7,10 @@ public class UserMask implements Comparable<UserMask> {
this.mask = mask;
}
public UserMask(String nickname, String username, String hostname) {
this(nickname + "!" + username + "@" + hostname);
}
public String getNickname() {
if (mask.contains("!")) {
return mask.substring(0, mask.indexOf("!"));
......
......@@ -6,7 +6,7 @@
<groupId>ninja.nonemu</groupId>
<artifactId>samurai</artifactId>
<version>1.0.2</version>
<version>1.1</version>
<modules>
<module>api</module>
<module>runtime</module>
......
......@@ -5,7 +5,7 @@
<parent>
<artifactId>samurai</artifactId>
<groupId>ninja.nonemu</groupId>
<version>1.0.2</version>
<version>1.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
......@@ -27,6 +27,12 @@
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
......
......@@ -52,6 +52,7 @@ public class BotImpl implements Bot {
private final ConnectionManagerImpl connectionManager;
private final CommandSystemImpl commandSystem;
private final PluginManagerImpl pluginManager;
private String version = null;
private boolean running;
private int exitCode;
......@@ -70,15 +71,8 @@ public class BotImpl implements Bot {
running = true;
logger.trace("Loading properties");
File propertiesFile = new File("bot.properties");
try {
if (propertiesFile.exists()) {
properties.load(new FileInputStream(propertiesFile));
} else {
properties.load(getClass().getResourceAsStream("bot.properties"));
properties.store(new FileOutputStream(propertiesFile), "Samurai bot properties");
}
loadConfig();
} catch (IOException e) {
logger.error("Unable to load bot properties.", e);
quit(1);
......@@ -108,8 +102,28 @@ public class BotImpl implements Bot {
connectionManager.cleanup();
eventSystem.cleanup();
consoleManager.cleanup();
}
running = false;
private void loadConfig() throws IOException {
logger.trace("Loading configuration");
File propertiesFile = new File("./bot.properties");
if (propertiesFile.exists()) {
properties.load(new FileInputStream(propertiesFile));
} else {
properties.load(getClass().getResourceAsStream("bot.properties"));
properties.store(new FileOutputStream(propertiesFile), "Samurai bot properties");
}
Properties versionProperties = new Properties();
versionProperties.load(getClass().getResourceAsStream("version.properties"));
version = versionProperties.getProperty("version");
}
private void storeConfig() throws IOException {
logger.trace("Saving configuration");
properties.store(new FileOutputStream("./bot.properties"), "Samurai bot properties");
}
public boolean isRunning() {
......@@ -155,4 +169,9 @@ public class BotImpl implements Bot {
running = false;
this.exitCode = exitCode;
}
@Override
public String getVersion() {
return version;
}
}
package ninja.nonemu.samurai.command;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import ninja.nonemu.samurai.BotImpl;
import ninja.nonemu.samurai.connection.Connection;
import ninja.nonemu.samurai.connection.User;
import ninja.nonemu.samurai.connection.UserMask;
public class CommandExecutorImpl {
......@@ -14,6 +15,12 @@ public class CommandExecutorImpl {
this.bot = bot;
CommandSystem commandSystem = bot.getCommandSystem();
commandSystem.registerCommand(new CommandInfo(
"version",
"Provides information about the bot and its version.",
"version",
false
), this::handleVersion);
commandSystem.registerCommand(new CommandInfo(
"help",
"Lists all commands available to the user.",
......@@ -46,20 +53,20 @@ public class CommandExecutorImpl {
), this::handleBlacklist);
commandSystem.registerCommand(new CommandInfo(
"quit",
"Gracefully quits from the IRC network.",
"quit [comment]",
"Gracefully shuts the bot down",
"quit",
true
), this::handleQuit);
commandSystem.registerCommand(new CommandInfo(
"join",
"Requests that the bot join a channel on the network.",
"join <channel>",
"join <channel> [connection]",
true
), this::handleJoin);
commandSystem.registerCommand(new CommandInfo(
"leave",
"Requests that the bot leave a channel on the network.",
"leave <channel> [comment]",
"leave <channel> [connection]",
true
), this::handleLeave);
commandSystem.registerCommand(new CommandInfo(
......@@ -70,6 +77,13 @@ public class CommandExecutorImpl {
), this::handleMsg);
}
private boolean handleVersion(CommandSender sender, CommandInfo info, String label, String[] args) {
sender.sendChat("Samurai v" + bot.getVersion() + ", a lightweight extensible IRC bot.");
sender.sendChat("Written by Adam Gausmann (nonemu) <adam@nonemu.ninja>");
sender.sendChat("Source code available at https://gitlab.com/AGausmann/samurai");
return true;
}
private boolean handleHelp(CommandSender sender, CommandInfo info, String label, String[] args) {
int page = 1;
......@@ -190,37 +204,43 @@ public class CommandExecutorImpl {
return false;
}
private boolean handleQuit(CommandSender sender, CommandInfo info, String label, String[] args) throws IOException {
sender.sendChat("Goodbye!");
if (args.length > 0) {
bot.getConnectionManager().disconnect(String.join(" ", (CharSequence[]) args));
} else {
bot.getConnectionManager().disconnect();
}
private boolean handleQuit(CommandSender sender, CommandInfo info, String label, String[] args) {
sender.sendChat("Bye!");
bot.quit();
return true;
}
private boolean handleJoin(CommandSender sender, CommandInfo info, String label, String[] args) {
if (args.length > 0) {
sender.sendChat("Joining channel " + args[0]);
bot.getConnectionManager().joinChannel(args[0]);
if (args.length == 1) {
if (!(sender instanceof User)) {
sender.sendChat("Console must provide a connection name.");
return true;
}
sender.sendChat("Joining channel " + args[0]);
((User) sender).getConnection().joinChannel(args[0]);
} else {
sender.sendChat("Joining channel " + args[0] + " on " + args[1]);
getConnection(args[1]).joinChannel(args[0]);
}
return true;
}
return false;
}
private boolean handleLeave(CommandSender sender, CommandInfo info, String label, String[] args) {
if (args.length > 1) {
sender.sendChat("Leaving channel " + args[0]);
String[] newArgs = new String[args.length - 1];
System.arraycopy(args, 1, newArgs, 0, newArgs.length);
bot.getConnectionManager().leaveChannel(args[0], String.join(" ", (CharSequence[]) newArgs));
return true;
}
if (args.length > 0) {
sender.sendChat("Leaving channel " + args[0]);
bot.getConnectionManager().leaveChannel(args[0]);
if (args.length == 1) {
if (!(sender instanceof User)) {
sender.sendChat("Console must provide a connection name.");
return true;
}
sender.sendChat("Leaving channel " + args[0]);
((User) sender).getConnection().leaveChannel(args[0]);
} else {
sender.sendChat("Leaving channel " + args[0] + " on " + args[1]);
getConnection(args[1]).leaveChannel(args[0], "Requested by " + sender.getName());
}
return true;
}
return false;
......@@ -228,13 +248,22 @@ public class CommandExecutorImpl {
private boolean handleMsg(CommandSender sender, CommandInfo info, String label, String[] args) {
if (args.length > 1) {
if (!(sender instanceof User)) {
sender.sendChat("Console may not use this command.");
return true;
}
String[] words = new String[args.length - 1];
System.arraycopy(args, 1, words, 0, words.length);
bot.getConnectionManager().sendChat(args[0], String.join(" ", (CharSequence[]) words));
((User) sender).getConnection().sendChat(args[0], String.join(" ", (CharSequence[]) words));
return true;
}
return false;
}
private Connection getConnection(String name) {
return bot.getConnectionManager().getConnection(name);
}
}
......@@ -12,6 +12,7 @@ import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import ninja.nonemu.irc.Color;
import ninja.nonemu.samurai.BotImpl;
import ninja.nonemu.samurai.connection.ChatEvent;
import ninja.nonemu.samurai.connection.User;
......@@ -201,7 +202,8 @@ public class CommandSystemImpl implements CommandSystem {
@EventHandler(priority = Priority.MONITOR, acceptSubclass = false)
public void handleChatEvent(ChatEvent event) {
if (!event.getText().startsWith(commandPrefix)) {
String text = Color.stripColors(event.getText());
if (!text.startsWith(commandPrefix)) {
return;
}
......@@ -209,7 +211,7 @@ public class CommandSystemImpl implements CommandSystem {
return;
}
String[] words = event.getText().split("\\s");
String[] words = text.split("\\s");
String label = words[0].substring(commandPrefix.length());
String[] args = new String[words.length - 1];
System.arraycopy(words, 1, args, 0, args.length);
......
package ninja.nonemu.samurai.connection;
import java.io.IOException;
import ninja.nonemu.samurai.BotImpl;
public class ChannelImpl implements Channel {
private final BotImpl bot;
private final ConnectionImpl connection;
private final String name;
public ChannelImpl(BotImpl bot, String name) {
this.bot = bot;
public ChannelImpl(ConnectionImpl connection, String name) {
this.connection = connection;
this.name = name;
}
......@@ -19,13 +16,13 @@ public class ChannelImpl implements Channel {
}
@Override
public ninja.nonemu.irc.Channel moreInfo() throws IOException {
return bot.getConnectionManager().getConnection().getChannel(name);
public Connection getConnection() {
return connection;
}
@Override
public void sendChat(String message) {
bot.getConnectionManager().sendChat(name, message);
connection.sendChat(name, message);
}
@Override
......@@ -35,13 +32,14 @@ public class ChannelImpl implements Channel {
Channel channel = (Channel) o;
return name.equals(channel.getName());
return connection.equals(channel.getConnection())
&& name.equalsIgnoreCase(channel.getName());
}
@Override
public int hashCode() {
int result = bot.hashCode();
result = 31 * result + name.hashCode();
int result = connection.hashCode();
result = 31 * result + name.toLowerCase().hashCode();
return result;
}
......
package ninja.nonemu.samurai.connection;
import java.io.IOException;
import ninja.nonemu.irc.ChatRecipient;
import ninja.nonemu.irc.IrcUtil;
import ninja.nonemu.irc.Message;
import ninja.nonemu.samurai.BotImpl;