Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions Source/Process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,78 @@ export class Process {
const responseJSON = await response.json();
return new Result(true, "获得统计数据成功", responseJSON);
},
SetUserSettings: async (Data: object): Promise<Result> => {
// Enforce a maximum allowed size for the settings payload to avoid
// excessively large entries being written to D1.
const MAX_SETTINGS_LENGTH = 10000;

ThrowErrorIfFailed(this.CheckParams(Data, {
"Settings": "string"
}));

const SettingsString = Data["Settings"];
if (typeof SettingsString !== "string") {
return new Result(false, "设置格式有误");
}
if (SettingsString.length > MAX_SETTINGS_LENGTH) {
return new Result(false, "设置内容过大");
}

let SettingsObject: object;
try {
SettingsObject = JSON.parse(SettingsString);
} catch (_) {
return new Result(false, "设置格式有误");
}
if (typeof SettingsObject !== "object" || Array.isArray(SettingsObject) || SettingsObject === null) {
return new Result(false, "设置格式有误");
}
try {
// Try to insert first. If a unique/primary key constraint is hit (row already exists),
// fall back to updating the existing row. This avoids a non-atomic check-then-insert flow.
ThrowErrorIfFailed(await this.XMOJDatabase.Insert("user_settings", {
user_id: this.Username,
settings: SettingsString
}));
} catch (e) {
const errorMessage = e instanceof Error ? e.message : (typeof e === "object" && e !== null && "Message" in e ? String((e as { Message?: unknown }).Message) : String(e));
if (/UNIQUE|constraint|duplicate/i.test(errorMessage)) {
// Row for this user_id already exists, perform an update instead.
ThrowErrorIfFailed(await this.XMOJDatabase.Update("user_settings", {
settings: SettingsString
}, {
user_id: this.Username
}));
} else {
// Propagate non-uniqueness errors.
throw e;
}
}
return new Result(true, "保存设置成功");
},
GetUserSettings: async (Data: object): Promise<Result> => {
ThrowErrorIfFailed(this.CheckParams(Data, {}));
const SettingsData = ThrowErrorIfFailed(await this.XMOJDatabase.Select("user_settings", ["settings"], {
user_id: this.Username
}));
if (SettingsData.length === 0) {
return new Result(true, "获得设置成功", {
"Settings": {}
});
}
let SettingsObject: object;
try {
SettingsObject = JSON.parse(SettingsData[0]["settings"]);
} catch (_) {
return new Result(false, "设置数据损坏");
}
Comment thread
PythonSmall-Q marked this conversation as resolved.
if (typeof SettingsObject !== "object" || Array.isArray(SettingsObject) || SettingsObject === null) {
return new Result(false, "设置数据损坏");
}
return new Result(true, "获得设置成功", {
"Settings": SettingsObject
});
},
LastOnline: async (Data: object): Promise<Result> => {
ThrowErrorIfFailed(this.CheckParams(Data, {
"Username": "string"
Expand Down
6 changes: 6 additions & 0 deletions migrations/0004_add_user_settings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Migration number: 0004 2026-03-22

CREATE TABLE IF NOT EXISTS user_settings (
user_id TEXT PRIMARY KEY NOT NULL,
settings TEXT NOT NULL DEFAULT '{}'
);
104 changes: 104 additions & 0 deletions test/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,107 @@ test('AddBBSMention pushes full mention payload for websocket clients', async ()
});
assert.strictEqual(typeof pushedBody.notification.data.MentionTime, 'number');
});

test('SetUserSettings creates new settings row when none exists', async () => {
const insertedRows = [];
const proc = createProcess({
db: {
GetTableSize: async () => new Result(true, '', { TableSize: 0 }),
Insert: async (table, data) => {
insertedRows.push({ table, data });
return new Result(true, '', { InsertID: 1 });
}
}
});
const result = await proc.ProcessFunctions['SetUserSettings']({ Settings: '{"Discussion":"true","Theme":"dark"}' });
assert.ok(result.Success);
assert.strictEqual(result.Message, '保存设置成功');
assert.strictEqual(insertedRows.length, 1);
assert.strictEqual(insertedRows[0].table, 'user_settings');
assert.strictEqual(insertedRows[0].data.user_id, 'testuser');
assert.strictEqual(insertedRows[0].data.settings, '{"Discussion":"true","Theme":"dark"}');
});

test('SetUserSettings updates existing settings row', async () => {
const updatedRows = [];
const proc = createProcess({
db: {
Insert: async () => {
// Simulate a primary key conflict (row already exists)
throw new Error('UNIQUE constraint failed: user_settings.user_id');
},
Update: async (table, data, where) => {
updatedRows.push({ table, data, where });
return new Result(true, '');
}
}
});
const result = await proc.ProcessFunctions['SetUserSettings']({ Settings: '{"Discussion":"false"}' });
assert.ok(result.Success);
assert.strictEqual(result.Message, '保存设置成功');
assert.strictEqual(updatedRows.length, 1);
assert.strictEqual(updatedRows[0].table, 'user_settings');
assert.strictEqual(updatedRows[0].data.settings, '{"Discussion":"false"}');
assert.deepStrictEqual(updatedRows[0].where, { user_id: 'testuser' });
});

test('SetUserSettings fails with invalid JSON', async () => {
const proc = createProcess();
const result = await proc.ProcessFunctions['SetUserSettings']({ Settings: 'not-json' });
assert.strictEqual(result.Success, false);
assert.strictEqual(result.Message, '设置格式有误');
});

test('SetUserSettings fails when Settings is an array', async () => {
const proc = createProcess();
const result = await proc.ProcessFunctions['SetUserSettings']({ Settings: '["a","b"]' });
assert.strictEqual(result.Success, false);
assert.strictEqual(result.Message, '设置格式有误');
});

test('GetUserSettings returns empty object when no settings stored', async () => {
const proc = createProcess({
db: {
Select: async () => new Result(true, '', [])
}
});
const result = await proc.ProcessFunctions['GetUserSettings']({});
assert.ok(result.Success);
assert.strictEqual(result.Message, '获得设置成功');
assert.deepStrictEqual(result.Data.Settings, {});
});

test('GetUserSettings returns stored settings', async () => {
const proc = createProcess({
db: {
Select: async () => new Result(true, '', [{ settings: '{"Discussion":"true","Theme":"dark"}' }])
}
});
const result = await proc.ProcessFunctions['GetUserSettings']({});
assert.ok(result.Success);
assert.strictEqual(result.Message, '获得设置成功');
assert.deepStrictEqual(result.Data.Settings, { Discussion: 'true', Theme: 'dark' });
});

test('GetUserSettings fails when stored settings JSON is corrupted', async () => {
const proc = createProcess({
db: {
Select: async () => new Result(true, '', [{ settings: 'corrupted{json' }])
}
});
const result = await proc.ProcessFunctions['GetUserSettings']({});
assert.strictEqual(result.Success, false);
assert.strictEqual(result.Message, '设置数据损坏');
});

Comment thread
PythonSmall-Q marked this conversation as resolved.
test('GetUserSettings fails when stored settings JSON is valid but not an object', async () => {
const proc = createProcess({
db: {
// settings is valid JSON (an array) but not an object
Select: async () => new Result(true, '', [{ settings: '["a","b"]' }])
}
});
const result = await proc.ProcessFunctions['GetUserSettings']({});
assert.strictEqual(result.Success, false);
assert.strictEqual(result.Message, '设置数据损坏');
});
Loading