diff --git a/src/api/routes/users/@me/settings-proto/1.ts b/src/api/routes/users/@me/settings-proto/1.ts
new file mode 100644
index 00000000..3ac64292
--- /dev/null
+++ b/src/api/routes/users/@me/settings-proto/1.ts
@@ -0,0 +1,187 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2025 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
+import {
+ OrmUtils,
+ SettingsProtoJsonResponse,
+ SettingsProtoResponse,
+ SettingsProtoUpdateJsonSchema,
+ SettingsProtoUpdateSchema,
+ UserSettingsProtos,
+} from "@spacebar/util";
+import { PreloadedUserSettings } from "discord-protos";
+
+const router: Router = Router();
+
+//#region Protobuf
+router.get(
+ "/",
+ route({
+ responses: {
+ 200: {
+ body: "SettingsProtoResponse",
+ },
+ },
+ query: {
+ atomic: {
+ type: "boolean",
+ description:
+ "Whether to try to apply the settings update atomically (default false)",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const userSettings = await UserSettingsProtos.getOrCreate(req.user_id);
+
+ res.json({
+ settings: PreloadedUserSettings.toBase64(
+ userSettings.userSettings!,
+ ),
+ } as SettingsProtoResponse);
+ },
+);
+
+router.patch(
+ "/",
+ route({
+ requestBody: "SettingsProtoUpdateSchema",
+ responses: {
+ 200: {
+ body: "SettingsProtoUpdateResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const { settings, required_data_version } =
+ req.body as SettingsProtoUpdateSchema;
+ const { atomic } = req.query;
+ const updatedSettings = PreloadedUserSettings.fromBase64(settings);
+
+ const resultObj = await patchUserSettings(
+ req.user_id,
+ updatedSettings,
+ required_data_version,
+ atomic == "true",
+ );
+
+ res.json({
+ settings: PreloadedUserSettings.toBase64(resultObj.settings),
+ out_of_date: resultObj.out_of_date,
+ });
+ },
+);
+
+//#endregion
+//#region JSON
+router.get(
+ "/json",
+ route({
+ responses: {
+ 200: {
+ body: "SettingsProtoJsonResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const userSettings = await UserSettingsProtos.getOrCreate(req.user_id);
+
+ res.json({
+ settings: PreloadedUserSettings.toJson(userSettings.userSettings!),
+ } as SettingsProtoJsonResponse);
+ },
+);
+
+router.patch(
+ "/json",
+ route({
+ requestBody: "SettingsProtoUpdateJsonSchema",
+ responses: {
+ 200: {
+ body: "SettingsProtoUpdateJsonResponse",
+ },
+ },
+ query: {
+ atomic: {
+ type: "boolean",
+ description:
+ "Whether to try to apply the settings update atomically (default false)",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const { settings, required_data_version } =
+ req.body as SettingsProtoUpdateJsonSchema;
+ const { atomic } = req.query;
+ const updatedSettings = PreloadedUserSettings.fromJson(settings);
+
+ const resultObj = await patchUserSettings(
+ req.user_id,
+ updatedSettings,
+ required_data_version,
+ atomic == "true",
+ );
+
+ res.json({
+ settings: PreloadedUserSettings.toJson(resultObj.settings),
+ out_of_date: resultObj.out_of_date,
+ });
+ },
+);
+
+//#endregion
+
+async function patchUserSettings(
+ userId: string,
+ updatedSettings: PreloadedUserSettings,
+ required_data_version: number | undefined,
+ atomic: boolean = false,
+) {
+ const userSettings = await UserSettingsProtos.getOrCreate(userId);
+ let settings = userSettings.userSettings!;
+
+ if (
+ required_data_version &&
+ settings.versions &&
+ settings.versions.dataVersion > required_data_version
+ ) {
+ return {
+ settings: settings,
+ out_of_date: true,
+ };
+ }
+
+ console.log(`Updating user settings for user ${userId} with atomic=${atomic}:`, updatedSettings);
+
+ if (!atomic) {
+ settings = Object.assign(settings, updatedSettings);
+ } else {
+ settings = OrmUtils.mergeDeep(settings, updatedSettings);
+ }
+
+ userSettings.userSettings = settings;
+ await userSettings.save();
+
+ return {
+ settings: settings,
+ out_of_date: false,
+ };
+}
+
+export default router;
diff --git a/src/api/routes/users/@me/settings-proto/2.ts b/src/api/routes/users/@me/settings-proto/2.ts
new file mode 100644
index 00000000..e0cf786f
--- /dev/null
+++ b/src/api/routes/users/@me/settings-proto/2.ts
@@ -0,0 +1,187 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2025 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
+import {
+ OrmUtils,
+ SettingsProtoJsonResponse,
+ SettingsProtoResponse,
+ SettingsProtoUpdateJsonSchema,
+ SettingsProtoUpdateSchema,
+ UserSettingsProtos,
+} from "@spacebar/util";
+import { FrecencyUserSettings } from "discord-protos";
+
+const router: Router = Router();
+
+//#region Protobuf
+router.get(
+ "/",
+ route({
+ responses: {
+ 200: {
+ body: "SettingsProtoResponse",
+ },
+ },
+ query: {
+ atomic: {
+ type: "boolean",
+ description:
+ "Whether to try to apply the settings update atomically (default false)",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const userSettings = await UserSettingsProtos.getOrCreate(req.user_id);
+
+ res.json({
+ settings: FrecencyUserSettings.toBase64(
+ userSettings.frecencySettings!,
+ ),
+ } as SettingsProtoResponse);
+ },
+);
+
+router.patch(
+ "/",
+ route({
+ requestBody: "SettingsProtoUpdateSchema",
+ responses: {
+ 200: {
+ body: "SettingsProtoUpdateResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const { settings, required_data_version } =
+ req.body as SettingsProtoUpdateSchema;
+ const { atomic } = req.query;
+ const updatedSettings = FrecencyUserSettings.fromBase64(settings);
+
+ const resultObj = await patchUserSettings(
+ req.user_id,
+ updatedSettings,
+ required_data_version,
+ atomic == "true",
+ );
+
+ res.json({
+ settings: FrecencyUserSettings.toBase64(resultObj.settings),
+ out_of_date: resultObj.out_of_date,
+ });
+ },
+);
+
+//#endregion
+//#region JSON
+router.get(
+ "/json",
+ route({
+ responses: {
+ 200: {
+ body: "SettingsProtoJsonResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const userSettings = await UserSettingsProtos.getOrCreate(req.user_id);
+
+ res.json({
+ settings: FrecencyUserSettings.toJson(userSettings.frecencySettings!),
+ } as SettingsProtoJsonResponse);
+ },
+);
+
+router.patch(
+ "/json",
+ route({
+ requestBody: "SettingsProtoUpdateJsonSchema",
+ responses: {
+ 200: {
+ body: "SettingsProtoUpdateJsonResponse",
+ },
+ },
+ query: {
+ atomic: {
+ type: "boolean",
+ description:
+ "Whether to try to apply the settings update atomically (default false)",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const { settings, required_data_version } =
+ req.body as SettingsProtoUpdateJsonSchema;
+ const { atomic } = req.query;
+ const updatedSettings = FrecencyUserSettings.fromJson(settings);
+
+ const resultObj = await patchUserSettings(
+ req.user_id,
+ updatedSettings,
+ required_data_version,
+ atomic == "true",
+ );
+
+ res.json({
+ settings: FrecencyUserSettings.toJson(resultObj.settings),
+ out_of_date: resultObj.out_of_date,
+ });
+ },
+);
+
+//#endregion
+
+async function patchUserSettings(
+ userId: string,
+ updatedSettings: FrecencyUserSettings,
+ required_data_version: number | undefined,
+ atomic: boolean = false,
+) {
+ const userSettings = await UserSettingsProtos.getOrCreate(userId);
+ let settings = userSettings.frecencySettings!;
+
+ if (
+ required_data_version &&
+ settings.versions &&
+ settings.versions.dataVersion > required_data_version
+ ) {
+ return {
+ settings: settings,
+ out_of_date: true,
+ };
+ }
+
+ console.log(`Updating frecency settings for user ${userId} with atomic=${atomic}:`, updatedSettings);
+
+ if (!atomic) {
+ settings = Object.assign(settings, updatedSettings);
+ } else {
+ settings = OrmUtils.mergeDeep(settings, updatedSettings);
+ }
+
+ userSettings.frecencySettings = settings;
+ await userSettings.save();
+
+ return {
+ settings: settings,
+ out_of_date: false,
+ };
+}
+
+export default router;
|