feat: broadcast command
This commit is contained in:
parent
3895793965
commit
62247b81bc
|
@ -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);
|
||||
}
|
|
@ -7,5 +7,7 @@ export async function cancelCommand(ctx: ErisContext) {
|
|||
.filter((job) => job.lockUntil < new Date())
|
||||
.filter((j) => j.state.from.id === ctx.from?.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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -28,30 +28,34 @@ async function img2img(
|
|||
state: QuestionState = {},
|
||||
): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const jobs = await generationQueue.getAllJobs();
|
||||
if (jobs.length >= config.maxJobs) {
|
||||
await ctx.reply(
|
||||
`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`,
|
||||
);
|
||||
await ctx.reply(`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, {
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id);
|
||||
if (userJobs.length >= config.maxUserJobs) {
|
||||
await ctx.reply(
|
||||
`You already have ${config.maxUserJobs} jobs in queue. Try again later.`,
|
||||
);
|
||||
await ctx.reply(`You already have ${config.maxUserJobs} jobs in queue. Try again later.`, {
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -91,7 +95,11 @@ async function img2img(
|
|||
await ctx.reply(
|
||||
"Please show me a picture to repaint." +
|
||||
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;
|
||||
}
|
||||
|
@ -100,12 +108,18 @@ async function img2img(
|
|||
await ctx.reply(
|
||||
"Please describe the picture you want to repaint." +
|
||||
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;
|
||||
}
|
||||
|
||||
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({
|
||||
task: { type: "img2img", fileId: state.fileId, params: state.params },
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Api, Bot, Context, RawApi, session, SessionFlavor } from "grammy";
|
||||
import { autoQuote } from "grammy_autoquote";
|
||||
import { FileFlavor, hydrateFiles } from "grammy_files";
|
||||
import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode";
|
||||
import { getLogger } from "std/log";
|
||||
import { getConfig, setConfig } from "../app/config.ts";
|
||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||
import { broadcastCommand } from "./broadcastCommand.ts";
|
||||
import { cancelCommand } from "./cancelCommand.ts";
|
||||
import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.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(session<SessionData, ErisContext>({
|
||||
type: "multi",
|
||||
|
@ -129,6 +128,8 @@ bot.command("queue", queueCommand);
|
|||
|
||||
bot.command("cancel", cancelCommand);
|
||||
|
||||
bot.command("broadcast", broadcastCommand);
|
||||
|
||||
bot.command("pause", async (ctx) => {
|
||||
if (!ctx.from?.username) return;
|
||||
const config = await getConfig();
|
||||
|
|
|
@ -23,7 +23,11 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
|||
await ctx.reply(
|
||||
"Please send me a PNG file." +
|
||||
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;
|
||||
}
|
||||
|
@ -43,7 +47,7 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
|||
]);
|
||||
|
||||
await ctx.reply(paramsText.text, {
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
entities: paramsText.entities,
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@ import { ErisContext } from "./mod.ts";
|
|||
|
||||
export async function queueCommand(ctx: CommandContext<ErisContext>) {
|
||||
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);
|
||||
|
||||
async function getMessageText() {
|
||||
|
|
|
@ -20,30 +20,32 @@ export async function txt2imgCommand(ctx: CommandContext<ErisContext>) {
|
|||
|
||||
async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolean): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const jobs = await generationQueue.getAllJobs();
|
||||
if (jobs.length >= config.maxJobs) {
|
||||
await ctx.reply(
|
||||
`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`,
|
||||
);
|
||||
await ctx.reply(`The queue is full. Try again later. (Max queue size: ${config.maxJobs})`, {
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userJobs = jobs.filter((job) => job.state.from.id === ctx.message?.from?.id);
|
||||
if (userJobs.length >= config.maxUserJobs) {
|
||||
await ctx.reply(
|
||||
`You already have ${config.maxUserJobs} jobs in queue. Try again later.`,
|
||||
);
|
||||
await ctx.reply(`You already have ${config.maxUserJobs} jobs in queue. Try again later.`, {
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -69,12 +71,18 @@ async function txt2img(ctx: ErisContext, match: string, includeRepliedTo: boolea
|
|||
await ctx.reply(
|
||||
"Please tell me what you want to see." +
|
||||
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;
|
||||
}
|
||||
|
||||
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({
|
||||
task: { type: "txt2img", params },
|
||||
|
|
Loading…
Reference in New Issue