package heufybot.core; import heufybot.config.GlobalConfig.PasswordType; import heufybot.config.ServerConfig; import heufybot.core.events.EventListenerManager; import heufybot.core.events.types.BotMessageEvent; import heufybot.modules.ModuleInterface; import heufybot.utils.SSLSocketUtils; import javax.net.SocketFactory; import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class IRCServer { // Not sure what to do with this one since the RFC doesn't specify it. // Assume 512 until documentation states otherwise. private final int MAX_LINE_LENGTH = 512; // Locking stuff for the output to server private final ReentrantLock writeLock = new ReentrantLock(true); private final Condition writeNowCondition = this.writeLock.newCondition(); private String name; private ServerConfig config; private ModuleInterface moduleInterface; private Socket socket; private BufferedReader inputReader; private OutputStreamWriter outputWriter; private Thread inputThread; private InputParser inputParser; private ConnectionState connectionState; private ArrayList<IRCChannel> channels; private ArrayList<IRCUser> users; private ServerInfo serverInfo; private List<String> userModes; private List<String> enabledCapabilities; private EventListenerManager eventListenerManager; private long lastSentLine = 0; private String nickname; public IRCServer(String name, ServerConfig config) { this.socket = new Socket(); this.inputParser = new InputParser(this); this.connectionState = ConnectionState.Initializing; this.channels = new ArrayList<IRCChannel>(); this.users = new ArrayList<IRCUser>(); this.serverInfo = new ServerInfo(); this.enabledCapabilities = new ArrayList<String>(); this.eventListenerManager = new EventListenerManager(); this.userModes = new ArrayList<String>(); this.name = name; this.config = config; this.nickname = ""; } public boolean connect(String server, int port) { if (this.connectionState == ConnectionState.Connected) { Logger.error("IRC Connect", "Already connected to a server. Connection failed."); return false; } SocketFactory sf; if (this.config.getSettingWithDefault("ssl", false)) { // Trust all certificates, since making Java recognize a valid // certificate is annoying. sf = new SSLSocketUtils().trustAllCertificates(); } else { sf = SocketFactory.getDefault(); } Logger.log("*** Trying to connect to " + server + "..."); try { InetAddress[] foundIPs = InetAddress.getAllByName(server); Logger.log("*** " + foundIPs.length + " IP(s) found for host " + server + "."); for (InetAddress curAddress : foundIPs) { try { Logger.log("*** Trying IP address " + curAddress.getHostAddress() + ":" + port + "..."); this.socket = sf.createSocket(curAddress, port, null, 0); this.inputReader = new BufferedReader(new InputStreamReader( this.socket.getInputStream(), Charset.forName("UTF-8"))); this.outputWriter = new OutputStreamWriter(this.socket.getOutputStream(), Charset.forName("UTF-8")); Logger.log("*** Connected to the server."); return true; } catch (Exception e) { Logger.error("IRC Connect", "Could not connect to " + server + " on IP address " + curAddress); } } Logger.error("IRC Connect", "Could not connect to " + server); return false; } catch (UnknownHostException e) { Logger.error("IRC Connect", "Host could not be resolved. Connection failed."); return false; } } public void login() { String nickname = this.config.getSettingWithDefault("nickname", "RE_HeufyBot"); String password = this.config.getSettingWithDefault("password", ""); String username = this.config.getSettingWithDefault("username", "RE_HeufyBot"); String realname = this.config.getSettingWithDefault("realname", "RE_HeufyBot IRC Bot"); PasswordType passwordType = this.config.getSettingWithDefault("passwordType", PasswordType.None); if (passwordType == PasswordType.ServerPass) { this.cmdPASS(password); } this.cmdCAP("LS", ""); this.cmdNICK(nickname); this.cmdUSER(username, realname); this.startProcessing(); } public void disconnect(boolean reconnect) { this.connectionState = ConnectionState.Disconnected; this.nickname = ""; this.serverInfo.clear(); this.enabledCapabilities.clear(); this.userModes.clear(); try { this.inputThread.interrupt(); this.inputReader.close(); this.outputWriter.flush(); this.outputWriter.close(); this.socket.close(); this.channels.clear(); this.users.clear(); } catch (IOException e) { Logger.error("IRC Disconnect", "Error closing connection"); } if (reconnect && this.config.getSettingWithDefault("autoReconnect", false)) { this.reconnect(); } } public void reconnect() { // TODO This code needs a major overhaul, because it doesn't work // properly and multiserver will break it even more int reconnectAttempts = this.config.getSettingWithDefault("reconnectAttempts", 3); int reconnectInterval = this.config.getSettingWithDefault("reconnectInterval", 600); String server = this.config.getSettingWithDefault("server", "irc.foo.bar"); int port = this.config.getSettingWithDefault("port", 6667); int reconnects = 0; while (reconnects < reconnectAttempts) { reconnects++; Logger.log("*** Reconnection attempt #" + reconnects + "..."); boolean success = this.connect(server, port); if (success) { this.login(); return; } else { if (reconnects < reconnectAttempts) { Logger.log("*** Connection failed. Trying again in " + reconnectInterval + " second(s)..."); try { Thread.sleep(reconnectInterval * 1000); } catch (InterruptedException e) { Logger.error("IRC - Reconnect", "Thread interrupted while trying to reconnect"); } } } } Logger.log("*** Connection failed. Giving up."); } public void startProcessing() { this.inputThread = new Thread(new Runnable() { @Override public void run() { while (true) { String line; try { line = IRCServer.this.inputReader.readLine(); } catch (InterruptedIOException iioe) { IRCServer.this.cmdPING("" + System.currentTimeMillis() / 1000); continue; } catch (Exception e) { line = null; Logger.log("*** Connection to the server was lost. Trying to reconnect..."); IRCServer.this.disconnect(true); } if (line == null) { break; } if (!line.equals("")) { IRCServer.this.inputParser.parseLine(line); } if (Thread.interrupted()) { return; } } } }); this.inputThread.start(); } public void sendRaw(String line) { int messageDelay = this.config.getSettingWithDefault("messageDelay", 500); this.writeLock.lock(); try { long currentNanos = System.nanoTime(); while (this.lastSentLine + messageDelay * 1000000 > currentNanos) { this.writeNowCondition.await(this.lastSentLine + messageDelay * 1000000 - currentNanos, TimeUnit.NANOSECONDS); currentNanos = System.nanoTime(); } this.lastSentLine = System.nanoTime(); this.outputWriter.write(line + "\r\n"); this.outputWriter.flush(); } catch (IOException e) { Logger.error("IRC Output", "Error sending line"); } catch (InterruptedException e) { Logger.error("IRC Output", "Error while waiting to send line"); } finally { this.writeLock.unlock(); } } public void sendRawNow(String line) { this.writeLock.lock(); try { this.lastSentLine = System.nanoTime(); this.outputWriter.write(line + "\r\n"); this.outputWriter.flush(); } catch (IOException e) { e.printStackTrace(); Logger.error("IRC Output", "Error sending line"); } finally { this.writeLock.unlock(); } } public void sendRawSplit(String prefix, String message, String suffix) { String fullMessage = prefix + message + suffix; if (fullMessage.length() < this.MAX_LINE_LENGTH - 2) { this.sendRaw(fullMessage); return; } int maxLength = this.MAX_LINE_LENGTH - 2 - (prefix + suffix).length(); int iterations = (int) Math.ceil(message.length() / (double) maxLength); for (int i = 0; i < iterations; i++) { int endPoint = i != iterations - 1 ? (i + 1) * maxLength : message.length(); String currentPart = prefix + message.substring(i * maxLength, endPoint) + suffix; this.sendRaw(currentPart); } } public void sendRawSplit(String prefix, String message) { this.sendRawSplit(prefix, message, ""); } public void cmdNICK(String nick) { this.sendRawNow("NICK " + nick); } public void cmdUSER(String user, String realname) { this.sendRawNow("USER " + user + " 8 * :" + realname); } public void cmdPING(String ping) { this.sendRawNow("PING " + ping); } public void cmdPONG(String response) { this.sendRawNow("PONG " + response); } public void cmdQUIT(String message) { this.sendRaw("QUIT :" + message); } public void cmdPRIVMSG(String target, String message) { this.sendRawSplit("PRIVMSG " + target + " :", message); this.eventListenerManager.dispatchEvent(new BotMessageEvent(this.name, this .getUser(this.nickname), target, message)); } public void cmdJOIN(String channel, String key) { this.sendRaw("JOIN " + channel + " " + key); } public void cmdPART(String channel, String message) { this.sendRaw("PART " + channel + " :" + message); } public void cmdPASS(String password) { this.sendRaw("PASS " + password); } public void cmdWHO(String target) { this.sendRaw("WHO " + target); } public void cmdWHOIS(String target) { this.sendRaw("WHOIS " + target); } public void cmdWHOWAS(String target) { this.sendRaw("WHOWAS " + target); } public void cmdMODE(String target, String mode) { this.sendRaw("MODE " + target + " " + mode); } public void cmdNOTICE(String target, String notice) { this.sendRawSplit("NOTICE " + target + " :", notice); } public void cmdCAP(String capCommand, String arguments) { this.sendRawNow("CAP " + capCommand + arguments); } public void cmdACTION(String target, String action) { this.ctcpCommand(target, "ACTION " + action); } public void nickservIdentify(String password) { this.cmdPRIVMSG("NickServ", "IDENTIFY " + password); } public void ctcpCommand(String target, String command) { this.sendRawSplit("PRIVMSG " + target + " :\u0001", command, "\u0001"); } public void ctcpReply(String target, String replyType, String reply) { this.cmdNOTICE(target, "\u0001" + replyType + " " + reply + "\u0001"); } public String getAccessLevelOnUser(IRCChannel channel, IRCUser user) { for (String accessLevel : this.serverInfo.getUserPrefixes().keySet()) { if (channel.getModesOnUser(user).contains(accessLevel)) { return accessLevel; } } return ""; } public IRCChannel getChannel(String channelName) { for (IRCChannel channel : this.channels) { if (channel.getName().equalsIgnoreCase(channelName)) { return channel; } } return null; } public IRCUser getUser(String nickname) { for (IRCUser user : this.users) { if (user.getNickname().equalsIgnoreCase(nickname)) { return user; } } IRCUser user = new IRCUser(nickname); this.users.add(user); return user; } public String getName() { return this.name; } public ServerConfig getConfig() { return this.config; } public ModuleInterface getModuleInterface() { return this.moduleInterface; } public void setModuleInterface(ModuleInterface moduleInterface) { this.moduleInterface = moduleInterface; } public ConnectionState getConnectionState() { return this.connectionState; } public void setConnectionState(ConnectionState connectionState) { this.connectionState = connectionState; } public String getNickname() { return this.nickname; } public void setLoggedInNick(String nickname) { this.nickname = nickname; } public ArrayList<IRCChannel> getChannels() { return this.channels; } public ArrayList<IRCUser> getUsers() { return this.users; } public ServerInfo getServerInfo() { return this.serverInfo; } public List<String> getEnabledCapabilities() { return this.enabledCapabilities; } public EventListenerManager getEventListenerManager() { return this.eventListenerManager; } public List<String> getUserModes() { return this.userModes; } public void parseUserModesChange(String modeChange) { boolean adding = true; for (char curChar : modeChange.toCharArray()) { if (curChar == '-') { adding = false; } else if (curChar == '+') { adding = true; } else if (adding) { String current = Character.toString(curChar); if (!this.userModes.contains(current)) { this.userModes.add(current); } } else { String current = Character.toString(curChar); if (this.userModes.contains(current)) { this.userModes.remove(current); } } } } public enum ConnectionState { Initializing, Connected, Disconnected } }