chore(auto): task completed - unknown
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
export { registerReactionCapture } from "./reactionCapture.js";
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
|
import type { Client, MessageReaction, User } from "discord.js-selfbot-v13";
|
||||||
|
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");
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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;
|
||||||
|
return guildIds.includes(guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmojiIdentifier(reaction: MessageReaction): {
|
||||||
|
emoji: string;
|
||||||
|
emojiId: string | null;
|
||||||
|
animated: boolean;
|
||||||
|
} {
|
||||||
|
const emoji = reaction.emoji;
|
||||||
|
if (emoji.id) {
|
||||||
|
return {
|
||||||
|
emoji: emoji.name ?? emoji.id,
|
||||||
|
emojiId: emoji.id,
|
||||||
|
animated: Boolean((emoji as any).animated),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
emoji: emoji.name ?? "unknown",
|
||||||
|
emojiId: null,
|
||||||
|
animated: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Event Handlers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function handleReactionAdd(
|
||||||
|
reaction: MessageReaction,
|
||||||
|
user: User,
|
||||||
|
): Promise<void> {
|
||||||
|
const guildId = reaction.message.guildId;
|
||||||
|
if (!isMonitoredGuild(guildId)) return;
|
||||||
|
if (user.bot) return;
|
||||||
|
|
||||||
|
const { emoji, emojiId, animated } = getEmojiIdentifier(reaction);
|
||||||
|
const now = Date.now();
|
||||||
|
const id = `${reaction.message.id}-${emojiId ?? emoji}-${user.id}`;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
{ messageId: reaction.message.id, emoji, userId: user.id },
|
||||||
|
"Reaction recorded",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ messageId: reaction.message.id, error: String(error) },
|
||||||
|
"Failed to record reaction",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReactionRemove(
|
||||||
|
reaction: MessageReaction,
|
||||||
|
user: User,
|
||||||
|
): Promise<void> {
|
||||||
|
const guildId = reaction.message.guildId;
|
||||||
|
if (!isMonitoredGuild(guildId)) return;
|
||||||
|
if (user.bot) return;
|
||||||
|
|
||||||
|
const { emoji, emojiId, animated } = getEmojiIdentifier(reaction);
|
||||||
|
const now = Date.now();
|
||||||
|
const id = `${reaction.message.id}-${emojiId ?? emoji}-${user.id}`;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
{ messageId: reaction.message.id, emoji, userId: user.id },
|
||||||
|
"Reaction removal recorded",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ messageId: reaction.message.id, error: String(error) },
|
||||||
|
"Failed to record reaction removal",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Registration ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function registerReactionCapture(
|
||||||
|
client: Client,
|
||||||
|
eventBroadcaster: EventBroadcaster,
|
||||||
|
): void {
|
||||||
|
logger.info("Registering reaction capture");
|
||||||
|
|
||||||
|
client.on("messageReactionAdd", async (reaction, user) => {
|
||||||
|
await handleReactionAdd(reaction, user);
|
||||||
|
|
||||||
|
const guildId = reaction.message.guildId;
|
||||||
|
if (!isMonitoredGuild(guildId)) return;
|
||||||
|
|
||||||
|
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(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("messageReactionRemove", async (reaction, user) => {
|
||||||
|
await handleReactionRemove(reaction, user);
|
||||||
|
|
||||||
|
const guildId = reaction.message.guildId;
|
||||||
|
if (!isMonitoredGuild(guildId)) return;
|
||||||
|
|
||||||
|
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(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
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 { buildMuxFfmpegArgs, runFfmpeg } from "./ffmpegProcess.js";
|
||||||
|
|
||||||
|
const logger = createChildLogger("muxer");
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface MuxerJobData {
|
||||||
|
inputs: string[];
|
||||||
|
output: string;
|
||||||
|
guildId: string;
|
||||||
|
channelId: string;
|
||||||
|
sessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── State ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
// ─── Public API ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a muxer job that merges multiple OGG files into one.
|
||||||
|
*/
|
||||||
|
export async function enqueueMuxerJob(data: MuxerJobData): Promise<void> {
|
||||||
|
const db = getDatabase() as unknown as NodePgDatabase<typeof schema>;
|
||||||
|
const id = `${data.sessionId}-${Date.now()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.insert(muxerJobsTable).values({
|
||||||
|
id,
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
status: "pending",
|
||||||
|
attempts: 0,
|
||||||
|
maxAttempts: 3,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
logger.info({ id, sessionId: data.sessionId }, "Muxer job enqueued");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ id, error: error instanceof Error ? error.message : String(error) },
|
||||||
|
"Failed to enqueue muxer job",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the background worker that polls for pending muxer jobs.
|
||||||
|
*/
|
||||||
|
export function startMuxerWorker(): void {
|
||||||
|
if (pollTimer) return;
|
||||||
|
logger.info("Starting muxer worker (interval: 10s)");
|
||||||
|
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
processNextJobs().catch((err: unknown) => {
|
||||||
|
logger.error(
|
||||||
|
{ error: String(err) },
|
||||||
|
"Muxer worker tick failed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, 10_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the background worker.
|
||||||
|
*/
|
||||||
|
export function stopMuxerWorker(): void {
|
||||||
|
if (!pollTimer) return;
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
pollTimer = null;
|
||||||
|
logger.info("Muxer worker stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Internal ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function processNextJobs(): Promise<void> {
|
||||||
|
const db = getDatabase() as unknown as NodePgDatabase<typeof schema>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jobs = await db
|
||||||
|
.select()
|
||||||
|
.from(muxerJobsTable)
|
||||||
|
.where(eq(muxerJobsTable.status, "pending"))
|
||||||
|
.limit(5);
|
||||||
|
|
||||||
|
if (jobs.length === 0) return;
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
jobs.map((job) => processJob(db, job)),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
logger.error(
|
||||||
|
{ jobId: jobs[i].id, error: result.reason },
|
||||||
|
"Muxer job failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ error: error instanceof Error ? error.message : String(error) },
|
||||||
|
"Failed to fetch pending muxer jobs",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processJob(
|
||||||
|
db: NodePgDatabase<typeof schema>,
|
||||||
|
job: typeof muxerJobsTable.$inferSelect,
|
||||||
|
): Promise<void> {
|
||||||
|
// Mark as processing
|
||||||
|
await db
|
||||||
|
.update(muxerJobsTable)
|
||||||
|
.set({ status: "processing", updatedAt: Date.now() })
|
||||||
|
.where(eq(muxerJobsTable.id, job.id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{ jobId: job.id, inputs: data.inputs.length, output: data.output },
|
||||||
|
"Processing muxer job",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build filter: concat all inputs with a crossfade or simple concat
|
||||||
|
// We use amix for mixing multiple audio streams (not sequential concat)
|
||||||
|
const inputLabels = data.inputs.map((_, i) => `[${i}:a]`);
|
||||||
|
const filterComplex = `${inputLabels.join("")}amix=inputs=${data.inputs.length}:duration=first:dropout_transition=2[out]`;
|
||||||
|
|
||||||
|
const args = buildMuxFfmpegArgs({
|
||||||
|
inputs: data.inputs,
|
||||||
|
filter: filterComplex,
|
||||||
|
output: data.output,
|
||||||
|
codec: "libopus",
|
||||||
|
audioFrequency: 48000,
|
||||||
|
audioChannels: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runFfmpeg(args);
|
||||||
|
|
||||||
|
// Mark as completed
|
||||||
|
await db
|
||||||
|
.update(muxerJobsTable)
|
||||||
|
.set({ status: "completed", updatedAt: Date.now() })
|
||||||
|
.where(eq(muxerJobsTable.id, job.id));
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (newAttempts >= job.maxAttempts) {
|
||||||
|
await db
|
||||||
|
.update(muxerJobsTable)
|
||||||
|
.set({
|
||||||
|
status: "failed",
|
||||||
|
error: errMsg,
|
||||||
|
attempts: newAttempts,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
.where(eq(muxerJobsTable.id, job.id));
|
||||||
|
logger.error({ jobId: job.id, error: errMsg }, "Muxer job failed permanently");
|
||||||
|
} else {
|
||||||
|
await db
|
||||||
|
.update(muxerJobsTable)
|
||||||
|
.set({
|
||||||
|
status: "pending",
|
||||||
|
error: errMsg,
|
||||||
|
attempts: newAttempts,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
.where(eq(muxerJobsTable.id, job.id));
|
||||||
|
logger.warn(
|
||||||
|
{ jobId: job.id, error: errMsg, attempt: newAttempts },
|
||||||
|
"Muxer job will be retried",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user