| 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.reactionSuppressionEmoji
? ` ${item.reactionSuppressionEmoji}`
: item.isPriority
? ` ${item.priorityEmoji ?? "โก"}`
: "";
const position = item.reactionSuppressionEmoji
? `${item.queuePosition}.`
: `${item.queuePosition}.`;
const heading = `${position}${badge}`;
const preview = `${escapeTelegramQueueMenuHtmlPreview(item.promptText)}`;
return `${heading}\n${preview}`;
}
function buildTelegramQueueItemSubmenuReplyMarkup(
chatId: number,
replyToMessageId: number,
isPriority: boolean,
isSkipped: 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: isSkipped ? "โซ๏ธ Keep" : "๐ข Keep",
callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:keep`,
},
{
text: isSkipped ? "๐ด Skip" : "โซ๏ธ Skip",
callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:skip`,
},
],
],
};
}
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;
setSkipped: (
chatId: number,
replyToMessageId: number,
skipped: boolean,
) => 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 skipSetMatch = data.match(
/^queue:skip-set:(\d+):(\d+):(skip|keep)$/,
);
if (skipSetMatch) {
await handleTelegramQueueMenuSkipSet(
callbackQueryId,
replyChatId,
replyMessageId,
Number(skipSetMatch[1]),
Number(skipSetMatch[2]),
skipSetMatch[3] === "skip",
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,
item.reactionSuppressionEmoji !== undefined,
),
);
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,
updated.reactionSuppressionEmoji !== undefined,
),
);
await deps.answerCallbackQuery(
callbackQueryId,
updated.isPriority ? "Prioritized." : "Normal priority.",
);
}
async function handleTelegramQueueMenuSkipSet(
callbackQueryId: string,
replyChatId: number,
replyMessageId: number,
chatId: number,
msgId: number,
skipped: boolean,
ctx: Context,
deps: TelegramQueueMenuCallbackDeps,
): Promise {
const item = deps.findItem(chatId, msgId);
if (!item) {
return refreshStaleTelegramQueueMenuItem(
callbackQueryId,
replyChatId,
replyMessageId,
deps,
);
}
deps.setSkipped(chatId, msgId, skipped);
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,
updated.reactionSuppressionEmoji !== undefined,
),
);
await deps.answerCallbackQuery(callbackQueryId);
}
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,
});
},
setSkipped: (cId, rId, skipped) => {
return setQueuedTelegramPromptSkipped(cId, rId, skipped, ctx, {
getQueueSnapshot,
queueMutationRuntime: deps.queueMutationRuntime,
});
},
updateQueueMessage: deps.editInteractiveMessage,
answerCallbackQuery: deps.answerCallbackQuery,
updateStatus: deps.updateStatus,
},
);
};
}
function getQueueMenuReactionDisposition(
item: Queue.TelegramQueueItem,
priority: boolean,
skipped: boolean,
): Queue.TelegramQueueReactionDisposition {
if (priority && skipped) {
return {
kind: "priority-suppressed",
priorityEmoji:
item.kind === "prompt" ? item.priorityEmoji ?? "โก" : "โก",
suppressionEmoji:
item.kind === "prompt"
? item.reactionSuppressionEmoji ?? "๐"
: "๐",
};
}
if (priority) {
return {
kind: "priority",
emoji: item.kind === "prompt" ? item.priorityEmoji ?? "โก" : "โก",
};
}
if (skipped) {
return {
kind: "suppressed",
emoji:
item.kind === "prompt"
? item.reactionSuppressionEmoji ?? "๐"
: "๐",
};
}
return { kind: "default" };
}
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;
deps.queueMutationRuntime.applyReactionByMessageId(
replyToMessageId,
getQueueMenuReactionDisposition(
item,
item.queueLane !== "priority",
item.kind === "prompt" &&
item.reactionSuppressionEmoji !== undefined,
),
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;
deps.queueMutationRuntime.applyReactionByMessageId(
replyToMessageId,
getQueueMenuReactionDisposition(
item,
enabled,
item.kind === "prompt" &&
item.reactionSuppressionEmoji !== undefined,
),
ctx,
);
return true;
}
function setQueuedTelegramPromptSkipped(
chatId: number,
replyToMessageId: number,
skipped: boolean,
ctx: Context,
deps: {
getQueueSnapshot: () => Queue.TelegramQueueItem[];
queueMutationRuntime: Queue.TelegramQueueMutationController;
},
): boolean {
const item = findTelegramQueueItem(
deps.getQueueSnapshot(),
chatId,
replyToMessageId,
);
if (!item || item.kind !== "prompt") return false;
deps.queueMutationRuntime.applyReactionByMessageId(
replyToMessageId,
getQueueMenuReactionDisposition(
item,
item.queueLane === "priority",
skipped,
),
ctx,
);
return true;
}
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;
});
};
}