///
import { existsSync as fsExistsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { AuthorizedDeviceType, SESSION_VALID_TERM_MS } from "@onlineworkspace/workspace-backend/src/systems/authorization.ts";
import { FEATURE_FLAG_DESCRIPTIONS, WorkspacesFeatureFlags } from "@onlineworkspace/workspace-backend/src/systems/configuration.ts";
import { WorkspacesNotificationPriority } from "@onlineworkspace/workspace-backend/src/systems/notifications.ts";
import { GlobalApplicationSetting } from "@onlineworkspace/workspace-backend/src/systems/settings/applicationSetting/applicationSetting.ts";
import { initTRPC, TRPCError } from "@trpc/server";
import { octetInputParser } from "@trpc/server/http";
import sharp from "sharp";
import z from "zod";
import { adminProcedure, createOnlineWorkspaceTRPCContext, procedure } from "@onlineworkspace/workspace-backend/src/systems/trpc/coreRouter.ts";
import { crypto } from "@std/crypto";
import {getCookies} from "@std/http"
import { FileMediaType } from "../../../backend/src/systems/filesystem.ts";
const log = instance.log.createLogger("uk.ewsgit.settings");
export const t = initTRPC.context>().create();
const router = t.router({
overview: {
user: {
fullName: procedure.output(z.string()).query(async (opt) => {
const fullName = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.getFullName();
return `${fullName?.forename} ${fullName?.surname || ""}` || "Unknown User";
}),
role: procedure.output(z.string()).query(async (opt) => {
const isAdministrator = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.isAdministrator();
return isAdministrator ? "Administrator" : "User";
}),
getAvatar: procedure.output(z.string()).query(async (opt) => {
return `${opt.ctx.instance.sys.configuration.proxy.secure ? "https://" : "http://"}${opt.ctx.instance.sys.configuration.proxy.hostname}/api/user/me/avatar/l`;
}),
},
},
profile: {
getName: procedure.output(z.string()).query(async (opt) => {
const fullName = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.getFullName();
return `${fullName?.forename} ${fullName?.surname || ""}` || "Unknown User";
}),
setName: procedure.input(z.string()).mutation(async (opt) => {
const fullNameSplit = opt.input.split(" ");
await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.setFullName(fullNameSplit.shift() || "Unknown", fullNameSplit.join(" "));
return true;
}),
getUsername: procedure.output(z.string()).query(async (opt) => {
const username = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.getUsername();
return username || "unknown";
}),
setUsername: procedure.input(z.string()).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.setUsername(opt.input.toLowerCase());
return true;
}),
getGender: procedure.output(z.string()).query(async (opt) => {
const gender = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.getGender();
return gender || "female";
}),
setGender: procedure.input(z.string()).mutation(async (opt) => {
if (opt.input !== "male" && opt.input !== "female" && opt.input !== "other") return;
await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.setGender(opt.input);
return true;
}),
getEmail: procedure.output(z.string()).query(async (opt) => {
const email = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.getEmail();
return email || "unknown";
}),
setEmail: procedure.input(z.email()).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.setEmail(opt.input);
return true;
}),
getBio: procedure.output(z.string()).query(async (opt) => {
const bio = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.getBio();
return bio || "";
}),
setBio: procedure.input(z.string()).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.setBio(opt.input);
return true;
}),
getRole: procedure.output(z.string()).query(async (opt) => {
const isAdministrator = await (await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId))?.isAdministrator();
return isAdministrator ? "Administrator" : "User";
}),
setProfilePicture: procedure.input(octetInputParser).mutation(async (opt) => {
const user = await opt.ctx.user();
const userPath = user.getPath();
if (!userPath) return false;
const filePath = path.join(userPath, "system/temp/avatar");
const fileResponse = new Response(opt.input);
await fs.writeFile(filePath, await fileResponse.bytes());
await user.setAvatar(filePath);
await user.generateAvatars(true);
opt.ctx.instance.sys.notifications.send(
user.userId,
"uk.ewsgit.settings.profile.setProfilePicture",
WorkspacesNotificationPriority.Normal,
{
title: "Profile Picture Change",
body: "Your profile picture has now been changed, please refresh the page to see your new avatar!",
icon: "person",
},
{
buttons: [
{
id: "reload",
label: "Refresh",
type: "filled",
},
],
},
{
onButton(optionId) {
if (optionId === "reload") {
return {
action: {
type: "reload",
},
};
}
},
},
);
return true;
}),
getProfilePicture: procedure.output(z.string()).query(async (opt) => {
return `${opt.ctx.instance.sys.configuration.proxy.secure ? "https://" : "http://"}${opt.ctx.instance.sys.configuration.proxy.hostname}/api/user/me/avatar/l`;
}),
},
authentication: {
hasPassword: procedure.output(z.boolean()).query(async (opt) => {
return instance.sys.authorization.hasPassword(opt.ctx.userId);
}),
hasTwoFactor: procedure.output(z.boolean()).query(async (opt) => {
return await instance.sys.authorization.hasTwoFactorAuthenticationSecret(opt.ctx.userId);
}),
hasPasskey: procedure.output(z.boolean()).query(async (opt) => {
return await instance.sys.authorization.hasPasskey(opt.ctx.userId);
}),
requestNewPasskey: procedure.mutation(async (opt) => {
return opt.ctx.instance.sys.authorization.requestNewPasskey(opt.ctx.userId);
}),
registerPasskey: procedure.input(z.any()).mutation(async (opt) => {
return opt.ctx.instance.sys.authorization.registerPasskey(opt.ctx.userId, opt.input);
}),
getPasskeys: procedure
.output(
z
.object({
id: z.string(),
creationTimestamp: z.string(),
lastUsedTimestamp: z.string(),
deviceType: z.string(),
usedTimes: z.string(),
})
.array(),
)
.query(async (opt) => {
const db = instance.sys.database.postgres();
const passkeys =
await db`SELECT passkey_id, creation_timetamp, last_used_timestamp, device_type, counter FROM public.passkeys WHERE user_id = ${opt.ctx.userId}` as { passkey_id: string; creation_timetamp: Date; last_used_timestamp: Date; device_type: string; counter: string }[]
return passkeys.map((passkey: { passkey_id: string; creation_timetamp: Date; last_used_timestamp: Date; device_type: string; counter: string }) => {
return {
id: passkey.passkey_id,
creationTimestamp: passkey.creation_timetamp.toString() || "",
lastUsedTimestamp: passkey.last_used_timestamp.toString() || "",
deviceType: passkey.device_type,
usedTimes: passkey.counter,
};
});
}),
removePasskey: procedure.input(z.object({ id: z.string() })).mutation(async (opt) => {
await instance.sys.authorization.removePasskey(opt.ctx.userId, opt.input.id);
return { success: true };
}),
getSessions: procedure
.output(
z
.object({
sessionId: z.number(),
deviceType: z.enum(AuthorizedDeviceType),
firstLoginTimestamp: z.number(),
ipAddress: z.string(),
isCurrent: z.boolean(),
loginMethod: z.string(),
})
.array(),
)
.query(async (opt) => {
const user = await opt.ctx.instance.sys.users.getUserById(opt.ctx.userId);
if (!user) return [];
const db = instance.sys.database.postgres();
const sessions =
(await db`SELECT session_id, device_type, valid_until, ip_address, session_token FROM public.sessions WHERE user_id = ${user.userId}`) as {
session_id: number;
device_type: AuthorizedDeviceType;
valid_until: number;
ip_address: string;
session_token: string;
login_method: string;
}[];
const cookies = getCookies(opt.ctx.rawRequest.req.headers);
if (!cookies.Authorization) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "missing auth cookie",
});
}
const [_, _userId, token] = decodeURIComponent(cookies.Authorization).split(":");
return sessions.map((s) => {
return {
sessionId: s.session_id,
deviceType: s.device_type,
firstLoginTimestamp: s.valid_until - SESSION_VALID_TERM_MS,
ipAddress: s.ip_address,
isCurrent: s.session_token === token,
loginMethod: s.login_method || "Unknown",
};
});
}),
setPassword: procedure.input(z.object({ password: z.string() })).mutation(async (opt) => {
await opt.ctx.instance.sys.authorization.setPassword(opt.ctx.userId, opt.input.password);
return true;
}),
deleteSession: procedure.input(z.object({ sessionId: z.number() })).mutation(async (opt) => {
await opt.ctx.instance.sys.authorization.endSessionById(opt.ctx.userId, opt.input.sessionId);
return true;
}),
},
instance: {
hasFeature: procedure
.input(z.string())
.output(z.boolean())
.query(async (opt) => {
return opt.ctx.instance.sys.configuration.hasFeature(opt.input);
}),
getUsers: adminProcedure.output(z.number().array()).query(async (_opt) => {
const users = await instance.sys.users.getAllUsers();
return users.map((u) => u.userId);
}),
getUser: adminProcedure
.input(z.object({ userId: z.number() }))
.output(
z
.object({
id: z.number(),
username: z.string(),
fullName: z.object({
forename: z.string().optional(),
surname: z.string().optional(),
}),
email: z.string().optional(),
isAdministrator: z.boolean(),
})
.or(z.undefined()),
)
.query(async (opt) => {
const u = await instance.sys.users.getUserById(opt.input.userId);
if (!u) return undefined;
return {
id: u.userId,
username: (await u.getUsername()) || "unknown",
fullName: await u.getFullName(),
email: await u.getEmail(),
isAdministrator: (await u.isAdministrator()) || false,
};
}),
user: {
getForename: adminProcedure
.input(z.number())
.output(z.string())
.query(async (opt) => {
const forename = await (await opt.ctx.instance.sys.users.getUserById(opt.input))?.getForename();
return `${forename}`;
}),
setForename: adminProcedure.input(z.object({ userId: z.number(), forename: z.string() })).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.input.userId))?.setForename(opt.input.forename);
return true;
}),
getSurname: adminProcedure
.input(z.number())
.output(z.string())
.query(async (opt) => {
const surname = await (await opt.ctx.instance.sys.users.getUserById(opt.input))?.getSurname();
return `${surname}`;
}),
setSurname: adminProcedure.input(z.object({ userId: z.number(), surname: z.string() })).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.input.userId))?.setSurname(opt.input.surname);
return true;
}),
getUsername: adminProcedure
.input(z.number())
.output(z.string())
.query(async (opt) => {
const username = await (await opt.ctx.instance.sys.users.getUserById(opt.input))?.getUsername();
return username || "unknown";
}),
setUsername: adminProcedure.input(z.object({ userId: z.number(), username: z.string() })).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.input.userId))?.setUsername(opt.input.username.toLowerCase());
return true;
}),
getEmail: adminProcedure
.input(z.number())
.output(z.string())
.query(async (opt) => {
const email = await (await opt.ctx.instance.sys.users.getUserById(opt.input))?.getEmail();
return email || "unknown";
}),
setEmail: adminProcedure
.input(
z.object({
userId: z.number(),
email: z.email().or(z.literal("unknown")),
}),
)
.mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.input.userId))?.setEmail(opt.input.email);
return true;
}),
getIsAdministrator: adminProcedure
.input(z.number())
.output(z.boolean())
.query(async (opt) => {
const isAdministrator = await (await opt.ctx.instance.sys.users.getUserById(opt.input))?.isAdministrator();
return isAdministrator || false;
}),
setIsAdministrator: adminProcedure.input(z.object({ userId: z.number(), administrator: z.boolean() })).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.input.userId))?.setIsAdministrator(opt.input.administrator);
return true;
}),
getIsMe: adminProcedure
.input(z.number())
.output(z.boolean())
.query(async (opt) => {
return opt.input === opt.ctx.userId;
}),
delete: adminProcedure.input(z.object({ userId: z.number() })).mutation(async (opt) => {
await (await opt.ctx.instance.sys.users.getUserById(opt.input.userId))?.delete();
return true;
}),
boop: adminProcedure.input(z.object({ userId: z.number() })).mutation(async (opt) => {
instance.sys.notifications.send(
opt.input.userId,
"commands.notify",
WorkspacesNotificationPriority.Important,
{
title: "Boop",
body: "You have been booped by an administrator!",
icon: "person",
},
{
buttons: [
{
id: "a",
label: "label",
type: "filled",
},
{
id: "a",
label: "label",
type: "tonal",
},
],
},
);
return true;
}),
},
isUserAdministrator: procedure.query(async (opt) => {
const user = await opt.ctx.user();
if (user) {
return await user.isAdministrator();
}
return false;
}),
createUser: procedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async (opt) => {
await opt.ctx.instance.sys.users.createUser(opt.input.username.toLowerCase(), opt.input.password);
return true;
}),
getFeatures: procedure
.output(
z
.object({
name: z.string(),
id: z.string(),
enabled: z.boolean(),
description: z.string().optional(),
})
.array(),
)
.query(async () => {
const availableFlags = Object.keys(WorkspacesFeatureFlags);
return availableFlags.map((f) => {
return {
name: f,
// @ts-ignore
id: WorkspacesFeatureFlags[f],
enabled: instance.sys.configuration.hasFeature(
// @ts-ignore
WorkspacesFeatureFlags[f],
),
// @ts-ignore
description: FEATURE_FLAG_DESCRIPTIONS[WorkspacesFeatureFlags[f]],
};
});
}),
setFeature: procedure.input(z.object({ id: z.string(), value: z.boolean() })).mutation(async (opt) => {
if (opt.input.value) {
await instance.sys.configuration.enableFeature(opt.input.id);
} else {
await instance.sys.configuration.disableFeature(opt.input.id);
}
return true;
}),
mailserver: {
get: procedure
.output(
z
.object({
host: z.string(),
port: z.number(),
secure: z.boolean(),
auth: z.object({
user: z.string(),
pass: z.string(),
}),
})
.or(z.undefined()),
)
.query(async () => {
const mailserverConfig = instance.sys.configuration.mailServer;
if (!mailserverConfig) return undefined;
return {
host: mailserverConfig.host,
port: mailserverConfig.port,
secure: mailserverConfig.secure,
auth: {
user: mailserverConfig.auth.user,
pass: "********",
},
};
}),
},
},
customization: {
wallpaper: {
wallpaperHistory: procedure.output(z.object({ name: z.string(), previewSrc: z.string() }).array()).query(async (opt) => {
const wallpapersPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
const output: {
name: string;
previewSrc: string;
}[] = [];
for (const wallpaperName of await fs.readdir(wallpapersPath)) {
if (wallpaperName === "current.webp" || wallpaperName === "resized" || !wallpaperName.endsWith(".webp")) continue;
const wallpaperPath = path.join(wallpapersPath, wallpaperName);
output.push({
name: wallpaperName,
previewSrc:
opt.ctx.instance.sys.configuration.proxy +
(await instance.sys.image.serveImage(opt.ctx.userId, wallpaperPath, {
resize: {
dimensions: { width: 296, height: 192 },
},
})),
});
}
return output;
}),
getDefaultWallpapers: procedure.output(z.object({ name: z.string(), previewSrc: z.string() }).array()).query(async (opt) => {
const DEFAULT_WALLPAPERS_PATH = path.join(instance.sys.filesystem.SRC_ROOT, "assets/wallpapers");
const output: {
name: string;
previewSrc: string;
}[] = [];
for (const wallpaperName of await fs.readdir(DEFAULT_WALLPAPERS_PATH)) {
const wallpaperPath = path.join(DEFAULT_WALLPAPERS_PATH, wallpaperName);
output.push({
name: wallpaperName,
previewSrc:
opt.ctx.instance.sys.configuration.proxy +
(await instance.sys.image.serveImage(opt.ctx.userId, wallpaperPath, { resize: { dimensions: { height: 140, width: 250 }, position: "centre" } })),
});
}
return output;
}),
getCurrentWallpaper: procedure.query(async (opt) => {
const wallpapersRootPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
const rawWallpaperPath = path.join(wallpapersRootPath, "current.webp");
const resizedWallpapersPath = path.join(wallpapersRootPath, "resized");
const requiredResizedWallpaperPath = path.join(resizedWallpapersPath, `${504}x${280}.webp`);
if (!fsExistsSync(rawWallpaperPath)) {
return undefined;
}
if (!fsExistsSync(requiredResizedWallpaperPath)) {
const options = JSON.parse((await fs.readFile(path.join(wallpapersRootPath, "config.json"))).toString());
await instance.sys.image.resizeImage(
rawWallpaperPath,
requiredResizedWallpaperPath,
{ width: 504, height: 280 },
{
changeFormatTo: "webp",
fit: options?.fit,
position: options?.position,
background: options?.background,
},
);
}
return (
opt.ctx.instance.sys.configuration.proxy +
(await opt.ctx.instance.sys.image.serveImage(opt.ctx.userId, requiredResizedWallpaperPath, {
isPublic: false,
evadeCache: true,
}))
);
}),
upload: procedure.input(octetInputParser).mutation(async (opt) => {
const wallpapersPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
const wallpaperUUID = crypto.randomUUID();
// @ts-ignore
const bytes = await opt.input.bytes();
await sharp(bytes)
.toFormat("webp")
.toFile(path.join(wallpapersPath, `${wallpaperUUID}.webp`));
log.info(
`converted '${wallpaperUUID}' to WEBP -> '${path.relative(instance.sys.filesystem.FS_ROOT, path.join(wallpapersPath, `${wallpaperUUID}.webp`))}'`,
);
return `${wallpaperUUID}.webp`;
}),
delete: procedure.input(z.object({ name: z.string() })).mutation(async (opt) => {
const wallpapersPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
await fs.rm(path.join(wallpapersPath, opt.input.name));
return true;
}),
getOptions: procedure
.output(
z.object({
fit: z.string(),
position: z.tuple([z.string(), z.string()]).or(z.tuple([z.string()])),
}),
)
.query(async (opt) => {
const wallpaperPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
if (fsExistsSync(path.join(wallpaperPath, "config.json"))) {
const options = JSON.parse((await fs.readFile(path.join(wallpaperPath, "config.json"))).toString());
options.position = options.position.split(" ");
return options;
} else {
return { fit: "cover", position: ["center"] };
}
}),
setOptions: procedure
.input(
z.object({
fit: z.string(),
position: z.string(),
background: z.string(),
}),
)
.mutation(async (opt) => {
const wallpaperPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
const resizedWallpapersPath = path.join(wallpaperPath, "resized");
for (const resizedWallpaper of await fs.readdir(resizedWallpapersPath)) {
await fs.rm(path.join(resizedWallpapersPath, resizedWallpaper));
}
const options = {
fit: opt.input.fit,
position: opt.input.position,
background: opt.input.background || "#0000",
};
await fs.writeFile(path.join(wallpaperPath, "config.json"), JSON.stringify(options));
return true;
}),
setWallpaperToCustomWallpaper: procedure.input(z.object({ name: z.string() })).mutation(async (opt) => {
const userWallpapersPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
const resizedWallpapersPath = path.join(userWallpapersPath, "resized");
const currentWallpaperPath = path.join(userWallpapersPath, "current.webp");
for (const resizedWallpaper of await fs.readdir(resizedWallpapersPath)) {
await fs.rm(path.join(resizedWallpapersPath, resizedWallpaper));
}
if (fsExistsSync(currentWallpaperPath)) {
await fs.rm(currentWallpaperPath);
}
await fs.copyFile(path.join(userWallpapersPath, opt.input.name.replace(".preview", "")), currentWallpaperPath);
return true;
}),
setWallpaperToDefaultWallpaper: procedure.input(z.object({ name: z.string() })).mutation(async (opt) => {
const userWallpapersPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers");
const officialWallpaperPath = path.join(instance.sys.filesystem.SRC_ROOT, "assets/wallpapers");
const resizedWallpapersPath = path.join(userWallpapersPath, "resized");
const currentWallpaperPath = path.join(userWallpapersPath, "current.webp");
for (const resizedWallpaper of await fs.readdir(resizedWallpapersPath)) {
await fs.rm(path.join(resizedWallpapersPath, resizedWallpaper));
}
if (fsExistsSync(currentWallpaperPath)) {
await fs.rm(currentWallpaperPath);
}
await fs.copyFile(path.join(officialWallpaperPath, opt.input.name), currentWallpaperPath);
return true;
}),
},
colorTheme: {
getWallpaperPixelData: procedure.output(z.number().array()).query(async (opt) => {
const wallpaperPath = path.join((await opt.ctx.user()).getPath(), "assets/wallpapers/resized", `${504}x${280}.webp`);
const buf = (await sharp(wallpaperPath).raw().toBuffer({ resolveWithObject: true })).data;
const newBuf = [];
for (let i = 0; i < buf.length; i += 4) {
newBuf.push(buf[i + 3]);
newBuf.push(buf[i]);
newBuf.push(buf[i + 1]);
newBuf.push(buf[i + 2]);
}
return newBuf;
}),
setColorTheme: procedure.input(z.any()).mutation(async (opt) => {
const db = instance.sys.database.postgres();
if (opt.input === undefined) {
await db`UPDATE public.users SET color_scheme = NULL WHERE id = ${opt.ctx.userId}`;
return true;
}
await db`UPDATE public.users SET color_scheme = ${opt.input} WHERE id = ${opt.ctx.userId}`;
return true;
}),
},
quickShortcuts: {
getSettingData: procedure
.output(
z.object({
displayName: z.string(),
defaultShortcuts: z.string().array(),
enabledShortcuts: z.string().array().or(z.undefined()),
shortcutMetadata: z.record(
z.string(),
z.object({ id: z.string(), displayName: z.string(), icon: z.object({ type: z.enum(["icon", "image"]), value: z.string() }) }),
),
}),
)
.query(async (opt) => {
const a = instance.sys.settings.applicationSettings.core.find((s) => s.id === "quick_shortcuts");
if (!a) throw "The core:quick_shortcuts setting is somehow missing???";
const enabledShortcutIds = ((await a.onValueChange(opt.ctx.userId)) as string[] | undefined) || [];
const enabledApplications = instance.sys.applications.getEnabledApplications();
return {
displayName: a.displayName,
defaultShortcuts: a.defaultValue,
enabledShortcuts: enabledShortcutIds,
shortcutMetadata: Object.fromEntries(
enabledApplications
.map((app) => {
let icon = {
type: "icon" as "icon" | "image",
value: "indeterminate_question_box",
};
if (app.manifest?.icon) {
if (app.manifest.icon.type === "image") {
icon = {
type: "image",
value: `${opt.ctx.instance.sys.configuration.proxy.secure ? "https://" : "http://"}${opt.ctx.instance.sys.configuration.proxy.hostname}/api/application/${app.manifest.id}/icon/`,
};
} else {
icon = {
type: "icon",
value: `${opt.ctx.instance.sys.configuration.proxy.secure ? "https://" : "http://"}${opt.ctx.instance.sys.configuration.proxy.hostname}/api/application/${app.manifest.id}/icon/`,
};
}
}
return [
app.manifest!.id,
{
id: app.manifest!.id,
displayName: app.manifest!.displayName,
icon: icon,
},
];
})
.filter((a) => a !== undefined),
),
};
}),
availableShortcuts: procedure.query(async (opt) => {
const applications = instance.sys.applications.enabledApplications;
const shortcuts: {
id: string;
displayName: string;
icon: { type: "icon" | "image"; value: string };
}[] = [];
const quickShortcutsSetting = instance.sys.settings.applicationSettings.core?.find((s) => s.id === "quick_shortcuts");
if (!quickShortcutsSetting) return [];
const userShortcuts = (await quickShortcutsSetting.onValueChange(opt.ctx.userId)) || [];
for (const applicationId of applications) {
const application = instance.sys.applications.availableApplications.find((aa) => aa.manifest?.id === applicationId);
if (!application) continue;
if (userShortcuts.includes(applicationId)) {
continue;
}
let icon = {
type: "icon" as "icon" | "image",
value: "indeterminate_question_box",
};
if (application.manifest?.icon) {
if (application.manifest.icon.type === "image") {
icon = {
type: "image",
value: `${opt.ctx.instance.sys.configuration.proxy.secure ? "https://" : "http://"}${opt.ctx.instance.sys.configuration.proxy.hostname}/api/application/${application.manifest.id}/icon/`,
};
} else {
icon = application.manifest.icon;
}
}
shortcuts.push({
id: application.manifest?.id || applicationId,
displayName: application.manifest?.displayName || applicationId,
icon: icon,
});
}
return shortcuts;
}),
},
},
storage: {
usage: procedure
.output(
z
.object({
displayName: z.string(),
percentage: z.number(),
size: z.number(),
})
.array(),
)
.query(async (opt) => {
async function getChildFiles(dir: string) {
const children = await fs.readdir(dir);
let output: {
type: FileMediaType;
size: number;
path: string;
}[] = [];
for (const child of children) {
const childPath = path.join(dir, child);
const childLstat = await fs.lstat(childPath);
if (childLstat.isDirectory()) {
output = [...output, ...(await getChildFiles(childPath))];
} else {
output.push({
path: childPath,
size: childLstat.size,
type: instance.sys.filesystem.getFileType(childPath),
});
}
}
return output;
}
const files = await getChildFiles((await opt.ctx.user()).getPath());
const categories: {
[categoryId: string]: {
fileCount: number;
size: number;
percentage: number;
};
} = {};
for (const file of files) {
if (file.type === undefined) {
file.type = FileMediaType.Unknown;
}
if (!categories[file.type]) {
categories[file.type] = {
fileCount: 0,
size: 0,
percentage: 0,
};
}
categories[file.type] = {
fileCount: categories[file.type].fileCount + 1,
size: categories[file.type].size + file.size,
percentage: 0,
};
}
let output: {
displayName: string;
percentage: number;
size: number;
}[] = [];
const storageQuota = (await (await opt.ctx.user()).getQuota()) || 1;
for (const categoryName of Object.keys(categories)) {
const category = categories[categoryName];
output.push({
displayName: categoryName,
percentage: Number((category.size / 1000000000 / storageQuota).toFixed(2)),
size: category.size / 1000000000,
});
}
output = output.sort((i, j) => i.size - j.size).reverse();
return output;
}),
},
application: {
getApplications: procedure.output(z.object({ displayName: z.string(), id: z.string() }).array()).query(async () => {
return instance.sys.applications.enabledApplications.map((enabledApplication) => {
return {
displayName:
instance.sys.applications.availableApplications.find((availableApplication) => availableApplication.manifest?.id === enabledApplication)?.manifest
?.displayName || `Failed to find application '${enabledApplication}'`,
id: enabledApplication,
};
});
}),
getApplication: procedure
.input(z.object({ id: z.string() }))
.output(
z.object({
displayName: z.string(),
icon: z.object({
type: z.literal("icon").or(z.literal("image")),
value: z.string(),
}),
settings: z
.object({
displayName: z.string(),
defaultValue: z.any(),
currentValue: z.any().or(z.undefined()),
type: z.string(),
id: z.string(),
global: z.boolean(),
description: z.string(),
})
.array(),
}),
)
.query(async (opt) => {
const application = instance.sys.applications.availableApplications.find((aa) => aa.manifest?.id === opt.input.id);
if (!application) throw { error: true };
let icon = {
type: "icon" as "icon" | "image",
value: "indeterminate_question_box",
};
if (application.manifest?.icon) {
if (application.manifest.icon.type === "image") {
icon = {
type: "image",
value: `${opt.ctx.instance.sys.configuration.proxy.secure ? "https://" : "http://"}${opt.ctx.instance.sys.configuration.proxy.hostname}/api/application/${application.manifest.id}/icon/`,
};
} else {
icon = {
type: "icon",
value: `${opt.ctx.instance.sys.configuration.proxy.secure ? "https://" : "http://"}${opt.ctx.instance.sys.configuration.proxy.hostname}/api/application/${application.manifest.id}/icon/`,
};
}
}
const settings = (
await Promise.all(
instance.sys.settings.applicationSettings[opt.input.id]?.map(async (a) => {
if (!(a instanceof GlobalApplicationSetting)) {
if (a.hidden) return undefined;
return {
displayName: a.displayName,
defaultValue: a.defaultValue,
currentValue: await a.onValueChange(opt.ctx.userId),
type: a.type,
id: a.id,
global: false,
description: a.description || "No description provided.",
};
}
}) || [],
)
).filter((a) => a !== undefined);
const globalSettings = (
await Promise.all(
instance.sys.settings.applicationSettings[opt.input.id]?.map(async (a) => {
if (a instanceof GlobalApplicationSetting) {
if (a.hidden) return undefined;
return {
displayName: a.displayName,
defaultValue: a.defaultValue,
currentValue: await a.onValueChange(),
type: a.type,
id: a.id,
global: false,
description: a.description || "No description provided.",
};
}
}) || [],
)
).filter((a) => a !== undefined);
return {
displayName: application?.manifest?.displayName || opt.input.id,
icon: icon,
settings: settings || [],
globalSettings: globalSettings || [],
};
}),
setApplicationBooleanSettingValue: procedure
.input(
z.object({
applicationId: z.string(),
id: z.string(),
value: z.boolean(),
}),
)
.mutation(async (opt) => {
await instance.sys.settings.setUserApplicationSetting(opt.ctx.userId, opt.input.applicationId, opt.input.id, opt.input.value);
return true;
}),
setApplicationStringSettingValue: procedure
.input(
z.object({
applicationId: z.string(),
id: z.string(),
value: z.string(),
}),
)
.mutation(async (opt) => {
await instance.sys.settings.setUserApplicationSetting(opt.ctx.userId, opt.input.applicationId, opt.input.id, opt.input.value);
return true;
}),
setApplicationStringListSettingValue: procedure
.input(
z.object({
applicationId: z.string(),
id: z.string(),
value: z.string().array(),
}),
)
.mutation(async (opt) => {
await instance.sys.settings.setUserApplicationSetting(opt.ctx.userId, opt.input.applicationId, opt.input.id, opt.input.value);
return true;
}),
},
});
export type TRPCRouter = typeof router;
instance.sys.tRPC.registerTRPCRouter(router, "/api/app/uk.ewsgit.settings")