Skip to content

Commit

Permalink
Add support for multiplatform projects
Browse files Browse the repository at this point in the history
  • Loading branch information
Su5eD committed Dec 4, 2024
1 parent 15c2b2f commit 4685d9d
Show file tree
Hide file tree
Showing 16 changed files with 454 additions and 545 deletions.
23 changes: 19 additions & 4 deletions sql/20241115210200_up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@ create table project
id text not null
primary key,
name text not null,
platform varchar(50) not null,
slug text not null,
platforms varchar(255) not null,
source_path text not null,
source_repo text not null,
source_branch text not null,
is_community boolean default false not null,
type varchar(255) not null,
created_at timestamp(3) default CURRENT_TIMESTAMP not null
created_at timestamp(3) default CURRENT_TIMESTAMP not null,
search_vector tsvector
);

create unique index "project_source_repo_source_path_key"
CREATE UNIQUE INDEX "project_source_repo_source_path_key"
on project (source_repo, source_path);

CREATE INDEX project_search_vector_idx ON project USING gin(search_vector);

CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('english', NEW.id || ' ' || NEW.name);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_search_vector
BEFORE INSERT ON project
FOR EACH ROW
EXECUTE FUNCTION update_search_vector();
17 changes: 0 additions & 17 deletions sql/20241116210100_search.sql
Original file line number Diff line number Diff line change
@@ -1,18 +1 @@
ALTER TABLE project ADD COLUMN search_vector tsvector;

UPDATE project SET search_vector = to_tsvector('english', id || ' ' || name);

CREATE INDEX project_search_vector_idx ON project USING gin(search_vector);

CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('english', NEW.id || ' ' || NEW.name);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_search_vector
BEFORE INSERT ON project
FOR EACH ROW
EXECUTE FUNCTION update_search_vector();
4 changes: 2 additions & 2 deletions src/api/v1/browse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <models/Project.h>

#include "log/log.h"
#include <service/util.h>

#include <string>

Expand Down Expand Up @@ -30,8 +31,7 @@ namespace api::v1 {
itemJson["id"] = item.getValueOfId();
itemJson["name"] = item.getValueOfName();
itemJson["type"] = item.getValueOfType();
itemJson["platform"] = item.getValueOfPlatform();
itemJson["slug"] = item.getValueOfSlug();
itemJson["platforms"] = parseJsonString(item.getValueOfPlatforms()).value_or(Json::Value());
itemJson["is_community"] = item.getValueOfIsCommunity();
itemJson["created_at"] = item.getValueOfCreatedAt().toDbStringLocal();
data.append(itemJson);
Expand Down
8 changes: 4 additions & 4 deletions src/api/v1/docs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace api::v1 {
if (const auto [versions, versionsError](co_await documentation.getAvailableVersionsFiltered(project, installationToken));
versions)
{
for (auto it = versions->begin(); it != versions->end(); it++) {
for (auto it = versions->begin(); it != versions->end(); ++it) {
if (it.key().isString() && it->isString() && it.key().asString() == *version) {
co_return it->asString();
}
Expand Down Expand Up @@ -88,7 +88,7 @@ namespace api::v1 {

Json::Value root;
{
Json::Value projectJson = proj->project.toJson();
Json::Value projectJson = projectToJson(proj->project);
projectJson["is_public"] = isPublic;
if (versions) {
projectJson["versions"] = *versions;
Expand Down Expand Up @@ -145,7 +145,7 @@ namespace api::v1 {

Json::Value root;
{
Json::Value projectJson = proj->project.toJson();
Json::Value projectJson = projectToJson(proj->project);
projectJson["is_public"] = isPublic;
if (versions) {
projectJson["versions"] = *versions;
Expand Down Expand Up @@ -194,7 +194,7 @@ namespace api::v1 {

nlohmann::json root;
{
Json::Value projectJson = proj->project.toJson();
Json::Value projectJson = projectToJson(proj->project);
projectJson["is_public"] = isPublic;
if (versions) {
projectJson["versions"] = *versions;
Expand Down
126 changes: 86 additions & 40 deletions src/api/v1/projects.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,72 @@ namespace api::v1 {
co_return false;
}

nlohmann::json processPlatforms(const std::vector<std::string> &valid, const Json::Value &metadata) {
nlohmann::json projects(nlohmann::json::value_t::object);
if (metadata.isMember("platforms") && metadata["platforms"].isObject()) {
const auto platforms = metadata["platforms"];
for (const auto &platform: valid) {
if (platforms.isMember(platform) && platforms[platform].isString()) {
projects[platform] = platforms[platform].asString();
}
}
}
if (metadata.isMember("platform") && metadata["platform"].isString() && !projects.contains(metadata["platform"].asString()) &&
metadata.isMember("slug") && metadata["slug"].isString())
{
projects[metadata["platform"].asString()] = metadata["slug"].asString();
}
return projects;
}

ProjectsController::ProjectsController(GitHub &gh, Platforms &pf, Database &db, Documentation &dc) :
github_(gh), platforms_(pf), database_(db), documentation_(dc) {}

Task<std::optional<PlatformProject>> ProjectsController::validatePlatform(const std::string &id, const std::string &repo,
const std::string &mrCode, const std::string &platform,
const std::string &slug, const bool checkExisting,
std::function<void(const HttpResponsePtr &)> callback) const {
if (platform == PLATFORM_CURSEFORGE && !platforms_.curseforge_.isAvailable()) {
simpleError(Error::ErrBadRequest, "cf_unavailable", callback);
co_return std::nullopt;
}

const auto platformProj = co_await platforms_.getProject(platform, slug);
if (!platformProj) {
simpleError(Error::ErrBadRequest, "no_project", callback,
[&](Json::Value &root) { root["details"] = "Platform: " + platform; });
co_return std::nullopt;
}

if (platformProj->type == _unknown) {
simpleError(Error::ErrBadRequest, "unsupported_type", callback,
[&](Json::Value &root) { root["details"] = "Platform: " + platform; });
co_return std::nullopt;
}

bool skipCheck = false;
if (checkExisting) {
if (const auto [proj, projErr] = co_await database_.getProjectSource(id); proj) {
if (const auto platforms = parseJsonString(proj->getValueOfPlatforms());
platforms && platforms->isMember(platform) && (*platforms)[platform] == slug)
{
skipCheck = true;
}
}
}
if (!skipCheck) {
if (const auto verified = co_await verifyProjectOwnership(platforms_, *platformProj, repo, mrCode); !verified) {
simpleError(Error::ErrBadRequest, "ownership", callback, [&](Json::Value &root) {
root["details"] = "Platform: " + platform;
root["can_verify_mr"] = platform == PLATFORM_MODRINTH && platforms_.modrinth_.isOAuthConfigured();
});
co_return std::nullopt;
}
}

co_return platformProj;
}

/**
* Possible errors:
* - user_github_auth: Invalid GitHub auth token
Expand All @@ -48,15 +111,16 @@ namespace api::v1 {
* - no_meta: No metadata file
* - invalid_meta: Metadata file format is invalid
* - exists: Duplicate project ID
* - no_platforms: No defined platform projects
* - cf_unavailable: CF Project registration unavailable due to missing API key
* - no_project: Platform project not found
* - unsupported_type: Unsupported platform project type
* - ownership: Failed to verify platform project ownership (data: can_verify_mr)
* - internal: Internal server error
*/
Task<std::optional<std::pair<std::string, Project>>> ProjectsController::validateProjectData(const Json::Value &json, const std::string &token,
std::function<void(const HttpResponsePtr &)> callback,
const bool checkExisting) const {
Task<std::optional<ValidatedProjectData>>
ProjectsController::validateProjectData(const Json::Value &json, const std::string &token,
std::function<void(const HttpResponsePtr &)> callback, const bool checkExisting) const {
const auto [username, usernameError](co_await github_.getUsername(token));
if (!username) {
simpleError(Error::ErrBadRequest, "user_github_auth", callback);
Expand Down Expand Up @@ -109,52 +173,34 @@ namespace api::v1 {
}
const auto jsonContent = *contents;
const auto id = jsonContent["id"].asString();
const auto platform = jsonContent["platform"].asString();
const auto slug = jsonContent["slug"].asString();

if (platform == PLATFORM_CURSEFORGE && !platforms_.curseforge_.isAvailable()) {
simpleError(Error::ErrBadRequest, "cf_unavailable", callback);
co_return std::nullopt;
}

const auto platformProj = co_await platforms_.getProject(platform, slug);
if (!platformProj) {
simpleError(Error::ErrBadRequest, "no_project", callback);
co_return std::nullopt;
}

if (platformProj->type == _unknown) {
simpleError(Error::ErrBadRequest, "unsupported_type", callback);
const auto platforms = processPlatforms(platforms_.getAvailablePlatforms(), jsonContent);
if (platforms.empty()) {
simpleError(Error::ErrBadRequest, "no_platforms", callback);
co_return std::nullopt;
}

bool skipCheck = false;
if (checkExisting) {
if (const auto [proj, projErr] = co_await database_.getProjectSource(id);
proj && proj->getValueOfPlatform() == platform && proj->getValueOfSlug() == slug) {
skipCheck = true;
}
}
if (!skipCheck) {
if (const auto verified = co_await verifyProjectOwnership(platforms_, *platformProj, repo, mrCode); !verified) {
simpleError(Error::ErrBadRequest, "ownership", callback, [&](Json::Value &root) {
root["can_verify_mr"] = platform == PLATFORM_MODRINTH && platforms_.modrinth_.isOAuthConfigured();
});
std::unordered_map<std::string, PlatformProject> platformProjects;
for (const auto &[key, val]: platforms.items()) {
const auto platformProj = co_await validatePlatform(id, repo, mrCode, key, val, checkExisting, callback);
if (!platformProj) {
co_return std::nullopt;
}
platformProjects.emplace(key, *platformProj);
}
const auto &preferredProj =
platforms.contains(PLATFORM_MODRINTH) ? platformProjects[PLATFORM_MODRINTH] : platformProjects.begin()->second;

Project project;
project.setId(id);
project.setName(platformProj->name);
project.setName(preferredProj.name);
project.setSourceRepo(repo);
project.setSourceBranch(branch);
project.setSourcePath(path);
project.setPlatform(platform);
project.setType(projectTypeToString(platformProj->type));
project.setSlug(slug);
project.setType(projectTypeToString(preferredProj.type));
project.setPlatforms(platforms.dump());

co_return std::pair{*username, project};
co_return ValidatedProjectData{*username, project, platforms};
}

Task<std::optional<Project>> ProjectsController::validateProjectAccess(const std::string &id, const std::string &token,
Expand Down Expand Up @@ -259,7 +305,7 @@ namespace api::v1 {
const auto projects = co_await database_.getProjectsForRepos(candidateRepos);
Json::Value projectsJson(Json::arrayValue);
for (const auto &project: projects) {
projectsJson.append(project.toJson());
projectsJson.append(projectToJson(project));
}

Json::Value root;
Expand Down Expand Up @@ -311,10 +357,10 @@ namespace api::v1 {
if (!validated) {
co_return;
}
auto [username, project] = *validated;
auto [username, project, platforms] = *validated;
project.setIsCommunity(isCommunity);

if (co_await database_.existsForData(project.getValueOfId(), project.getValueOfPlatform(), project.getValueOfSlug())) {
if (co_await database_.existsForData(project.getValueOfId(), platforms)) {
co_return simpleError(Error::ErrBadRequest, "exists", callback);
}

Expand Down Expand Up @@ -350,7 +396,7 @@ namespace api::v1 {
if (!validated) {
co_return;
}
auto [username, project] = *validated;
auto [username, project, platforms] = *validated;

if (const auto [proj, projErr] = co_await database_.getProjectSource(project.getValueOfId()); !proj) {
co_return simpleError(Error::ErrBadRequest, "not_found", callback);
Expand All @@ -366,7 +412,7 @@ namespace api::v1 {

Json::Value root;
root["message"] = "Project updated successfully";
root["project"] = project.toJson();
root["project"] = projectToJson(project);
const auto resp = HttpResponse::newHttpJsonResponse(root);
resp->setStatusCode(k200OK);
callback(resp);
Expand Down
13 changes: 12 additions & 1 deletion src/api/v1/projects.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
#include "service/platforms.h"

namespace api::v1 {
struct ValidatedProjectData {
std::string username;
Project project;
nlohmann::json platforms;
};

class ProjectsController final : public drogon::HttpController<ProjectsController, false> {
public:
explicit ProjectsController(GitHub &, Platforms &, Database &, Documentation &);
Expand Down Expand Up @@ -43,7 +49,12 @@ namespace api::v1 {
std::string token) const;

private:
drogon::Task<std::optional<std::pair<std::string, Project>>>
drogon::Task<std::optional<PlatformProject>>
validatePlatform(const std::string &id, const std::string &repo, const std::string &mrCode,
const std::string &platform, const std::string &slug, bool checkExisting,
std::function<void(const drogon::HttpResponsePtr &)> callback) const;

drogon::Task<std::optional<ValidatedProjectData>>
validateProjectData(const Json::Value &json, const std::string &token,
std::function<void(const drogon::HttpResponsePtr &)> callback, bool checkExisting) const;

Expand Down
Loading

0 comments on commit 4685d9d

Please sign in to comment.