package io.shockah.skylark; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.regex.Pattern; import org.pircbotx.Channel; import org.pircbotx.Colors; import org.pircbotx.Configuration; import org.pircbotx.Configuration.BotFactory; import org.pircbotx.InputParser; import org.pircbotx.PircBotX; import org.pircbotx.cap.EnableCapHandler; import org.pircbotx.hooks.ListenerAdapter; import org.pircbotx.hooks.events.ConnectEvent; import io.shockah.skylark.db.Server; import io.shockah.skylark.plugin.BotManagerService; import io.shockah.skylark.plugin.ListenerPlugin; import io.shockah.skylark.plugin.PluginManager; import io.shockah.skylark.util.Box; import io.shockah.skylark.util.ReadWriteList; import io.shockah.skylark.util.StringUtils; public class BotManager { public static final Charset CHARSET = Charset.forName("UTF-8"); public static final String CHANNELS_PER_CONNECTION_CAPABILITY = "CHANLIMIT"; public static final Pattern CHANNELS_PER_CONNECTION_CAPABILITY_VALUE_PATTERN = Pattern.compile("\\#\\:([0-9]+)"); public static final String DEFAULT_ELLIPSIS = "�"; public static final String DEFAULT_BOT_NAME = "Skylark"; public static final long DEFAULT_MESSAGE_DELAY = 500; public static final int DEFAULT_LINEBREAK_LENGTH = 400; public final ServerManager serverManager; public final String name; public final String host; public final Integer port; public Integer channelsPerConnection; public long messageDelay = DEFAULT_MESSAGE_DELAY; public String botName = DEFAULT_BOT_NAME; public Integer linebreakLength; public String ellipsis; public final ReadWriteList<Bot> bots = new ReadWriteList<>(new ArrayList<>()); public final ReadWriteList<BotManagerService> services = new ReadWriteList<>(new ArrayList<>()); public BotManager(ServerManager serverManager, String name, String host) { this(serverManager, name, host, null); } public BotManager(ServerManager serverManager, String name, String host, Integer port) { this.serverManager = serverManager; this.name = name; this.host = host; this.port = port; linebreakLength = serverManager.app.config.getObjectOrEmpty("messages").getOptionalInt("linebreakLength"); ellipsis = serverManager.app.config.getObjectOrEmpty("messages").getString("ellipsis", null); setupServices(); } public BotManager(ServerManager serverManager, Server server) { this(serverManager, server.name, server.host); channelsPerConnection = server.channelsPerConnection; messageDelay = server.messageDelay == null ? BotManager.DEFAULT_MESSAGE_DELAY : server.messageDelay; botName = server.botName == null ? BotManager.DEFAULT_BOT_NAME : server.botName; if (server.linebreakLength != null) linebreakLength = server.linebreakLength; if (server.ellipsis != null) ellipsis = server.ellipsis; } public void setupServices() { services.writeOperation(services -> { PluginManager pluginManager = serverManager.app.pluginManager; pluginManager.botManagerServiceFactories.iterate(factory -> { BotManagerService service = factory.createService(this); services.add(service); pluginManager.botManagerServices.add(service); }); }); } @SuppressWarnings("unchecked") public <T extends BotManagerService> T getService(Class<T> clazz) { return (T)services.filterFirst(service -> clazz.isInstance(service)); } public int getChannelsPerConnection() { if (channelsPerConnection == null) { return bots.readOperation(bots -> { if (bots.isEmpty()) { //placeholder until we actually connect return 1; } else { /*Bot bot = bots.get(0); String capValue = bot.getEnabledCapabilityValue(CHANNELS_PER_CONNECTION_CAPABILITY); if (capValue != null) { Matcher m = CHANNELS_PER_CONNECTION_CAPABILITY_VALUE_PATTERN.matcher(capValue); if (m.find()) return Integer.parseInt(m.group(1)); }*/ return Integer.MAX_VALUE; } }); } else { return channelsPerConnection; } } public int getLinebreakLength() { return linebreakLength == null ? DEFAULT_LINEBREAK_LENGTH : linebreakLength; } public String getEllipsis() { return ellipsis == null ? DEFAULT_ELLIPSIS : ellipsis; } public Bot getAnyBot() { return bots.readOperation(bots -> { if (bots.isEmpty()) return connectNewBot(); return bots.get(0); }); } public Bot joinChannel(String channelName) { return bots.writeOperation(bots -> { int channelsPerConnection = getChannelsPerConnection(); for (Bot bot : bots) { if (bot.getUserBot().getChannels().size() < channelsPerConnection) { bot.sendIRC().joinChannel(channelName); return bot; } } Bot bot = connectNewBot(); bot.sendIRC().joinChannel(channelName); return bot; }); } public Channel getChannel(String channelName) { return bots.firstResult(bot -> { for (Channel channel : bot.getUserBot().getChannels()) { if (channel.getName().equals(channelName)) return channel; } return null; }); } public Bot connectNewBot() { Box<Bot> newBotBox = new Box<>(); CountDownLatch latch = new CountDownLatch(1); new Thread(){ @Override public void run() { try { newBotBox.value = buildNewBot(latch); newBotBox.value.startBot(); } catch (Exception e) { newBotBox.value = null; latch.countDown(); } } }.start(); try { latch.await(); } catch (InterruptedException e) { } if (newBotBox.value != null) bots.add(newBotBox.value); return newBotBox.value; } protected Bot buildNewBot(CountDownLatch latch) { Configuration.Builder cfgb = new Configuration.Builder() .setBotFactory(new BotFactory(){ @Override public InputParser createInputParser(PircBotX bot) { return new SkylarkInputParser(bot); } }) .setEncoding(CHARSET) .setName(botName) .setAutoNickChange(true) .setMessageDelay(messageDelay) .setCapEnabled(true) .addCapHandler(new EnableCapHandler("extended-join", true)) .addCapHandler(new EnableCapHandler("account-notify", true)) .setAutoReconnect(true) .addListener(new ListenerAdapter(){ @Override public void onConnect(ConnectEvent event) throws Exception { latch.countDown(); } }); if (port == null) cfgb.addServer(host); else cfgb.addServer(host, port); PluginManager pluginManager = serverManager.app.pluginManager; pluginManager.plugins.iterate(plugin -> { if (plugin instanceof ListenerPlugin) cfgb.addListener(((ListenerPlugin)plugin).listener); }); return new Bot(cfgb.buildConfiguration(), this); } public List<String> linebreakIfNeeded(List<String> lines) { return linebreakIfNeeded(lines, null, getEllipsis()); } public List<String> linebreakIfNeeded(List<String> lines, Integer maxLines) { return linebreakIfNeeded(lines, maxLines, getEllipsis()); } public List<String> linebreakIfNeeded(List<String> lines, Integer maxLines, String ellipsis) { if (maxLines != null && maxLines < 1) throw new IllegalArgumentException(); List<String> newLines = new ArrayList<>(); for (String line : lines) { newLines.addAll(linebreakIfNeeded(line)); } if (maxLines != null && newLines.size() > maxLines) { newLines = newLines.subList(0, maxLines); if (ellipsis != null && !ellipsis.isEmpty()) { String lastLine = newLines.get(newLines.size() - 1); String replacement = ellipsis; if (!Colors.removeFormattingAndColors(lastLine).equals(lastLine)) replacement = "\u000F" + replacement; if (replacement.length() >= lastLine.length()) lastLine = replacement; else if (lastLine.length() + replacement.length() < getLinebreakLength() + 1) lastLine = String.format("%s %s", lastLine, replacement); else lastLine = lastLine.substring(0, lastLine.length() - replacement.length()) + replacement; newLines.set(newLines.size() - 1, lastLine); } } return newLines; } public List<String> linebreakIfNeeded(String line) { List<String> list = new ArrayList<>(); while (true) { String trimmedMessage = StringUtils.trimToByteLength(line, getLinebreakLength(), CHARSET); list.add(trimmedMessage); if (trimmedMessage.equals(line)) { break; } else { line = line.substring(trimmedMessage.length()); } } return list; } }