feat: enhance deployment instructions and exclude spam threads from message capture

This commit is contained in:
asepharyana
2026-07-05 06:58:17 +07:00
parent 5036cf0133
commit c0edb0fa40
6 changed files with 138 additions and 25 deletions
@@ -1,7 +1,7 @@
import type { PageResult } from "@bete/shared";
import { pgAttachmentsTable, pgMessagesTable } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import { and, desc, eq, inArray, like, lt, ne, type SQL } from "drizzle-orm";
import { and, desc, eq, inArray, isNull, like, lt, ne, notInArray, or, type SQL } from "drizzle-orm";
import { getDatabase } from "../../shared/database/index.js";
import { mapMessageRow } from "../../shared/utils/messageMapper.js";
import type {
@@ -10,6 +10,14 @@ import type {
MessageUpdate,
} from "./messages.schema.js";
/**
* Thread IDs to exclude from all message queries.
* Messages in these threads (e.g. bot/selfbot spam) are skipped
* both at capture time (discord-gateway) and when serving data
* (backend API).
*/
const EXCLUDED_THREAD_IDS = ["1522077685508083893"];
const logger = createChildLogger("messages.repository");
export interface AttachmentResult {
@@ -54,6 +62,16 @@ export class MessagesRepository {
conditions.push(lt(pgMessagesTable.created_at, Number(query.cursor)));
}
// Exclude spam threads (NULL-safe: non-thread messages are kept)
if (EXCLUDED_THREAD_IDS.length > 0) {
conditions.push(
or(
isNull(pgMessagesTable.thread_id),
notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS),
)!,
);
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
const rows = await db
.select()
@@ -96,6 +114,16 @@ export class MessagesRepository {
conditions.push(lt(pgMessagesTable.created_at, Number(query.cursor)));
}
// Exclude spam threads (NULL-safe)
if (EXCLUDED_THREAD_IDS.length > 0) {
conditions.push(
or(
isNull(pgMessagesTable.thread_id),
notInArray(pgMessagesTable.thread_id, EXCLUDED_THREAD_IDS),
)!,
);
}
const rows = await db
.select()
.from(pgMessagesTable)
@@ -304,6 +332,18 @@ export class MessagesRepository {
and(
eq(pgAttachmentsTable.guild_id, guildId),
like(pgAttachmentsTable.type, "image/%"),
// Exclude spam threads (NULL-safe for non-thread messages)
...(EXCLUDED_THREAD_IDS.length > 0
? [
or(
isNull(pgAttachmentsTable.thread_id),
notInArray(
pgAttachmentsTable.thread_id,
EXCLUDED_THREAD_IDS,
),
)!,
]
: []),
),
)
.orderBy(desc(pgAttachmentsTable.created_at))
@@ -49,6 +49,22 @@ const EXCLUDED_CHANNEL_IDS = new Set([
"1508059937031589949",
]);
/**
* Threads whose messages should be entirely ignored.
* Useful when a bot or selfbot is spamming inside a thread and
* you only want to ignore that one conversation, not the whole
* parent channel.
*/
const EXCLUDED_THREAD_IDS = new Set(["1522077685508083893"]);
function isExcludedThread(message: {
channel?: { isThread?: () => boolean; id?: string };
}): boolean {
return message.channel?.isThread?.() === true
&& typeof message.channel.id === "string"
&& EXCLUDED_THREAD_IDS.has(message.channel.id);
}
function getParentChannelId(
message: MessageLocationInput,
): string | null | undefined {
@@ -283,6 +299,7 @@ export function registerMessageCapture(client: Client): void {
if (!shouldCaptureForAnyTarget(message, targets)) return;
if (message.author?.bot) return;
if (isAgeRestrictedMessage(message)) return;
if (isExcludedThread(message)) return;
try {
await captureMessage(message, "text");
@@ -301,6 +318,7 @@ export function registerMessageCapture(client: Client): void {
if (!shouldCaptureForAnyTarget(newMessage, targets)) return;
if (newMessage.author?.bot) return;
if (isAgeRestrictedMessage(newMessage as Message)) return;
if (isExcludedThread(newMessage)) return;
try {
const existing = await getMessageById(newMessage.id);
@@ -367,6 +385,7 @@ export function registerMessageCapture(client: Client): void {
client.on("messageDelete", async (message) => {
if (!shouldCaptureForAnyTarget(message, targets)) return;
if (!message.author) return;
if (isExcludedThread(message)) return;
try {
const deletedAt = Date.now();
@@ -44,8 +44,9 @@ fn render_emojis(content: &str) -> Vec<AnyView> {
}
fn time_ago(ts: i64) -> String {
let now = (js_sys::Date::now() / 1000.0) as i64;
let secs = if now > ts { now - ts } else { 0 };
let now = js_sys::Date::now() as i64;
// created_at is in milliseconds (from Discord's message.createdTimestamp)
let secs = if now > ts { (now - ts) / 1000 } else { 0 };
if secs < 60 {
format!("{}s ago", secs)
} else if secs < 3600 {
@@ -53,13 +54,13 @@ fn time_ago(ts: i64) -> String {
} else if secs < 86400 {
format!("{}h ago", secs / 3600)
} else {
let d = js_sys::Date::new(&JsValue::from_f64((ts as f64) * 1000.0));
let d = js_sys::Date::new(&JsValue::from_f64(ts as f64));
format!("{}", d.to_locale_date_string("en-US", &JsValue::UNDEFINED))
}
}
fn fmt_time(ts: i64) -> String {
let d = js_sys::Date::new(&JsValue::from_f64((ts as f64) * 1000.0));
let d = js_sys::Date::new(&JsValue::from_f64(ts as f64));
format!("{:02}:{:02}", d.get_hours(), d.get_minutes())
}
@@ -3,6 +3,17 @@ use shared_types::message::{AiStatus, MessageRecord, PageResult};
use std::sync::Arc;
use wasm_bindgen_futures::spawn_local;
/// Threads whose messages should be hidden from both the feed and the
/// Images tab. A bot or selfbot may be spamming in a thread, polluting
/// the dashboard — add its thread ID here to keep the view clean.
const EXCLUDED_THREAD_IDS: &[&str] = &["1522077685508083893"];
fn is_excluded_thread(m: &MessageRecord) -> bool {
m.thread_id
.as_deref()
.is_some_and(|tid| EXCLUDED_THREAD_IDS.contains(&tid))
}
pub mod components;
pub mod hooks;
@@ -82,6 +93,7 @@ pub fn MessagesPanel() -> impl IntoView {
}
format!("{:?}", status).to_lowercase() == filter
})
.filter(|m| !is_excluded_thread(m))
.collect()
});
@@ -174,30 +186,36 @@ pub fn MessagesPanel() -> impl IntoView {
// Fetch image messages when Images tab is selected
let fetch_images = {
let image_messages = image_messages.clone();
move || {
spawn_local({
async move {
if let Some(config) = use_context::<crate::app::AppConfig>() {
if let Some(ref guild_id) = config.monitor_guild_id.get() {
match crate::api::messages::get_images(guild_id, Some(100)).await {
Ok(PageResult { data, .. }) => {
web_sys::console::log_2(
&"[images] fetch OK".into(),
&format!("count={}", data.len()).into(),
);
image_messages.set(data);
}
Err(e) => {
web_sys::console::log_2(
&"[images] fetch ERROR".into(),
&format!("{}", e).into(),
);
}
let guild_id = use_context::<crate::app::AppConfig>()
.and_then(|c| c.monitor_guild_id.get());
if let Some(gid) = guild_id {
spawn_local({
let image_messages = image_messages.clone();
async move {
match crate::api::messages::get_images(&gid, Some(100)).await {
Ok(PageResult { data, .. }) => {
web_sys::console::log_2(
&"[images] fetch OK".into(),
&format!("count={}", data.len()).into(),
);
image_messages.set(
data.into_iter()
.filter(|m| !is_excluded_thread(m))
.collect(),
);
}
Err(e) => {
web_sys::console::log_2(
&"[images] fetch ERROR".into(),
&format!("{}", e).into(),
);
}
}
}
}
});
});
}
}
};
// Fetch images when tab changes to Images