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;
}
}