Skip to content

Commit

Permalink
feat: added level verification!
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahkittyy committed Jan 8, 2023
1 parent 545afc7 commit 6400817
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 73 deletions.
66 changes: 61 additions & 5 deletions backend/controllers/Level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
validate,
validateOrReject,
} from 'class-validator';
import { uploadReplay } from './Replay';

const SortableFields = [
'id',
Expand Down Expand Up @@ -107,6 +108,7 @@ export interface ILevelResponse {
records: number;
comments: number;
myVote?: 1 | 0 | -1;
verificationId?: number;
}

/* what is returned from the search endpoint */
Expand Down Expand Up @@ -352,9 +354,9 @@ export default class Level {
return res.status(400).send({ error: 'Description too long! Max 256 characters' });
}

const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
name: token.username,
id: token.id,
},
include: { levels: true },
});
Expand All @@ -375,6 +377,16 @@ export default class Level {
}
}

const verificationb64: string | undefined = req.body.verification;
if (verificationb64 == null) {
return res.status(400).send({ error: 'You must verify a level before posting it!' });
}
const verification: Buffer = Buffer.from(verificationb64, 'base64');
const replayData: tools.IReplayData | undefined = tools.decodeRawReplay(verification);
if (replayData == null) {
return res.status(400).send({ error: 'Invalid level verification.' });
}

const existingLevel = await prisma.level.findFirst({ where: { title } });
if (existingLevel) {
if (user.id !== existingLevel.authorId) {
Expand All @@ -387,16 +399,41 @@ export default class Level {
.status(409)
.send({ error: `A level with that title already exists (ID ${existingLevel.id})` });
}
const updatedLevel = await prisma.level.update({
const updatedLevelPreVerify = await prisma.level.update({
where: { id: existingLevel.id },
data: {
title,
description,
code,
updatedAt: new Date(),
scores: {
create: {
user: { connect: { id: token.id } },
replay: replayData.raw,
time: replayData.header.time,
version: replayData.header.version,
alt: replayData.header.alt,
},
},
},
include: {
scores: {
orderBy: { createdAt: 'desc' },
take: 1,
},
},
...LevelQueryInclude,
});
const updatedLevel =
updatedLevelPreVerify != null
? await prisma.level.update({
where: { id: updatedLevelPreVerify.id },
data: {
verification: { connect: { id: updatedLevelPreVerify.scores[0].id } },
},
...LevelQueryInclude,
})
: null;

if (!updatedLevel)
return res.status(500).send({ error: 'Internal server error (NO_OVERWRITE_LEVEL)' });
log.info(
Expand All @@ -407,15 +444,34 @@ export default class Level {
});
}

const newLevel = await prisma.level.create({
let newLevel = await prisma.level.create({
data: {
code,
author: { connect: { id: user.id } },
title,
description,
scores: {
create: {
user: { connect: { id: user.id } },
replay: replayData.raw,
time: replayData.header.time,
version: replayData.header.version,
alt: replayData.header.alt,
},
},
},
...LevelQueryInclude,
});
newLevel =
newLevel != null
? await prisma.level.update({
where: { id: newLevel.id },
data: {
verification: { connect: { id: newLevel.scores[0].id } },
},
...LevelQueryInclude,
})
: newLevel;

if (!newLevel) {
return res.status(500).send({ error: 'Internal server error (NO_POST_LEVEL)' });
Expand Down
105 changes: 69 additions & 36 deletions backend/controllers/Replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,7 @@ import { prisma } from '@db/index';
import log from '@/log';
import { UserLevelScore } from '@prisma/client';

import {
IsBoolean,
IsIn,
IsInt,
IsNumber,
IsOptional,
IsString,
Max,
Min,
validate,
validateOrReject,
} from 'class-validator';
import { IsIn, IsInt, IsNumber, IsOptional, Max, Min, validate } from 'class-validator';

const SortableFields = ['time', 'author', 'createdAt', 'updatedAt'] as const;
const SortDirections = ['asc', 'desc'] as const;
Expand All @@ -47,6 +36,7 @@ export class IReplaySearchOptions {
}

export interface IReplayResponse {
id: number;
user: string;
levelId: number;
time: number;
Expand All @@ -57,41 +47,84 @@ export interface IReplayResponse {
alt: boolean;
}

export async function uploadParsedReplay(
data: tools.IReplayData,
userId: number,
levelId?: number
): Promise<tools.UserLevelScoreRunner | undefined> {
try {
let score: tools.UserLevelScoreRunner | undefined = await prisma.userLevelScore.findFirst({
where: {
userId: userId,
levelId: levelId ?? data.header.levelId,
},
include: {
user: true,
},
});
if (score == undefined || data.header.time <= score.time) {
score = await prisma.userLevelScore.create({
data: {
user: { connect: { id: userId } },
level: { connect: { id: levelId ?? data.header.levelId } },
replay: data.raw,
time: data.header.time,
version: data.header.version,
alt: data.header.alt,
},
include: {
user: true,
},
});
}
return score;
} catch (e) {
log.warn(`Failure to upload level score: ${stringify(e)}`);
return undefined;
}
}

export async function uploadReplay(
b64replay: string,
userId: number,
levelId?: number
): Promise<tools.UserLevelScoreRunner | undefined> {
const buf = Buffer.from(b64replay, 'base64');
const data: tools.IReplayData | undefined = tools.decodeRawReplay(buf);
if (!data) return undefined;
return await uploadParsedReplay(data, userId, levelId);
}

// replay controller
export default class Replay {
// upload a new replay file
static async upload(req: Request, res: Response) {
const token: tools.IAuthToken = res.locals.token;
const b64replay = req.body.replay;
const buf = Buffer.from(b64replay, 'base64');
const data: tools.IReplayData | undefined = tools.decodeRawReplay(buf);
if (!data) return res.status(400).send({ error: 'Invalid base64 replay file.', data });

const replay: tools.UserLevelScoreRunner | undefined = await uploadReplay(b64replay, token.id);
if (replay == null) {
return res.status(400).send({ error: 'Invalid replay base64 data.' });
}
return res.status(200).send({ newBest: replay.time });
}

static async get(req: Request, res: Response) {
const token: tools.IAuthToken | undefined = res.locals.token;
const id = parseInt(req.params.id ?? 'nan');
if (isNaN(id)) return res.status(400).send({ error: 'Invalid id parameter.' });
try {
let score: UserLevelScore | undefined = await prisma.userLevelScore.findFirst({
where: {
userId: token.id,
levelId: data.header.levelId,
const replay = await prisma.userLevelScore.findUnique({
where: { id },
include: {
user: true,
},
});
if (score == undefined || data.header.time <= score.time) {
score = await prisma.userLevelScore.create({
data: {
user: { connect: { id: token.id } },
level: { connect: { id: data.header.levelId } },
replay: buf,
time: data.header.time,
version: data.header.version,
alt: data.header.alt,
},
});
}
return res.status(200).send({ newBest: score.time });
if (replay == null)
return res.status(400).send({ error: 'A replay with that ID was not found.' });
return res.status(200).send({ replay: tools.toReplayResponse(replay) });
} catch (e) {
log.warn(`Failure to upload level score: ${stringify(e)}`);
return res
.status(400)
.send({ error: `Score failed to send ${e?.code ?? '(UNKNOWN_ERROR)'}` });
return res.status(500).send({ error: 'Internal server error (NO_FETCH_RPL)' });
}
}

Expand Down
1 change: 1 addition & 0 deletions backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ app.post('/level/:id', checkAuth(), Level.getById);

app.post('/replay/upload', requireAuth(0), Replay.upload);
app.post('/replay/search/:levelId(\\d+)', checkAuth(), Replay.search);
app.post('/replay/:id(\\d+)', checkAuth(), Replay.get);

app.post('/comments/level/:levelId(\\d+)', Comment.get);
app.post('/comments/new/:levelId(\\d+)', requireAuth(0), Comment.post);
Expand Down
4 changes: 4 additions & 0 deletions backend/util/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export function toCommentResponse(comment: UserLevelCommentPoster): ICommentResp

export function toReplayResponse(replay: UserLevelScoreRunner): IReplayResponse {
return {
id: replay.id,
user: replay.user.name,
levelId: replay.levelId,
raw: replay.replay.toString('base64'),
Expand Down Expand Up @@ -276,6 +277,9 @@ export function toLevelResponse(lvl: LevelMetadataIncluded, userId?: number): IL
records: lvl.scores.length,
comments: lvl._count.comments,
myVote: userId != null ? userVote(userId, lvl.votes) : undefined,
...(lvl.verificationId != null && {
verificationId: lvl.verificationId,
}),
};
}

Expand Down
19 changes: 13 additions & 6 deletions game/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,8 @@ std::future<api::level_response> api::quickplay_level() {
});
}

std::future<api::level_response> api::upload_level(::level l, const char *title, const char *description, bool override) {
return std::async([this, l, title, description, override]() -> api::level_response {
std::future<api::level_response> api::upload_level(::level l, ::replay verify, const char *title, const char *description, bool override) {
return std::async([this, l, title, description, override, verify]() -> api::level_response {
if (!auth::get().authed()) {
return {
.success = false,
Expand All @@ -450,9 +450,10 @@ std::future<api::level_response> api::upload_level(::level l, const char *title,
}
try {
nlohmann::json body;
body["code"] = l.map().save();
body["title"] = title;
body["description"] = description;
body["code"] = l.map().save();
body["title"] = title;
body["description"] = description;
body["verification"] = verify.serialize_b64();
auth::get().add_jwt_to_body(body);
std::string path = override ? "/level/upload/confirm" : "/level/upload";
if (auto res = m_cli.Post(path, body.dump(), "application/json")) {
Expand Down Expand Up @@ -608,7 +609,8 @@ std::future<api::update_response> api::is_up_to_date() {
}

bool api::replay::operator==(const api::replay &other) const {
return other.createdAt == createdAt && //
return other.id == id &&
other.createdAt == createdAt && //
other.updatedAt == updatedAt &&
other.levelId == levelId &&
other.time == time &&
Expand Down Expand Up @@ -709,6 +711,9 @@ void from_json(const nlohmann::json &j, api::level &l) {
if (j.contains("records")) {
l.records = j["records"].get<int>();
}
if (j.contains("verificationId")) {
l.verificationId = j["verificationId"].get<int>();
}

if (j.contains("myRecord")) {
l.myRecord = j["myRecord"].get<api::level_record>();
Expand All @@ -733,5 +738,7 @@ void to_json(nlohmann::json &j, const api::level &l) {
j["record"] = *l.record;
if (l.myRecord)
j["myRecord"] = *l.myRecord;
if (l.verificationId)
j["verificationId"] = *l.verificationId;
j["records"] = l.records;
}
9 changes: 7 additions & 2 deletions game/api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ class api {
std::optional<level_record> myRecord;
int records;
std::optional<int> myVote;
std::optional<int> verificationId;
inline bool verified() const {
return verificationId.has_value();
}
};

struct replay {
int id;
std::string user;
int levelId;
float time;
Expand All @@ -49,7 +54,7 @@ class api {
std::time_t createdAt;
std::time_t updatedAt;
bool alt;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(api::replay, user, levelId, time, version, raw, createdAt, updatedAt, alt);
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(api::replay, id, user, levelId, time, version, raw, createdAt, updatedAt, alt);
bool operator==(const replay& other) const;
bool operator!=(const replay& other) const;
};
Expand Down Expand Up @@ -197,7 +202,7 @@ class api {
std::optional<api::user_stats> stats;
};

std::future<level_response> upload_level(::level l, const char* title, const char* description, bool override = false);
std::future<level_response> upload_level(::level l, ::replay verify, const char* title, const char* description, bool override = false);
std::future<level_response> download_level(int id);
std::future<level_response> quickplay_level();
std::future<vote_response> vote_level(api::level lvl, vote v);
Expand Down
7 changes: 7 additions & 0 deletions game/gui/leaderboard_modal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ void leaderboard_modal::imdraw(fsm* sm) {
}
ImGui::SameLine();
}
if (m_lvl.verificationId.has_value() && *m_lvl.verificationId == score.id) {
ImGui::Image(resource::get().imtex("assets/gui/home.png"), sf::Vector2f(16, 16));
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Original verification");
}
ImGui::SameLine();
}
ImGui::Text("%s", score.user.c_str());
ImGui::TableNextColumn();
ImGui::Text("%.2f", score.time);
Expand Down
Loading

0 comments on commit 6400817

Please sign in to comment.