diff --git a/nuxt.config.ts b/nuxt.config.ts index b531bcd..96b9415 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,6 +1,6 @@ import tailwindcss from "@tailwindcss/vite"; -const dropVersion = "0.3"; +const dropVersion = "v0.3.0"; // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ diff --git a/package.json b/package.json index 398a063..b9a52f9 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "nuxt": "^3.16.2", "nuxt-security": "2.2.0", "prisma": "^6.7.0", + "semver": "^7.7.1", "stream-mime-type": "^2.0.0", "turndown": "^7.2.0", "unstorage": "^1.15.0", @@ -53,6 +54,7 @@ "@types/bcryptjs": "^3.0.0", "@types/luxon": "^3.6.2", "@types/node": "^22.13.16", + "@types/semver": "^7.7.0", "@types/turndown": "^5.0.5", "autoprefixer": "^10.4.20", "eslint": "^9.24.0", diff --git a/server/h3.d.ts b/server/h3.d.ts index 76ff537..67eced1 100644 --- a/server/h3.d.ts +++ b/server/h3.d.ts @@ -1 +1,5 @@ export type MinimumRequestObject = { headers: Headers }; + +export type TaskReturn = + | { success: true; data: T; error?: never } + | { success: false; data?: never; error: { message: string } }; diff --git a/server/internal/config/sys-conf.ts b/server/internal/config/sys-conf.ts index 306d81e..560a661 100644 --- a/server/internal/config/sys-conf.ts +++ b/server/internal/config/sys-conf.ts @@ -1,6 +1,12 @@ class SystemConfig { private libraryFolder = process.env.LIBRARY ?? "./.data/library"; private dataFolder = process.env.DATA ?? "./.data/data"; + private dropVersion = "v0.3.0"; + private checkForUpdates = + process.env.CHECK_FOR_UPDATES !== undefined && + process.env.CHECK_FOR_UPDATES.toLocaleLowerCase() === "true" + ? true + : false; getLibraryFolder() { return this.libraryFolder; @@ -9,6 +15,14 @@ class SystemConfig { getDataFolder() { return this.dataFolder; } + + getDropVersion() { + return this.dropVersion; + } + + shouldCheckForUpdates() { + return this.checkForUpdates; + } } export const systemConfig = new SystemConfig(); diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index f50e8e4..8721459 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -10,6 +10,7 @@ import type { } from "./types"; import { ObjectTransactionalHandler } from "../objects/transactional"; import { PriorityListIndexed } from "../utils/prioritylist"; +import { systemConfig } from "../config/sys-conf"; export class MissingMetadataProviderConfig extends Error { private providerName: string; @@ -25,7 +26,7 @@ export class MissingMetadataProviderConfig extends Error { } // TODO: add useragent to all outbound api calls (best practice) -export const DropUserAgent = "Drop/0.2"; +export const DropUserAgent = `Drop/${systemConfig.getDropVersion()}`; export abstract class MetadataProvider { abstract name(): string; diff --git a/server/internal/notifications/index.ts b/server/internal/notifications/index.ts index 930f317..67570dc 100644 --- a/server/internal/notifications/index.ts +++ b/server/internal/notifications/index.ts @@ -9,6 +9,7 @@ Design goals: import type { Notification } from "~/prisma/client"; import prisma from "../db/database"; +// TODO: document notification action format export type NotificationCreateArgs = Pick< Notification, "title" | "description" | "actions" | "nonce" @@ -61,14 +62,18 @@ class NotificationSystem { throw new Error("No nonce in notificationCreateArgs"); const notification = await prisma.notification.upsert({ where: { - nonce: notificationCreateArgs.nonce, + userId_nonce: { + nonce: notificationCreateArgs.nonce, + userId, + }, }, update: { - userId: userId, + // we don't need to update the userid right? + // userId: userId, ...notificationCreateArgs, }, create: { - userId: userId, + userId, ...notificationCreateArgs, }, }); @@ -84,13 +89,34 @@ class NotificationSystem { }, }); + const res: Promise[] = []; for (const user of users) { - await this.push(user.id, notificationCreateArgs); + res.push(this.push(user.id, notificationCreateArgs)); } + // wait for all notifications to pass + await Promise.all(res); } async systemPush(notificationCreateArgs: NotificationCreateArgs) { - return await this.push("system", notificationCreateArgs); + await this.push("system", notificationCreateArgs); + } + + async pushAllAdmins(notificationCreateArgs: NotificationCreateArgs) { + const users = await prisma.user.findMany({ + where: { + admin: true, + }, + select: { + id: true, + }, + }); + + const res: Promise[] = []; + for (const user of users) { + res.push(this.push(user.id, notificationCreateArgs)); + } + // wait for all notifications to pass + await Promise.all(res); } } diff --git a/server/plugins/tasks.ts b/server/plugins/tasks.ts index 3c8dde4..a822007 100644 --- a/server/plugins/tasks.ts +++ b/server/plugins/tasks.ts @@ -3,5 +3,8 @@ export default defineNitroPlugin(async (_nitro) => { await Promise.all([ runTask("cleanup:invitations"), runTask("cleanup:sessions"), + // TODO: maybe implement some sort of rate limit thing to prevent this from calling github api a bunch in the event of crashloop or whatever? + // probably will require custom task scheduler for object cleanup anyway, so something to thing about + runTask("check:update"), ]); }); diff --git a/server/tasks/check/update.ts b/server/tasks/check/update.ts new file mode 100644 index 0000000..96de84d --- /dev/null +++ b/server/tasks/check/update.ts @@ -0,0 +1,143 @@ +import { type } from "arktype"; +import { systemConfig } from "../../internal/config/sys-conf"; +import * as semver from "semver"; +import type { TaskReturn } from "../../h3"; +import notificationSystem from "../../internal/notifications"; + +const latestRelease = type({ + url: "string", // api url for specific release + html_url: "string", // user facing url + id: "number", // release id + tag_name: "string", // tag used for release + name: "string", // release name + draft: "boolean", + prerelease: "boolean", + created_at: "string", + published_at: "string", +}); + +export default defineTask({ + meta: { + name: "check:update", + }, + async run() { + if (systemConfig.shouldCheckForUpdates()) { + console.log("[Task check:update]: Checking for update"); + try { + const response = await fetch( + "https://api.github.com/repos/Drop-OSS/drop/releases/latest", + ); + + if (!response.ok) { + console.log("[Task check:update]: Failed to check for update", { + status: response.status, + body: response.body, + }); + + return { + result: { + success: false, + error: { + message: "" + response.status, + }, + }, + }; + } + + const resJson = await response.json(); + const body = latestRelease(resJson); + if (body instanceof type.errors) { + console.error(body.summary); + console.log("GitHub Api response", resJson); + return { + result: { + success: false, + error: { + message: body.summary, + }, + }, + }; + } + + // const currVerStr = systemConfig.getDropVersion() + const currVerStr = "v0.1"; + + const latestVer = semver.coerce(body.tag_name); + const currVer = semver.coerce(currVerStr); + if (latestVer === null) { + const msg = "Github Api returned invalid semver tag"; + console.log("[Task check:update]:", msg); + return { + result: { + success: false, + error: { + message: msg, + }, + }, + }; + } else if (currVer === null) { + const msg = "Drop provided a invalid semver tag"; + console.log("[Task check:update]:", msg); + return { + result: { + success: false, + error: { + message: msg, + }, + }, + }; + } + + if (semver.gt(latestVer, currVer)) { + console.log("[Task check:update]: Update available"); + notificationSystem.pushAllAdmins({ + nonce: `drop-update-available-${currVer}-to-${latestVer}`, + title: `Update available to v${latestVer}`, + description: `A new version of Drop is available v${latestVer}`, + actions: [`View|${body.html_url}`], + }); + } else { + console.log("[Task check:update]: no update available"); + } + + console.log("[Task check:update]: Done"); + } catch (e) { + console.error(e); + if (typeof e === "string") { + return { + result: { + success: false, + error: { + message: e, + }, + }, + }; + } else if (e instanceof Error) { + return { + result: { + success: false, + error: { + message: e.message, + }, + }, + }; + } + + return { + result: { + success: false, + error: { + message: "unknown cause, please check console", + }, + }, + }; + } + } + return { + result: { + success: true, + data: undefined, + }, + }; + }, +}); diff --git a/yarn.lock b/yarn.lock index 99ff8c7..634ac27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1830,6 +1830,11 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== +"@types/semver@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" + integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA== + "@types/turndown@^5.0.5": version "5.0.5" resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.5.tgz#614de24fc9ace4d8c0d9483ba81dc8c1976dd26f"