feat: broadcast command

This commit is contained in:
pinks 2023-09-24 21:58:09 +02:00
parent 3895793965
commit 62247b81bc
7 changed files with 130 additions and 27 deletions

71
bot/broadcastCommand.ts Normal file
View File

@ -0,0 +1,71 @@
import { CommandContext } from "grammy";
import { bold, fmt, FormattedString } from "grammy_parse_mode";
import { distinctBy } from "std/collections";
import { getConfig } from "../app/config.ts";
import { generationStore } from "../app/generationStore.ts";
import { ErisContext, logger } from "./mod.ts";
import { formatUserChat } from "../utils/formatUserChat.ts";
export async function broadcastCommand(ctx: CommandContext<ErisContext>) {
if (!ctx.from?.username) {
return ctx.reply("I don't know who you are.");
}
const config = await getConfig();
if (!config.adminUsernames.includes(ctx.from.username)) {
return ctx.reply("Only a bot admin can use this command.");
}
const text = ctx.match.trim();
if (!text) {
return ctx.reply("Please specify a message to broadcast.");
}
// find users who interacted with bot in the last 24 hours
const gens = await generationStore.getAll(
{ after: new Date(Date.now() - 24 * 60 * 60 * 1000) },
{ reverse: true },
).then((gens) => distinctBy(gens, (gen) => gen.value.from.id));
let sentCount = 0;
const errors: FormattedString[] = [];
const getMessage = () =>
fmt([
fmt`Broadcasted to ${sentCount}/${gens.length} users.\n\n`,
errors.length > 0 ? fmt([bold("Errors:"), "\n", ...errors]) : "",
]);
const replyMessage = await ctx.replyFmt(getMessage(), {
reply_to_message_id: ctx.message?.message_id,
});
// send message to each user
for (const gen of gens) {
try {
await ctx.api.sendMessage(gen.value.from.id, text);
logger().info(`Broadcasted to ${formatUserChat({ from: gen.value.from })}`);
sentCount++;
} catch (err) {
logger().error(`Broadcasting to ${formatUserChat({ from: gen.value.from })} failed: ${err}`);
errors.push(fmt`${bold(formatUserChat({ from: gen.value.from }))} - ${err.message}\n`);
}
const fmtMessage = getMessage();
if (sentCount % 20 === 0) {
await ctx.api.editMessageText(
replyMessage.chat.id,
replyMessage.message_id,
fmtMessage.text,
{ entities: fmtMessage.entities },
).catch(() => undefined);
}
}
const fmtMessage = getMessage();
await ctx.api.editMessageText(
replyMessage.chat.id,
replyMessage.message_id,
fmtMessage.text,
{ entities: fmtMessage.entities },
).catch(() => undefined);
}

View File

@ -7,5 +7,7 @@ export async function cancelCommand(ctx: ErisContext) {
.filter((job) => job.lockUntil < new Date()) .filter((job) => job.lockUntil < new Date())
.filter((j) => j.state.from.id === ctx.from?.id); .filter((j) => j.state.from.id === ctx.from?.id);
for (const job of userJobs) await generationQueue.deleteJob(job.id); for (const job of userJobs) await generationQueue.deleteJob(job.id);
await ctx.reply(`Cancelled ${userJobs.length} jobs`); await ctx.reply(`Cancelled ${userJobs.length} jobs`, {
reply_to_message_id: ctx.message?.message_id,
});
} }

View File

@ -28,30 +28,34 @@ async function img2img(
state: QuestionState = {}, state: QuestionState = {},
): Promise<void> { ): Promise<void> {
if (!ctx.message?.from?.id) { if (!ctx.message?.from?.id) {
await ctx.reply("I don't know who you are"); await ctx.reply("I don't know who you are", {
reply_to_message_id: ctx.message?.message_id,
});
return; return;
} }
const config = await getConfig(); const config = await getConfig();
if (config.pausedReason != null) { if (config.pausedReason != null) {
await ctx.reply(`I'm paused: ${config.pausedReason || "No reason given"}`); await ctx.reply(`I'm paused: ${config.pausedReason || "No reason given"}`, {
reply_to_message_id: ctx.message?.message_id,
});
return; return;
} }
const jobs = await generationQueue.getAllJobs(); const jobs = await generationQueue.getAllJobs();
if (jobs.length >= config.maxJobs) { if (jobs.length >= config.maxJobs) {
await ctx.reply( await ctx.reply(`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, {
`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, reply_to_message_id: ctx.message?.message_id,
); });
return; return;
} }
const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id); const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id);
if (userJobs.length >= config.maxUserJobs) { if (userJobs.length >= config.maxUserJobs) {
await ctx.reply( await ctx.reply(`You already have ${config.maxUserJobs} jobs in queue. Try again later.`, {
`You already have ${config.maxUserJobs} jobs in queue. Try again later.`, reply_to_message_id: ctx.message?.message_id,
); });
return; return;
} }
@ -91,7 +95,11 @@ async function img2img(
await ctx.reply( await ctx.reply(
"Please show me a picture to repaint." + "Please show me a picture to repaint." +
img2imgQuestion.messageSuffixMarkdown(JSON.stringify(state satisfies QuestionState)), img2imgQuestion.messageSuffixMarkdown(JSON.stringify(state satisfies QuestionState)),
{ reply_markup: { force_reply: true, selective: true }, parse_mode: "Markdown" }, {
reply_markup: { force_reply: true, selective: true },
parse_mode: "Markdown",
reply_to_message_id: ctx.message?.message_id,
},
); );
return; return;
} }
@ -100,12 +108,18 @@ async function img2img(
await ctx.reply( await ctx.reply(
"Please describe the picture you want to repaint." + "Please describe the picture you want to repaint." +
img2imgQuestion.messageSuffixMarkdown(JSON.stringify(state satisfies QuestionState)), img2imgQuestion.messageSuffixMarkdown(JSON.stringify(state satisfies QuestionState)),
{ reply_markup: { force_reply: true, selective: true }, parse_mode: "Markdown" }, {
reply_markup: { force_reply: true, selective: true },
parse_mode: "Markdown",
reply_to_message_id: ctx.message?.message_id,
},
); );
return; return;
} }
const replyMessage = await ctx.reply("Accepted. You are now in queue."); const replyMessage = await ctx.reply("Accepted. You are now in queue.", {
reply_to_message_id: ctx.message?.message_id,
});
await generationQueue.pushJob({ await generationQueue.pushJob({
task: { type: "img2img", fileId: state.fileId, params: state.params }, task: { type: "img2img", fileId: state.fileId, params: state.params },

View File

@ -1,10 +1,10 @@
import { Api, Bot, Context, RawApi, session, SessionFlavor } from "grammy"; import { Api, Bot, Context, RawApi, session, SessionFlavor } from "grammy";
import { autoQuote } from "grammy_autoquote";
import { FileFlavor, hydrateFiles } from "grammy_files"; import { FileFlavor, hydrateFiles } from "grammy_files";
import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode"; import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode";
import { getLogger } from "std/log"; import { getLogger } from "std/log";
import { getConfig, setConfig } from "../app/config.ts"; import { getConfig, setConfig } from "../app/config.ts";
import { formatUserChat } from "../utils/formatUserChat.ts"; import { formatUserChat } from "../utils/formatUserChat.ts";
import { broadcastCommand } from "./broadcastCommand.ts";
import { cancelCommand } from "./cancelCommand.ts"; import { cancelCommand } from "./cancelCommand.ts";
import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts"; import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts";
import { pnginfoCommand, pnginfoQuestion } from "./pnginfoCommand.ts"; import { pnginfoCommand, pnginfoQuestion } from "./pnginfoCommand.ts";
@ -45,7 +45,6 @@ export const bot = new Bot<ErisContext, ErisApi>(
}, },
); );
bot.use(autoQuote);
bot.use(hydrateReply); bot.use(hydrateReply);
bot.use(session<SessionData, ErisContext>({ bot.use(session<SessionData, ErisContext>({
type: "multi", type: "multi",
@ -129,6 +128,8 @@ bot.command("queue", queueCommand);
bot.command("cancel", cancelCommand); bot.command("cancel", cancelCommand);
bot.command("broadcast", broadcastCommand);
bot.command("pause", async (ctx) => { bot.command("pause", async (ctx) => {
if (!ctx.from?.username) return; if (!ctx.from?.username) return;
const config = await getConfig(); const config = await getConfig();

View File

@ -23,7 +23,11 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
await ctx.reply( await ctx.reply(
"Please send me a PNG file." + "Please send me a PNG file." +
pnginfoQuestion.messageSuffixMarkdown(), pnginfoQuestion.messageSuffixMarkdown(),
{ reply_markup: { force_reply: true, selective: true }, parse_mode: "Markdown" }, {
reply_markup: { force_reply: true, selective: true },
parse_mode: "Markdown",
reply_to_message_id: ctx.message?.message_id,
},
); );
return; return;
} }
@ -43,7 +47,7 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
]); ]);
await ctx.reply(paramsText.text, { await ctx.reply(paramsText.text, {
reply_to_message_id: ctx.message?.message_id,
entities: paramsText.entities, entities: paramsText.entities,
reply_to_message_id: ctx.message?.message_id,
}); });
} }

View File

@ -7,7 +7,10 @@ import { ErisContext } from "./mod.ts";
export async function queueCommand(ctx: CommandContext<ErisContext>) { export async function queueCommand(ctx: CommandContext<ErisContext>) {
let formattedMessage = await getMessageText(); let formattedMessage = await getMessageText();
const queueMessage = await ctx.replyFmt(formattedMessage, { disable_notification: true }); const queueMessage = await ctx.replyFmt(formattedMessage, {
disable_notification: true,
reply_to_message_id: ctx.message?.message_id,
});
handleFutureUpdates().catch(() => undefined); handleFutureUpdates().catch(() => undefined);
async function getMessageText() { async function getMessageText() {

View File

@ -20,30 +20,32 @@ export async function txt2imgCommand(ctx: CommandContext<ErisContext>) {
async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolean): Promise<void> { async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolean): Promise<void> {
if (!ctx.message?.from?.id) { if (!ctx.message?.from?.id) {
await ctx.reply("I don't know who you are"); await ctx.reply("I don't know who you are", { reply_to_message_id: ctx.message?.message_id });
return; return;
} }
const config = await getConfig(); const config = await getConfig();
if (config.pausedReason != null) { if (config.pausedReason != null) {
await ctx.reply(`I'm paused: ${config.pausedReason || "No reason given"}`); await ctx.reply(`I'm paused: ${config.pausedReason || "No reason given"}`, {
reply_to_message_id: ctx.message?.message_id,
});
return; return;
} }
const jobs = await generationQueue.getAllJobs(); const jobs = await generationQueue.getAllJobs();
if (jobs.length >= config.maxJobs) { if (jobs.length >= config.maxJobs) {
await ctx.reply( await ctx.reply(`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, {
`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, reply_to_message_id: ctx.message?.message_id,
); });
return; return;
} }
const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id); const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id);
if (userJobs.length >= config.maxUserJobs) { if (userJobs.length >= config.maxUserJobs) {
await ctx.reply( await ctx.reply(`You already have ${config.maxUserJobs} jobs in queue. Try again later.`, {
`You already have ${config.maxUserJobs} jobs in queue. Try again later.`, reply_to_message_id: ctx.message?.message_id,
); });
return; return;
} }
@ -69,12 +71,18 @@ async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolea
await ctx.reply( await ctx.reply(
"Please tell me what you want to see." + "Please tell me what you want to see." +
txt2imgQuestion.messageSuffixMarkdown(), txt2imgQuestion.messageSuffixMarkdown(),
{ reply_markup: { force_reply: true, selective: true }, parse_mode: "Markdown" }, {
reply_markup: { force_reply: true, selective: true },
parse_mode: "Markdown",
reply_to_message_id: ctx.message?.message_id,
},
); );
return; return;
} }
const replyMessage = await ctx.reply("Accepted. You are now in queue."); const replyMessage = await ctx.reply("Accepted. You are now in queue.", {
reply_to_message_id: ctx.message?.message_id,
});
await generationQueue.pushJob({ await generationQueue.pushJob({
task: { type: "txt2img", params }, task: { type: "txt2img", params },