Files
clientsflow/omni_inbound/src/telegram.ts
2026-02-23 07:46:06 +07:00

168 lines
5.5 KiB
TypeScript

import { createHash } from "node:crypto";
import type { JsonObject, OmniInboundEnvelopeV1 } from "./types";
const MAX_TEXT_LENGTH = 4096;
function asObject(value: unknown): JsonObject {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
}
function pickMessage(update: JsonObject): JsonObject {
const candidates = [
update.message,
update.edited_message,
update.business_message,
update.edited_business_message,
update.channel_post,
update.edited_channel_post,
];
for (const candidate of candidates) {
const obj = asObject(candidate);
if (Object.keys(obj).length > 0) return obj;
}
return {};
}
function pickEventType(update: JsonObject): string {
if (update.business_message) return "business_message";
if (update.edited_business_message) return "edited_business_message";
if (update.business_connection) return "business_connection";
if (update.deleted_business_messages) return "deleted_business_messages";
if (update.message) return "message";
if (update.edited_message) return "edited_message";
return "unknown";
}
function isoFromUnix(value: unknown) {
if (typeof value !== "number" || !Number.isFinite(value)) return null;
return new Date(value * 1000).toISOString();
}
function cropText(value: unknown) {
if (typeof value !== "string") return null;
return value.slice(0, MAX_TEXT_LENGTH);
}
function normalizeString(value: unknown) {
if (typeof value !== "string") return null;
const normalized = value.trim();
return normalized || null;
}
function detectDirection(message: JsonObject, chat: JsonObject, from: JsonObject): "IN" | "OUT" {
if (typeof message.outgoing === "boolean") return message.outgoing ? "OUT" : "IN";
if (typeof message.is_outgoing === "boolean") return message.is_outgoing ? "OUT" : "IN";
if (typeof message.out === "boolean") return message.out ? "OUT" : "IN";
const chatType = normalizeString(chat.type);
if (chatType === "private" && from.is_bot === true) return "OUT";
const chatId = chat.id != null ? String(chat.id) : null;
const fromId = from.id != null ? String(from.id) : null;
if (chatType === "private" && chatId && fromId && chatId !== fromId) {
return "OUT";
}
return "IN";
}
function requireString(value: unknown, fallback: string) {
const v = String(value ?? "").trim();
return v || fallback;
}
function makeFallbackEventId(raw: unknown) {
return createHash("sha256").update(JSON.stringify(raw ?? null)).digest("hex");
}
export function parseTelegramBusinessUpdate(raw: unknown): OmniInboundEnvelopeV1 {
const update = asObject(raw);
const message = pickMessage(update);
const receivedAt = new Date().toISOString();
const updateId = update.update_id;
const messageId = message.message_id;
const businessConnection = asObject(update.business_connection);
const providerEventId =
(updateId != null && requireString(updateId, "")) ||
(messageId != null && requireString(messageId, "")) ||
makeFallbackEventId(raw);
const providerMessageId = messageId != null ? String(messageId) : null;
const chat = asObject(message.chat);
const from = asObject(message.from);
const direction = detectDirection(message, chat, from);
const contactSource = direction === "OUT" && Object.keys(chat).length > 0 ? chat : from;
const fallbackContactSource = direction === "OUT" ? from : chat;
const threadExternalId =
chat.id != null
? String(chat.id)
: businessConnection.user_chat_id != null
? String(businessConnection.user_chat_id)
: null;
const contactExternalId =
contactSource.id != null
? String(contactSource.id)
: fallbackContactSource.id != null
? String(fallbackContactSource.id)
: null;
const text = cropText(message.text) ?? cropText(message.caption);
const businessConnectionId =
message.business_connection_id != null
? String(message.business_connection_id)
: businessConnection.id != null
? String(businessConnection.id)
: null;
const occurredAt =
isoFromUnix(message.date) ??
isoFromUnix(businessConnection.date) ??
receivedAt;
const eventType = pickEventType(update);
const idempotencyKey = ["telegram_business", providerEventId, businessConnectionId || "no-bc"].join(":");
return {
version: 1,
idempotencyKey,
provider: "telegram_business",
channel: "TELEGRAM",
direction,
providerEventId,
providerMessageId,
eventType,
occurredAt,
receivedAt,
payloadRaw: raw,
payloadNormalized: {
threadExternalId,
contactExternalId,
text,
businessConnectionId,
updateId: updateId != null ? String(updateId) : null,
chatTitle: typeof chat.title === "string" ? chat.title : null,
chatUsername: normalizeString(chat.username),
chatFirstName: normalizeString(chat.first_name),
chatLastName: normalizeString(chat.last_name),
contactUsername: normalizeString(contactSource.username),
contactFirstName: normalizeString(contactSource.first_name),
contactLastName: normalizeString(contactSource.last_name),
contactTitle: normalizeString(contactSource.title),
contactAvatarUrl: normalizeString(contactSource.photo_url),
fromUsername: typeof from.username === "string" ? from.username : null,
fromFirstName: typeof from.first_name === "string" ? from.first_name : null,
fromLastName: typeof from.last_name === "string" ? from.last_name : null,
fromIsBot: from.is_bot === true,
},
};
}