| undefined {
return items.find((item) => {
return item.chatId === chatId && item.replyToMessageId === replyToMessageId;
});
}
function findTelegramQueueMenuItem(
items: readonly TelegramQueueMenuItem[],
chatId: number,
replyToMessageId: number,
): TelegramQueueMenuItem | undefined {
return items.find((item) => {
return item.chatId === chatId && item.replyToMessageId === replyToMessageId;
});
}
function escapeTelegramQueueMenuHtmlChar(char: string): string {
if (char === "&") return "&";
if (char === "<") return "<";
if (char === ">") return ">";
return char;
}
function escapeTelegramQueueMenuHtml(text: string): string {
return Array.from(text).map(escapeTelegramQueueMenuHtmlChar).join("");
}
function escapeTelegramQueueMenuHtmlPreview(text: string): string {
const suffix = escapeTelegramQueueMenuHtml(
QUEUE_ITEM_PROMPT_TRUNCATION_SUFFIX,
);
let escaped = "";
let truncated = false;
for (const char of text) {
const next = escapeTelegramQueueMenuHtmlChar(char);
if (
escaped.length + next.length + suffix.length >
QUEUE_ITEM_PROMPT_HTML_LIMIT
) {
truncated = true;
break;
}
escaped += next;
}
return truncated ? escaped + suffix : escaped;
}
function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
const badge = item.isPriority ? ` ${item.priorityEmoji ?? "โก"}` : "";
const heading = `${item.queuePosition}.${badge}`;
const preview = `${escapeTelegramQueueMenuHtmlPreview(item.promptText)}`;
return `${heading}\n${preview}`;
}
function buildTelegramQueueItemSubmenuReplyMarkup(
chatId: number,
replyToMessageId: number,
isPriority: boolean,
): TelegramQueueMenuReplyMarkup {
return {
inline_keyboard: [
[{ text: "โฌ๏ธ Back", callback_data: "queue:list" }],
[
{
text: isPriority ? "๐ก Priority" : "โซ๏ธ Priority",
callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
},
{
text: isPriority ? "โซ๏ธ Normal" : "๐ฃ Normal",
callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
},
],
[
{
text: "๐ Delete",
callback_data: `queue:delete:${chatId}:${replyToMessageId}`,
},
],
],
};
}
function buildTelegramQueueDeleteConfirmationReplyMarkup(
chatId: number,
replyToMessageId: number,
): TelegramQueueMenuReplyMarkup {
return {
inline_keyboard: [
[
{
text: "๐ Yes, delete",
callback_data: `queue:confirm-delete:${chatId}:${replyToMessageId}`,
},
{
text: "โ No",
callback_data: `queue:keep:${chatId}:${replyToMessageId}`,
},
],
],
};
}
interface TelegramQueueMenuCallbackDeps {
getQueuedItems: () => TelegramQueueMenuItem[];
findItem: (
chatId: number,
replyToMessageId: number,
) => TelegramQueueMenuItem | undefined;
togglePriority: (chatId: number, replyToMessageId: number) => boolean;
setPriority: (
chatId: number,
replyToMessageId: number,
enabled: boolean,
) => boolean;
cancelItem: (
chatId: number,
replyToMessageId: number,
ctx: Context,
) => boolean;
updateQueueMessage: (
chatId: number,
messageId: number,
text: string,
replyMarkup: TelegramQueueMenuReplyMarkup,
) => Promise;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise;
updateStatus: (ctx: Context) => void;
}
async function handleTelegramQueueMenuCallback(
callbackQueryId: string,
data: string,
replyChatId: number,
replyMessageId: number,
ctx: Context,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
if (!data.startsWith("queue:")) return false;
if (data === "queue:noop") {
await deps.answerCallbackQuery(callbackQueryId);
return true;
}
if (data === "queue:list") {
await updateTelegramQueueMenuList(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
return true;
}
const refreshMatch = data.match(/^queue:refresh(?::(\d+))?$/);
if (refreshMatch) {
await updateTelegramQueueMenuList(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
undefined,
refreshMatch[1] === undefined ? 0 : Number(refreshMatch[1]),
);
return true;
}
const pickMatch = data.match(/^queue:pick:(\d+):(\d+)$/);
if (pickMatch) {
await handleTelegramQueueMenuPick(
callbackQueryId,
replyChatId,
replyMessageId,
Number(pickMatch[1]),
Number(pickMatch[2]),
deps,
);
return true;
}
const prioSetMatch = data.match(
/^queue:prio-set:(\d+):(\d+):(priority|normal)$/,
);
if (prioSetMatch) {
await handleTelegramQueueMenuPrioritySet(
callbackQueryId,
replyChatId,
replyMessageId,
Number(prioSetMatch[1]),
Number(prioSetMatch[2]),
prioSetMatch[3] === "priority",
ctx,
deps,
);
return true;
}
const prioMatch = data.match(/^queue:prio:(\d+):(\d+)$/);
if (prioMatch) {
await handleTelegramQueueMenuPriority(
callbackQueryId,
replyChatId,
replyMessageId,
Number(prioMatch[1]),
Number(prioMatch[2]),
ctx,
deps,
);
return true;
}
const deleteMatch = data.match(/^queue:(?:delete|cancel):(\d+):(\d+)$/);
if (deleteMatch) {
await handleTelegramQueueMenuDeleteRequest(
callbackQueryId,
replyChatId,
replyMessageId,
Number(deleteMatch[1]),
Number(deleteMatch[2]),
deps,
);
return true;
}
const keepMatch = data.match(/^queue:keep:(\d+):(\d+)$/);
if (keepMatch) {
await handleTelegramQueueMenuKeep(
callbackQueryId,
replyChatId,
replyMessageId,
Number(keepMatch[1]),
Number(keepMatch[2]),
deps,
);
return true;
}
const confirmDeleteMatch = data.match(/^queue:confirm-delete:(\d+):(\d+)$/);
if (confirmDeleteMatch) {
await handleTelegramQueueMenuConfirmDelete(
callbackQueryId,
replyChatId,
replyMessageId,
Number(confirmDeleteMatch[1]),
Number(confirmDeleteMatch[2]),
ctx,
deps,
);
return true;
}
return false;
}
function getTelegramQueueMenuListText(
items: readonly TelegramQueueMenuItem[],
emptyRefreshIndex?: number,
): string {
if (items.length > 0) return "โณ Queue:";
if (emptyRefreshIndex === undefined) return "โ Queue is empty.";
return EMPTY_QUEUE_REFRESH_TITLES[
emptyRefreshIndex % EMPTY_QUEUE_REFRESH_TITLES.length
];
}
async function updateTelegramQueueMenuList(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
deps: TelegramQueueMenuCallbackDeps,
notice?: string,
emptyRefreshIndex?: number,
): Promise {
const items = deps.getQueuedItems();
await deps.updateQueueMessage(
replyChatId,
replyMessageId,
getTelegramQueueMenuListText(items, emptyRefreshIndex),
buildTelegramQueueMenuReplyMarkup(items, emptyRefreshIndex),
);
await deps.answerCallbackQuery(callbackQueryId, notice);
}
async function refreshStaleTelegramQueueMenuItem(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
await updateTelegramQueueMenuList(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
"Item no longer in queue.",
);
}
async function handleTelegramQueueMenuPick(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
const item = deps.findItem(chatId, msgId);
if (!item) {
return refreshStaleTelegramQueueMenuItem(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
}
await deps.updateQueueMessage(
replyChatId,
replyMessageId,
getTelegramQueueMenuItemText(item),
buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, item.isPriority),
);
await deps.answerCallbackQuery(callbackQueryId);
}
async function handleTelegramQueueMenuPriority(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
ctx: Context,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
const item = deps.findItem(chatId, msgId);
if (!item) {
return refreshStaleTelegramQueueMenuItem(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
}
await updateTelegramQueueMenuPriority(
callbackQueryId,
replyChatId,
replyMessageId,
chatId,
msgId,
!item.isPriority,
ctx,
deps,
);
}
async function handleTelegramQueueMenuPrioritySet(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
enabled: boolean,
ctx: Context,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
const item = deps.findItem(chatId, msgId);
if (!item) {
return refreshStaleTelegramQueueMenuItem(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
}
await updateTelegramQueueMenuPriority(
callbackQueryId,
replyChatId,
replyMessageId,
chatId,
msgId,
enabled,
ctx,
deps,
);
}
async function updateTelegramQueueMenuPriority(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
enabled: boolean,
ctx: Context,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
deps.setPriority(chatId, msgId, enabled);
deps.updateStatus(ctx);
const updated = deps.findItem(chatId, msgId);
if (!updated) {
return refreshStaleTelegramQueueMenuItem(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
}
await deps.updateQueueMessage(
replyChatId,
replyMessageId,
getTelegramQueueMenuItemText(updated),
buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, updated.isPriority),
);
await deps.answerCallbackQuery(
callbackQueryId,
updated.isPriority ? "Prioritized." : "Normal priority.",
);
}
async function handleTelegramQueueMenuDeleteRequest(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
const item = deps.findItem(chatId, msgId);
if (!item) {
return refreshStaleTelegramQueueMenuItem(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
}
await deps.updateQueueMessage(
replyChatId,
replyMessageId,
"Delete this queued prompt?",
buildTelegramQueueDeleteConfirmationReplyMarkup(chatId, msgId),
);
await deps.answerCallbackQuery(callbackQueryId);
}
async function handleTelegramQueueMenuKeep(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
const item = deps.findItem(chatId, msgId);
if (!item) {
return refreshStaleTelegramQueueMenuItem(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
}
await deps.updateQueueMessage(
replyChatId,
replyMessageId,
getTelegramQueueMenuItemText(item),
buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, item.isPriority),
);
await deps.answerCallbackQuery(callbackQueryId, "Kept in queue.");
}
async function handleTelegramQueueMenuConfirmDelete(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
ctx: Context,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
const removed = deps.cancelItem(chatId, msgId, ctx);
deps.updateStatus(ctx);
await updateTelegramQueueMenuList(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
removed ? "Deleted from queue." : "Item not found.",
);
}
interface TelegramQueueMenuCallbackQuery {
id: string;
data?: string;
message?: { chat?: { id?: number }; message_id?: number };
}
interface TelegramQueueMenuRuntime {
openQueueMenu: (
chatId: number,
replyToMessageId: number,
ctx: Context,
) => Promise;
handleCallbackQuery: (
query: TelegramQueueMenuCallbackQuery,
ctx: Context,
) => Promise;
}
export function createTelegramQueueMenuRuntime<
Context,
TModel extends MenuModel = MenuModel,
>(deps: {
telegramQueueStore: Queue.TelegramQueueStateStore;
queueMutationRuntime: Queue.TelegramQueueMutationController;
sendInteractiveMessage: (
chatId: number,
text: string,
mode: "html",
replyMarkup: TelegramQueueMenuReplyMarkup,
) => Promise;
editInteractiveMessage: (
chatId: number,
messageId: number,
text: string,
mode: "html",
replyMarkup: TelegramQueueMenuReplyMarkup,
) => Promise;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise;
getModelMenuState: (
chatId: number,
ctx: Context,
) => Promise>;
getStoredModelMenuState: (
messageId: number | undefined,
chatId?: number,
) => TelegramModelMenuState | undefined;
storeModelMenuState: (state: TelegramModelMenuState) => void;
updateStatusMessage: (
state: TelegramModelMenuState,
ctx: Context,
) => Promise;
updateStatus: (ctx: Context) => void;
}): TelegramQueueMenuRuntime {
const sendQueueMenuMessage = createQueueMenuSendMessageAdapter(
deps.sendInteractiveMessage,
);
const editQueueMenuMessage = createQueueMenuEditMessageAdapter(
deps.editInteractiveMessage,
);
return {
openQueueMenu: createOpenQueueMenu({
getQueuedItems: deps.telegramQueueStore.getQueuedItems,
getModelMenuState: deps.getModelMenuState,
storeModelMenuState: deps.storeModelMenuState,
sendInteractiveMessage: sendQueueMenuMessage,
}),
handleCallbackQuery: createQueueMenuCallbackHandler({
telegramQueueStore: deps.telegramQueueStore,
queueMutationRuntime: deps.queueMutationRuntime,
editInteractiveMessage: editQueueMenuMessage,
getStoredModelMenuState: deps.getStoredModelMenuState,
updateStatusMessage: deps.updateStatusMessage,
answerCallbackQuery: deps.answerCallbackQuery,
updateStatus: deps.updateStatus,
}),
};
}
function createOpenQueueMenu<
Context,
TModel extends MenuModel = MenuModel,
>(deps: {
getQueuedItems: () => Queue.TelegramQueueItem[];
getModelMenuState: (
chatId: number,
ctx: Context,
) => Promise>;
storeModelMenuState: (state: TelegramModelMenuState) => void;
sendInteractiveMessage: (
chatId: number,
replyToMessageId: number,
text: string,
replyMarkup: TelegramQueueMenuReplyMarkup,
) => Promise;
}) {
return async (
chatId: number,
replyToMessageId: number,
ctx: Context,
): Promise => {
const state = await deps.getModelMenuState(chatId, ctx);
const menuItems = toTelegramQueueMenuItems(deps.getQueuedItems());
const text = getTelegramQueueMenuListText(menuItems);
const messageId = await deps.sendInteractiveMessage(
chatId,
replyToMessageId,
text,
buildTelegramQueueMenuReplyMarkup(menuItems),
);
if (messageId === undefined) return;
state.messageId = messageId;
state.mode = "queue";
deps.storeModelMenuState(state);
};
}
function createQueueMenuCallbackHandler<
Context,
TModel extends MenuModel = MenuModel,
>(deps: {
telegramQueueStore: Queue.TelegramQueueStateStore;
queueMutationRuntime: Queue.TelegramQueueMutationController;
editInteractiveMessage: (
chatId: number,
messageId: number,
text: string,
replyMarkup: TelegramQueueMenuReplyMarkup,
) => Promise;
getStoredModelMenuState: (
messageId: number | undefined,
chatId?: number,
) => TelegramModelMenuState | undefined;
updateStatusMessage: (
state: TelegramModelMenuState,
ctx: Context,
) => Promise;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise;
updateStatus: (ctx: Context) => void;
}) {
return async (
query: TelegramQueueMenuCallbackQuery,
ctx: Context,
): Promise => {
const data = query.data;
const chatId = query.message?.chat?.id;
const messageId = query.message?.message_id;
if (!data || typeof chatId !== "number" || typeof messageId !== "number")
return false;
if (data === "menu:queue" || data === "status:queue") {
const state = deps.getStoredModelMenuState(messageId, chatId);
if (!state) {
await deps.answerCallbackQuery(
query.id,
"Interactive message expired.",
);
return true;
}
const menuItems = toTelegramQueueMenuItems(
deps.telegramQueueStore.getQueuedItems(),
);
await deps.editInteractiveMessage(
chatId,
messageId,
getTelegramQueueMenuListText(menuItems),
buildTelegramQueueMenuReplyMarkup(menuItems),
);
state.mode = "queue";
await deps.answerCallbackQuery(query.id);
return true;
}
if (!data.startsWith("queue:")) return false;
const getQueueSnapshot = () => {
return deps.telegramQueueStore.getQueuedItems();
};
const toMenuItems = () => {
return toTelegramQueueMenuItems(getQueueSnapshot());
};
const findItem = (cId: number, rId: number) => {
return findTelegramQueueMenuItem(toMenuItems(), cId, rId);
};
return handleTelegramQueueMenuCallback(
query.id,
data,
chatId,
messageId,
ctx,
{
getQueuedItems: toMenuItems,
findItem,
togglePriority: (cId, rId) => {
return toggleQueuedTelegramPromptPriority(cId, rId, ctx, {
getQueueSnapshot,
queueMutationRuntime: deps.queueMutationRuntime,
});
},
setPriority: (cId, rId, enabled) => {
return setQueuedTelegramPromptPriority(cId, rId, enabled, ctx, {
getQueueSnapshot,
queueMutationRuntime: deps.queueMutationRuntime,
});
},
cancelItem: (cId, rId, c) => {
return cancelQueuedTelegramItem(cId, rId, c, {
getQueueSnapshot,
queueMutationRuntime: deps.queueMutationRuntime,
});
},
updateQueueMessage: deps.editInteractiveMessage,
answerCallbackQuery: deps.answerCallbackQuery,
updateStatus: deps.updateStatus,
},
);
};
}
function toggleQueuedTelegramPromptPriority(
chatId: number,
replyToMessageId: number,
ctx: Context,
deps: {
getQueueSnapshot: () => Queue.TelegramQueueItem[];
queueMutationRuntime: Queue.TelegramQueueMutationController;
},
): boolean {
const item = findTelegramQueueItem(
deps.getQueueSnapshot(),
chatId,
replyToMessageId,
);
if (!item) return false;
if (item.queueLane === "priority") {
deps.queueMutationRuntime.clearPriorityByMessageId(replyToMessageId, ctx);
} else {
deps.queueMutationRuntime.prioritizeByMessageId(replyToMessageId, ctx);
}
return true;
}
function setQueuedTelegramPromptPriority(
chatId: number,
replyToMessageId: number,
enabled: boolean,
ctx: Context,
deps: {
getQueueSnapshot: () => Queue.TelegramQueueItem[];
queueMutationRuntime: Queue.TelegramQueueMutationController;
},
): boolean {
const item = findTelegramQueueItem(
deps.getQueueSnapshot(),
chatId,
replyToMessageId,
);
if (!item) return false;
if (enabled) {
deps.queueMutationRuntime.prioritizeByMessageId(replyToMessageId, ctx);
} else {
deps.queueMutationRuntime.clearPriorityByMessageId(replyToMessageId, ctx);
}
return true;
}
function cancelQueuedTelegramItem(
chatId: number,
replyToMessageId: number,
ctx: Context,
deps: {
getQueueSnapshot: () => Queue.TelegramQueueItem[];
queueMutationRuntime: Queue.TelegramQueueMutationController;
},
): boolean {
const item = findTelegramQueueItem(
deps.getQueueSnapshot(),
chatId,
replyToMessageId,
);
if (!item) return false;
return (
deps.queueMutationRuntime.removeByMessageIds([item.replyToMessageId], ctx) >
0
);
}
function createQueueMenuSendMessageAdapter(
sendInteractiveMessage: (
chatId: number,
text: string,
mode: "html",
replyMarkup: TelegramQueueMenuReplyMarkup,
) => Promise,
) {
return (
chatId: number,
_replyToMessageId: number,
text: string,
replyMarkup: TelegramQueueMenuReplyMarkup,
): Promise => {
return sendInteractiveMessage(chatId, text, "html", replyMarkup);
};
}
function createQueueMenuEditMessageAdapter(
editInteractiveMessage: (
chatId: number,
messageId: number,
text: string,
mode: "html",
replyMarkup: TelegramQueueMenuReplyMarkup,
) => Promise,
) {
return (
chatId: number,
messageId: number,
text: string,
replyMarkup: TelegramQueueMenuReplyMarkup,
): Promise => {
return editInteractiveMessage(
chatId,
messageId,
text,
"html",
replyMarkup,
).then(() => {
return undefined as number | undefined;
});
};
}