Big commit
This commit is contained in:
626
electron/database.js
Normal file
626
electron/database.js
Normal file
@@ -0,0 +1,626 @@
|
||||
/**
|
||||
* Electron main-process SQLite database module.
|
||||
*
|
||||
* All SQL queries live here – the renderer communicates exclusively via IPC.
|
||||
* Uses sql.js (WASM SQLite) loaded in Node.js.
|
||||
*/
|
||||
|
||||
const { ipcMain, app } = require('electron');
|
||||
const fs = require('fs');
|
||||
const fsp = fs.promises;
|
||||
const path = require('path');
|
||||
|
||||
let db = null;
|
||||
let dbPath = '';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Migrations */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const migrations = [
|
||||
{
|
||||
version: 1,
|
||||
description: 'Initial schema – messages, users, rooms, reactions, bans, meta',
|
||||
up(database) {
|
||||
database.run(`
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
roomId TEXT NOT NULL,
|
||||
channelId TEXT,
|
||||
senderId TEXT NOT NULL,
|
||||
senderName TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
editedAt INTEGER,
|
||||
reactions TEXT NOT NULL DEFAULT '[]',
|
||||
isDeleted INTEGER NOT NULL DEFAULT 0,
|
||||
replyToId TEXT
|
||||
);
|
||||
`);
|
||||
database.run('CREATE INDEX IF NOT EXISTS idx_messages_roomId ON messages(roomId);');
|
||||
|
||||
database.run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
oderId TEXT,
|
||||
username TEXT,
|
||||
displayName TEXT,
|
||||
avatarUrl TEXT,
|
||||
status TEXT,
|
||||
role TEXT,
|
||||
joinedAt INTEGER,
|
||||
peerId TEXT,
|
||||
isOnline INTEGER,
|
||||
isAdmin INTEGER,
|
||||
isRoomOwner INTEGER,
|
||||
voiceState TEXT,
|
||||
screenShareState TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
database.run(`
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
topic TEXT,
|
||||
hostId TEXT NOT NULL,
|
||||
password TEXT,
|
||||
isPrivate INTEGER NOT NULL DEFAULT 0,
|
||||
createdAt INTEGER NOT NULL,
|
||||
userCount INTEGER NOT NULL DEFAULT 0,
|
||||
maxUsers INTEGER,
|
||||
icon TEXT,
|
||||
iconUpdatedAt INTEGER,
|
||||
permissions TEXT,
|
||||
channels TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
database.run(`
|
||||
CREATE TABLE IF NOT EXISTS reactions (
|
||||
id TEXT PRIMARY KEY,
|
||||
messageId TEXT NOT NULL,
|
||||
oderId TEXT,
|
||||
userId TEXT,
|
||||
emoji TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
database.run('CREATE INDEX IF NOT EXISTS idx_reactions_messageId ON reactions(messageId);');
|
||||
|
||||
database.run(`
|
||||
CREATE TABLE IF NOT EXISTS bans (
|
||||
oderId TEXT NOT NULL,
|
||||
userId TEXT,
|
||||
roomId TEXT NOT NULL,
|
||||
bannedBy TEXT NOT NULL,
|
||||
displayName TEXT,
|
||||
reason TEXT,
|
||||
expiresAt INTEGER,
|
||||
timestamp INTEGER NOT NULL,
|
||||
PRIMARY KEY (oderId, roomId)
|
||||
);
|
||||
`);
|
||||
database.run('CREATE INDEX IF NOT EXISTS idx_bans_roomId ON bans(roomId);');
|
||||
|
||||
database.run(`
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
description: 'Attachments table',
|
||||
up(database) {
|
||||
database.run(`
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
messageId TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
mime TEXT NOT NULL,
|
||||
isImage INTEGER NOT NULL DEFAULT 0,
|
||||
uploaderPeerId TEXT,
|
||||
filePath TEXT,
|
||||
savedPath TEXT
|
||||
);
|
||||
`);
|
||||
database.run('CREATE INDEX IF NOT EXISTS idx_attachments_messageId ON attachments(messageId);');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function runMigrations() {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS schema_version (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
version INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
|
||||
const row = db.exec('SELECT version FROM schema_version WHERE id = 1');
|
||||
let currentVersion = row.length > 0 ? row[0].values[0][0] : 0;
|
||||
|
||||
if (row.length === 0) {
|
||||
db.run('INSERT INTO schema_version (id, version) VALUES (1, 0)');
|
||||
}
|
||||
|
||||
for (const migration of migrations) {
|
||||
if (migration.version > currentVersion) {
|
||||
console.log(`[ElectronDB] Running migration v${migration.version}: ${migration.description}`);
|
||||
migration.up(db);
|
||||
currentVersion = migration.version;
|
||||
db.run('UPDATE schema_version SET version = ? WHERE id = 1', [currentVersion]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Persistence */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function persist() {
|
||||
if (!db) return;
|
||||
const data = db.export();
|
||||
const buffer = Buffer.from(data);
|
||||
fs.writeFileSync(dbPath, buffer);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Initialisation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
async function initDatabase() {
|
||||
const initSqlJs = require('sql.js');
|
||||
const SQL = await initSqlJs();
|
||||
|
||||
const dbDir = path.join(app.getPath('userData'), 'metoyou');
|
||||
await fsp.mkdir(dbDir, { recursive: true });
|
||||
dbPath = path.join(dbDir, 'metoyou.sqlite');
|
||||
|
||||
if (fs.existsSync(dbPath)) {
|
||||
const fileBuffer = fs.readFileSync(dbPath);
|
||||
db = new SQL.Database(fileBuffer);
|
||||
} else {
|
||||
db = new SQL.Database();
|
||||
}
|
||||
|
||||
db.run('PRAGMA journal_mode = MEMORY;');
|
||||
db.run('PRAGMA synchronous = NORMAL;');
|
||||
|
||||
runMigrations();
|
||||
persist();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** Run a prepared-statement query and return rows as plain objects. */
|
||||
function queryAll(sql, params = []) {
|
||||
const stmt = db.prepare(sql);
|
||||
stmt.bind(params);
|
||||
const results = [];
|
||||
while (stmt.step()) results.push(stmt.getAsObject());
|
||||
stmt.free();
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Return a single row as object or null. */
|
||||
function queryOne(sql, params = []) {
|
||||
const stmt = db.prepare(sql);
|
||||
stmt.bind(params);
|
||||
let result = null;
|
||||
if (stmt.step()) result = stmt.getAsObject();
|
||||
stmt.free();
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Row → model mappers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function rowToMessage(r) {
|
||||
return {
|
||||
id: String(r.id),
|
||||
roomId: String(r.roomId),
|
||||
channelId: r.channelId ? String(r.channelId) : undefined,
|
||||
senderId: String(r.senderId),
|
||||
senderName: String(r.senderName),
|
||||
content: String(r.content),
|
||||
timestamp: Number(r.timestamp),
|
||||
editedAt: r.editedAt != null ? Number(r.editedAt) : undefined,
|
||||
reactions: JSON.parse(String(r.reactions || '[]')),
|
||||
isDeleted: !!r.isDeleted,
|
||||
replyToId: r.replyToId ? String(r.replyToId) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function rowToUser(r) {
|
||||
return {
|
||||
id: String(r.id),
|
||||
oderId: String(r.oderId ?? ''),
|
||||
username: String(r.username ?? ''),
|
||||
displayName: String(r.displayName ?? ''),
|
||||
avatarUrl: r.avatarUrl ? String(r.avatarUrl) : undefined,
|
||||
status: String(r.status ?? 'offline'),
|
||||
role: String(r.role ?? 'member'),
|
||||
joinedAt: Number(r.joinedAt ?? 0),
|
||||
peerId: r.peerId ? String(r.peerId) : undefined,
|
||||
isOnline: !!r.isOnline,
|
||||
isAdmin: !!r.isAdmin,
|
||||
isRoomOwner: !!r.isRoomOwner,
|
||||
voiceState: r.voiceState ? JSON.parse(String(r.voiceState)) : undefined,
|
||||
screenShareState: r.screenShareState ? JSON.parse(String(r.screenShareState)) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function rowToRoom(r) {
|
||||
return {
|
||||
id: String(r.id),
|
||||
name: String(r.name),
|
||||
description: r.description ? String(r.description) : undefined,
|
||||
topic: r.topic ? String(r.topic) : undefined,
|
||||
hostId: String(r.hostId),
|
||||
password: r.password ? String(r.password) : undefined,
|
||||
isPrivate: !!r.isPrivate,
|
||||
createdAt: Number(r.createdAt),
|
||||
userCount: Number(r.userCount),
|
||||
maxUsers: r.maxUsers != null ? Number(r.maxUsers) : undefined,
|
||||
icon: r.icon ? String(r.icon) : undefined,
|
||||
iconUpdatedAt: r.iconUpdatedAt != null ? Number(r.iconUpdatedAt) : undefined,
|
||||
permissions: r.permissions ? JSON.parse(String(r.permissions)) : undefined,
|
||||
channels: r.channels ? JSON.parse(String(r.channels)) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function rowToReaction(r) {
|
||||
return {
|
||||
id: String(r.id),
|
||||
messageId: String(r.messageId),
|
||||
oderId: String(r.oderId ?? ''),
|
||||
userId: String(r.userId ?? ''),
|
||||
emoji: String(r.emoji),
|
||||
timestamp: Number(r.timestamp),
|
||||
};
|
||||
}
|
||||
|
||||
function rowToAttachment(r) {
|
||||
return {
|
||||
id: String(r.id),
|
||||
messageId: String(r.messageId),
|
||||
filename: String(r.filename),
|
||||
size: Number(r.size),
|
||||
mime: String(r.mime),
|
||||
isImage: !!r.isImage,
|
||||
uploaderPeerId: r.uploaderPeerId ? String(r.uploaderPeerId) : undefined,
|
||||
filePath: r.filePath ? String(r.filePath) : undefined,
|
||||
savedPath: r.savedPath ? String(r.savedPath) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function rowToBan(r) {
|
||||
return {
|
||||
oderId: String(r.oderId),
|
||||
userId: String(r.userId ?? ''),
|
||||
roomId: String(r.roomId),
|
||||
bannedBy: String(r.bannedBy),
|
||||
displayName: r.displayName ? String(r.displayName) : undefined,
|
||||
reason: r.reason ? String(r.reason) : undefined,
|
||||
expiresAt: r.expiresAt != null ? Number(r.expiresAt) : undefined,
|
||||
timestamp: Number(r.timestamp),
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* IPC handler registration */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function registerDatabaseIpc() {
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:initialize', async () => {
|
||||
await initDatabase();
|
||||
return true;
|
||||
});
|
||||
|
||||
// ── Messages ───────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:saveMessage', (_e, message) => {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO messages
|
||||
(id, roomId, channelId, senderId, senderName, content, timestamp, editedAt, reactions, isDeleted, replyToId)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
message.id,
|
||||
message.roomId,
|
||||
message.channelId ?? null,
|
||||
message.senderId,
|
||||
message.senderName,
|
||||
message.content,
|
||||
message.timestamp,
|
||||
message.editedAt ?? null,
|
||||
JSON.stringify(message.reactions ?? []),
|
||||
message.isDeleted ? 1 : 0,
|
||||
message.replyToId ?? null,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getMessages', (_e, roomId, limit = 100, offset = 0) => {
|
||||
const rows = queryAll(
|
||||
'SELECT * FROM messages WHERE roomId = ? ORDER BY timestamp ASC LIMIT ? OFFSET ?',
|
||||
[roomId, limit, offset],
|
||||
);
|
||||
return rows.map(rowToMessage);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:deleteMessage', (_e, messageId) => {
|
||||
db.run('DELETE FROM messages WHERE id = ?', [messageId]);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:updateMessage', (_e, messageId, updates) => {
|
||||
const row = queryOne('SELECT * FROM messages WHERE id = ? LIMIT 1', [messageId]);
|
||||
if (!row) return;
|
||||
const msg = { ...rowToMessage(row), ...updates };
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO messages
|
||||
(id, roomId, channelId, senderId, senderName, content, timestamp, editedAt, reactions, isDeleted, replyToId)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
msg.id, msg.roomId, msg.channelId ?? null, msg.senderId, msg.senderName,
|
||||
msg.content, msg.timestamp, msg.editedAt ?? null,
|
||||
JSON.stringify(msg.reactions ?? []), msg.isDeleted ? 1 : 0, msg.replyToId ?? null,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getMessageById', (_e, messageId) => {
|
||||
const row = queryOne('SELECT * FROM messages WHERE id = ? LIMIT 1', [messageId]);
|
||||
return row ? rowToMessage(row) : null;
|
||||
});
|
||||
|
||||
ipcMain.handle('db:clearRoomMessages', (_e, roomId) => {
|
||||
db.run('DELETE FROM messages WHERE roomId = ?', [roomId]);
|
||||
persist();
|
||||
});
|
||||
|
||||
// ── Reactions ──────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:saveReaction', (_e, reaction) => {
|
||||
const check = db.exec(
|
||||
'SELECT 1 FROM reactions WHERE messageId = ? AND userId = ? AND emoji = ?',
|
||||
[reaction.messageId, reaction.userId, reaction.emoji],
|
||||
);
|
||||
if (check.length > 0 && check[0].values.length > 0) return;
|
||||
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO reactions (id, messageId, oderId, userId, emoji, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[reaction.id, reaction.messageId, reaction.oderId, reaction.userId, reaction.emoji, reaction.timestamp],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:removeReaction', (_e, messageId, userId, emoji) => {
|
||||
db.run('DELETE FROM reactions WHERE messageId = ? AND userId = ? AND emoji = ?', [messageId, userId, emoji]);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getReactionsForMessage', (_e, messageId) => {
|
||||
const rows = queryAll('SELECT * FROM reactions WHERE messageId = ?', [messageId]);
|
||||
return rows.map(rowToReaction);
|
||||
});
|
||||
|
||||
// ── Users ──────────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:saveUser', (_e, user) => {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO users
|
||||
(id, oderId, username, displayName, avatarUrl, status, role, joinedAt,
|
||||
peerId, isOnline, isAdmin, isRoomOwner, voiceState, screenShareState)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
user.id,
|
||||
user.oderId ?? null,
|
||||
user.username ?? null,
|
||||
user.displayName ?? null,
|
||||
user.avatarUrl ?? null,
|
||||
user.status ?? null,
|
||||
user.role ?? null,
|
||||
user.joinedAt ?? null,
|
||||
user.peerId ?? null,
|
||||
user.isOnline ? 1 : 0,
|
||||
user.isAdmin ? 1 : 0,
|
||||
user.isRoomOwner ? 1 : 0,
|
||||
user.voiceState ? JSON.stringify(user.voiceState) : null,
|
||||
user.screenShareState ? JSON.stringify(user.screenShareState) : null,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getUser', (_e, userId) => {
|
||||
const row = queryOne('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
|
||||
return row ? rowToUser(row) : null;
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getCurrentUser', () => {
|
||||
const rows = db.exec("SELECT value FROM meta WHERE key = 'currentUserId'");
|
||||
if (rows.length === 0 || rows[0].values.length === 0) return null;
|
||||
const userId = String(rows[0].values[0][0]);
|
||||
const row = queryOne('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
|
||||
return row ? rowToUser(row) : null;
|
||||
});
|
||||
|
||||
ipcMain.handle('db:setCurrentUserId', (_e, userId) => {
|
||||
db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('currentUserId', ?)", [userId]);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getUsersByRoom', (_e, _roomId) => {
|
||||
const rows = queryAll('SELECT * FROM users');
|
||||
return rows.map(rowToUser);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:updateUser', (_e, userId, updates) => {
|
||||
const row = queryOne('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
|
||||
if (!row) return;
|
||||
const user = { ...rowToUser(row), ...updates };
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO users
|
||||
(id, oderId, username, displayName, avatarUrl, status, role, joinedAt,
|
||||
peerId, isOnline, isAdmin, isRoomOwner, voiceState, screenShareState)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
user.id, user.oderId ?? null, user.username ?? null, user.displayName ?? null,
|
||||
user.avatarUrl ?? null, user.status ?? null, user.role ?? null, user.joinedAt ?? null,
|
||||
user.peerId ?? null, user.isOnline ? 1 : 0, user.isAdmin ? 1 : 0, user.isRoomOwner ? 1 : 0,
|
||||
user.voiceState ? JSON.stringify(user.voiceState) : null,
|
||||
user.screenShareState ? JSON.stringify(user.screenShareState) : null,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
// ── Rooms ──────────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:saveRoom', (_e, room) => {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO rooms
|
||||
(id, name, description, topic, hostId, password, isPrivate, createdAt,
|
||||
userCount, maxUsers, icon, iconUpdatedAt, permissions, channels)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
room.id, room.name, room.description ?? null, room.topic ?? null,
|
||||
room.hostId, room.password ?? null, room.isPrivate ? 1 : 0, room.createdAt,
|
||||
room.userCount, room.maxUsers ?? null, room.icon ?? null,
|
||||
room.iconUpdatedAt ?? null,
|
||||
room.permissions ? JSON.stringify(room.permissions) : null,
|
||||
room.channels ? JSON.stringify(room.channels) : null,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getRoom', (_e, roomId) => {
|
||||
const row = queryOne('SELECT * FROM rooms WHERE id = ? LIMIT 1', [roomId]);
|
||||
return row ? rowToRoom(row) : null;
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getAllRooms', () => {
|
||||
const rows = queryAll('SELECT * FROM rooms');
|
||||
return rows.map(rowToRoom);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:deleteRoom', (_e, roomId) => {
|
||||
db.run('DELETE FROM rooms WHERE id = ?', [roomId]);
|
||||
db.run('DELETE FROM messages WHERE roomId = ?', [roomId]);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:updateRoom', (_e, roomId, updates) => {
|
||||
const row = queryOne('SELECT * FROM rooms WHERE id = ? LIMIT 1', [roomId]);
|
||||
if (!row) return;
|
||||
const room = { ...rowToRoom(row), ...updates };
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO rooms
|
||||
(id, name, description, topic, hostId, password, isPrivate, createdAt,
|
||||
userCount, maxUsers, icon, iconUpdatedAt, permissions, channels)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
room.id, room.name, room.description ?? null, room.topic ?? null,
|
||||
room.hostId, room.password ?? null, room.isPrivate ? 1 : 0, room.createdAt,
|
||||
room.userCount, room.maxUsers ?? null, room.icon ?? null,
|
||||
room.iconUpdatedAt ?? null,
|
||||
room.permissions ? JSON.stringify(room.permissions) : null,
|
||||
room.channels ? JSON.stringify(room.channels) : null,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
// ── Bans ───────────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:saveBan', (_e, ban) => {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO bans
|
||||
(oderId, userId, roomId, bannedBy, displayName, reason, expiresAt, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
ban.oderId, ban.userId ?? null, ban.roomId, ban.bannedBy,
|
||||
ban.displayName ?? null, ban.reason ?? null, ban.expiresAt ?? null, ban.timestamp,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:removeBan', (_e, oderId) => {
|
||||
db.run('DELETE FROM bans WHERE oderId = ?', [oderId]);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getBansForRoom', (_e, roomId) => {
|
||||
const now = Date.now();
|
||||
const rows = queryAll(
|
||||
'SELECT * FROM bans WHERE roomId = ? AND (expiresAt IS NULL OR expiresAt > ?)',
|
||||
[roomId, now],
|
||||
);
|
||||
return rows.map(rowToBan);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:isUserBanned', (_e, userId, roomId) => {
|
||||
const now = Date.now();
|
||||
const rows = queryAll(
|
||||
'SELECT * FROM bans WHERE roomId = ? AND (expiresAt IS NULL OR expiresAt > ?)',
|
||||
[roomId, now],
|
||||
);
|
||||
return rows.some((r) => String(r.oderId) === userId);
|
||||
});
|
||||
|
||||
// ── Attachments ─────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:saveAttachment', (_e, attachment) => {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO attachments
|
||||
(id, messageId, filename, size, mime, isImage, uploaderPeerId, filePath, savedPath)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
attachment.id, attachment.messageId, attachment.filename,
|
||||
attachment.size, attachment.mime, attachment.isImage ? 1 : 0,
|
||||
attachment.uploaderPeerId ?? null, attachment.filePath ?? null,
|
||||
attachment.savedPath ?? null,
|
||||
],
|
||||
);
|
||||
persist();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getAttachmentsForMessage', (_e, messageId) => {
|
||||
const rows = queryAll('SELECT * FROM attachments WHERE messageId = ?', [messageId]);
|
||||
return rows.map(rowToAttachment);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:getAllAttachments', () => {
|
||||
const rows = queryAll('SELECT * FROM attachments');
|
||||
return rows.map(rowToAttachment);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:deleteAttachmentsForMessage', (_e, messageId) => {
|
||||
db.run('DELETE FROM attachments WHERE messageId = ?', [messageId]);
|
||||
persist();
|
||||
});
|
||||
|
||||
// ── Utilities ──────────────────────────────────────────────────────
|
||||
ipcMain.handle('db:clearAllData', () => {
|
||||
db.run('DELETE FROM messages');
|
||||
db.run('DELETE FROM users');
|
||||
db.run('DELETE FROM rooms');
|
||||
db.run('DELETE FROM reactions');
|
||||
db.run('DELETE FROM bans');
|
||||
db.run('DELETE FROM attachments');
|
||||
db.run('DELETE FROM meta');
|
||||
persist();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { registerDatabaseIpc };
|
||||
@@ -2,6 +2,7 @@ const { app, BrowserWindow, ipcMain, desktopCapturer } = require('electron');
|
||||
const fs = require('fs');
|
||||
const fsp = fs.promises;
|
||||
const path = require('path');
|
||||
const { registerDatabaseIpc } = require('./database');
|
||||
|
||||
let mainWindow;
|
||||
|
||||
@@ -9,6 +10,10 @@ let mainWindow;
|
||||
app.commandLine.appendSwitch('disable-features', 'Autofill,AutofillAssistant,AutofillServerCommunication');
|
||||
// Allow media autoplay without user gesture (bypasses Chromium autoplay policy)
|
||||
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
|
||||
// Accept self-signed certificates in development (for --ssl dev server)
|
||||
if (process.env.SSL === 'true') {
|
||||
app.commandLine.appendSwitch('ignore-certificate-errors');
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
@@ -29,7 +34,10 @@ function createWindow() {
|
||||
|
||||
// In development, load from Angular dev server
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.loadURL('http://localhost:4200');
|
||||
const devUrl = process.env.SSL === 'true'
|
||||
? 'https://localhost:4200'
|
||||
: 'http://localhost:4200';
|
||||
mainWindow.loadURL(devUrl);
|
||||
if (process.env.DEBUG_DEVTOOLS === '1') {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
@@ -44,6 +52,9 @@ function createWindow() {
|
||||
});
|
||||
}
|
||||
|
||||
// Register database IPC handlers before app is ready
|
||||
registerDatabaseIpc();
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
|
||||
@@ -17,4 +17,52 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
writeFile: (filePath, data) => ipcRenderer.invoke('write-file', filePath, data),
|
||||
fileExists: (filePath) => ipcRenderer.invoke('file-exists', filePath),
|
||||
ensureDir: (dirPath) => ipcRenderer.invoke('ensure-dir', dirPath),
|
||||
|
||||
// ── Database operations (all SQL lives in main process) ───────────
|
||||
db: {
|
||||
initialize: () => ipcRenderer.invoke('db:initialize'),
|
||||
|
||||
// Messages
|
||||
saveMessage: (message) => ipcRenderer.invoke('db:saveMessage', message),
|
||||
getMessages: (roomId, limit, offset) => ipcRenderer.invoke('db:getMessages', roomId, limit, offset),
|
||||
deleteMessage: (messageId) => ipcRenderer.invoke('db:deleteMessage', messageId),
|
||||
updateMessage: (messageId, updates) => ipcRenderer.invoke('db:updateMessage', messageId, updates),
|
||||
getMessageById: (messageId) => ipcRenderer.invoke('db:getMessageById', messageId),
|
||||
clearRoomMessages: (roomId) => ipcRenderer.invoke('db:clearRoomMessages', roomId),
|
||||
|
||||
// Reactions
|
||||
saveReaction: (reaction) => ipcRenderer.invoke('db:saveReaction', reaction),
|
||||
removeReaction: (messageId, userId, emoji) => ipcRenderer.invoke('db:removeReaction', messageId, userId, emoji),
|
||||
getReactionsForMessage: (messageId) => ipcRenderer.invoke('db:getReactionsForMessage', messageId),
|
||||
|
||||
// Users
|
||||
saveUser: (user) => ipcRenderer.invoke('db:saveUser', user),
|
||||
getUser: (userId) => ipcRenderer.invoke('db:getUser', userId),
|
||||
getCurrentUser: () => ipcRenderer.invoke('db:getCurrentUser'),
|
||||
setCurrentUserId: (userId) => ipcRenderer.invoke('db:setCurrentUserId', userId),
|
||||
getUsersByRoom: (roomId) => ipcRenderer.invoke('db:getUsersByRoom', roomId),
|
||||
updateUser: (userId, updates) => ipcRenderer.invoke('db:updateUser', userId, updates),
|
||||
|
||||
// Rooms
|
||||
saveRoom: (room) => ipcRenderer.invoke('db:saveRoom', room),
|
||||
getRoom: (roomId) => ipcRenderer.invoke('db:getRoom', roomId),
|
||||
getAllRooms: () => ipcRenderer.invoke('db:getAllRooms'),
|
||||
deleteRoom: (roomId) => ipcRenderer.invoke('db:deleteRoom', roomId),
|
||||
updateRoom: (roomId, updates) => ipcRenderer.invoke('db:updateRoom', roomId, updates),
|
||||
|
||||
// Bans
|
||||
saveBan: (ban) => ipcRenderer.invoke('db:saveBan', ban),
|
||||
removeBan: (oderId) => ipcRenderer.invoke('db:removeBan', oderId),
|
||||
getBansForRoom: (roomId) => ipcRenderer.invoke('db:getBansForRoom', roomId),
|
||||
isUserBanned: (userId, roomId) => ipcRenderer.invoke('db:isUserBanned', userId, roomId),
|
||||
|
||||
// Attachments
|
||||
saveAttachment: (attachment) => ipcRenderer.invoke('db:saveAttachment', attachment),
|
||||
getAttachmentsForMessage: (messageId) => ipcRenderer.invoke('db:getAttachmentsForMessage', messageId),
|
||||
getAllAttachments: () => ipcRenderer.invoke('db:getAllAttachments'),
|
||||
deleteAttachmentsForMessage: (messageId) => ipcRenderer.invoke('db:deleteAttachmentsForMessage', messageId),
|
||||
|
||||
// Utilities
|
||||
clearAllData: () => ipcRenderer.invoke('db:clearAllData'),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user