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((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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
Loading…
Reference in New Issue