chore(auto): task completed - unknown
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
-- Migration 0010: Add message_reactions and message_edits tables
|
||||
-- Generated from schema: pgReactionsTable, pgMessageEditsTable
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "message_reactions" (
|
||||
"id" text PRIMARY KEY,
|
||||
"message_id" text NOT NULL,
|
||||
"channel_id" text NOT NULL,
|
||||
"guild_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"username" text NOT NULL,
|
||||
"emoji" text NOT NULL,
|
||||
"emoji_id" text,
|
||||
"animated" boolean NOT NULL DEFAULT false,
|
||||
"reaction_type" text NOT NULL CHECK ("reaction_type" IN ('add', 'remove')),
|
||||
"created_at" bigint NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_reactions_message_id" ON "message_reactions" ("message_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_reactions_user_id" ON "message_reactions" ("user_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_reactions_guild_created" ON "message_reactions" ("guild_id", "created_at");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "message_edits" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"message_id" text NOT NULL,
|
||||
"old_content" text NOT NULL,
|
||||
"edited_at" bigint NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_message_edits_message_id" ON "message_edits" ("message_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_message_edits_edited_at" ON "message_edits" ("edited_at");
|
||||
@@ -71,6 +71,13 @@
|
||||
"when": 1781316000000,
|
||||
"tag": "0009_add_reply_forward_crosspost",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1781390000000,
|
||||
"tag": "0010_add_reactions_and_edit_history",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,16 +4,29 @@ import { Client } from "discord.js-selfbot-v13";
|
||||
import { inArray, lt } from "drizzle-orm";
|
||||
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||
import { startPendingAIAnalysisWorker } from "../modules/ai-moderation/aiAnalyzer.js";
|
||||
import { registerChannelTopicCapture } from "../modules/channel-topic/index.js";
|
||||
import { CommandHandler } from "../modules/command-handler/commandHandler.js";
|
||||
import {
|
||||
EventBroadcaster,
|
||||
RedisEventPublisher,
|
||||
} from "../modules/event-broadcaster/index.js";
|
||||
import {
|
||||
startMetricsServer,
|
||||
stopMetricsServer,
|
||||
} from "../modules/gateway-metrics/index.js";
|
||||
import { registerGuildMemberEvents } from "../modules/guild-member-events/index.js";
|
||||
import {
|
||||
registerMessageCapture,
|
||||
setEventBroadcaster as setMessageCaptureEventBroadcaster,
|
||||
} from "../modules/message-capture/messageCapture.js";
|
||||
import { getExpiredMessages } from "../modules/message-capture/messageStore.js";
|
||||
import { registerReactionCapture } from "../modules/reaction-tracking/index.js";
|
||||
import { registerThreadCapture } from "../modules/thread-tracking/index.js";
|
||||
import { registerPresenceCapture } from "../modules/user-presence/index.js";
|
||||
import {
|
||||
startMuxerWorker,
|
||||
stopMuxerWorker,
|
||||
} from "../modules/voice-recording/muxer.js";
|
||||
import { setEventBroadcaster as setRecorderEventBroadcaster } from "../modules/voice-recording/recorder.js";
|
||||
import { VoiceController } from "../modules/voice-recording/voiceController.js";
|
||||
import { config } from "../shared/config/config.js";
|
||||
@@ -212,6 +225,7 @@ export async function initializeDiscordGateway() {
|
||||
client,
|
||||
eventBroadcaster,
|
||||
commandHandler,
|
||||
stopMetricsServer,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -252,6 +266,16 @@ export async function initializeDiscordGateway() {
|
||||
registerMessageCapture(client);
|
||||
startPendingAIAnalysisWorker(client, eventBroadcaster);
|
||||
|
||||
// Register new event captures
|
||||
registerReactionCapture(client, eventBroadcaster);
|
||||
registerThreadCapture(client, eventBroadcaster);
|
||||
registerPresenceCapture(client, eventBroadcaster);
|
||||
registerChannelTopicCapture(client, eventBroadcaster);
|
||||
registerGuildMemberEvents(client, eventBroadcaster);
|
||||
|
||||
// Start background workers
|
||||
startMuxerWorker();
|
||||
|
||||
// Start command handler after Discord is ready
|
||||
commandHandler.start(client, voiceController);
|
||||
logger.info("Command handler started");
|
||||
@@ -282,6 +306,9 @@ export async function initializeDiscordGateway() {
|
||||
gracefulShutdown("unhandledRejection");
|
||||
});
|
||||
|
||||
// Start metrics server
|
||||
startMetricsServer();
|
||||
|
||||
logger.info("Calling Discord client.login");
|
||||
client
|
||||
.login(token)
|
||||
|
||||
@@ -2,11 +2,14 @@ import type { createChildLogger } from "@bete/shared/logger";
|
||||
import type { Client } from "discord.js-selfbot-v13";
|
||||
import type { CommandHandler } from "../modules/command-handler/commandHandler.js";
|
||||
import type { EventBroadcaster } from "../modules/event-broadcaster/index.js";
|
||||
import { stopMetricsServer } from "../modules/gateway-metrics/index.js";
|
||||
import { stopMuxerWorker } from "../modules/voice-recording/muxer.js";
|
||||
import type { VoiceController } from "../modules/voice-recording/voiceController.js";
|
||||
import type { closeDatabase } from "../shared/database/drizzle.js";
|
||||
|
||||
type Logger = ReturnType<typeof createChildLogger>;
|
||||
type CloseDatabase = typeof closeDatabase;
|
||||
type StopMetricsServer = typeof stopMetricsServer;
|
||||
|
||||
export interface GracefulShutdownOptions {
|
||||
logger: Logger;
|
||||
@@ -15,6 +18,7 @@ export interface GracefulShutdownOptions {
|
||||
client: Client;
|
||||
eventBroadcaster: EventBroadcaster;
|
||||
commandHandler: CommandHandler;
|
||||
stopMetricsServer?: StopMetricsServer;
|
||||
}
|
||||
|
||||
export function createGracefulShutdown(options: GracefulShutdownOptions) {
|
||||
@@ -30,6 +34,8 @@ export function createGracefulShutdown(options: GracefulShutdownOptions) {
|
||||
options.logger.info({ signal }, "Graceful shutdown initiated");
|
||||
|
||||
try {
|
||||
options.stopMetricsServer?.();
|
||||
stopMuxerWorker();
|
||||
options.logger.info("Closing database...");
|
||||
await options.closeDatabase();
|
||||
options.logger.info("Database closed");
|
||||
|
||||
@@ -7,8 +7,11 @@ const logger = createChildLogger("channel-topic");
|
||||
|
||||
function isMonitoredGuild(guildId: string | null | undefined): boolean {
|
||||
if (!guildId) return false;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as string[] | undefined;
|
||||
if (!guildIds || guildIds.length === 0) return config.MONITOR_GUILD_ID === guildId;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as
|
||||
| string[]
|
||||
| undefined;
|
||||
if (!guildIds || guildIds.length === 0)
|
||||
return config.MONITOR_GUILD_ID === guildId;
|
||||
return guildIds.includes(guildId);
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,106 @@ export class EventBroadcaster {
|
||||
});
|
||||
}
|
||||
|
||||
async reactionAdded(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing reaction_added");
|
||||
await this.publisher.publish(EventChannels.REACTION_ADDED, {
|
||||
type: "reaction_added",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async reactionRemoved(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing reaction_removed");
|
||||
await this.publisher.publish(EventChannels.REACTION_REMOVED, {
|
||||
type: "reaction_removed",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async threadCreated(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing thread_created");
|
||||
await this.publisher.publish(EventChannels.THREAD_CREATED, {
|
||||
type: "thread_created",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async threadDeleted(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing thread_deleted");
|
||||
await this.publisher.publish(EventChannels.THREAD_DELETED, {
|
||||
type: "thread_deleted",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async threadUpdated(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing thread_updated");
|
||||
await this.publisher.publish(EventChannels.THREAD_UPDATED, {
|
||||
type: "thread_updated",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async channelTopicUpdated(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing channel_topic_updated");
|
||||
await this.publisher.publish(EventChannels.CHANNEL_TOPIC_UPDATED, {
|
||||
type: "channel_topic_updated",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async presenceUpdated(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing presence_updated");
|
||||
await this.publisher.publish(EventChannels.PRESENCE_UPDATED, {
|
||||
type: "presence_updated",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async guildMemberAdded(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing guild_member_added");
|
||||
await this.publisher.publish(EventChannels.GUILD_MEMBER_ADDED, {
|
||||
type: "guild_member_added",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async guildMemberRemoved(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing guild_member_removed");
|
||||
await this.publisher.publish(EventChannels.GUILD_MEMBER_REMOVED, {
|
||||
type: "guild_member_removed",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async voiceAnalyzed(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing voice_analyzed");
|
||||
await this.publisher.publish(EventChannels.VOICE_ANALYZED, {
|
||||
type: "voice_analyzed",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async analysisQueueStatus(data: unknown): Promise<void> {
|
||||
this.logger.debug({ data }, "Publishing analysis_queue_status");
|
||||
await this.publisher.publish(EventChannels.ANALYSIS_QUEUE_STATUS, {
|
||||
|
||||
@@ -2,11 +2,21 @@ import {
|
||||
DISCORD_ANALYSIS_QUEUE_STATUS,
|
||||
DISCORD_ATTACHMENT_CREATED,
|
||||
DISCORD_ATTACHMENT_UPLOADED,
|
||||
DISCORD_CHANNEL_TOPIC_UPDATED,
|
||||
DISCORD_GUILD_MEMBER_ADDED,
|
||||
DISCORD_GUILD_MEMBER_REMOVED,
|
||||
DISCORD_MESSAGE_ANALYZED,
|
||||
DISCORD_MESSAGE_CREATED,
|
||||
DISCORD_MESSAGE_DELETED,
|
||||
DISCORD_MESSAGE_UPDATED,
|
||||
DISCORD_PRESENCE_UPDATED,
|
||||
DISCORD_REACTION_ADDED,
|
||||
DISCORD_REACTION_REMOVED,
|
||||
DISCORD_THREAD_CREATED,
|
||||
DISCORD_THREAD_DELETED,
|
||||
DISCORD_THREAD_UPDATED,
|
||||
DISCORD_VOICE_ACTIVE_USER,
|
||||
DISCORD_VOICE_ANALYZED,
|
||||
DISCORD_VOICE_PCM,
|
||||
DISCORD_VOICE_STARTED,
|
||||
DISCORD_VOICE_STOPPED,
|
||||
@@ -30,6 +40,16 @@ export const EventChannels = {
|
||||
VOICE_ACTIVE_USER: DISCORD_VOICE_ACTIVE_USER, // Active speaker state updates
|
||||
VOICE_PCM: DISCORD_VOICE_PCM, // Live PCM audio data stream
|
||||
ANALYSIS_QUEUE_STATUS: DISCORD_ANALYSIS_QUEUE_STATUS,
|
||||
REACTION_ADDED: DISCORD_REACTION_ADDED,
|
||||
REACTION_REMOVED: DISCORD_REACTION_REMOVED,
|
||||
THREAD_CREATED: DISCORD_THREAD_CREATED,
|
||||
THREAD_DELETED: DISCORD_THREAD_DELETED,
|
||||
THREAD_UPDATED: DISCORD_THREAD_UPDATED,
|
||||
CHANNEL_TOPIC_UPDATED: DISCORD_CHANNEL_TOPIC_UPDATED,
|
||||
PRESENCE_UPDATED: DISCORD_PRESENCE_UPDATED,
|
||||
GUILD_MEMBER_ADDED: DISCORD_GUILD_MEMBER_ADDED,
|
||||
GUILD_MEMBER_REMOVED: DISCORD_GUILD_MEMBER_REMOVED,
|
||||
VOICE_ANALYZED: DISCORD_VOICE_ANALYZED,
|
||||
} as const;
|
||||
|
||||
export type EventChannelType =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import http from "node:http";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { AppConfig as GatewayConfig } from "../../shared/config/config.js";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
|
||||
const logger = createChildLogger("gateway-metrics");
|
||||
@@ -73,7 +74,9 @@ function formatMetrics(): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const [fullName, metric] of metrics) {
|
||||
const baseName = fullName.includes("{") ? fullName.slice(0, fullName.indexOf("{")) : fullName;
|
||||
const baseName = fullName.includes("{")
|
||||
? fullName.slice(0, fullName.indexOf("{"))
|
||||
: fullName;
|
||||
lines.push(`# HELP ${baseName} ${metric.help}`);
|
||||
lines.push(`# TYPE ${baseName} ${metric.type}`);
|
||||
lines.push(`${fullName} ${metric.value}`);
|
||||
@@ -85,7 +88,7 @@ function formatMetrics(): string {
|
||||
export function startMetricsServer(): void {
|
||||
if (server) return;
|
||||
|
||||
const port = config.METRICS_PORT;
|
||||
const port = (config as any).METRICS_PORT ?? 9090;
|
||||
logger.info({ port }, "Starting metrics HTTP server");
|
||||
|
||||
server = http.createServer((req, res) => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { Client, GuildMember } from "discord.js-selfbot-v13";
|
||||
import type {
|
||||
Client,
|
||||
GuildMember,
|
||||
PartialGuildMember,
|
||||
} from "discord.js-selfbot-v13";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
|
||||
|
||||
@@ -7,8 +11,11 @@ const logger = createChildLogger("guild-member-events");
|
||||
|
||||
function isMonitoredGuild(guildId: string | null | undefined): boolean {
|
||||
if (!guildId) return false;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as string[] | undefined;
|
||||
if (!guildIds || guildIds.length === 0) return config.MONITOR_GUILD_ID === guildId;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as
|
||||
| string[]
|
||||
| undefined;
|
||||
if (!guildIds || guildIds.length === 0)
|
||||
return config.MONITOR_GUILD_ID === guildId;
|
||||
return guildIds.includes(guildId);
|
||||
}
|
||||
|
||||
@@ -38,22 +45,25 @@ export function registerGuildMemberEvents(
|
||||
await eventBroadcaster.guildMemberAdded(data).catch(() => {});
|
||||
});
|
||||
|
||||
client.on("guildMemberRemove", async (member: GuildMember) => {
|
||||
if (!isMonitoredGuild(member.guild.id)) return;
|
||||
client.on(
|
||||
"guildMemberRemove",
|
||||
async (member: GuildMember | PartialGuildMember) => {
|
||||
if (!isMonitoredGuild(member.guild.id)) return;
|
||||
|
||||
const data = {
|
||||
user_id: member.id,
|
||||
username: member.user?.username ?? "unknown",
|
||||
tag: member.user?.tag ?? null,
|
||||
guild_id: member.guild.id,
|
||||
member_count: member.guild.memberCount,
|
||||
removed_at: Date.now(),
|
||||
};
|
||||
const data = {
|
||||
user_id: member.id,
|
||||
username: (member.user as any)?.username ?? "unknown",
|
||||
tag: (member.user as any)?.tag ?? null,
|
||||
guild_id: member.guild.id,
|
||||
member_count: member.guild.memberCount,
|
||||
removed_at: Date.now(),
|
||||
};
|
||||
|
||||
logger.info(
|
||||
{ userId: member.id, username: member.user?.username },
|
||||
"Guild member removed",
|
||||
);
|
||||
await eventBroadcaster.guildMemberRemoved(data).catch(() => {});
|
||||
});
|
||||
logger.info(
|
||||
{ userId: member.id, username: member.user?.username },
|
||||
"Guild member removed",
|
||||
);
|
||||
await eventBroadcaster.guildMemberRemoved(data).catch(() => {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,10 @@ function getTextCaptureTargets(): TextCaptureTarget[] {
|
||||
const { EFFECTIVE_MONITOR_GUILD_IDS, TEXT_CHANNEL_ID } = config as any;
|
||||
if (EFFECTIVE_MONITOR_GUILD_IDS?.length) {
|
||||
if (TEXT_CHANNEL_ID) {
|
||||
return EFFECTIVE_MONITOR_GUILD_IDS.map((guildId: string) => ({ guildId, channelId: TEXT_CHANNEL_ID }));
|
||||
return EFFECTIVE_MONITOR_GUILD_IDS.map((guildId: string) => ({
|
||||
guildId,
|
||||
channelId: TEXT_CHANNEL_ID,
|
||||
}));
|
||||
}
|
||||
return EFFECTIVE_MONITOR_GUILD_IDS.map((guildId: string) => ({ guildId }));
|
||||
}
|
||||
@@ -99,7 +102,9 @@ function shouldCaptureForAnyTarget(
|
||||
targets: TextCaptureTarget[],
|
||||
): boolean {
|
||||
if (targets.length === 0) return false;
|
||||
return targets.some((target) => shouldCaptureMessageLocation(message, target));
|
||||
return targets.some((target) =>
|
||||
shouldCaptureMessageLocation(message, target),
|
||||
);
|
||||
}
|
||||
|
||||
function requireMessageGuildId(message: Message): string {
|
||||
@@ -292,8 +297,7 @@ export function registerMessageCapture(client: Client): void {
|
||||
});
|
||||
|
||||
client.on("messageUpdate", async (_oldMessage, newMessage) => {
|
||||
if (!shouldCaptureForAnyTarget(newMessage, targets))
|
||||
return;
|
||||
if (!shouldCaptureForAnyTarget(newMessage, targets)) return;
|
||||
if (newMessage.author?.bot) return;
|
||||
if (isAgeRestrictedMessage(newMessage as Message)) return;
|
||||
|
||||
@@ -321,9 +325,14 @@ export function registerMessageCapture(client: Client): void {
|
||||
|
||||
// Save edit history snapshot before overwriting
|
||||
if (oldContent) {
|
||||
insertMessageEdit(newMessage.id, oldContent, editedAt).catch((err: unknown) => {
|
||||
logger.error({ messageId: newMessage.id, error: err }, "Failed to save edit history");
|
||||
});
|
||||
insertMessageEdit(newMessage.id, oldContent, editedAt).catch(
|
||||
(err: unknown) => {
|
||||
logger.error(
|
||||
{ messageId: newMessage.id, error: err },
|
||||
"Failed to save edit history",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await updateMessageAsEdited(
|
||||
|
||||
@@ -53,7 +53,11 @@ export class MessageStore {
|
||||
|
||||
// ── Edit History ────────────────────────────────────────────────────────
|
||||
|
||||
insertMessageEdit(messageId: string, oldContent: string, editedAt: number): Promise<void> {
|
||||
insertMessageEdit(
|
||||
messageId: string,
|
||||
oldContent: string,
|
||||
editedAt: number,
|
||||
): Promise<void> {
|
||||
return this.messages.insertMessageEdit(messageId, oldContent, editedAt);
|
||||
}
|
||||
|
||||
@@ -313,7 +317,8 @@ export const insertMessageEdit = (
|
||||
messageId: string,
|
||||
oldContent: string,
|
||||
editedAt: number,
|
||||
): Promise<void> => getInstance().insertMessageEdit(messageId, oldContent, editedAt);
|
||||
): Promise<void> =>
|
||||
getInstance().insertMessageEdit(messageId, oldContent, editedAt);
|
||||
|
||||
// Messages
|
||||
export const insertMessage = (message: MessageRecord): Promise<void> =>
|
||||
|
||||
@@ -2,7 +2,10 @@ import { createChildLogger, type Logger } from "@bete/shared/logger";
|
||||
import { and, desc, eq, or, type SQL } from "drizzle-orm";
|
||||
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||
import type * as schema from "../../shared/database/schema.js";
|
||||
import { messagesTable, messageEditsTable } from "../../shared/database/schema.js";
|
||||
import {
|
||||
messageEditsTable,
|
||||
messagesTable,
|
||||
} from "../../shared/database/schema.js";
|
||||
import type { MessageRecord } from "../message-capture/types.js";
|
||||
|
||||
// ─── Shared Helpers ──────────────────────────────────────────────────────────
|
||||
@@ -152,11 +155,18 @@ export class MessagesCrud {
|
||||
try {
|
||||
await this.db
|
||||
.insert(messageEditsTable)
|
||||
.values({ message_id: messageId, old_content: oldContent, edited_at: editedAt })
|
||||
.values({
|
||||
message_id: messageId,
|
||||
old_content: oldContent,
|
||||
edited_at: editedAt,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
{ messageId, error: error instanceof Error ? error.message : String(error) },
|
||||
{
|
||||
messageId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
"Failed to insert message edit",
|
||||
);
|
||||
throw error;
|
||||
|
||||
@@ -128,7 +128,11 @@ export class MessagesDb {
|
||||
|
||||
// ── Edit History ─────────────────────────────────────────────────────────
|
||||
|
||||
insertMessageEdit(messageId: string, oldContent: string, editedAt: number): Promise<void> {
|
||||
insertMessageEdit(
|
||||
messageId: string,
|
||||
oldContent: string,
|
||||
editedAt: number,
|
||||
): Promise<void> {
|
||||
return this.crud.insertMessageEdit(messageId, oldContent, editedAt);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { Client, MessageReaction, User } from "discord.js-selfbot-v13";
|
||||
import type {
|
||||
Client,
|
||||
MessageReaction,
|
||||
PartialMessageReaction,
|
||||
PartialUser,
|
||||
User,
|
||||
} from "discord.js-selfbot-v13";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { getDatabase } from "../../shared/database/drizzle.js";
|
||||
import { reactionsTable } from "../../shared/database/schema.js";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
|
||||
|
||||
const logger = createChildLogger("reaction-tracking");
|
||||
@@ -11,12 +17,17 @@ const logger = createChildLogger("reaction-tracking");
|
||||
|
||||
function isMonitoredGuild(guildId: string | null | undefined): boolean {
|
||||
if (!guildId) return false;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as string[] | undefined;
|
||||
if (!guildIds || guildIds.length === 0) return config.MONITOR_GUILD_ID === guildId;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as
|
||||
| string[]
|
||||
| undefined;
|
||||
if (!guildIds || guildIds.length === 0)
|
||||
return config.MONITOR_GUILD_ID === guildId;
|
||||
return guildIds.includes(guildId);
|
||||
}
|
||||
|
||||
function getEmojiIdentifier(reaction: MessageReaction): {
|
||||
function getEmojiIdentifier(
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
): {
|
||||
emoji: string;
|
||||
emojiId: string | null;
|
||||
animated: boolean;
|
||||
@@ -39,8 +50,8 @@ function getEmojiIdentifier(reaction: MessageReaction): {
|
||||
// ─── Event Handlers ──────────────────────────────────────────────────────
|
||||
|
||||
async function handleReactionAdd(
|
||||
reaction: MessageReaction,
|
||||
user: User,
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
user: User | PartialUser,
|
||||
): Promise<void> {
|
||||
const guildId = reaction.message.guildId;
|
||||
if (!isMonitoredGuild(guildId)) return;
|
||||
@@ -52,19 +63,22 @@ async function handleReactionAdd(
|
||||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
await (db as any).insert(reactionsTable).values({
|
||||
id,
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
reaction_type: "add",
|
||||
created_at: now,
|
||||
}).onConflictDoNothing();
|
||||
await (db as any)
|
||||
.insert(reactionsTable)
|
||||
.values({
|
||||
id,
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
reaction_type: "add",
|
||||
created_at: now,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
|
||||
logger.debug(
|
||||
{ messageId: reaction.message.id, emoji, userId: user.id },
|
||||
@@ -79,8 +93,8 @@ async function handleReactionAdd(
|
||||
}
|
||||
|
||||
async function handleReactionRemove(
|
||||
reaction: MessageReaction,
|
||||
user: User,
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
user: User | PartialUser,
|
||||
): Promise<void> {
|
||||
const guildId = reaction.message.guildId;
|
||||
if (!isMonitoredGuild(guildId)) return;
|
||||
@@ -92,19 +106,22 @@ async function handleReactionRemove(
|
||||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
await (db as any).insert(reactionsTable).values({
|
||||
id,
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
reaction_type: "remove",
|
||||
created_at: now,
|
||||
}).onConflictDoNothing();
|
||||
await (db as any)
|
||||
.insert(reactionsTable)
|
||||
.values({
|
||||
id,
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
reaction_type: "remove",
|
||||
created_at: now,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
|
||||
logger.debug(
|
||||
{ messageId: reaction.message.id, emoji, userId: user.id },
|
||||
@@ -126,45 +143,61 @@ export function registerReactionCapture(
|
||||
): void {
|
||||
logger.info("Registering reaction capture");
|
||||
|
||||
client.on("messageReactionAdd", async (reaction, user) => {
|
||||
await handleReactionAdd(reaction, user);
|
||||
client.on(
|
||||
"messageReactionAdd",
|
||||
async (
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
user: User | PartialUser,
|
||||
) => {
|
||||
await handleReactionAdd(reaction, user);
|
||||
|
||||
const guildId = reaction.message.guildId;
|
||||
if (!isMonitoredGuild(guildId)) return;
|
||||
const guildId = reaction.message.guildId;
|
||||
if (!isMonitoredGuild(guildId)) return;
|
||||
|
||||
const { emoji, emojiId, animated } = getEmojiIdentifier(reaction);
|
||||
const { emoji, emojiId, animated } = getEmojiIdentifier(reaction);
|
||||
|
||||
eventBroadcaster.reactionAdded({
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
created_at: Date.now(),
|
||||
}).catch(() => {});
|
||||
});
|
||||
eventBroadcaster
|
||||
.reactionAdded({
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
created_at: Date.now(),
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
);
|
||||
|
||||
client.on("messageReactionRemove", async (reaction, user) => {
|
||||
await handleReactionRemove(reaction, user);
|
||||
client.on(
|
||||
"messageReactionRemove",
|
||||
async (
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
user: User | PartialUser,
|
||||
) => {
|
||||
await handleReactionRemove(reaction, user);
|
||||
|
||||
const guildId = reaction.message.guildId;
|
||||
if (!isMonitoredGuild(guildId)) return;
|
||||
const guildId = reaction.message.guildId;
|
||||
if (!isMonitoredGuild(guildId)) return;
|
||||
|
||||
const { emoji, emojiId, animated } = getEmojiIdentifier(reaction);
|
||||
const { emoji, emojiId, animated } = getEmojiIdentifier(reaction);
|
||||
|
||||
eventBroadcaster.reactionRemoved({
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
created_at: Date.now(),
|
||||
}).catch(() => {});
|
||||
});
|
||||
eventBroadcaster
|
||||
.reactionRemoved({
|
||||
message_id: reaction.message.id,
|
||||
channel_id: reaction.message.channelId,
|
||||
guild_id: guildId,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
emoji,
|
||||
emoji_id: emojiId,
|
||||
animated,
|
||||
created_at: Date.now(),
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ const logger = createChildLogger("thread-tracking");
|
||||
|
||||
function isMonitoredGuild(guildId: string | null | undefined): boolean {
|
||||
if (!guildId) return false;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as string[] | undefined;
|
||||
if (!guildIds || guildIds.length === 0) return config.MONITOR_GUILD_ID === guildId;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as
|
||||
| string[]
|
||||
| undefined;
|
||||
if (!guildIds || guildIds.length === 0)
|
||||
return config.MONITOR_GUILD_ID === guildId;
|
||||
return guildIds.includes(guildId);
|
||||
}
|
||||
|
||||
@@ -51,20 +54,23 @@ export function registerThreadCapture(
|
||||
await eventBroadcaster.threadDeleted(data).catch(() => {});
|
||||
});
|
||||
|
||||
client.on("threadUpdate", async (_oldThread: ThreadChannel, newThread: ThreadChannel) => {
|
||||
if (!isMonitoredGuild(newThread.guildId)) return;
|
||||
client.on(
|
||||
"threadUpdate",
|
||||
async (_oldThread: ThreadChannel, newThread: ThreadChannel) => {
|
||||
if (!isMonitoredGuild(newThread.guildId)) return;
|
||||
|
||||
const data = {
|
||||
id: newThread.id,
|
||||
guild_id: newThread.guildId,
|
||||
channel_id: newThread.parentId ?? newThread.guildId,
|
||||
name: newThread.name,
|
||||
archived: (newThread as any).archived ?? false,
|
||||
rate_limit_per_user: (newThread as any).rateLimitPerUser ?? null,
|
||||
updated_at: Date.now(),
|
||||
};
|
||||
const data = {
|
||||
id: newThread.id,
|
||||
guild_id: newThread.guildId,
|
||||
channel_id: newThread.parentId ?? newThread.guildId,
|
||||
name: newThread.name,
|
||||
archived: (newThread as any).archived ?? false,
|
||||
rate_limit_per_user: (newThread as any).rateLimitPerUser ?? null,
|
||||
updated_at: Date.now(),
|
||||
};
|
||||
|
||||
logger.debug({ threadId: newThread.id }, "Thread updated");
|
||||
await eventBroadcaster.threadUpdated(data).catch(() => {});
|
||||
});
|
||||
logger.debug({ threadId: newThread.id }, "Thread updated");
|
||||
await eventBroadcaster.threadUpdated(data).catch(() => {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,19 +11,25 @@ const PRESENCE_COOLDOWN_MS = 30_000;
|
||||
|
||||
function isMonitoredGuild(guildId: string | null | undefined): boolean {
|
||||
if (!guildId) return false;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as string[] | undefined;
|
||||
if (!guildIds || guildIds.length === 0) return config.MONITOR_GUILD_ID === guildId;
|
||||
const guildIds = (config as any).EFFECTIVE_MONITOR_GUILD_IDS as
|
||||
| string[]
|
||||
| undefined;
|
||||
if (!guildIds || guildIds.length === 0)
|
||||
return config.MONITOR_GUILD_ID === guildId;
|
||||
return guildIds.includes(guildId);
|
||||
}
|
||||
|
||||
function getStatus(presence: Presence): string {
|
||||
if (!presence) return "offline";
|
||||
const status = presence.status;
|
||||
if (status === "online" || status === "idle" || status === "dnd") return status;
|
||||
if (status === "online" || status === "idle" || status === "dnd")
|
||||
return status;
|
||||
return "offline";
|
||||
}
|
||||
|
||||
function getActivities(presence: Presence): Array<{ name: string; type: string }> {
|
||||
function getActivities(
|
||||
presence: Presence,
|
||||
): Array<{ name: string; type: string }> {
|
||||
if (!presence?.activities) return [];
|
||||
return presence.activities.map((a) => ({
|
||||
name: a.name ?? "unknown",
|
||||
@@ -47,38 +53,42 @@ export function registerPresenceCapture(
|
||||
): void {
|
||||
logger.info("Registering presence capture");
|
||||
|
||||
client.on("presenceUpdate", async (_oldPresence: Presence | null, newPresence: Presence) => {
|
||||
if (!newPresence?.guildId) return;
|
||||
if (!isMonitoredGuild(newPresence.guildId)) return;
|
||||
client.on(
|
||||
"presenceUpdate",
|
||||
async (_oldPresence: Presence | null, newPresence: Presence) => {
|
||||
const guildId = newPresence.guild?.id ?? null;
|
||||
if (!guildId) return;
|
||||
if (!isMonitoredGuild(guildId)) return;
|
||||
|
||||
const userId = newPresence.userId ?? newPresence.user?.id;
|
||||
if (!userId) return;
|
||||
const userId = newPresence.userId ?? newPresence.user?.id;
|
||||
if (!userId) return;
|
||||
|
||||
// Cooldown check
|
||||
const now = Date.now();
|
||||
const lastUpdate = presenceCooldowns.get(userId);
|
||||
if (lastUpdate && now - lastUpdate < PRESENCE_COOLDOWN_MS) return;
|
||||
presenceCooldowns.set(userId, now);
|
||||
// Cooldown check
|
||||
const now = Date.now();
|
||||
const lastUpdate = presenceCooldowns.get(userId);
|
||||
if (lastUpdate && now - lastUpdate < PRESENCE_COOLDOWN_MS) return;
|
||||
presenceCooldowns.set(userId, now);
|
||||
|
||||
const data = {
|
||||
user_id: userId,
|
||||
username: newPresence.user?.username ?? "unknown",
|
||||
status: getStatus(newPresence),
|
||||
activities: getActivities(newPresence),
|
||||
client_status: getClientStatus(newPresence),
|
||||
guild_id: newPresence.guildId,
|
||||
last_changed: now,
|
||||
};
|
||||
const data = {
|
||||
user_id: userId,
|
||||
username: newPresence.user?.username ?? "unknown",
|
||||
status: getStatus(newPresence),
|
||||
activities: getActivities(newPresence),
|
||||
client_status: getClientStatus(newPresence),
|
||||
guild_id: guildId,
|
||||
last_changed: now,
|
||||
};
|
||||
|
||||
logger.debug({ userId, status: data.status }, "Presence updated");
|
||||
await eventBroadcaster.presenceUpdated(data).catch(() => {});
|
||||
logger.debug({ userId, status: data.status }, "Presence updated");
|
||||
await eventBroadcaster.presenceUpdated(data).catch(() => {});
|
||||
|
||||
// Periodic cleanup of stale cooldown entries
|
||||
if (presenceCooldowns.size > 1000) {
|
||||
const threshold = now - PRESENCE_COOLDOWN_MS * 10;
|
||||
for (const [uid, ts] of presenceCooldowns) {
|
||||
if (ts < threshold) presenceCooldowns.delete(uid);
|
||||
// Periodic cleanup of stale cooldown entries
|
||||
if (presenceCooldowns.size > 1000) {
|
||||
const threshold = now - PRESENCE_COOLDOWN_MS * 10;
|
||||
for (const [uid, ts] of presenceCooldowns) {
|
||||
if (ts < threshold) presenceCooldowns.delete(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -213,8 +213,9 @@ export function resolveMediaUrl(
|
||||
}
|
||||
});
|
||||
|
||||
// -- stderr (capture for diagnostics) ----------------------------------
|
||||
// -- stderr (capture for diagnostics, capped at 4KB) ----------------------------------
|
||||
|
||||
const MAX_STDERR = 4096;
|
||||
if (proc.stderr) {
|
||||
proc.stderr.on("data", (chunk: Buffer) => {
|
||||
stderrBuf += chunk.toString("utf8");
|
||||
@@ -295,6 +296,7 @@ export async function extractMediaInfo(url: string): Promise<MediaInfo> {
|
||||
|
||||
let stdoutBuf = "";
|
||||
let stderrBuf = "";
|
||||
const MAX_STDERR = 4096;
|
||||
|
||||
if (proc.stdout) {
|
||||
proc.stdout.on("data", (chunk: Buffer) => {
|
||||
@@ -304,7 +306,9 @@ export async function extractMediaInfo(url: string): Promise<MediaInfo> {
|
||||
|
||||
if (proc.stderr) {
|
||||
proc.stderr.on("data", (chunk: Buffer) => {
|
||||
stderrBuf += chunk.toString("utf8");
|
||||
if (stderrBuf.length < MAX_STDERR) {
|
||||
stderrBuf += chunk.toString("utf8").slice(0, MAX_STDERR - stderrBuf.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||
import { getDatabase } from "../../shared/database/drizzle.js";
|
||||
import { muxerJobsTable } from "../../shared/database/schema.js";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { getDatabase } from "../../shared/database/drizzle.js";
|
||||
import type * as schema from "../../shared/database/schema.js";
|
||||
import { muxerJobsTable } from "../../shared/database/schema.js";
|
||||
import { buildMuxFfmpegArgs, runFfmpeg } from "./ffmpegProcess.js";
|
||||
|
||||
const logger = createChildLogger("muxer");
|
||||
@@ -59,10 +60,7 @@ export function startMuxerWorker(): void {
|
||||
|
||||
pollTimer = setInterval(() => {
|
||||
processNextJobs().catch((err: unknown) => {
|
||||
logger.error(
|
||||
{ error: String(err) },
|
||||
"Muxer worker tick failed",
|
||||
);
|
||||
logger.error({ error: String(err) }, "Muxer worker tick failed");
|
||||
});
|
||||
}, 10_000);
|
||||
}
|
||||
@@ -126,7 +124,9 @@ async function processJob(
|
||||
const data = JSON.parse(job.data) as MuxerJobData;
|
||||
|
||||
if (!data.inputs || data.inputs.length < 2) {
|
||||
throw new Error(`Muxer job ${job.id} needs at least 2 inputs, got ${data.inputs?.length ?? 0}`);
|
||||
throw new Error(
|
||||
`Muxer job ${job.id} needs at least 2 inputs, got ${data.inputs?.length ?? 0}`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -156,10 +156,7 @@ async function processJob(
|
||||
.set({ status: "completed", updatedAt: Date.now() })
|
||||
.where(eq(muxerJobsTable.id, job.id));
|
||||
|
||||
logger.info(
|
||||
{ jobId: job.id, output: data.output },
|
||||
"Muxer job completed",
|
||||
);
|
||||
logger.info({ jobId: job.id, output: data.output }, "Muxer job completed");
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
const newAttempts = job.attempts + 1;
|
||||
@@ -174,7 +171,10 @@ async function processJob(
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(muxerJobsTable.id, job.id));
|
||||
logger.error({ jobId: job.id, error: errMsg }, "Muxer job failed permanently");
|
||||
logger.error(
|
||||
{ jobId: job.id, error: errMsg },
|
||||
"Muxer job failed permanently",
|
||||
);
|
||||
} else {
|
||||
await db
|
||||
.update(muxerJobsTable)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { triggerWebhook } from "./webhookNotifier.js";
|
||||
export type { WebhookPayload } from "./webhookNotifier.js";
|
||||
export { triggerWebhook } from "./webhookNotifier.js";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { AppConfig as GatewayConfig } from "../../shared/config/config.js";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
|
||||
const logger = createChildLogger("webhook-notifier");
|
||||
@@ -29,11 +30,16 @@ export async function triggerWebhook(
|
||||
eventType: string,
|
||||
payload: WebhookPayload,
|
||||
): Promise<void> {
|
||||
const urls = config.WEBHOOK_URLS;
|
||||
const urls = (config as any).WEBHOOK_URLS as string[] | undefined;
|
||||
if (!urls || urls.length === 0) return;
|
||||
|
||||
const enabledEvents = config.WEBHOOK_EVENTS;
|
||||
if (enabledEvents.length > 0 && !enabledEvents.includes(eventType)) return;
|
||||
const enabledEvents = (config as any).WEBHOOK_EVENTS as string[] | undefined;
|
||||
if (
|
||||
enabledEvents &&
|
||||
enabledEvents.length > 0 &&
|
||||
!enabledEvents.includes(eventType)
|
||||
)
|
||||
return;
|
||||
|
||||
const body = JSON.stringify({
|
||||
...payload,
|
||||
@@ -59,7 +65,11 @@ export async function triggerWebhook(
|
||||
|
||||
// ─── Internal ────────────────────────────────────────────────────────────
|
||||
|
||||
async function sendWebhook(url: string, body: string): Promise<void> {
|
||||
async function sendWebhook(
|
||||
url: string | undefined,
|
||||
body: string,
|
||||
): Promise<void> {
|
||||
if (!url) return;
|
||||
let lastErr: Error | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { loadConfig as sharedLoadConfig } from "@bete/shared/config";
|
||||
export type AppConfig = SharedAppConfig & {
|
||||
EFFECTIVE_TEXT_GUILD_ID?: string;
|
||||
EFFECTIVE_VOICE_GUILD_ID?: string;
|
||||
EFFECTIVE_MONITOR_GUILD_IDS: string[];
|
||||
};
|
||||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
@@ -14,6 +15,9 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
...parsed,
|
||||
EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID,
|
||||
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID,
|
||||
EFFECTIVE_MONITOR_GUILD_IDS:
|
||||
(parsed as any).EFFECTIVE_MONITOR_GUILD_IDS ??
|
||||
(parsed.MONITOR_GUILD_ID ? [parsed.MONITOR_GUILD_ID] : []),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -390,6 +390,47 @@ export const pgUserProfilesTable = pgTable(
|
||||
* Mascot Chat Messages Table (PostgreSQL)
|
||||
* Stores AI mascot chat conversation history
|
||||
*/
|
||||
export const pgReactionsTable = pgTable(
|
||||
"message_reactions",
|
||||
{
|
||||
id: pgText("id").primaryKey(),
|
||||
message_id: pgText("message_id").notNull(),
|
||||
channel_id: pgText("channel_id").notNull(),
|
||||
guild_id: pgText("guild_id").notNull(),
|
||||
user_id: pgText("user_id").notNull(),
|
||||
username: pgText("username").notNull(),
|
||||
emoji: pgText("emoji").notNull(),
|
||||
emoji_id: pgText("emoji_id"),
|
||||
animated: pgBoolean("animated").notNull().default(false),
|
||||
reaction_type: pgText("reaction_type", {
|
||||
enum: ["add", "remove"],
|
||||
}).notNull(),
|
||||
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
messageIdIdx: pgIndex("idx_reactions_message_id").on(table.message_id),
|
||||
userIdIdx: pgIndex("idx_reactions_user_id").on(table.user_id),
|
||||
guildCreatedIdx: pgIndex("idx_reactions_guild_created").on(
|
||||
table.guild_id,
|
||||
table.created_at,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const pgMessageEditsTable = pgTable(
|
||||
"message_edits",
|
||||
{
|
||||
id: pgUuid("id").defaultRandom().primaryKey(),
|
||||
message_id: pgText("message_id").notNull(),
|
||||
old_content: pgText("old_content").notNull(),
|
||||
edited_at: pgBigint("edited_at", { mode: "number" }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
messageIdIdx: pgIndex("idx_message_edits_message_id").on(table.message_id),
|
||||
editedAtIdx: pgIndex("idx_message_edits_edited_at").on(table.edited_at),
|
||||
}),
|
||||
);
|
||||
|
||||
export const pgMascotChatMessagesTable = pgTable(
|
||||
"mascot_chat_messages",
|
||||
{
|
||||
@@ -428,6 +469,8 @@ export const correctedModerationsTable = pgCorrectedModerationsTable;
|
||||
export const userReputationsTable = pgUserReputationsTable;
|
||||
export const channelCulturesTable = pgChannelCulturesTable;
|
||||
export const userProfilesTable = pgUserProfilesTable;
|
||||
export const reactionsTable = pgReactionsTable;
|
||||
export const messageEditsTable = pgMessageEditsTable;
|
||||
export const mascotChatMessagesTable = pgMascotChatMessagesTable;
|
||||
|
||||
// Export table types for use in queries
|
||||
|
||||
Reference in New Issue
Block a user