Tidy chat output and pick search results by reaction

A search reply carried ten markdown links, and Stoat expands every link
into a full-size player — the result was a wall of embeds burying the
list. Stoat's embed generator skips links inside code spans or angle
brackets, so links now go out quietly, and list lines carry no links at
all: title, artist, length, source. Only `!nowplaying` keeps a preview,
where it was asked for.

Choosing a track no longer needs a second command: the results message
gets 1️⃣5️⃣ reactions and picking one queues the track, with `!pick`
still there when reactions fail or five results are not enough. Only the
person who searched can pick, so a list cannot be hijacked.

Also `!playvideo` to turn video on and queue a track in one go, and
setVideo no longer requires a running player — the switch is a setting,
and demanding a player made it fail on a server nothing had played on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-09 05:41:20 +03:00
co-authored by Claude Opus 5
parent 7cca374e66
commit e7e80e704b
5 changed files with 612 additions and 459 deletions
+34 -4
View File
@@ -3,7 +3,7 @@ import { config } from "../config.js";
import { logger } from "../logger.js";
import type { MusicManager } from "../core/manager.js";
import { UserFacingError } from "../types.js";
import { findCommand, type CommandContext } from "./commands.js";
import { enqueueChoice, findCommand, trackForReaction, type CommandContext } from "./commands.js";
import { BotStoatContext } from "./context.js";
const log = logger.child({ mod: "bot" });
@@ -33,6 +33,13 @@ export async function startBot(manager: MusicManager): Promise<Bot> {
});
});
client.on("messageReactionAdd", (message, userId, emoji) => {
if (userId === client.user?.id) return;
void handleChoice(manager, message, userId, emoji).catch((err) => {
log.error({ err }, "unhandled reaction failure");
});
});
await client.loginBot(config.STOAT_BOT_TOKEN);
return {
@@ -44,15 +51,38 @@ export async function startBot(manager: MusicManager): Promise<Bot> {
};
}
/** Picking a search result by reacting to the bot's list of choices. */
async function handleChoice(
manager: MusicManager,
message: Message,
userId: string,
emoji: string,
): Promise<void> {
const serverId = message.server?.id;
if (!serverId) return;
const track = trackForReaction(message.id, userId, emoji);
if (!track) return;
const username = message.server?.getMember(userId)?.nickname ?? track.requestedBy.username;
try {
const text = await enqueueChoice(manager, serverId, message.channelId, { id: userId, username }, track);
await message.channel?.sendMessage(text);
} catch (err) {
if (err instanceof UserFacingError) {
await message.channel?.sendMessage(`⚠️ ${err.message}`);
return;
}
throw err;
}
}
async function handleMessage(manager: MusicManager, message: Message): Promise<void> {
const content = message.content?.trim();
if (!content || !content.startsWith(config.COMMAND_PREFIX)) return;
if (!message.authorId || message.author?.bot) return;
const serverId = message.server?.id;
const reply = async (text: string) => {
await message.channel?.sendMessage(text);
};
const reply = async (text: string) => message.channel?.sendMessage(text);
if (!serverId) {
await reply("Команды работают только внутри сервера.");