mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-31 19:16:08 +00:00
97 lines
4.1 KiB
JavaScript
97 lines
4.1 KiB
JavaScript
'use strict';
|
|
|
|
// Protected messages (messages.is_archived = 1, "Protect" in the message
|
|
// menu) are kept for history and evidence. No delete removes one: not its
|
|
// author, a moderator, a bot or the admin, and no bulk or automatic cleanup.
|
|
// Unprotecting it (unarchive-message) is the one way back.
|
|
//
|
|
// A thread reply goes with the message it hangs from (thread_id is ON DELETE
|
|
// CASCADE), so a thread head that holds a protected reply is kept as well.
|
|
// Its other replies can still go.
|
|
//
|
|
// installGuard adds a trigger that refuses to delete a protected row, so a
|
|
// path that misses these checks (a channel or account cascade included)
|
|
// fails instead of losing the message.
|
|
|
|
const REFUSAL = 'This message is protected. Unprotect it first.';
|
|
const THREAD_REFUSAL = 'This thread holds a protected reply. Unprotect it first.';
|
|
const CHANNEL_REFUSAL = 'This channel holds protected messages. Unprotect them first, or ask the admin.';
|
|
const CONVERSATION_REFUSAL = 'This conversation holds protected messages. Unprotect them first, or ask the admin.';
|
|
|
|
/** Why message `id` cannot be deleted, or null when it can. A failed check
|
|
* refuses. */
|
|
function refusal(db, id) {
|
|
try {
|
|
const row = db.prepare(`SELECT m.is_archived, ${keptSql('m')} AS kept FROM messages m WHERE m.id = ?`).get(id);
|
|
if (!row || !row.kept) return null;
|
|
return row.is_archived ? REFUSAL : THREAD_REFUSAL;
|
|
} catch (err) {
|
|
console.warn('[protected] message check failed, keeping it:', err.message);
|
|
return REFUSAL;
|
|
}
|
|
}
|
|
|
|
/** SQL that is true for a message row (table alias `alias`) that must stay. */
|
|
function keptSql(alias = 'm') {
|
|
return `(${alias}.is_archived = 1 OR EXISTS (SELECT 1 FROM messages pkr WHERE pkr.thread_id = ${alias}.id AND pkr.is_archived = 1))`;
|
|
}
|
|
|
|
/** SQL that is true for a message row that may be deleted. */
|
|
function deletableSql(alias = 'm') {
|
|
return `NOT ${keptSql(alias)}`;
|
|
}
|
|
|
|
/** How many messages in these channels must stay: protected ones, and
|
|
* thread heads whose protected reply sits elsewhere. A failed check counts
|
|
* as one, so the caller keeps the channel. */
|
|
function protectedCount(db, channelIds) {
|
|
const ids = (channelIds || []).filter(Number.isInteger);
|
|
if (!ids.length) return 0;
|
|
try {
|
|
const ph = ids.map(() => '?').join(',');
|
|
return db.prepare(`SELECT COUNT(*) AS n FROM messages m WHERE m.channel_id IN (${ph}) AND ${keptSql('m')}`).get(...ids).n;
|
|
} catch (err) {
|
|
console.warn('[protected] channel check failed, keeping it:', err.message);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/** Delete a channel's messages except the ones that must stay, with their
|
|
* reactions and pins. Returns how many went. Runs inside the caller's
|
|
* transaction when there is one. */
|
|
function clearUnprotected(db, channelId) {
|
|
const doomed = `SELECT m.id FROM messages m WHERE m.channel_id = ? AND ${deletableSql('m')}`;
|
|
db.prepare(`DELETE FROM reactions WHERE message_id IN (${doomed})`).run(channelId);
|
|
db.prepare(`DELETE FROM pinned_messages WHERE message_id IN (${doomed})`).run(channelId);
|
|
return db.prepare(`DELETE FROM messages WHERE id IN (${doomed})`).run(channelId).changes;
|
|
}
|
|
|
|
/** An automatic delete found protected messages in this channel: clear the
|
|
* rest, keep the channel and turn its timer off, so it is not tried again.
|
|
* Returns how many messages went. */
|
|
function keepChannel(db, channel, why) {
|
|
const removed = db.transaction(() => {
|
|
const n = clearUnprotected(db, channel.id);
|
|
db.prepare('UPDATE channels SET expires_at = NULL, is_temp_voice = 0 WHERE id = ?').run(channel.id);
|
|
return n;
|
|
})();
|
|
console.log(`[protected] Kept channel "${channel.code}" (${why}): it holds protected messages. Cleared ${removed} other message(s) and turned its timer off.`);
|
|
return removed;
|
|
}
|
|
|
|
function installGuard(db) {
|
|
db.exec(`
|
|
CREATE TRIGGER IF NOT EXISTS messages_keep_protected
|
|
BEFORE DELETE ON messages
|
|
WHEN OLD.is_archived = 1
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'protected message');
|
|
END;
|
|
`);
|
|
}
|
|
|
|
module.exports = {
|
|
REFUSAL, THREAD_REFUSAL, CHANNEL_REFUSAL, CONVERSATION_REFUSAL,
|
|
keptSql, deletableSql, refusal, protectedCount, clearUnprotected, keepChannel, installGuard,
|
|
};
|