package org.royaldev.thehumanity; import com.google.common.base.Preconditions; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; import kotlin.Unit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONObject; import org.json.JSONWriter; import org.kitteh.irc.client.library.element.Channel; import org.kitteh.irc.client.library.element.User; import org.kitteh.irc.client.library.element.mode.ChannelUserMode; import org.kitteh.irc.client.library.feature.auth.NickServ; import org.royaldev.thehumanity.cards.cardcast.CardcastFetcher; import org.royaldev.thehumanity.cards.packs.CAHCardPack; import org.royaldev.thehumanity.cards.packs.CardPackParser; import org.royaldev.thehumanity.cards.packs.CardcastCardPack; import org.royaldev.thehumanity.commands.impl.CardCountsCommand; import org.royaldev.thehumanity.commands.impl.CardsCommand; import org.royaldev.thehumanity.commands.impl.HelpCommand; import org.royaldev.thehumanity.commands.impl.HostCommand; import org.royaldev.thehumanity.commands.impl.JoinGameCommand; import org.royaldev.thehumanity.commands.impl.KickCommand; import org.royaldev.thehumanity.commands.impl.LeaveGameCommand; import org.royaldev.thehumanity.commands.impl.LoadCardPackCommand; import org.royaldev.thehumanity.commands.impl.NeverHaveIEverCommand; import org.royaldev.thehumanity.commands.impl.PacksCommand; import org.royaldev.thehumanity.commands.impl.PickCardCommand; import org.royaldev.thehumanity.commands.impl.RebootTheUniverseCommand; import org.royaldev.thehumanity.commands.impl.ScoreCommand; import org.royaldev.thehumanity.commands.impl.SkipCommand; import org.royaldev.thehumanity.commands.impl.StartGameCommand; import org.royaldev.thehumanity.commands.impl.StopGameCommand; import org.royaldev.thehumanity.commands.impl.VersionCommand; import org.royaldev.thehumanity.commands.impl.WhoCommand; import org.royaldev.thehumanity.commands.impl.game.GameCommand; import org.royaldev.thehumanity.commands.impl.ping.PingListCommand; import org.royaldev.thehumanity.configuration.TheHumanityConfiguration; import org.royaldev.thehumanity.game.TheHumanityGame; import org.royaldev.thehumanity.history.History; import org.royaldev.thehumanity.ping.PingRegistry; import org.royaldev.thehumanity.ping.task.SavePingRegistryTask; import org.royaldev.thehumanity.player.TheHumanityPlayer; import org.royaldev.thehumanity.server.GameServer; import org.royaldev.thehumanity.server.configurations.HumanityConfiguration; import org.royaldev.thehumanity.util.Pair; import xyz.cardstock.cardstock.Cardstock; import xyz.cardstock.cardstock.commands.CommandRegistrar; import xyz.cardstock.cardstock.configuration.CommandLineConfiguration; import xyz.cardstock.cardstock.configuration.Configuration; import xyz.cardstock.cardstock.games.GameRegistrar; import java.io.IOException; import java.io.StringWriter; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.SortedSet; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import static khttp.KHttp.post; public class TheHumanity extends Cardstock { private final List<CAHCardPack> loadedCardPacks = Collections.synchronizedList(new ArrayList<>()); private final Cache<String, Pair<String, String>> gistCache = CacheBuilder.newBuilder().build(); private final Logger l = Logger.getLogger("org.royaldev.thehumanity"); private final ScheduledThreadPoolExecutor stpe = new ScheduledThreadPoolExecutor(1); private final PingRegistry pingRegistry; private final History history = new History(this); private final CommandLineConfiguration commandLineConfiguration; private final TheHumanityConfiguration configuration; private final CommandRegistrar commandRegistrar = new CommandRegistrar(); private final GameRegistrar<TheHumanity, TheHumanityPlayer, TheHumanityGame> gameRegistrar; @Nullable private final GameServer gameServer; private TheHumanity(@NotNull final String[] args) { Preconditions.checkNotNull(args, "args was null"); this.commandLineConfiguration = new CommandLineConfiguration(args, this); this.configuration = new TheHumanityConfiguration(this.commandLineConfiguration.getConfigurationFile()); this.gameRegistrar = new GameRegistrar<>(this, TheHumanityGame::new); if (this.configuration.isWebEnabled()) { HumanityConfiguration.setHumanity(this); this.gameServer = new GameServer(this.configuration.getWebHost(), this.configuration.getWebPort()); if (this.configuration.isOnlyRunWeb()) { this.pingRegistry = null; return; } } else { this.gameServer = null; } this.pingRegistry = PingRegistry.deserializeOrMakePingRegistry(); // Schedule a repeatedly running saver task, just in case we're not shut down properly this.stpe.scheduleAtFixedRate(new SavePingRegistryTask(this.pingRegistry), 5L, 10L, TimeUnit.MINUTES); this.loadCardPacks(); this.registerCommands(); final BaseListeners baseListeners = new BaseListeners(this); final GameListeners gameListeners = new GameListeners(this); this.getShutdownHook().getBeginningHooks().add(cardstock -> { Sets.newHashSet(this.getGameRegistrar().all()).forEach(game -> game.stop(TheHumanityGame.GameEndCause.JAVA_SHUTDOWN)); return Unit.INSTANCE; }); this.getShutdownHook().getEndHooks().add(cardstock -> { this.getPingRegistry().save(); return Unit.INSTANCE; }); this.start(); if (this.configuration.isDebug()) { this.getClients().forEach( client -> { client.setOutputListener(s -> System.out.println("output = " + s)); client.setInputListener(s -> System.out.println("input = " + s)); } ); } this.getClients().forEach(client -> Stream.of( baseListeners, gameListeners ).forEach(client.getEventManager()::registerEventListener)); final String nickservPassword = this.configuration.getNickservPassword(); if (nickservPassword != null && !nickservPassword.isEmpty()) { this.getClients().forEach(client -> client.getAuthManager().addProtocol(new NickServ(client, client.getIntendedNick(), nickservPassword))); } } public static void main(final String[] args) { new TheHumanity(args); } @Nullable private Manifest getManifest() { final Class<?> clazz = this.getClass(); final String className = clazz.getSimpleName() + ".class"; final String classPath = clazz.getResource(className).toString(); final String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF"; try { return new Manifest(new URL(manifestPath).openStream()); } catch (final IOException ex) { return null; } } private void loadCardPacks() { new CardPackParser(this).parseCardPacks(this.configuration.getCardPackFiles()).forEach(this::addCardPack); } private void registerCommands() { Arrays.asList( new StartGameCommand(this), new JoinGameCommand(this), new PickCardCommand(this), new StopGameCommand(this), new LeaveGameCommand(this), new PacksCommand(this), new WhoCommand(this), new KickCommand(this), new SkipCommand(this), new HelpCommand(this), new CardsCommand(this), new RebootTheUniverseCommand(this), new CardCountsCommand(this), new ScoreCommand(this), new HostCommand(this), new GameCommand(this), new NeverHaveIEverCommand(this), new VersionCommand(this), new LoadCardPackCommand(this), new PingListCommand(this) ).forEach(this.getCommandRegistrar()::registerCommand); } public void addCardPack(@NotNull final CAHCardPack cp) { Preconditions.checkNotNull(cp, "cp was null"); synchronized (this.loadedCardPacks) { this.loadedCardPacks.add(cp); } } public boolean areCardcastPacksKept() { return this.configuration.isKeepLoaded(); } /** * Gists the given contents under the given file name. This returns the URL to the Gist or a string of the following * format: "An error occurred: [error message]" * <p/> * The given ID is used for caching purposes. The cacheString should be a String that identifies the contents. If * cacheString changes, then the current cache for the given ID will be invalidated, and a new Gist will be made. * * @param key Key of this cached gist * @param cacheString Identifier for the contents * @param fileName Filename for the contents * @param contents Contents of the Gist * @return URL of Gist or error message */ @NotNull public String cachedGist(@NotNull final String key, @NotNull final String cacheString, @NotNull final String fileName, @NotNull final String contents) { // Ensure nothing is null Preconditions.checkNotNull(key, "key was null"); Preconditions.checkNotNull(cacheString, "cacheString was null"); Preconditions.checkNotNull(fileName, "fileName was null"); Preconditions.checkNotNull(contents, "contents was null"); // Get the pair of hashed cacheString and gist URL from the given key. If key is missing, this will be null final Pair<String, String> hashGist = this.gistCache.getIfPresent(key); // Compute the hash of the given cacheString. This has a cost, but saves on memory required to store long // strings final String hash = Hashing.md5().hashUnencodedChars(cacheString).toString(); // If the cache didn't have anything or if the hashes are no longer equal if (hashGist == null || !hash.equals(hashGist.getLeft())) { // First, let's invalidate the key, since it is no longer valid this.gistCache.invalidate(key); // Now, let's gist and return the URL or error message return this.gist(fileName, contents); } else { // What if cache was not kill? // Return the cached gist URL return hashGist.getRight(); } } @Nullable public CAHCardPack getCardPack(@NotNull final String name) { Preconditions.checkNotNull(name, "name was null"); synchronized (this.loadedCardPacks) { return this.loadedCardPacks.stream().filter(cp -> cp.getName().equals(name)).findFirst().orElse(null); } } @NotNull public List<CAHCardPack> getCardPacksFromArguments(final String[] args) { final List<CAHCardPack> packs = CardPackParser.getListOfCardPackNames(args, this.getDefaultPacks()).stream() .map(this::getOrDownloadCardPack) .filter(cp -> cp != null) .collect(Collectors.toList()); if (this.areCardcastPacksKept()) { packs.stream() .filter(pack -> pack instanceof CardcastCardPack) .filter(pack -> !this.getLoadedCardPacks().contains(pack)) .forEach(this::addCardPack); } return packs; } @NotNull public List<String> getDefaultPacks() { return Lists.newArrayList(this.configuration.getDefaultPacks()); } @Nullable @Deprecated public TheHumanityGame getGameFor(@NotNull final Channel c) { Preconditions.checkNotNull(c, "c was null"); return this.gameRegistrar.find(c); } @Nullable public GameServer getGameServer() { return this.gameServer; } public History getHistory() { return this.history; } @NotNull public List<CAHCardPack> getLoadedCardPacks() { synchronized (this.loadedCardPacks) { return this.loadedCardPacks; } } @NotNull public Logger getLogger() { return this.l; } @Nullable public CAHCardPack getOrDownloadCardPack(@NotNull final String name) { Preconditions.checkNotNull(name, "name was null"); final CAHCardPack cp = this.getCardPack(name); if (cp != null) return cp; if (!name.toLowerCase().startsWith("cc:")) return null; return new CardcastFetcher(name.substring(3)).getCardPack(); } public PingRegistry getPingRegistry() { return this.pingRegistry; } @NotNull public ScheduledThreadPoolExecutor getThreadPool() { return this.stpe; } @NotNull public String getVersion() { final Manifest mf = this.getManifest(); if (mf == null) return "Error: null Manifest"; final Attributes a = mf.getAttributes("Version-Info"); if (a == null) return "Error: No Version-Info"; return String.format( "%s %s (%s)", a.getValue("Project-Name"), a.getValue("Project-Version"), a.getValue("Git-Describe") ); } /** * Gists the given contents under the given file name. This returns the URL to the Gist or a string of the following * format: "An error occurred: [error message]" * * @param fileName Filename for the contents * @param contents Contents of the Gist * @return URL of Gist or error message */ @NotNull public String gist(@NotNull final String fileName, @NotNull final String contents) { // Ensure nothing is null Preconditions.checkNotNull(fileName, "fileName was null"); Preconditions.checkNotNull(contents, "contents was null"); // Let's gist the contents using the given fileName. // Make a StringWriter to turn this JSON into a String, easily final StringWriter sw = new StringWriter(); final JSONWriter jw = new JSONWriter(sw); // Create the gist object for sending to the API jw.object().key("files") .object().key(fileName) .object().key("content").value(contents) .endObject().endObject().endObject(); try { final Map<String, String> headers = Maps.newHashMap(); headers.put("Content-Type", "application/json"); // POST the gist object to the appropriate API URL and grab the response as JSON final JSONObject response = post( "https://api.github.com/gists", headers, // headers Maps.<String, String>newHashMap(), // params sw.toString() // data ).getJsonObject(); // This should be the URL at which the gist can be accessed. Will throw exception if key isn't present // Finally, let's give the caller the URL to the gist return response.getString("html_url"); } catch (final Exception ex) { return "An error occurred: " + ex.getMessage(); } } public boolean hasChannelMode(@NotNull final Channel c, @NotNull final User u, final char mode) { Preconditions.checkNotNull(c, "Channel was null"); Preconditions.checkNotNull(u, "User was null"); Optional<SortedSet<ChannelUserMode>> set = c.getUserModes(u.getNick()); return set.isPresent() && set.get().stream().anyMatch(m -> m.getChar() == mode); } @Nullable public CAHCardPack parseOrDownloadCardPack(@NotNull final String name) { Preconditions.checkNotNull(name, "name was null"); if (name.toLowerCase().startsWith("cc:")) { final String id = name.substring(3).toUpperCase(); CardcastFetcher.invalidateCacheFor(id); return new CardcastFetcher(id).getCardPack(); } return new CardPackParser(this).parseCardPack(name); } public void removeCardPack(@NotNull final CAHCardPack cp) { Preconditions.checkNotNull(cp, "cp was null"); synchronized (this.loadedCardPacks) { this.loadedCardPacks.remove(cp); } } public boolean usersMatch(@NotNull final User u, @NotNull final User u2) { Preconditions.checkNotNull(u, "User was null"); Preconditions.checkNotNull(u2, "Second user was null"); return u.getNick().equals(u2.getNick()); } @NotNull @Override public CommandLineConfiguration getCommandLineConfiguration() { return this.commandLineConfiguration; } @NotNull @Override public Configuration getConfiguration() { return this.configuration; } @NotNull @Override public CommandRegistrar getCommandRegistrar() { return this.commandRegistrar; } @NotNull @Override public GameRegistrar<TheHumanity, TheHumanityPlayer, TheHumanityGame> getGameRegistrar() { return this.gameRegistrar; } }