2 Commits

Author SHA1 Message Date
4c9a2c681a fix: remaining type issues 2025-09-25 12:13:07 +10:00
55878bdf5f fix: fixes for Nuxt v4 update 2025-09-25 09:15:29 +10:00
32 changed files with 1206 additions and 1543 deletions

View File

@ -92,7 +92,7 @@ import type { Locale } from "vue-i18n";
const { showText = true } = defineProps<{ showText?: boolean }>(); const { showText = true } = defineProps<{ showText?: boolean }>();
const { availableLocales, locale: currLocale, setLocale } = useI18n(); const { locale: currLocale, setLocale, locales } = useI18n();
function changeLocale(locale: Locale) { function changeLocale(locale: Locale) {
setLocale(locale); setLocale(locale);
@ -102,7 +102,7 @@ function changeLocale(locale: Locale) {
useHead({ useHead({
htmlAttrs: { htmlAttrs: {
lang: locale, lang: locale,
// dir: availableLocales.find((l) => l === locale)?.dir || "ltr", dir: locales.value.find((l) => l.code === locale)?.dir || "ltr",
}, },
}); });
} }
@ -150,6 +150,6 @@ const wiredLocale = computed({
}, },
}); });
const currentLocaleInformation = computed(() => const currentLocaleInformation = computed(() =>
availableLocales.find((e) => e == wiredLocale.value), locales.value.find((e) => e.code == wiredLocale.value),
); );
</script> </script>

View File

@ -106,7 +106,7 @@ const emit = defineEmits<{
}>(); }>();
const props = defineProps<{ const props = defineProps<{
value?: string; value?: string | undefined;
guesses?: Array<{ platform: PlatformRenderable; filename: string }>; guesses?: Array<{ platform: PlatformRenderable; filename: string }>;
}>(); }>();

View File

@ -5,7 +5,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import AdminSourcesPage from "~~/pages/admin/library/sources/index.vue"; import AdminSourcesPage from "~/pages/admin/library/sources/index.vue";
const complete = defineModel<boolean>({ required: true }); const complete = defineModel<boolean>({ required: true });
// Only runs on component load, so it's fine // Only runs on component load, so it's fine

View File

@ -1,10 +1,10 @@
<template> <template>
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900"> <div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
<UserHeader class="z-50" hydrate-on-idle /> <LazyUserHeader class="z-50" hydrate-on-idle />
<div class="grow flex"> <div class="grow flex">
<NuxtPage /> <NuxtPage />
</div> </div>
<UserFooter class="z-50" hydrate-on-interaction /> <LazyUserFooter class="z-50" hydrate-on-interaction />
</div> </div>
<div v-else class="flex w-full min-h-screen bg-zinc-900"> <div v-else class="flex w-full min-h-screen bg-zinc-900">
<NuxtPage /> <NuxtPage />

View File

@ -252,7 +252,8 @@
>Uninstall command</label >Uninstall command</label
> >
<p class="text-zinc-400 text-xs"> <p class="text-zinc-400 text-xs">
Executable to be run on uninstalling a game. Useful for installer-only games. Executable to be run on uninstalling a game. Useful for installer-only
games.
</p> </p>
<div class="mt-2"> <div class="mt-2">
<div <div
@ -301,7 +302,8 @@
</SwitchDescription> </SwitchDescription>
</span> </span>
<Switch <Switch
v-model="versionSettings.delta" :model-value="versionSettings.delta || false"
@update:model-value="(v) => (versionSettings.delta = v)"
:class="[ :class="[
versionSettings.delta ? 'bg-blue-600' : 'bg-zinc-800', versionSettings.delta ? 'bg-blue-600' : 'bg-zinc-800',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
@ -489,7 +491,6 @@ const versionGuesses =
Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>> Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>>
>(); >();
function updateLaunchCommand(idx: number, value: string) { function updateLaunchCommand(idx: number, value: string) {
versionSettings.value.launches![idx].launchCommand = value; versionSettings.value.launches![idx].launchCommand = value;
autosetPlatform(value); autosetPlatform(value);

View File

@ -0,0 +1 @@
<template></template>

View File

@ -163,9 +163,9 @@ const scheduledTasks: {
name: "", name: "",
description: "", description: "",
}, },
debug: { "import:version": {
name: "Debug Task", name: "",
description: "Does debugging things.", description: "",
}, },
}; };

View File

@ -44,7 +44,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AuthMec } from "~~/prisma/client/enums"; import type { AuthMec } from "~~/prisma/client/enums";
import DropLogo from "~~/components/DropLogo.vue";
const { t } = useI18n(); const { t } = useI18n();
const enabledAuths = await $dropFetch("/api/v1/auth"); const enabledAuths = await $dropFetch("/api/v1/auth");

View File

@ -224,14 +224,14 @@ const scopes = [
href: "/docs/access/status", href: "/docs/access/status",
icon: UserGroupIcon, icon: UserGroupIcon,
}, },
clientData.capabilities["peerAPI"] && { clientData.capabilities["PeerAPI"] && {
name: "Access the Drop network", name: "Access the Drop network",
description: description:
"The client will be able to establish P2P connections with other users to enable features like download aggregation, Remote LAN play and P2P multiplayer.", "The client will be able to establish P2P connections with other users to enable features like download aggregation, Remote LAN play and P2P multiplayer.",
href: "/docs/access/network", href: "/docs/access/network",
icon: LockClosedIcon, icon: LockClosedIcon,
}, },
clientData.capabilities["cloudSaves"] && { clientData.capabilities["CloudSaves"] && {
name: "Upload and sync cloud saves", name: "Upload and sync cloud saves",
description: description:
"The client will be able to upload new cloud saves, and edit your existing ones.", "The client will be able to upload new cloud saves, and edit your existing ones.",

View File

@ -105,14 +105,14 @@ function input(index: number) {
function select(index: number) { function select(index: number) {
if (!codeElements.value) return; if (!codeElements.value) return;
if (index >= codeElements.value.length) return; if (index >= codeElements.value.length) return;
codeElements.value[index].select(); codeElements.value[index]!.select();
} }
function paste(index: number, event: ClipboardEvent) { function paste(index: number, event: ClipboardEvent) {
const newCode = event.clipboardData!.getData("text/plain"); const newCode = event.clipboardData!.getData("text/plain");
for (let i = 0; i < newCode.length && i < codeLength; i++) { for (let i = 0; i < newCode.length && i < codeLength; i++) {
code.value[i] = newCode[i]; code.value[i] = newCode[i]!;
codeElements.value![i].focus(); codeElements.value![i]!.focus();
} }
event.preventDefault(); event.preventDefault();
} }

View File

@ -110,7 +110,7 @@
> >
<div> <div>
<component <component
:is="actions[currentAction].page" :is="actions[currentAction]!.page"
v-model="actionsComplete[currentAction]" v-model="actionsComplete[currentAction]"
:token="bearerToken" :token="bearerToken"
/> />

View File

@ -254,7 +254,13 @@ import { StarIcon } from "@heroicons/vue/24/solid";
import { micromark } from "micromark"; import { micromark } from "micromark";
const route = useRoute(); const route = useRoute();
const gameId = route.params.id.toString(); const gameId = route.params.id?.toString();
if (!gameId)
throw createError({
statusCode: 404,
message: "Game not found",
fatal: true,
});
const user = useUser(); const user = useUser();

View File

@ -19,6 +19,7 @@ export default withNuxt([
}, },
], ],
"@intlify/vue-i18n/no-missing-keys": "error", "@intlify/vue-i18n/no-missing-keys": "error",
"vue/multi-word-component-names": "ignore",
}, },
settings: { settings: {
"vue-i18n": { "vue-i18n": {

View File

@ -18,6 +18,7 @@ const twemojiJson = module.findPackageJSON(
if (!twemojiJson) { if (!twemojiJson) {
throw new Error("Could not find @discordapp/twemoji package."); throw new Error("Could not find @discordapp/twemoji package.");
} }
const svgSrcDir = path.join(path.dirname(twemojiJson), "dist", "svg");
// get drop version // get drop version
const dropVersion = getDropVersion(); const dropVersion = getDropVersion();
@ -74,14 +75,13 @@ export default defineNuxtConfig({
vite: { vite: {
plugins: [ plugins: [
// eslint-disable-next-line @typescript-eslint/no-explicit-any tailwindcss(),
tailwindcss() as any,
// only used in dev server, not build because nitro sucks // only used in dev server, not build because nitro sucks
// see build hook below // see build hook below
viteStaticCopy({ viteStaticCopy({
targets: [ targets: [
{ {
src: "node_modules/@discordapp/twemoji/dist/svg/*", src: `${svgSrcDir}/*`,
dest: "twemoji", dest: "twemoji",
}, },
], ],
@ -96,7 +96,7 @@ export default defineNuxtConfig({
// https://github.com/nuxt/nuxt/issues/18918#issuecomment-1925774964 // https://github.com/nuxt/nuxt/issues/18918#issuecomment-1925774964
// copy emojis to .output/public/twemoji // copy emojis to .output/public/twemoji
const targetDir = path.join(nitro.options.output.publicDir, "twemoji"); const targetDir = path.join(nitro.options.output.publicDir, "twemoji");
cpSync(path.join(path.dirname(twemojiJson), "dist", "svg"), targetDir, { cpSync(svgSrcDir, targetDir, {
recursive: true, recursive: true,
}); });
}, },
@ -163,9 +163,11 @@ export default defineNuxtConfig({
tsConfig: { tsConfig: {
compilerOptions: { compilerOptions: {
// Not having these options on is sloppy, but it's a task for later me
verbatimModuleSyntax: false, verbatimModuleSyntax: false,
strictNullChecks: true, strictNullChecks: true,
exactOptionalPropertyTypes: true, exactOptionalPropertyTypes: false,
//erasableSyntaxOnly: true,
noUncheckedIndexedAccess: false, noUncheckedIndexedAccess: false,
}, },
}, },

View File

@ -24,7 +24,6 @@
"@drop-oss/droplet": "3.0.1", "@drop-oss/droplet": "3.0.1",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@lobomfz/prismark": "0.0.3",
"@nuxt/fonts": "^0.11.0", "@nuxt/fonts": "^0.11.0",
"@nuxt/image": "^1.10.0", "@nuxt/image": "^1.10.0",
"@nuxtjs/i18n": "^9.5.5", "@nuxtjs/i18n": "^9.5.5",
@ -40,7 +39,7 @@
"fast-fuzzy": "^1.12.0", "fast-fuzzy": "^1.12.0",
"file-type-mime": "^0.4.3", "file-type-mime": "^0.4.3",
"jdenticon": "^3.3.0", "jdenticon": "^3.3.0",
"jsdom": "^26.1.0", "jsdom": "^27.0.0",
"luxon": "^3.6.1", "luxon": "^3.6.1",
"micromark": "^4.0.1", "micromark": "^4.0.1",
"normalize-url": "^8.0.2", "normalize-url": "^8.0.2",
@ -48,7 +47,7 @@
"nuxt-security": "2.2.0", "nuxt-security": "2.2.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
"prisma": "^6.14.0", "prisma": "^6.11.1",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"semver": "^7.7.1", "semver": "^7.7.1",
"stream-mime-type": "^2.0.0", "stream-mime-type": "^2.0.0",
@ -88,5 +87,8 @@
"vue3-carousel": "^0.16.0" "vue3-carousel": "^0.16.0"
} }
}, },
"prisma": {
"schema": "./prisma"
},
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b" "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
} }

2439
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
import { defineConfig } from "prisma/config";
import path from "node:path";
export default defineConfig({
schema: path.join("prisma", "schema.prisma"),
});

View File

@ -10,15 +10,6 @@ generator client {
binaryTargets = ["native", "debian-openssl-3.0.x"] binaryTargets = ["native", "debian-openssl-3.0.x"]
} }
/**
* generator arktype {
* provider = "yarn prismark"
* output = "./validate"
* fileName = "schema.ts"
* nullish = true
* }
*/
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")

View File

@ -42,10 +42,10 @@ export default defineEventHandler<{
await objectHandler.deleteAsSystem(imageId); await objectHandler.deleteAsSystem(imageId);
if (game.mBannerObjectId === imageId) { if (game.mBannerObjectId === imageId) {
game.mBannerObjectId = game.mImageLibraryObjectIds[0]; game.mBannerObjectId = game.mImageLibraryObjectIds[0] ?? "";
} }
if (game.mCoverObjectId === imageId) { if (game.mCoverObjectId === imageId) {
game.mCoverObjectId = game.mImageLibraryObjectIds[0]; game.mCoverObjectId = game.mImageLibraryObjectIds[0] ?? "";
} }
const result = await prisma.game.update({ const result = await prisma.game.update({

View File

@ -2,11 +2,10 @@ import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype"; import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import aclManager from "~~/server/internal/acls"; import aclManager from "~~/server/internal/acls";
import taskHandler from "~~/server/internal/tasks"; import taskHandler from "~~/server/internal/tasks";
import type { TaskGroup } from "~~/server/internal/tasks/group"; import { TASK_GROUPS, type TaskGroup } from "~~/server/internal/tasks/group";
import { taskGroups } from "~~/server/internal/tasks/group";
const StartTask = type({ const StartTask = type({
taskGroup: type("string"), taskGroup: type.enumerated(...TASK_GROUPS),
}).configure(throwingArktype); }).configure(throwingArktype);
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
@ -14,14 +13,8 @@ export default defineEventHandler(async (h3) => {
if (!allowed) throw createError({ statusCode: 403 }); if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, StartTask); const body = await readDropValidatedBody(h3, StartTask);
const taskGroup = body.taskGroup as TaskGroup;
if (!taskGroups[taskGroup])
throw createError({
statusCode: 400,
message: "Invalid task group.",
});
const task = await taskHandler.runTaskGroupByName(taskGroup); const task = await taskHandler.runTaskGroupByName(body.taskGroup);
if (!task) if (!task)
throw createError({ throw createError({
statusCode: 500, statusCode: 500,

View File

@ -1,20 +1,20 @@
import { type } from "arktype"; import { type } from "arktype";
import { ClientCapabilities } from "~~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype"; import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import type { import type {
CapabilityConfiguration, CapabilityConfiguration,
InternalClientCapability,
} from "~~/server/internal/clients/capabilities"; } from "~~/server/internal/clients/capabilities";
import capabilityManager, { import capabilityManager, {
validCapabilities, validCapabilities,
} from "~~/server/internal/clients/capabilities"; } from "~~/server/internal/clients/capabilities";
import clientHandler, { AuthMode } from "~~/server/internal/clients/handler"; import clientHandler, { AuthMode, AuthModes } from "~~/server/internal/clients/handler";
import { parsePlatform } from "~~/server/internal/utils/parseplatform"; import { parsePlatform } from "~~/server/internal/utils/parseplatform";
const ClientAuthInitiate = type({ const ClientAuthInitiate = type({
name: "string", name: "string",
platform: "string", platform: "string",
capabilities: "object", capabilities: "object",
mode: type.valueOf(AuthMode).default(AuthMode.Callback), mode: type.enumerated(...AuthModes).default("callback"),
}).configure(throwingArktype); }).configure(throwingArktype);
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
@ -32,7 +32,7 @@ export default defineEventHandler(async (h3) => {
}); });
const capabilityIterable = Object.entries(capabilities) as Array< const capabilityIterable = Object.entries(capabilities) as Array<
[InternalClientCapability, object] [ClientCapabilities, object]
>; >;
if ( if (
capabilityIterable.length > 0 && capabilityIterable.length > 0 &&

View File

@ -1,39 +1,24 @@
import type { InternalClientCapability } from "~~/server/internal/clients/capabilities"; import { type } from "arktype";
import { ClientCapabilities } from "~~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
import capabilityManager, { import capabilityManager, {
validCapabilities, validCapabilities,
} from "~~/server/internal/clients/capabilities"; } from "~~/server/internal/clients/capabilities";
import { defineClientEventHandler } from "~~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~~/server/internal/clients/event-handler";
import notificationSystem from "~~/server/internal/notifications"; import notificationSystem from "~~/server/internal/notifications";
const SetCapability = type({
capability: type.enumerated(...Object.values(ClientCapabilities)),
configuration: "object"
}).configure(throwingArktype);
export default defineClientEventHandler( export default defineClientEventHandler(
async (h3, { clientId, fetchClient, fetchUser }) => { async (h3, { clientId, fetchClient, fetchUser }) => {
const body = await readBody(h3); const body = await readDropValidatedBody(h3, SetCapability);
const rawCapability = body.capability;
const configuration = body.configuration;
if (!rawCapability || typeof rawCapability !== "string")
throw createError({
statusCode: 400,
message: "capability must be a string",
});
if (!configuration || typeof configuration !== "object")
throw createError({
statusCode: 400,
message: "configuration must be an object",
});
const capability = rawCapability as InternalClientCapability;
if (!validCapabilities.includes(capability))
throw createError({
statusCode: 400,
message: "Invalid capability.",
});
const isValid = await capabilityManager.validateCapabilityConfiguration( const isValid = await capabilityManager.validateCapabilityConfiguration(
capability, body.capability,
configuration, body.configuration,
); );
if (!isValid) if (!isValid)
throw createError({ throw createError({
@ -42,8 +27,8 @@ export default defineClientEventHandler(
}); });
await capabilityManager.upsertClientCapability( await capabilityManager.upsertClientCapability(
capability, body.capability,
configuration, body.configuration,
clientId, clientId,
); );
@ -51,9 +36,9 @@ export default defineClientEventHandler(
const user = await fetchUser(); const user = await fetchUser();
await notificationSystem.push(user.id, { await notificationSystem.push(user.id, {
nonce: `capability-${clientId}-${capability}`, nonce: `capability-${clientId}-${body.capability}`,
title: `"${client.name}" can now access ${capability}`, title: `"${client.name}" can now access ${body.capability}`,
description: `A device called "${client.name}" now has access to your ${capability}.`, description: `A device called "${client.name}" now has access to your ${body.capability}.`,
actions: ["Review|/account/devices"], actions: ["Review|/account/devices"],
acls: ["user:clients:read"], acls: ["user:clients:read"],
}); });

View File

@ -24,7 +24,7 @@ export class CertificateAuthority {
let ca; let ca;
if (root === undefined) { if (root === undefined) {
const [cert, priv] = droplet.generateRootCa(); const [cert, priv] = droplet.generateRootCa();
const bundle: CertificateBundle = { priv, cert }; const bundle: CertificateBundle = { priv: priv!, cert: cert! };
await store.store("ca", bundle); await store.store("ca", bundle);
ca = new CertificateAuthority(store, bundle); ca = new CertificateAuthority(store, bundle);
} else { } else {
@ -50,8 +50,8 @@ export class CertificateAuthority {
caCertificate.priv, caCertificate.priv,
); );
const certBundle: CertificateBundle = { const certBundle: CertificateBundle = {
priv, priv: priv!,
cert, cert: cert!,
}; };
return certBundle; return certBundle;
} }

View File

@ -2,28 +2,18 @@ import type { EnumDictionary } from "../utils/types";
import prisma from "../db/database"; import prisma from "../db/database";
import { ClientCapabilities } from "~~/prisma/client/enums"; import { ClientCapabilities } from "~~/prisma/client/enums";
// These values are technically mapped to the database,
// but Typescript/Prisma doesn't let me link them
// They are also what are required by clients in the API
// BREAKING CHANGE
export enum InternalClientCapability {
PeerAPI = "peerAPI",
UserStatus = "userStatus",
CloudSaves = "cloudSaves",
TrackPlaytime = "trackPlaytime",
}
export const validCapabilities = Object.values(InternalClientCapability); export const validCapabilities = Object.values(ClientCapabilities);
export type CapabilityConfiguration = { export type CapabilityConfiguration = {
[InternalClientCapability.PeerAPI]: object; [ClientCapabilities.PeerAPI]: object;
[InternalClientCapability.UserStatus]: object; [ClientCapabilities.UserStatus]: object;
[InternalClientCapability.CloudSaves]: object; [ClientCapabilities.CloudSaves]: object;
}; };
class CapabilityManager { class CapabilityManager {
private validationFunctions: EnumDictionary< private validationFunctions: EnumDictionary<
InternalClientCapability, ClientCapabilities,
(configuration: object) => Promise<boolean> (configuration: object) => Promise<boolean>
> = { > = {
/* /*
@ -77,14 +67,14 @@ class CapabilityManager {
return valid; return valid;
}, },
*/ */
[InternalClientCapability.PeerAPI]: async () => true, [ClientCapabilities.PeerAPI]: async () => true,
[InternalClientCapability.UserStatus]: async () => true, // No requirements for user status [ClientCapabilities.UserStatus]: async () => true, // No requirements for user status
[InternalClientCapability.CloudSaves]: async () => true, // No requirements for cloud saves [ClientCapabilities.CloudSaves]: async () => true, // No requirements for cloud saves
[InternalClientCapability.TrackPlaytime]: async () => true, [ClientCapabilities.TrackPlaytime]: async () => true,
}; };
async validateCapabilityConfiguration( async validateCapabilityConfiguration(
capability: InternalClientCapability, capability: ClientCapabilities,
configuration: object, configuration: object,
) { ) {
const validationFunction = this.validationFunctions[capability]; const validationFunction = this.validationFunctions[capability];
@ -93,15 +83,15 @@ class CapabilityManager {
} }
async upsertClientCapability( async upsertClientCapability(
capability: InternalClientCapability, capability: ClientCapabilities,
rawCapabilityConfiguration: object, rawCapabilityConfiguration: object,
clientId: string, clientId: string,
) { ) {
const upsertFunctions: EnumDictionary< const upsertFunctions: EnumDictionary<
InternalClientCapability, ClientCapabilities,
() => Promise<void> | void () => Promise<void> | void
> = { > = {
[InternalClientCapability.PeerAPI]: async function () { [ClientCapabilities.PeerAPI]: async function () {
// const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI]; // const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
const currentClient = await prisma.client.findUnique({ const currentClient = await prisma.client.findUnique({
@ -139,10 +129,10 @@ class CapabilityManager {
}, },
}); });
}, },
[InternalClientCapability.UserStatus]: function (): Promise<void> | void { [ClientCapabilities.UserStatus]: function (): Promise<void> | void {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
[InternalClientCapability.CloudSaves]: async function () { [ClientCapabilities.CloudSaves]: async function () {
const currentClient = await prisma.client.findUnique({ const currentClient = await prisma.client.findUnique({
where: { id: clientId }, where: { id: clientId },
select: { select: {
@ -162,7 +152,7 @@ class CapabilityManager {
}, },
}); });
}, },
[InternalClientCapability.TrackPlaytime]: async function () { [ClientCapabilities.TrackPlaytime]: async function () {
const currentClient = await prisma.client.findUnique({ const currentClient = await prisma.client.findUnique({
where: { id: clientId }, where: { id: clientId },
select: { select: {

View File

@ -1,18 +1,15 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import prisma from "../db/database"; import prisma from "../db/database";
import type { HardwarePlatform } from "~~/prisma/client/enums"; import type { ClientCapabilities, HardwarePlatform } from "~~/prisma/client/enums";
import { useCertificateAuthority } from "~~/server/plugins/ca"; import { useCertificateAuthority } from "~~/server/plugins/ca";
import type { import type {
CapabilityConfiguration, CapabilityConfiguration,
InternalClientCapability,
} from "./capabilities"; } from "./capabilities";
import capabilityManager from "./capabilities"; import capabilityManager from "./capabilities";
import type { PeerImpl } from "../tasks"; import type { PeerImpl } from "../tasks";
export enum AuthMode { export const AuthModes = ["callback", "code"] as const;
Callback = "callback", export type AuthMode = (typeof AuthModes)[number];
Code = "code",
}
export interface ClientMetadata { export interface ClientMetadata {
name: string; name: string;
@ -62,9 +59,9 @@ export class ClientHandler {
}); });
switch (metadata.mode) { switch (metadata.mode) {
case AuthMode.Callback: case "callback":
return `/client/authorize/${clientId}`; return `/client/authorize/${clientId}`;
case AuthMode.Code: { case "code": {
const code = randomUUID() const code = randomUUID()
.replaceAll(/-/g, "") .replaceAll(/-/g, "")
.slice(0, 7) .slice(0, 7)
@ -171,7 +168,7 @@ export class ClientHandler {
metadata.data.capabilities, metadata.data.capabilities,
)) { )) {
await capabilityManager.upsertClientCapability( await capabilityManager.upsertClientCapability(
capability as InternalClientCapability, capability as ClientCapabilities,
configuration, configuration,
client.id, client.id,
); );

View File

@ -200,7 +200,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
return url.pathname.replace("/games/", "").replace(/\/$/, ""); return url.pathname.replace("/games/", "").replace(/\/$/, "");
} }
default: { default: {
logger.warn("Pcgamingwiki, unknown host", url.hostname); logger.warn("Pcgamingwiki, unknown host: %s", url.hostname);
return undefined; return undefined;
} }
} }
@ -234,7 +234,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
}); });
if (ratingObj instanceof type.errors) { if (ratingObj instanceof type.errors) {
logger.info( logger.info(
"pcgamingwiki: failed to properly get review rating", "pcgamingwiki: failed to properly get review rating: %s",
ratingObj.summary, ratingObj.summary,
); );
return undefined; return undefined;

View File

@ -123,7 +123,7 @@ export class FsObjectBackend extends ObjectBackend {
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8")); const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
const metadata = objectMetadata(metadataRaw); const metadata = objectMetadata(metadataRaw);
if (metadata instanceof type.errors) { if (metadata instanceof type.errors) {
logger.error("FsObjectBackend#fetchMetadata", metadata.summary); logger.error("FsObjectBackend#fetchMetadata: %s", metadata.summary);
return undefined; return undefined;
} }
await this.metadataCache.set(id, metadata); await this.metadataCache.set(id, metadata);
@ -194,11 +194,13 @@ export class FsObjectBackend extends ObjectBackend {
try { try {
fs.rmSync(filePath); fs.rmSync(filePath);
cleanupLogger.info( cleanupLogger.info(
`[FsObjectBackend#cleanupMetadata]: Removed ${file}`, `[FsObjectBackend#cleanupMetadata]: Removed %s`,
file
); );
} catch (error) { } catch (error) {
cleanupLogger.error( cleanupLogger.error(
`[FsObjectBackend#cleanupMetadata]: Failed to remove ${file}`, `[FsObjectBackend#cleanupMetadata]: Failed to remove %s: %s`,
file,
error, error,
); );
} }

View File

@ -32,15 +32,12 @@ export const objectMetadata = type({
}); });
export type ObjectMetadata = typeof objectMetadata.infer; export type ObjectMetadata = typeof objectMetadata.infer;
export enum ObjectPermission { export const ObjectPermissions = ["read", "write", "delete"] as const;
Read = "read", export type ObjectPermission = (typeof ObjectPermissions)[number];
Write = "write",
Delete = "delete",
}
export const ObjectPermissionPriority: Array<ObjectPermission> = [ export const ObjectPermissionPriority: Array<ObjectPermission> = [
ObjectPermission.Read, "read",
ObjectPermission.Write, "write",
ObjectPermission.Delete, "delete",
]; ];
export type Object = { mime: string; data: Source }; export type Object = { mime: string; data: Source };

View File

@ -1,22 +1,32 @@
export const taskGroups = { export const TASK_GROUPS = [
"cleanup:invitations": { "cleanup:invitations",
concurrency: false, "cleanup:objects",
}, "cleanup:sessions",
"cleanup:objects": { "check:update",
concurrency: false, "import:game",
}, "import:version",
"cleanup:sessions": { ] as const;
concurrency: false,
},
"check:update": {
concurrency: false,
},
"import:game": {
concurrency: true,
},
debug: {
concurrency: true,
},
} as const;
export type TaskGroup = keyof typeof taskGroups; export type TaskGroup = (typeof TASK_GROUPS)[number];
export const TASK_GROUP_CONFIG: { [key in TaskGroup]: { concurrency: boolean } } =
{
"cleanup:invitations": {
concurrency: false
},
"cleanup:objects": {
concurrency: false
},
"cleanup:sessions": {
concurrency: false
},
"check:update": {
concurrency: false
},
"import:game": {
concurrency: true
},
"import:version": {
concurrency: true
}
};

View File

@ -7,7 +7,7 @@ import cleanupInvites from "./registry/invitations";
import cleanupSessions from "./registry/sessions"; import cleanupSessions from "./registry/sessions";
import checkUpdate from "./registry/update"; import checkUpdate from "./registry/update";
import cleanupObjects from "./registry/objects"; import cleanupObjects from "./registry/objects";
import { taskGroups, type TaskGroup } from "./group"; import { TASK_GROUP_CONFIG, type TaskGroup } from "./group";
import prisma from "../db/database"; import prisma from "../db/database";
import { type } from "arktype"; import { type } from "arktype";
import pino from "pino"; import pino from "pino";
@ -54,7 +54,6 @@ class TaskHandler {
"cleanup:invitations", "cleanup:invitations",
"cleanup:sessions", "cleanup:sessions",
"check:update", "check:update",
"debug",
]; ];
private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"]; private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"];
@ -83,7 +82,7 @@ class TaskHandler {
let logOffset: number = 0; let logOffset: number = 0;
// if taskgroup disallows concurrency // if taskgroup disallows concurrency
if (!taskGroups[task.taskGroup].concurrency) { if (!TASK_GROUP_CONFIG[task.taskGroup].concurrency) {
for (const existingTask of this.taskPool.values()) { for (const existingTask of this.taskPool.values()) {
// if a task is already running, we don't want to start another // if a task is already running, we don't want to start another
if (existingTask.taskGroup === task.taskGroup) { if (existingTask.taskGroup === task.taskGroup) {
@ -150,7 +149,7 @@ class TaskHandler {
} }
} catch (e) { } catch (e) {
// fallback: ignore or log error // fallback: ignore or log error
logger.error("Failed to parse log chunk", { logger.error("Failed to parse log chunk %s", {
error: e, error: e,
chunk: chunk, chunk: chunk,
}); });
@ -178,7 +177,7 @@ class TaskHandler {
const progress = (progress: number) => { const progress = (progress: number) => {
if (progress < 0 || progress > 100) { if (progress < 0 || progress > 100) {
logger.error("Progress must be between 0 and 100", { progress }); logger.error("Progress must be between 0 and 100, actually %d", progress);
return; return;
} }
const taskEntry = this.taskPool.get(task.id); const taskEntry = this.taskPool.get(task.id);

View File

@ -1,5 +1,6 @@
import { defineDropTask } from ".."; import { defineDropTask } from "..";
/*
export default defineDropTask({ export default defineDropTask({
buildId: () => `debug:${new Date().toISOString()}`, buildId: () => `debug:${new Date().toISOString()}`,
name: "Debug Task", name: "Debug Task",
@ -16,3 +17,4 @@ export default defineDropTask({
} }
}, },
}); });
*/

View File

@ -49,7 +49,7 @@ export default defineDropTask({
// if response failed somehow // if response failed somehow
if (!response.ok) { if (!response.ok) {
logger.info("Failed to check for update ", { logger.info("Failed to check for update: %s", {
status: response.status, status: response.status,
body: response.body, body: response.body,
}); });