chore: fork @llblab/[email protected].0 as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a drop-in extension for can1357/oh-my-pi (omp >= 16.5.2). Changes vs upstream: - Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram - Bump version: 0.22.0 -> 0.22.0-evandro.1 - Update repository/homepage/bugs URLs to evandrodevbr - Add omp field in package.json (displayName + compatibleWith) - README: prepend 'Oh My Pi (omp) Compatibility' section with install commands, verified-components table, upstream lineage, and rationale - CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased - docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram - tests/public-api.test.ts: rename package import paths Gates re-run on the fork: - npm run typecheck -> tsc --noEmit (success) - npm test -> 1229 pass / 0 fail / 1 platform-only skip - npm run audit -> found 0 vulnerabilities - npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz Verified: - omp install --link ./pi-telegram -> Linked OK - omp plugin list -> @evandrodevbr/[email protected].0-evandro.1 Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
This commit is contained in:
commit
d1d04001d0
155 files changed
+107848
No files matched your search
+846
@@ -0,0 +1,846 @@
|
||||
/**
|
||||
* Telegram preview streaming helpers
|
||||
* Zones: telegram outbound, native rich markdown drafts
|
||||
* Owns safe draft preview selection, runtime updates, and preview finalization
|
||||
*/
|
||||
|
||||
import { normalizeTelegramNativeMarkdown } from "./replies.ts";
|
||||
import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
|
||||
import {
|
||||
getTelegramTargetThreadParams,
|
||||
type TelegramTarget,
|
||||
} from "./target.ts";
|
||||
import { shouldSuppressPreviewForVoice } from "./voice.ts";
|
||||
|
||||
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
||||
const TELEGRAM_DRAFT_PREVIEW_MAX_CHARS = 4096;
|
||||
|
||||
export type TelegramDraftSupport = "unknown" | "supported";
|
||||
|
||||
export interface TelegramPreviewState {
|
||||
mode: "draft";
|
||||
draftId?: number;
|
||||
pendingText: string;
|
||||
lastSentText: string;
|
||||
}
|
||||
|
||||
export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
|
||||
flushPromise?: Promise<void>;
|
||||
flushRequested?: boolean;
|
||||
}
|
||||
|
||||
export type TelegramPreviewReplyMarkup = unknown;
|
||||
|
||||
export interface TelegramPreviewRuntimeDeps {
|
||||
getState: () => TelegramPreviewRuntimeState | undefined;
|
||||
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
||||
maxMessageLength: number;
|
||||
getDraftSupport: () => TelegramDraftSupport;
|
||||
setDraftSupport: (support: TelegramDraftSupport) => void;
|
||||
allocateDraftId: () => number;
|
||||
sendDraft: (
|
||||
chatId: number,
|
||||
draftId: number,
|
||||
text?: string,
|
||||
options?: {
|
||||
parse_mode?: string;
|
||||
entities?: unknown[];
|
||||
message_thread_id?: number;
|
||||
},
|
||||
) => Promise<unknown>;
|
||||
canSend?: () => boolean;
|
||||
recordRuntimeEvent?: (
|
||||
category: string,
|
||||
error: unknown,
|
||||
details?: Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface TelegramPreviewActiveTurn {
|
||||
chatId: number;
|
||||
replyToMessageId?: number;
|
||||
target?: TelegramTarget;
|
||||
voiceReplyPreferred?: boolean;
|
||||
voiceReplyRequired?: boolean;
|
||||
}
|
||||
|
||||
export interface TelegramAssistantMessagePreviewStartDeps<
|
||||
TMessage,
|
||||
TReplyMarkup = TelegramPreviewReplyMarkup,
|
||||
> {
|
||||
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
||||
isAssistantMessage: (message: TMessage) => boolean;
|
||||
getState: () => TelegramPreviewRuntimeState | undefined;
|
||||
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
||||
createPreviewState: () => TelegramPreviewRuntimeState;
|
||||
canSend?: () => boolean;
|
||||
finalizePreview: (chatId: number) => Promise<boolean>;
|
||||
finalizeMarkdownPreview: (
|
||||
chatId: number,
|
||||
markdown: string,
|
||||
replyToMessageId?: number,
|
||||
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
|
||||
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
||||
isAssistantMessage: (message: TMessage) => boolean;
|
||||
getState: () => TelegramPreviewRuntimeState | undefined;
|
||||
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
||||
createPreviewState: () => TelegramPreviewRuntimeState;
|
||||
canSend?: () => boolean;
|
||||
getMessageText: (message: TMessage) => string;
|
||||
schedulePreviewFlush: (
|
||||
chatId: number,
|
||||
options?: { target?: TelegramTarget },
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type TelegramAssistantMessagePreviewHookDeps<
|
||||
TMessage,
|
||||
TReplyMarkup = TelegramPreviewReplyMarkup,
|
||||
> = TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup> &
|
||||
TelegramAssistantMessagePreviewUpdateDeps<TMessage>;
|
||||
|
||||
export interface TelegramAssistantMessagePreviewHookEvent<TMessage> {
|
||||
message: TMessage;
|
||||
}
|
||||
|
||||
export interface TelegramAssistantMessagePreviewHooks<TMessage> {
|
||||
onMessageStart: (
|
||||
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
|
||||
) => Promise<void>;
|
||||
onMessageUpdate: (
|
||||
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface TelegramPreviewControllerDeps {
|
||||
getDefaultReplyToMessageId?: () => number | undefined;
|
||||
maxMessageLength?: number;
|
||||
initialDraftSupport?: TelegramDraftSupport;
|
||||
sendDraft: (
|
||||
chatId: number,
|
||||
draftId: number,
|
||||
text?: string,
|
||||
options?: {
|
||||
parse_mode?: string;
|
||||
entities?: unknown[];
|
||||
message_thread_id?: number;
|
||||
},
|
||||
) => Promise<unknown>;
|
||||
canSend?: () => boolean;
|
||||
maxDraftId?: number;
|
||||
recordRuntimeEvent?: (
|
||||
category: string,
|
||||
error: unknown,
|
||||
details?: Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface TelegramPreviewController {
|
||||
getState: () => TelegramPreviewRuntimeState | undefined;
|
||||
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
||||
setPendingText: (text: string) => void;
|
||||
createState: () => TelegramPreviewRuntimeState;
|
||||
resetState: () => void;
|
||||
invalidate: () => void;
|
||||
clear: (
|
||||
chatId: number,
|
||||
options?: { awaitFlush?: boolean; target?: TelegramTarget },
|
||||
) => Promise<void>;
|
||||
flush: (
|
||||
chatId: number,
|
||||
options?: { target?: TelegramTarget },
|
||||
) => Promise<void>;
|
||||
scheduleFlush: (
|
||||
chatId: number,
|
||||
options?: { target?: TelegramTarget },
|
||||
) => void;
|
||||
finalize: (
|
||||
chatId: number,
|
||||
replyToMessageId?: number,
|
||||
options?: { target?: TelegramTarget },
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export type TelegramPreviewControllerRuntimeDeps =
|
||||
TelegramPreviewControllerDeps;
|
||||
|
||||
export function createTelegramPreviewControllerRuntime(
|
||||
deps: TelegramPreviewControllerRuntimeDeps,
|
||||
): TelegramPreviewController {
|
||||
return createTelegramPreviewController({
|
||||
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
|
||||
maxMessageLength: deps.maxMessageLength,
|
||||
initialDraftSupport: deps.initialDraftSupport,
|
||||
sendDraft: deps.sendDraft,
|
||||
canSend: deps.canSend,
|
||||
maxDraftId: deps.maxDraftId,
|
||||
recordRuntimeEvent: deps.recordRuntimeEvent,
|
||||
});
|
||||
}
|
||||
|
||||
export interface TelegramAssistantPreviewRuntimeDeps<
|
||||
TMessage,
|
||||
TReplyMarkup = TelegramPreviewReplyMarkup,
|
||||
> extends TelegramPreviewControllerRuntimeDeps {
|
||||
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
||||
isAssistantMessage: (message: TMessage) => boolean;
|
||||
getMessageText: (message: TMessage) => string;
|
||||
sendMarkdownReply: (
|
||||
chatId: number,
|
||||
replyToMessageId: number | undefined,
|
||||
markdown: string,
|
||||
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
||||
) => Promise<number | undefined>;
|
||||
}
|
||||
|
||||
export type TelegramAssistantPreviewRuntime<
|
||||
TMessage,
|
||||
TReplyMarkup = TelegramPreviewReplyMarkup,
|
||||
> = TelegramPreviewController &
|
||||
TelegramAssistantMessagePreviewHooks<TMessage> & {
|
||||
finalizeMarkdown: (
|
||||
chatId: number,
|
||||
markdown: string,
|
||||
replyToMessageId?: number,
|
||||
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
||||
) => Promise<boolean>;
|
||||
};
|
||||
|
||||
export function createTelegramNativeMarkdownPreviewFinalizer<
|
||||
TReplyMarkup,
|
||||
>(deps: {
|
||||
getState: () => TelegramPreviewRuntimeState | undefined;
|
||||
clear: (
|
||||
chatId: number,
|
||||
options?: { awaitFlush?: boolean; target?: TelegramTarget },
|
||||
) => Promise<void>;
|
||||
discard?: () => void;
|
||||
sendMarkdownReply: (
|
||||
chatId: number,
|
||||
replyToMessageId: number | undefined,
|
||||
markdown: string,
|
||||
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
||||
) => Promise<number | undefined>;
|
||||
}): (
|
||||
chatId: number,
|
||||
markdown: string,
|
||||
replyToMessageId?: number,
|
||||
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
||||
) => Promise<boolean> {
|
||||
return async (chatId, markdown, replyToMessageId, options) => {
|
||||
const state = deps.getState();
|
||||
if (state?.flushPromise) {
|
||||
await state.flushPromise.catch(() => {});
|
||||
if (deps.getState() !== state) return false;
|
||||
}
|
||||
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
|
||||
if (deps.getState() === state) deps.discard?.();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export function createTelegramAssistantPreviewRuntime<
|
||||
TMessage,
|
||||
TReplyMarkup = TelegramPreviewReplyMarkup,
|
||||
>(
|
||||
deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
|
||||
): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
|
||||
const controller = createTelegramPreviewControllerRuntime(deps);
|
||||
const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
|
||||
getState: controller.getState,
|
||||
clear: controller.clear,
|
||||
discard: () => controller.setState(undefined),
|
||||
sendMarkdownReply: deps.sendMarkdownReply,
|
||||
});
|
||||
return {
|
||||
...controller,
|
||||
finalizeMarkdown: finalizeMarkdownPreview,
|
||||
...createTelegramAssistantMessagePreviewHooks({
|
||||
getActiveTurn: deps.getActiveTurn,
|
||||
isAssistantMessage: deps.isAssistantMessage,
|
||||
getState: controller.getState,
|
||||
setState: controller.setState,
|
||||
createPreviewState: controller.createState,
|
||||
canSend: deps.canSend,
|
||||
finalizePreview: controller.finalize,
|
||||
finalizeMarkdownPreview,
|
||||
getMessageText: deps.getMessageText,
|
||||
schedulePreviewFlush: controller.scheduleFlush,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTelegramPreviewController(
|
||||
deps: TelegramPreviewControllerDeps,
|
||||
): TelegramPreviewController {
|
||||
let state: TelegramPreviewRuntimeState | undefined;
|
||||
let generation = 0;
|
||||
const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
|
||||
const maxMessageLength =
|
||||
deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
|
||||
let draftSupport = deps.initialDraftSupport ?? "unknown";
|
||||
let nextDraftId = 0;
|
||||
const getRuntimeDeps = (
|
||||
operationGeneration = generation,
|
||||
): TelegramPreviewRuntimeDeps => ({
|
||||
getState: () => state,
|
||||
setState: (nextState) => {
|
||||
state = nextState;
|
||||
},
|
||||
maxMessageLength,
|
||||
getDraftSupport: () => draftSupport,
|
||||
setDraftSupport: (support) => {
|
||||
draftSupport = support;
|
||||
},
|
||||
allocateDraftId: () => {
|
||||
nextDraftId = allocateTelegramDraftId(nextDraftId, maxDraftId);
|
||||
return nextDraftId;
|
||||
},
|
||||
sendDraft: deps.sendDraft,
|
||||
canSend: () =>
|
||||
operationGeneration === generation && (deps.canSend?.() ?? true),
|
||||
recordRuntimeEvent: deps.recordRuntimeEvent,
|
||||
});
|
||||
return {
|
||||
getState: () => state,
|
||||
setState: (nextState) => {
|
||||
state = nextState;
|
||||
},
|
||||
setPendingText: (text) => {
|
||||
if (state) state.pendingText = text;
|
||||
},
|
||||
createState: () => createTelegramPreviewRuntimeState(),
|
||||
resetState: () => {
|
||||
generation += 1;
|
||||
state = createTelegramPreviewRuntimeState();
|
||||
},
|
||||
invalidate: () => {
|
||||
generation += 1;
|
||||
state = undefined;
|
||||
},
|
||||
clear: (chatId, options) =>
|
||||
clearTelegramPreview(chatId, getRuntimeDeps(), options),
|
||||
flush: (chatId, options) =>
|
||||
flushTelegramPreview(chatId, getRuntimeDeps(), options),
|
||||
scheduleFlush: (chatId, options) => {
|
||||
if (!state) return;
|
||||
void flushTelegramPreview(chatId, getRuntimeDeps(), options);
|
||||
},
|
||||
finalize: (chatId, _replyToMessageId, options) =>
|
||||
finalizeTelegramPreview(chatId, getRuntimeDeps(), options),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTelegramAssistantMessagePreviewHooks<
|
||||
TMessage,
|
||||
TReplyMarkup = TelegramPreviewReplyMarkup,
|
||||
>(
|
||||
deps: TelegramAssistantMessagePreviewHookDeps<TMessage, TReplyMarkup>,
|
||||
): TelegramAssistantMessagePreviewHooks<TMessage> {
|
||||
return {
|
||||
onMessageStart: async (
|
||||
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
|
||||
): Promise<void> => {
|
||||
await handleTelegramAssistantMessagePreviewStart(event.message, deps);
|
||||
},
|
||||
onMessageUpdate: async (
|
||||
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
|
||||
): Promise<void> => {
|
||||
await handleTelegramAssistantMessagePreviewUpdate(event.message, deps);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleTelegramAssistantMessagePreviewStart<
|
||||
TMessage,
|
||||
TReplyMarkup = TelegramPreviewReplyMarkup,
|
||||
>(
|
||||
message: TMessage,
|
||||
deps: TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup>,
|
||||
): Promise<void> {
|
||||
const turn = deps.getActiveTurn();
|
||||
if (!turn || !deps.isAssistantMessage(message)) return;
|
||||
if (deps.canSend && !deps.canSend()) {
|
||||
deps.setState(undefined);
|
||||
return;
|
||||
}
|
||||
if (shouldSuppressPreviewForVoice(turn)) {
|
||||
deps.setState(undefined);
|
||||
return;
|
||||
}
|
||||
const state = deps.getState();
|
||||
if (
|
||||
state &&
|
||||
(state.pendingText.trim().length > 0 ||
|
||||
state.lastSentText.trim().length > 0)
|
||||
) {
|
||||
const previousText = state.pendingText.trim();
|
||||
if (previousText.length > 0) {
|
||||
await deps.finalizeMarkdownPreview(
|
||||
turn.chatId,
|
||||
previousText,
|
||||
turn.replyToMessageId,
|
||||
{
|
||||
target: turn.target,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await deps.finalizePreview(turn.chatId);
|
||||
}
|
||||
}
|
||||
deps.setState(deps.createPreviewState());
|
||||
}
|
||||
|
||||
export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
|
||||
message: TMessage,
|
||||
deps: TelegramAssistantMessagePreviewUpdateDeps<TMessage>,
|
||||
): Promise<void> {
|
||||
const turn = deps.getActiveTurn();
|
||||
if (!turn || !deps.isAssistantMessage(message)) return;
|
||||
if (deps.canSend && !deps.canSend()) {
|
||||
deps.setState(undefined);
|
||||
return;
|
||||
}
|
||||
if (shouldSuppressPreviewForVoice(turn)) return;
|
||||
let state = deps.getState();
|
||||
if (!state) {
|
||||
state = deps.createPreviewState();
|
||||
deps.setState(state);
|
||||
}
|
||||
state.pendingText = stripTelegramCommentMarkupForPreview(
|
||||
deps.getMessageText(message),
|
||||
);
|
||||
deps.schedulePreviewFlush(turn.chatId, { target: turn.target });
|
||||
}
|
||||
|
||||
export function buildTelegramPreviewFinalText(
|
||||
state: TelegramPreviewState,
|
||||
): string | undefined {
|
||||
const finalText = state.pendingText.trim();
|
||||
if (finalText) return finalText;
|
||||
return state.lastSentText.trim() || undefined;
|
||||
}
|
||||
|
||||
export function createTelegramPreviewRuntimeState(): TelegramPreviewRuntimeState {
|
||||
return {
|
||||
mode: "draft",
|
||||
pendingText: "",
|
||||
lastSentText: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function allocateTelegramDraftId(
|
||||
currentDraftId: number,
|
||||
maxDraftId: number,
|
||||
): number {
|
||||
return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
|
||||
}
|
||||
|
||||
interface TelegramNativeMarkdownPreviewSnapshot {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function shouldUseTelegramDraftPreview(_options: {
|
||||
draftSupport: TelegramDraftSupport;
|
||||
snapshot?: TelegramNativeMarkdownPreviewSnapshot;
|
||||
}): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function clearTelegramPreview(
|
||||
chatId: number,
|
||||
deps: TelegramPreviewRuntimeDeps,
|
||||
options: { awaitFlush?: boolean; target?: TelegramTarget } = {},
|
||||
): Promise<void> {
|
||||
const state = deps.getState();
|
||||
if (!state) return;
|
||||
if (state.flushPromise && options.awaitFlush !== false) {
|
||||
state.flushRequested = false;
|
||||
await state.flushPromise.catch(() => {});
|
||||
if (deps.getState() !== state) return;
|
||||
}
|
||||
deps.setState(undefined);
|
||||
if (state.mode === "draft" && state.draftId !== undefined) {
|
||||
try {
|
||||
await deps.sendDraft(chatId, state.draftId, undefined, {
|
||||
...getTelegramTargetThreadParams(options.target ?? { chatId }),
|
||||
});
|
||||
} catch (error) {
|
||||
deps.recordRuntimeEvent?.("preview", error, {
|
||||
phase: "clear-draft",
|
||||
chatId,
|
||||
draftId: state.draftId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TelegramDraftInlineState {
|
||||
codeTicks: number;
|
||||
htmlComment: boolean;
|
||||
displayMath: boolean;
|
||||
fence?: { marker: "`" | "~"; length: number };
|
||||
strongAsterisk: boolean;
|
||||
emphasisAsterisk: boolean;
|
||||
strongUnderscore: boolean;
|
||||
emphasisUnderscore: boolean;
|
||||
strike: boolean;
|
||||
linkText: boolean;
|
||||
linkDestination: boolean;
|
||||
}
|
||||
|
||||
function createTelegramDraftInlineState(): TelegramDraftInlineState {
|
||||
return {
|
||||
codeTicks: 0,
|
||||
htmlComment: false,
|
||||
displayMath: false,
|
||||
strongAsterisk: false,
|
||||
emphasisAsterisk: false,
|
||||
strongUnderscore: false,
|
||||
emphasisUnderscore: false,
|
||||
strike: false,
|
||||
linkText: false,
|
||||
linkDestination: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isTelegramDraftInlineStateClosed(
|
||||
state: TelegramDraftInlineState,
|
||||
): boolean {
|
||||
return (
|
||||
state.codeTicks === 0 &&
|
||||
!state.htmlComment &&
|
||||
!state.displayMath &&
|
||||
!state.fence &&
|
||||
!state.strongAsterisk &&
|
||||
!state.emphasisAsterisk &&
|
||||
!state.strongUnderscore &&
|
||||
!state.emphasisUnderscore &&
|
||||
!state.strike &&
|
||||
!state.linkText &&
|
||||
!state.linkDestination
|
||||
);
|
||||
}
|
||||
|
||||
function countRepeatedChars(text: string, index: number, char: string): number {
|
||||
let count = 0;
|
||||
while (text[index + count] === char) count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
function isEscapedMarkdownChar(text: string, index: number): boolean {
|
||||
let slashCount = 0;
|
||||
for (
|
||||
let cursor = index - 1;
|
||||
cursor >= 0 && text[cursor] === "\\";
|
||||
cursor -= 1
|
||||
) {
|
||||
slashCount += 1;
|
||||
}
|
||||
return slashCount % 2 === 1;
|
||||
}
|
||||
|
||||
function isInlineDelimiterCandidate(
|
||||
text: string,
|
||||
index: number,
|
||||
length: number,
|
||||
): boolean {
|
||||
const previous = text[index - 1] ?? "";
|
||||
const next = text[index + length] ?? "";
|
||||
if (!next || /\s/.test(next))
|
||||
return previous.length > 0 && !/\s/.test(previous);
|
||||
if (!previous || /\s/.test(previous)) return true;
|
||||
return /[\p{P}\p{S}]/u.test(previous) || /[\p{P}\p{S}]/u.test(next);
|
||||
}
|
||||
|
||||
function updateTelegramDraftInlineStateForLine(
|
||||
line: string,
|
||||
state: TelegramDraftInlineState,
|
||||
): void {
|
||||
if (state.fence || state.displayMath) return;
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
if (state.htmlComment) {
|
||||
const closeIndex = line.indexOf("-->", index);
|
||||
if (closeIndex === -1) return;
|
||||
state.htmlComment = false;
|
||||
index = closeIndex + 2;
|
||||
continue;
|
||||
}
|
||||
if (state.codeTicks > 0) {
|
||||
const ticks = countRepeatedChars(line, index, "`");
|
||||
if (ticks >= state.codeTicks) {
|
||||
state.codeTicks = 0;
|
||||
index += ticks - 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isEscapedMarkdownChar(line, index)) continue;
|
||||
if (line.startsWith("<!--", index)) {
|
||||
const closeIndex = line.indexOf("-->", index + 4);
|
||||
if (closeIndex === -1) {
|
||||
state.htmlComment = true;
|
||||
return;
|
||||
}
|
||||
index = closeIndex + 2;
|
||||
continue;
|
||||
}
|
||||
const ticks = countRepeatedChars(line, index, "`");
|
||||
if (ticks > 0) {
|
||||
state.codeTicks = ticks;
|
||||
index += ticks - 1;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("][", index) || line.startsWith("](", index)) {
|
||||
state.linkText = false;
|
||||
state.linkDestination = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (line[index] === "[" && !state.linkDestination) {
|
||||
state.linkText = true;
|
||||
continue;
|
||||
}
|
||||
if (line[index] === ")" && state.linkDestination) {
|
||||
state.linkDestination = false;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
line.startsWith("~~", index) &&
|
||||
isInlineDelimiterCandidate(line, index, 2)
|
||||
) {
|
||||
state.strike = !state.strike;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
line.startsWith("**", index) &&
|
||||
isInlineDelimiterCandidate(line, index, 2)
|
||||
) {
|
||||
state.strongAsterisk = !state.strongAsterisk;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (line[index] === "*" && isInlineDelimiterCandidate(line, index, 1)) {
|
||||
state.emphasisAsterisk = !state.emphasisAsterisk;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
line.startsWith("__", index) &&
|
||||
isInlineDelimiterCandidate(line, index, 2)
|
||||
) {
|
||||
state.strongUnderscore = !state.strongUnderscore;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (line[index] === "_" && isInlineDelimiterCandidate(line, index, 1)) {
|
||||
state.emphasisUnderscore = !state.emphasisUnderscore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTelegramDraftBlockStateForLine(
|
||||
line: string,
|
||||
state: TelegramDraftInlineState,
|
||||
): boolean {
|
||||
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
||||
if (state.fence) {
|
||||
if (
|
||||
new RegExp(
|
||||
`^ {0,3}${state.fence.marker}{${state.fence.length},}\\s*$`,
|
||||
).test(line)
|
||||
) {
|
||||
state.fence = undefined;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (state.displayMath) {
|
||||
if (line.trim() === "$$") state.displayMath = false;
|
||||
return true;
|
||||
}
|
||||
if (fenceMatch) {
|
||||
const markerText = fenceMatch[1] ?? "```";
|
||||
state.fence = {
|
||||
marker: markerText[0] as "`" | "~",
|
||||
length: markerText.length,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
if (line.trim() === "$$") {
|
||||
state.displayMath = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function findSafeTelegramRichMarkdownDraftEnd(markdown: string): number {
|
||||
const state = createTelegramDraftInlineState();
|
||||
let offset = 0;
|
||||
let safeEnd = 0;
|
||||
for (const line of markdown.split("\n")) {
|
||||
const lineEnd = offset + line.length;
|
||||
const consumedAsBlock = updateTelegramDraftBlockStateForLine(line, state);
|
||||
if (!consumedAsBlock) updateTelegramDraftInlineStateForLine(line, state);
|
||||
const nextOffset = lineEnd + 1;
|
||||
if (isTelegramDraftInlineStateClosed(state)) safeEnd = lineEnd;
|
||||
offset = nextOffset;
|
||||
}
|
||||
if (isTelegramDraftInlineStateClosed(state)) return markdown.length;
|
||||
return safeEnd;
|
||||
}
|
||||
|
||||
function hasTelegramPreviewVisibleContent(markdown: string): boolean {
|
||||
return /[\p{L}\p{N}]/u.test(markdown);
|
||||
}
|
||||
|
||||
export function getSafeTelegramRichMarkdownDraftPrefix(
|
||||
markdown: string,
|
||||
maxMessageLength: number,
|
||||
): string | undefined {
|
||||
const source = markdown.trim();
|
||||
if (!source) return undefined;
|
||||
const limited =
|
||||
source.length > maxMessageLength
|
||||
? source.slice(0, maxMessageLength)
|
||||
: source;
|
||||
const safeEnd = findSafeTelegramRichMarkdownDraftEnd(limited);
|
||||
if (safeEnd > 0) {
|
||||
const safePrefix = limited.slice(0, safeEnd).trimEnd();
|
||||
return hasTelegramPreviewVisibleContent(safePrefix)
|
||||
? safePrefix
|
||||
: undefined;
|
||||
}
|
||||
let candidateEnd = limited.length;
|
||||
while (candidateEnd > 0) {
|
||||
candidateEnd = limited.lastIndexOf(" ", candidateEnd - 1);
|
||||
if (candidateEnd <= 0) return undefined;
|
||||
const candidate = limited.slice(0, candidateEnd).trimEnd();
|
||||
if (
|
||||
hasTelegramPreviewVisibleContent(candidate) &&
|
||||
findSafeTelegramRichMarkdownDraftEnd(candidate) === candidate.length
|
||||
) {
|
||||
return candidate || undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildTelegramNativeMarkdownPreviewSnapshot(
|
||||
state: TelegramPreviewState,
|
||||
maxMessageLength: number,
|
||||
): TelegramNativeMarkdownPreviewSnapshot | undefined {
|
||||
const safeText = getSafeTelegramRichMarkdownDraftPrefix(
|
||||
state.pendingText,
|
||||
maxMessageLength,
|
||||
);
|
||||
if (!safeText || safeText === state.lastSentText) return undefined;
|
||||
return { text: safeText };
|
||||
}
|
||||
|
||||
async function performTelegramPreviewFlush(
|
||||
chatId: number,
|
||||
state: TelegramPreviewRuntimeState,
|
||||
deps: TelegramPreviewRuntimeDeps,
|
||||
options: { target?: TelegramTarget } = {},
|
||||
): Promise<void> {
|
||||
if (deps.canSend && !deps.canSend()) {
|
||||
await clearTelegramPreview(chatId, deps, {
|
||||
awaitFlush: false,
|
||||
target: options.target,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
|
||||
state,
|
||||
deps.maxMessageLength,
|
||||
);
|
||||
if (!snapshot) return;
|
||||
if (
|
||||
shouldUseTelegramDraftPreview({
|
||||
draftSupport: deps.getDraftSupport(),
|
||||
snapshot,
|
||||
})
|
||||
) {
|
||||
const draftId = state.draftId ?? deps.allocateDraftId();
|
||||
state.draftId = draftId;
|
||||
try {
|
||||
await deps.sendDraft(
|
||||
chatId,
|
||||
draftId,
|
||||
normalizeTelegramNativeMarkdown(snapshot.text),
|
||||
{ ...getTelegramTargetThreadParams(options.target ?? { chatId }) },
|
||||
);
|
||||
deps.setDraftSupport("supported");
|
||||
state.mode = "draft";
|
||||
state.lastSentText = snapshot.text;
|
||||
return;
|
||||
} catch (error) {
|
||||
deps.recordRuntimeEvent?.("preview", error, {
|
||||
phase: "draft",
|
||||
chatId,
|
||||
draftId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function flushTelegramPreview(
|
||||
chatId: number,
|
||||
deps: TelegramPreviewRuntimeDeps,
|
||||
options: { target?: TelegramTarget } = {},
|
||||
): Promise<void> {
|
||||
const state = deps.getState();
|
||||
if (!state) return;
|
||||
if (state.flushPromise) {
|
||||
state.flushRequested = true;
|
||||
await state.flushPromise;
|
||||
return;
|
||||
}
|
||||
state.flushPromise = (async () => {
|
||||
do {
|
||||
state.flushRequested = false;
|
||||
try {
|
||||
await performTelegramPreviewFlush(chatId, state, deps, options);
|
||||
} catch (error) {
|
||||
deps.recordRuntimeEvent?.("preview", error, {
|
||||
phase: "flush",
|
||||
chatId,
|
||||
draftId: state.draftId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
} while (deps.getState() === state && state.flushRequested);
|
||||
})();
|
||||
try {
|
||||
await state.flushPromise;
|
||||
} finally {
|
||||
if (deps.getState() === state) {
|
||||
state.flushPromise = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function finalizeTelegramPreview(
|
||||
chatId: number,
|
||||
deps: TelegramPreviewRuntimeDeps,
|
||||
options: { target?: TelegramTarget } = {},
|
||||
): Promise<boolean> {
|
||||
const state = deps.getState();
|
||||
if (!state) return false;
|
||||
if (deps.canSend && !deps.canSend()) {
|
||||
await clearTelegramPreview(chatId, deps, options);
|
||||
return false;
|
||||
}
|
||||
await flushTelegramPreview(chatId, deps, options);
|
||||
const finalText = buildTelegramPreviewFinalText(state);
|
||||
if (!finalText) {
|
||||
await clearTelegramPreview(chatId, deps, options);
|
||||
return false;
|
||||
}
|
||||
deps.setState(undefined);
|
||||
return false;
|
||||
}
|
||||
Reference in new issue
Block a user