geode/loader/src/server/Server.cpp

813 lines
28 KiB
C++
Raw Normal View History

2024-02-26 15:14:53 -05:00
#include "Server.hpp"
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/ranges.hpp>
#include <fmt/core.h>
2024-02-26 15:14:53 -05:00
#include <loader/ModMetadataImpl.hpp>
2024-03-02 17:28:06 -05:00
#include <fmt/chrono.h>
2024-02-26 15:14:53 -05:00
using namespace server;
2024-02-26 20:59:06 -05:00
#define GEODE_GD_VERSION_STR GEODE_STR(GEODE_GD_VERSION)
2024-02-26 15:14:53 -05:00
2024-05-30 12:11:18 -04:00
template <class K, class V>
requires std::equality_comparable<K> && std::copy_constructible<K>
class CacheMap final {
private:
2024-05-30 12:11:18 -04:00
// I know this looks like a goofy choice over just
// `std::unordered_map`, but hear me out:
2024-05-30 12:11:18 -04:00
//
// This needs preserved insertion order (so shrinking the cache
// to match size limits doesn't just have to erase random
// elements)
//
// If this used a map for values and another vector for storing
// insertion order, it would have a pretty big memory footprint
// (two copies of Query, one for order, one for map + two heap
// allocations on top of that)
2024-05-30 12:11:18 -04:00
//
// In addition, it would be a bad idea to have a cache of 1000s
// of items in any case (since that would likely take up a ton
// of memory, which we want to avoid since it's likely many
// crashes with the old index were due to too much memory
// usage)
2024-05-30 12:11:18 -04:00
//
// Linear searching a vector of at most a couple dozen items is
// lightning-fast (🚀), and besides the main performance benefit
// comes from the lack of a web request - not how many extra
// milliseconds we can squeeze out of a map access
std::vector<std::pair<K, V>> m_values;
size_t m_sizeLimit = 20;
public:
std::optional<V> get(K const& key) {
auto it = std::find_if(m_values.begin(), m_values.end(), [key](auto const& q) {
return q.first == key;
});
if (it != m_values.end()) {
return it->second;
}
return std::nullopt;
}
void add(K&& key, V&& value) {
auto pair = std::make_pair(std::move(key), std::move(value));
// Shift and replace last element if we're at cache size limit
if (m_values.size() >= m_sizeLimit) {
std::shift_left(m_values.begin(), m_values.end(), 1);
m_values.back() = std::move(pair);
}
// Otherwise append at end
else {
m_values.emplace_back(std::move(pair));
}
}
void remove(K const& key) {
ranges::remove(m_values, [&key](auto const& q) { return q.first == key; });
}
void clear() {
m_values.clear();
}
void limit(size_t size) {
m_sizeLimit = size;
m_values.clear();
}
size_t size() const {
return m_values.size();
}
size_t limit() const {
return m_sizeLimit;
}
};
template <class F>
struct ExtractFun;
template <class V, class... Args>
struct ExtractFun<ServerRequest<V>(*)(Args...)> {
using CacheKey = std::tuple<std::remove_cvref_t<Args>...>;
using Value = V;
template <class... CArgs>
static CacheKey key(CArgs const&... args) {
return std::make_tuple(args..., false);
}
template <class... CArgs>
static ServerRequest<V> invoke(auto&& func, CArgs const&... args) {
return func(args..., false);
}
};
template <auto F>
class FunCache final {
public:
using Extract = ExtractFun<decltype(F)>;
using CacheKey = typename Extract::CacheKey;
using Value = typename Extract::Value;
private:
std::mutex m_mutex;
CacheMap<CacheKey, ServerRequest<Value>> m_cache;
public:
FunCache() = default;
FunCache(FunCache const&) = delete;
FunCache(FunCache&&) = delete;
template <class... Args>
ServerRequest<Value> get(Args const&... args) {
std::unique_lock lock(m_mutex);
if (auto v = m_cache.get(Extract::key(args...))) {
return *v;
}
auto f = Extract::invoke(F, args...);
m_cache.add(Extract::key(args...), ServerRequest<Value>(f));
return f;
}
2024-05-30 12:11:18 -04:00
template <class... Args>
void remove(Args const&... args) {
std::unique_lock lock(m_mutex);
m_cache.remove(Extract::key(args...));
}
2024-04-22 08:21:44 -04:00
size_t size() {
std::unique_lock lock(m_mutex);
2024-04-22 08:21:44 -04:00
return m_cache.size();
}
2024-04-22 06:35:12 -04:00
void limit(size_t size) {
std::unique_lock lock(m_mutex);
2024-04-22 06:35:12 -04:00
m_cache.limit(size);
}
void clear() {
std::unique_lock lock(m_mutex);
m_cache.clear();
}
};
template <auto F>
FunCache<F>& getCache() {
static auto inst = FunCache<F>();
return inst;
}
static const char* jsonTypeToString(matjson::Type const& type) {
switch (type) {
case matjson::Type::Object: return "object";
case matjson::Type::Array: return "array";
case matjson::Type::Bool: return "boolean";
case matjson::Type::Number: return "number";
case matjson::Type::String: return "string";
case matjson::Type::Null: return "null";
default: return "unknown";
}
}
static Result<matjson::Value, ServerError> parseServerPayload(web::WebResponse const& response) {
auto asJson = response.json();
if (!asJson) {
return Err(ServerError(response.code(), "Response was not valid JSON: {}", asJson.unwrapErr()));
}
auto json = std::move(asJson).unwrap();
if (!json.is_object()) {
return Err(ServerError(response.code(), "Expected object, got {}", jsonTypeToString(json.type())));
}
auto obj = json.as_object();
if (!obj.contains("payload")) {
return Err(ServerError(response.code(), "Object does not contain \"payload\" key - got {}", json.dump()));
}
return Ok(obj["payload"]);
}
static ServerError parseServerError(web::WebResponse const& error) {
2024-02-26 17:55:33 -05:00
// The server should return errors as `{ "error": "...", "payload": "" }`
if (auto asJson = error.json()) {
auto json = asJson.unwrap();
if (json.is_object() && json.contains("error")) {
return ServerError(
error.code(),
"{}", json.template get<std::string>("error")
);
}
else {
return ServerError(error.code(), "Unknown (not valid JSON)");
}
2024-02-26 17:55:33 -05:00
}
// But if we get something else for some reason, return that
else {
return ServerError(
error.code(),
"{}", error.string().unwrapOr("Unknown (not a valid string)")
);
2024-02-26 17:55:33 -05:00
}
}
static ServerProgress parseServerProgress(web::WebProgress const& prog, auto msg) {
2024-02-26 17:55:33 -05:00
if (auto per = prog.downloadProgress()) {
return ServerProgress(msg, static_cast<uint8_t>(*per));
2024-02-26 17:55:33 -05:00
}
else {
return ServerProgress(msg);
2024-02-26 17:55:33 -05:00
}
}
2024-02-26 15:14:53 -05:00
const char* server::sortToString(ModsSort sorting) {
switch (sorting) {
default:
case ModsSort::Downloads: return "downloads";
case ModsSort::RecentlyUpdated: return "recently_updated";
case ModsSort::RecentlyPublished: return "recently_published";
}
}
2024-03-02 17:28:06 -05:00
std::string ServerDateTime::toAgoString() const {
auto const fmtPlural = [](auto count, auto unit) {
if (count == 1) {
return fmt::format("{} {} ago", count, unit);
}
return fmt::format("{} {}s ago", count, unit);
};
auto now = Clock::now();
auto len = std::chrono::duration_cast<std::chrono::minutes>(now - value).count();
if (len < 60) {
return fmtPlural(len, "minute");
}
len = std::chrono::duration_cast<std::chrono::hours>(now - value).count();
if (len < 24) {
return fmtPlural(len, "hour");
}
len = std::chrono::duration_cast<std::chrono::days>(now - value).count();
if (len < 31) {
return fmtPlural(len, "day");
}
return fmt::format("{:%b %d %Y}", value);
2024-03-02 17:28:06 -05:00
}
Result<ServerDateTime> ServerDateTime::parse(std::string const& str) {
std::stringstream ss(str);
std::tm value;
if (ss >> std::get_time(&value, "%Y-%m-%dT%H:%M:%SZ")) {
auto time = std::mktime(&value);
2024-03-02 17:28:06 -05:00
return Ok(ServerDateTime {
.value = Clock::from_time_t(time),
2024-03-02 17:28:06 -05:00
});
}
return Err("Invalid date time format '{}'", str);
}
2024-02-26 15:14:53 -05:00
Result<ServerModVersion> ServerModVersion::parse(matjson::Value const& raw) {
auto json = raw;
JsonChecker checker(json);
auto root = checker.root("ServerModVersion").obj();
auto res = ServerModVersion();
// Verify target Geode version
auto version = root.needs("geode").template get<VersionInfo>();
if (!semverCompare(Loader::get()->getVersion(), version)) {
return Err(
"Mod targets version {} but Geode is version {}",
version, Loader::get()->getVersion()
);
}
// Verify target GD version
auto gd = root.needs("gd").obj().needs(GEODE_PLATFORM_SHORT_IDENTIFIER).template get<std::string>();
if (gd != GEODE_GD_VERSION_STR && gd != "*") {
2024-02-26 15:14:53 -05:00
return Err(
"Mod targets GD version {} but current is version {}",
gd, GEODE_GD_VERSION_STR
);
}
// Get server info
root.needs("download_link").into(res.downloadURL);
root.needs("download_count").into(res.downloadCount);
root.needs("hash").into(res.hash);
// Get mod metadata info
res.metadata.setID(root.needs("mod_id").template get<std::string>());
res.metadata.setName(root.needs("name").template get<std::string>());
res.metadata.setDescription(root.needs("description").template get<std::string>());
res.metadata.setVersion(root.needs("version").template get<VersionInfo>());
res.metadata.setIsAPI(root.needs("api").template get<bool>());
std::vector<ModMetadata::Dependency> dependencies {};
for (auto dep : root.has("dependencies").iterate()) {
// todo: this should probably be generalized to use the same function as mod.json
auto obj = dep.obj();
bool onThisPlatform = !obj.has("platforms");
for (auto& plat : obj.has("platforms").iterate()) {
if (PlatformID::from(plat.get<std::string>()) == GEODE_PLATFORM_TARGET) {
onThisPlatform = true;
}
}
if (!onThisPlatform) {
continue;
}
ModMetadata::Dependency dependency;
obj.needs("mod_id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(dependency.id);
2024-02-26 15:14:53 -05:00
obj.needs("version").into(dependency.version);
obj.has("importance").into(dependency.importance);
// Check if this dependency is installed, and if so assign the `mod` member to mark that
auto mod = Loader::get()->getInstalledMod(dependency.id);
if (mod && dependency.version.compare(mod->getVersion())) {
dependency.mod = mod;
}
2024-02-26 15:14:53 -05:00
dependencies.push_back(dependency);
}
res.metadata.setDependencies(dependencies);
std::vector<ModMetadata::Incompatibility> incompatibilities {};
for (auto& incompat : root.has("incompatibilities").iterate()) {
auto obj = incompat.obj();
ModMetadata::Incompatibility incompatibility;
obj.needs("mod_id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(incompatibility.id);
2024-02-26 15:14:53 -05:00
obj.needs("version").into(incompatibility.version);
obj.has("importance").into(incompatibility.importance);
// Check if this incompatability is installed, and if so assign the `mod` member to mark that
auto mod = Loader::get()->getInstalledMod(incompatibility.id);
if (mod && incompatibility.version.compare(mod->getVersion())) {
incompatibility.mod = mod;
}
2024-02-26 15:14:53 -05:00
incompatibilities.push_back(incompatibility);
}
2024-03-26 16:18:34 -04:00
res.metadata.setIncompatibilities(incompatibilities);
2024-02-26 15:14:53 -05:00
// Check for errors and return result
if (root.isError()) {
return Err(root.getError());
}
return Ok(res);
}
2024-03-26 16:18:34 -04:00
Result<ServerModUpdate> ServerModUpdate::parse(matjson::Value const& raw) {
auto json = raw;
JsonChecker checker(json);
auto root = checker.root("ServerModUpdate").obj();
auto res = ServerModUpdate();
root.needs("id").into(res.id);
root.needs("version").into(res.version);
// Check for errors and return result
if (root.isError()) {
return Err(root.getError());
}
return Ok(res);
}
Result<std::vector<ServerModUpdate>> ServerModUpdate::parseList(matjson::Value const& raw) {
auto json = raw;
JsonChecker checker(json);
auto payload = checker.root("ServerModUpdatesList").array();
std::vector<ServerModUpdate> list {};
for (auto item : payload.iterate()) {
auto mod = ServerModUpdate::parse(item.json());
if (mod) {
list.push_back(mod.unwrap());
}
else {
log::error("Unable to parse mod update from the server: {}", mod.unwrapErr());
}
}
// Check for errors and return result
if (payload.isError()) {
return Err(payload.getError());
}
return Ok(list);
}
bool ServerModUpdate::hasUpdateForInstalledMod() const {
if (auto mod = Loader::get()->getInstalledMod(this->id)) {
2024-03-26 16:18:34 -04:00
return mod->getVersion() < this->version;
}
return false;
}
2024-02-26 15:14:53 -05:00
Result<ServerModMetadata> ServerModMetadata::parse(matjson::Value const& raw) {
auto json = raw;
JsonChecker checker(json);
auto root = checker.root("ServerModMetadata").obj();
auto res = ServerModMetadata();
root.needs("id").into(res.id);
root.needs("featured").into(res.featured);
root.needs("download_count").into(res.downloadCount);
root.has("about").into(res.about);
root.has("changelog").into(res.changelog);
2024-03-25 06:36:33 -04:00
root.has("repository").into(res.repository);
2024-03-02 17:28:06 -05:00
if (root.has("created_at")) {
GEODE_UNWRAP_INTO(res.createdAt, ServerDateTime::parse(root.has("created_at").template get<std::string>()));
}
if (root.has("updated_at")) {
GEODE_UNWRAP_INTO(res.updatedAt, ServerDateTime::parse(root.has("updated_at").template get<std::string>()));
}
2024-02-26 15:14:53 -05:00
2024-02-26 17:55:33 -05:00
std::vector<std::string> developerNames;
2024-02-26 15:14:53 -05:00
for (auto item : root.needs("developers").iterate()) {
auto obj = item.obj();
auto dev = ServerDeveloper();
obj.needs("username").into(dev.username);
obj.needs("display_name").into(dev.displayName);
obj.needs("is_owner").into(dev.isOwner);
2024-02-26 15:14:53 -05:00
res.developers.push_back(dev);
2024-02-26 17:55:33 -05:00
developerNames.push_back(dev.displayName);
2024-02-26 15:14:53 -05:00
}
for (auto item : root.needs("versions").iterate()) {
auto versionRes = ServerModVersion::parse(item.json());
if (versionRes) {
auto version = versionRes.unwrap();
version.metadata.setDetails(res.about);
version.metadata.setChangelog(res.changelog);
2024-02-26 17:55:33 -05:00
version.metadata.setDevelopers(developerNames);
2024-03-25 06:36:33 -04:00
version.metadata.setRepository(res.repository);
2024-02-26 15:14:53 -05:00
res.versions.push_back(version);
}
else {
log::error("Unable to parse mod '{}' version from the server: {}", res.id, versionRes.unwrapErr());
}
}
// Ensure there's at least one valid version
if (res.versions.empty()) {
return Err("Mod '{}' has no (valid) versions", res.id);
}
2024-05-30 12:11:18 -04:00
2024-02-26 15:14:53 -05:00
for (auto item : root.has("tags").iterate()) {
res.tags.insert(item.template get<std::string>());
}
root.needs("download_count").into(res.downloadCount);
// Check for errors and return result
if (root.isError()) {
return Err(root.getError());
}
return Ok(res);
}
std::string ServerModMetadata::formatDevelopersToString() const {
std::optional<ServerDeveloper> owner = ranges::find(developers, [] (auto item) {
return item.isOwner;
});
switch (developers.size()) {
case 0: return "Unknown"; break;
case 1: return developers.front().displayName; break;
case 2: return developers.front().displayName + " & " + developers.back().displayName; break;
default: {
if (owner) {
return fmt::format("{} + {} More", owner->displayName, developers.size() - 1);
} else {
return fmt::format("{} + {} More", developers.front().displayName, developers.size() - 1);
}
} break;
}
}
2024-02-26 15:14:53 -05:00
Result<ServerModsList> ServerModsList::parse(matjson::Value const& raw) {
auto json = raw;
JsonChecker checker(json);
auto payload = checker.root("ServerModsList").obj();
2024-02-26 15:14:53 -05:00
auto list = ServerModsList();
for (auto item : payload.needs("data").iterate()) {
2024-02-26 18:16:24 -05:00
auto mod = ServerModMetadata::parse(item.json());
if (mod) {
list.mods.push_back(mod.unwrap());
}
else {
log::error("Unable to parse mod from the server: {}", mod.unwrapErr());
}
2024-02-26 15:14:53 -05:00
}
payload.needs("count").into(list.totalModCount);
// Check for errors and return result
if (payload.isError()) {
return Err(payload.getError());
}
return Ok(list);
}
2024-03-05 11:25:35 -05:00
ModMetadata ServerModMetadata::latestVersion() const {
return this->versions.front().metadata;
}
bool ServerModMetadata::hasUpdateForInstalledMod() const {
if (auto mod = Loader::get()->getInstalledMod(this->id)) {
2024-03-05 11:25:35 -05:00
return mod->getVersion() < this->latestVersion().getVersion();
}
return false;
}
2024-02-26 15:14:53 -05:00
std::string server::getServerAPIBaseURL() {
return "https://api.geode-sdk.org/v1";
}
2024-04-28 17:31:14 -04:00
template <class... Args>
std::string formatServerURL(fmt::format_string<Args...> fmt, Args&&... args) {
return getServerAPIBaseURL() + fmt::format(fmt, std::forward<Args>(args)...);
}
2024-02-26 20:36:38 -05:00
std::string server::getServerUserAgent() {
// this may change in the future..
return fmt::format("Geode {}/{}",
2024-04-28 17:31:14 -04:00
Loader::get()->getVersion().toVString(),
2024-02-26 20:36:38 -05:00
PlatformID::toShortString(GEODE_PLATFORM_TARGET)
);
}
2024-04-22 06:35:12 -04:00
ServerRequest<ServerModsList> server::getMods(ModsQuery const& query, bool useCache) {
if (useCache) {
2024-04-22 06:35:12 -04:00
return getCache<getMods>().get(query);
}
auto req = web::WebRequest();
req.userAgent(getServerUserAgent());
// Always target current GD version and Loader version
req.param("gd", GEODE_GD_VERSION_STR);
2024-04-28 17:31:14 -04:00
req.param("geode", Loader::get()->getVersion().toVString());
// Add search params
if (query.query) {
req.param("query", *query.query);
}
if (query.platforms.size()) {
std::string plats = "";
bool first = true;
for (auto plat : query.platforms) {
if (!first) plats += ",";
plats += PlatformID::toShortString(plat.m_value);
first = false;
}
req.param("platforms", plats);
}
if (query.tags.size()) {
req.param("tags", ranges::join(query.tags, ","));
}
if (query.featured) {
req.param("featured", query.featured.value() ? "true" : "false");
}
req.param("sort", sortToString(query.sorting));
if (query.developer) {
req.param("developer", *query.developer);
}
// Paging (1-based on server, 0-based locally)
req.param("page", std::to_string(query.page + 1));
req.param("per_page", std::to_string(query.pageSize));
2024-04-28 17:31:14 -04:00
return req.get(formatServerURL("/mods")).map(
[](web::WebResponse* response) -> Result<ServerModsList, ServerError> {
if (response->ok()) {
// Parse payload
auto payload = parseServerPayload(*response);
if (!payload) {
return Err(payload.unwrapErr());
}
// Parse response
auto list = ServerModsList::parse(payload.unwrap());
if (!list) {
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
}
return Ok(list.unwrap());
}
2024-04-22 06:35:12 -04:00
// Treat a 404 as empty mods list
if (response->code() == 404) {
return Ok(ServerModsList());
}
2024-04-22 06:35:12 -04:00
return Err(parseServerError(*response));
},
[](web::WebProgress* progress) {
return parseServerProgress(*progress, "Downloading mods");
}
);
}
2024-04-22 06:35:12 -04:00
ServerRequest<ServerModMetadata> server::getMod(std::string const& id, bool useCache) {
if (useCache) {
return getCache<getMod>().get(id);
2024-02-26 15:14:53 -05:00
}
2024-02-26 17:55:33 -05:00
auto req = web::WebRequest();
2024-02-26 20:36:38 -05:00
req.userAgent(getServerUserAgent());
2024-04-28 17:31:14 -04:00
return req.get(formatServerURL("/mods/{}", id)).map(
2024-04-22 06:35:12 -04:00
[](web::WebResponse* response) -> Result<ServerModMetadata, ServerError> {
if (response->ok()) {
// Parse payload
2024-04-22 06:35:12 -04:00
auto payload = parseServerPayload(*response);
if (!payload) {
return Err(payload.unwrapErr());
}
// Parse response
auto list = ServerModMetadata::parse(payload.unwrap());
if (!list) {
2024-04-22 06:35:12 -04:00
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
}
return Ok(list.unwrap());
}
2024-04-22 06:35:12 -04:00
return Err(parseServerError(*response));
},
[id](web::WebProgress* progress) {
return parseServerProgress(*progress, "Downloading metadata for " + id);
}
);
}
ServerRequest<ServerModVersion> server::getModVersion(std::string const& id, ModVersion const& version, bool useCache) {
if (useCache) {
2024-05-30 12:11:18 -04:00
auto& cache = getCache<getModVersion>();
auto cachedRequest = cache.get(id, version);
// if mod installation was cancelled, remove it from cache and fetch again
if (cachedRequest.isCancelled()) {
cache.remove(id, version);
return cache.get(id, version);
} else {
return cachedRequest;
}
}
auto req = web::WebRequest();
req.userAgent(getServerUserAgent());
std::string versionURL;
std::visit(makeVisitor {
[&](ModVersionLatest const&) {
versionURL = "latest";
},
[&](ModVersionMajor const& ver) {
versionURL = "latest";
req.param("major", std::to_string(ver.major));
},
[&](ModVersionSpecific const& ver) {
versionURL = ver.toNonVString(false);
},
}, version);
2024-04-28 17:31:14 -04:00
return req.get(formatServerURL("/mods/{}/versions/{}", id, versionURL)).map(
[](web::WebResponse* response) -> Result<ServerModVersion, ServerError> {
if (response->ok()) {
// Parse payload
auto payload = parseServerPayload(*response);
if (!payload) {
return Err(payload.unwrapErr());
}
// Parse response
auto list = ServerModVersion::parse(payload.unwrap());
if (!list) {
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
}
return Ok(list.unwrap());
}
return Err(parseServerError(*response));
},
[id](web::WebProgress* progress) {
2024-04-22 06:35:12 -04:00
return parseServerProgress(*progress, "Downloading metadata for " + id);
}
);
}
2024-02-26 20:36:38 -05:00
2024-04-22 06:35:12 -04:00
ServerRequest<ByteVector> server::getModLogo(std::string const& id, bool useCache) {
if (useCache) {
return getCache<getModLogo>().get(id);
}
auto req = web::WebRequest();
req.userAgent(getServerUserAgent());
2024-04-28 17:31:14 -04:00
return req.get(formatServerURL("/mods/{}/logo", id)).map(
2024-04-22 06:35:12 -04:00
[](web::WebResponse* response) -> Result<ByteVector, ServerError> {
if (response->ok()) {
return Ok(response->data());
}
return Err(parseServerError(*response));
},
[id](web::WebProgress* progress) {
return parseServerProgress(*progress, "Downloading logo for " + id);
}
);
2024-02-26 15:14:53 -05:00
}
2024-03-24 05:27:28 -04:00
2024-04-22 06:35:12 -04:00
ServerRequest<std::unordered_set<std::string>> server::getTags(bool useCache) {
if (useCache) {
return getCache<getTags>().get();
}
2024-03-24 05:27:28 -04:00
auto req = web::WebRequest();
req.userAgent(getServerUserAgent());
2024-04-28 17:31:14 -04:00
return req.get(formatServerURL("/tags")).map(
2024-04-22 06:35:12 -04:00
[](web::WebResponse* response) -> Result<std::unordered_set<std::string>, ServerError> {
if (response->ok()) {
2024-03-24 05:27:28 -04:00
// Parse payload
2024-04-22 06:35:12 -04:00
auto payload = parseServerPayload(*response);
2024-03-24 05:27:28 -04:00
if (!payload) {
return Err(payload.unwrapErr());
}
matjson::Value json = payload.unwrap();
if (!json.is_array()) {
2024-04-22 06:35:12 -04:00
return Err(ServerError(response->code(), "Expected a string array"));
2024-03-24 05:27:28 -04:00
}
std::unordered_set<std::string> tags;
for (auto item : json.as_array()) {
if (!item.is_string()) {
2024-04-22 06:35:12 -04:00
return Err(ServerError(response->code(), "Expected a string array"));
2024-03-24 05:27:28 -04:00
}
tags.insert(item.as_string());
}
return Ok(tags);
}
2024-04-22 06:35:12 -04:00
return Err(parseServerError(*response));
},
[](web::WebProgress* progress) {
return parseServerProgress(*progress, "Downloading valid tags");
}
);
2024-03-24 05:27:28 -04:00
}
2024-03-26 16:18:34 -04:00
ServerRequest<std::optional<ServerModUpdate>> server::checkUpdates(Mod* mod) {
return checkAllUpdates().map(
[mod](Result<std::vector<ServerModUpdate>, ServerError>* result) -> Result<std::optional<ServerModUpdate>, ServerError> {
if (result->isOk()) {
for (auto& update : result->unwrap()) {
if (update.id == mod->getID() && update.version > mod->getVersion()) {
return Ok(update);
}
}
return Ok(std::nullopt);
}
return Err(result->unwrapErr());
}
);
}
ServerRequest<std::vector<ServerModUpdate>> server::checkAllUpdates(bool useCache) {
2024-04-22 06:35:12 -04:00
if (useCache) {
return getCache<checkAllUpdates>().get();
2024-04-22 06:35:12 -04:00
}
auto modIDs = ranges::map<std::vector<std::string>>(
Loader::get()->getAllMods(),
[](auto mod) { return mod->getID(); }
);
2024-05-30 12:11:18 -04:00
2024-03-26 16:18:34 -04:00
auto req = web::WebRequest();
req.userAgent(getServerUserAgent());
req.param("platform", GEODE_PLATFORM_SHORT_IDENTIFIER);
if (modIDs.size()) {
req.param("ids", ranges::join(modIDs, ";"));
}
2024-04-28 17:31:14 -04:00
return req.get(formatServerURL("/mods/updates")).map(
2024-04-22 06:35:12 -04:00
[](web::WebResponse* response) -> Result<std::vector<ServerModUpdate>, ServerError> {
if (response->ok()) {
2024-03-26 16:18:34 -04:00
// Parse payload
2024-04-22 06:35:12 -04:00
auto payload = parseServerPayload(*response);
2024-03-26 16:18:34 -04:00
if (!payload) {
return Err(payload.unwrapErr());
}
// Parse response
auto list = ServerModUpdate::parseList(payload.unwrap());
if (!list) {
2024-04-22 06:35:12 -04:00
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
2024-03-26 16:18:34 -04:00
}
return Ok(list.unwrap());
}
2024-04-22 06:35:12 -04:00
return Err(parseServerError(*response));
},
[](web::WebProgress* progress) {
return parseServerProgress(*progress, "Checking updates for mods");
}
);
2024-03-26 16:18:34 -04:00
}
2024-04-22 06:35:12 -04:00
void server::clearServerCaches(bool clearGlobalCaches) {
getCache<&getMods>().clear();
getCache<&getMod>().clear();
getCache<&getModLogo>().clear();
// Only clear global caches if explicitly requested
if (clearGlobalCaches) {
2024-04-22 06:35:12 -04:00
getCache<&getTags>().clear();
getCache<&checkAllUpdates>().clear();
}
}
2024-04-22 06:35:12 -04:00
$execute {
listenForSettingChanges<int64_t>("server-cache-size-limit", +[](int64_t size) {
getCache<&server::getMods>().limit(size);
getCache<&server::getMod>().limit(size);
getCache<&server::getModLogo>().limit(size);
getCache<&server::getTags>().limit(size);
getCache<&server::checkAllUpdates>().limit(size);
2024-04-22 06:35:12 -04:00
});
}