Files
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] 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)
2026-07-15 14:18:16 -03:00

267 lines
8.1 KiB
TypeScript

/**
* Telegram text-group coalescing helpers
* Zones: telegram inbound, queue admission, split-message recovery
* Owns conservative delayed grouping for Telegram text messages that look like automatic long-message splits
*/
const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 12;
export interface TelegramTextGroupMessage {
message_id: number;
media_group_id?: string;
chat: { id: number };
message_thread_id?: number;
from?: { id: number; is_bot?: boolean };
text?: string;
caption?: string;
}
export interface TelegramTextGroupState<TMessage, TContext = unknown> {
messages: TMessage[];
context?: TContext;
flushTimer?: ReturnType<typeof setTimeout>;
dispatching?: boolean;
suspended?: boolean;
reschedule?: () => void;
}
export interface TelegramTextGroupController<TMessage, TContext = unknown> {
queueMessage: (options: {
message: TMessage;
context: TContext;
dispatchMessages: (
messages: TMessage[],
ctx: TContext,
) => unknown | Promise<unknown>;
}) => boolean;
suspend: () => void;
resume: (context: TContext) => void;
clear: () => void;
}
export interface TelegramTextGroupControllerOptions {
debounceMs?: number;
minSplitLength?: number;
setTimer?: (
callback: () => void,
ms: number,
) => ReturnType<typeof setTimeout>;
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
}
export interface TelegramTextGroupDispatchRuntime<
TMessage extends TelegramTextGroupMessage,
TContext,
> {
handleMessage: (message: TMessage, ctx: TContext) => Promise<void>;
}
export interface TelegramGroupedInputClearerDeps {
clearMediaGroups: () => void;
clearTextGroups: () => void;
}
function extractTelegramTextGroupText(
message: TelegramTextGroupMessage,
): string {
return typeof message.text === "string" ? message.text : "";
}
function isTelegramTextGroupCommand(text: string): boolean {
return text.trimStart().startsWith("/");
}
function getTelegramTextGroupKey(
message: TelegramTextGroupMessage,
): string | undefined {
if (message.media_group_id) return undefined;
if (!message.from || message.from.is_bot) return undefined;
if (typeof message.text !== "string") return undefined;
const threadKey =
typeof message.message_thread_id === "number"
? `thread:${message.message_thread_id}`
: "private";
return `${message.chat.id}:${threadKey}:${message.from.id}`;
}
function canStartTelegramTextGroup(
message: TelegramTextGroupMessage,
minSplitLength: number,
): boolean {
const text = extractTelegramTextGroupText(message);
return text.length >= minSplitLength && !isTelegramTextGroupCommand(text);
}
function canAppendTelegramTextGroupMessage<
TMessage extends TelegramTextGroupMessage,
>(
state: TelegramTextGroupState<TMessage, unknown>,
message: TMessage,
): boolean {
const text = extractTelegramTextGroupText(message);
const previous = state.messages.at(-1);
return (
!!previous &&
message.message_id > previous.message_id &&
message.message_id <=
previous.message_id + TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP &&
text.length > 0 &&
!isTelegramTextGroupCommand(text)
);
}
export function queueTelegramTextGroupMessage<
TMessage extends TelegramTextGroupMessage,
TContext = unknown,
>(options: {
message: TMessage;
context: TContext;
groups: Map<string, TelegramTextGroupState<TMessage, TContext>>;
debounceMs: number;
minSplitLength: number;
setTimer: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
dispatchMessages: (
messages: TMessage[],
ctx: TContext,
) => unknown | Promise<unknown>;
}): boolean {
const key = getTelegramTextGroupKey(options.message);
if (!key) return false;
const existing = options.groups.get(key);
if (
!existing &&
!canStartTelegramTextGroup(options.message, options.minSplitLength)
)
return false;
if (existing && !canAppendTelegramTextGroupMessage(existing, options.message))
return false;
const state = existing ?? { messages: [] };
state.messages.push(options.message);
state.context = options.context;
const scheduleDispatch = (): void => {
if (state.suspended) return;
state.flushTimer = options.setTimer(() => {
state.flushTimer = undefined;
const queued = options.groups.get(key);
if (!queued || queued.context === undefined) return;
if (queued.dispatching) {
scheduleDispatch();
return;
}
const dispatchedMessages = [...queued.messages];
const dispatchedIds = new Set(
dispatchedMessages.map((message) => message.message_id),
);
queued.dispatching = true;
void Promise.resolve(
options.dispatchMessages(dispatchedMessages, queued.context),
).then(
() => {
if (options.groups.get(key) !== queued) return;
queued.messages = queued.messages.filter(
(message) => !dispatchedIds.has(message.message_id),
);
queued.dispatching = false;
if (queued.messages.length === 0) options.groups.delete(key);
else if (!queued.flushTimer) scheduleDispatch();
},
() => {
if (options.groups.get(key) !== queued) return;
queued.dispatching = false;
if (!queued.flushTimer) scheduleDispatch();
},
);
}, options.debounceMs);
state.flushTimer.unref?.();
};
state.reschedule = scheduleDispatch;
if (state.flushTimer) options.clearTimer(state.flushTimer);
scheduleDispatch();
options.groups.set(key, state);
return true;
}
export function createTelegramTextGroupController<
TMessage extends TelegramTextGroupMessage,
TContext = unknown,
>(
options: TelegramTextGroupControllerOptions = {},
): TelegramTextGroupController<TMessage, TContext> {
const groups = new Map<string, TelegramTextGroupState<TMessage, TContext>>();
const debounceMs = options.debounceMs ?? TELEGRAM_TEXT_GROUP_DEBOUNCE_MS;
const minSplitLength =
options.minSplitLength ?? TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH;
const setTimer =
options.setTimer ??
((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
setTimeout(callback, ms));
const clearTimer = options.clearTimer ?? clearTimeout;
return {
queueMessage: ({ message, context, dispatchMessages }) =>
queueTelegramTextGroupMessage({
message,
context,
groups,
debounceMs,
minSplitLength,
setTimer,
clearTimer,
dispatchMessages,
}),
suspend: () => {
for (const state of groups.values()) {
state.suspended = true;
if (state.flushTimer) clearTimer(state.flushTimer);
state.flushTimer = undefined;
}
},
resume: (context) => {
for (const state of groups.values()) {
state.context = context;
state.suspended = false;
if (!state.dispatching && !state.flushTimer) state.reschedule?.();
}
},
clear: () => {
for (const state of groups.values()) {
if (state.flushTimer) clearTimer(state.flushTimer);
}
groups.clear();
},
};
}
export function createTelegramTextGroupDispatchRuntime<
TMessage extends TelegramTextGroupMessage,
TContext,
>(deps: {
textGroups: TelegramTextGroupController<TMessage, TContext>;
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
dispatchSingleMessage: (message: TMessage, ctx: TContext) => Promise<void>;
}): TelegramTextGroupDispatchRuntime<TMessage, TContext> {
return {
handleMessage: async (message, ctx) => {
const queuedTextGroup = deps.textGroups.queueMessage({
message,
context: ctx,
dispatchMessages: (messages, queuedCtx) =>
deps.dispatchMessages(messages, queuedCtx),
});
if (queuedTextGroup) return;
await deps.dispatchSingleMessage(message, ctx);
},
};
}
export function createTelegramGroupedInputClearer(
deps: TelegramGroupedInputClearerDeps,
): () => void {
return () => {
deps.clearMediaGroups();
deps.clearTextGroups();
};
}