refactor chat delivery to graphql + hatchet services
This commit is contained in:
3
telegram_backend/src/hatchet/client.ts
Normal file
3
telegram_backend/src/hatchet/client.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
|
||||
|
||||
export const hatchet = HatchetClient.init();
|
||||
31
telegram_backend/src/hatchet/tasks.ts
Normal file
31
telegram_backend/src/hatchet/tasks.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { hatchet } from "./client";
|
||||
import type { OmniInboundEnvelopeV1 } from "../types";
|
||||
|
||||
export type TelegramOutboundTaskInput = {
|
||||
omniMessageId: string;
|
||||
chatId: string;
|
||||
text: string;
|
||||
businessConnectionId?: string | null;
|
||||
};
|
||||
|
||||
const processTelegramInbound = hatchet.task({
|
||||
name: "process-telegram-inbound",
|
||||
});
|
||||
|
||||
const processTelegramOutbound = hatchet.task({
|
||||
name: "process-telegram-outbound",
|
||||
});
|
||||
|
||||
export async function enqueueTelegramInboundTask(input: OmniInboundEnvelopeV1) {
|
||||
const run = await processTelegramInbound.runNoWait(input as any);
|
||||
return {
|
||||
runId: await run.runId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function enqueueTelegramOutboundTask(input: TelegramOutboundTaskInput) {
|
||||
const run = await processTelegramOutbound.runNoWait(input as any);
|
||||
return {
|
||||
runId: await run.runId,
|
||||
};
|
||||
}
|
||||
31
telegram_backend/src/index.ts
Normal file
31
telegram_backend/src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { startServer } from "./server";
|
||||
|
||||
const server = startServer();
|
||||
|
||||
async function shutdown(signal: string) {
|
||||
console.log(`[telegram_backend] shutting down by ${signal}`);
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
// ignore shutdown errors
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
void shutdown("SIGINT");
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
void shutdown("SIGTERM");
|
||||
});
|
||||
270
telegram_backend/src/server.ts
Normal file
270
telegram_backend/src/server.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { buildSchema, graphql } from "graphql";
|
||||
import { parseTelegramBusinessUpdate } from "./telegram";
|
||||
import { enqueueTelegramInboundTask, enqueueTelegramOutboundTask } from "./hatchet/tasks";
|
||||
|
||||
const PORT = Number(process.env.PORT || 8080);
|
||||
const MAX_BODY_SIZE_BYTES = Number(process.env.MAX_BODY_SIZE_BYTES || 1024 * 1024);
|
||||
const WEBHOOK_SECRET = String(process.env.TELEGRAM_WEBHOOK_SECRET || "").trim();
|
||||
const GRAPHQL_SHARED_SECRET = String(process.env.TELEGRAM_BACKEND_GRAPHQL_SHARED_SECRET || "").trim();
|
||||
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
health: Health!
|
||||
}
|
||||
|
||||
type Health {
|
||||
ok: Boolean!
|
||||
service: String!
|
||||
now: String!
|
||||
}
|
||||
|
||||
input TelegramOutboundTaskInput {
|
||||
omniMessageId: String!
|
||||
chatId: String!
|
||||
text: String!
|
||||
businessConnectionId: String
|
||||
}
|
||||
|
||||
input TelegramSendMessageInput {
|
||||
chatId: String!
|
||||
text: String!
|
||||
businessConnectionId: String
|
||||
}
|
||||
|
||||
type TaskEnqueueResult {
|
||||
ok: Boolean!
|
||||
message: String!
|
||||
runId: String
|
||||
}
|
||||
|
||||
type TelegramSendResult {
|
||||
ok: Boolean!
|
||||
message: String!
|
||||
providerMessageId: String
|
||||
responseJson: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
enqueueTelegramOutbound(input: TelegramOutboundTaskInput!): TaskEnqueueResult!
|
||||
sendTelegramMessage(input: TelegramSendMessageInput!): TelegramSendResult!
|
||||
}
|
||||
`);
|
||||
|
||||
function asString(value: unknown) {
|
||||
if (typeof value !== "string") return null;
|
||||
const v = value.trim();
|
||||
return v || null;
|
||||
}
|
||||
|
||||
function writeJson(res: ServerResponse, statusCode: number, body: unknown) {
|
||||
res.statusCode = statusCode;
|
||||
res.setHeader("content-type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify(body));
|
||||
}
|
||||
|
||||
function isWebhookAuthorized(req: IncomingMessage) {
|
||||
if (!WEBHOOK_SECRET) return true;
|
||||
const incoming = String(req.headers["x-telegram-bot-api-secret-token"] || "").trim();
|
||||
return incoming !== "" && incoming === WEBHOOK_SECRET;
|
||||
}
|
||||
|
||||
function isGraphqlAuthorized(req: IncomingMessage) {
|
||||
if (!GRAPHQL_SHARED_SECRET) return true;
|
||||
const incoming = String(req.headers["x-graphql-secret"] || "").trim();
|
||||
return incoming !== "" && incoming === GRAPHQL_SHARED_SECRET;
|
||||
}
|
||||
|
||||
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
||||
const chunks: Buffer[] = [];
|
||||
let total = 0;
|
||||
|
||||
for await (const chunk of req) {
|
||||
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
total += buf.length;
|
||||
if (total > MAX_BODY_SIZE_BYTES) {
|
||||
throw new Error(`payload_too_large:${MAX_BODY_SIZE_BYTES}`);
|
||||
}
|
||||
chunks.push(buf);
|
||||
}
|
||||
|
||||
const raw = Buffer.concat(chunks).toString("utf8");
|
||||
if (!raw) return {};
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
async function sendTelegramMessage(input: {
|
||||
chatId: string;
|
||||
text: string;
|
||||
businessConnectionId?: string | null;
|
||||
}) {
|
||||
const token = asString(process.env.TELEGRAM_BOT_TOKEN);
|
||||
if (!token) {
|
||||
throw new Error("TELEGRAM_BOT_TOKEN is required");
|
||||
}
|
||||
|
||||
const base = String(process.env.TELEGRAM_API_BASE || "https://api.telegram.org").replace(/\/+$/, "");
|
||||
const endpoint = `${base}/bot${token}/sendMessage`;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
chat_id: input.chatId,
|
||||
text: input.text,
|
||||
};
|
||||
if (input.businessConnectionId) {
|
||||
payload.business_connection_id = input.businessConnectionId;
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
const body = (() => {
|
||||
try {
|
||||
return JSON.parse(text) as Record<string, any>;
|
||||
} catch {
|
||||
return { raw: text };
|
||||
}
|
||||
})();
|
||||
|
||||
if (!response.ok || body?.ok === false) {
|
||||
throw new Error(`telegram send failed: ${response.status} ${JSON.stringify(body)}`);
|
||||
}
|
||||
|
||||
const providerMessageId =
|
||||
body?.result?.message_id != null
|
||||
? String(body.result.message_id)
|
||||
: body?.message_id != null
|
||||
? String(body.message_id)
|
||||
: null;
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
message: "sent",
|
||||
providerMessageId,
|
||||
responseJson: JSON.stringify(body),
|
||||
};
|
||||
}
|
||||
|
||||
const root = {
|
||||
health: () => ({
|
||||
ok: true,
|
||||
service: "telegram_backend",
|
||||
now: new Date().toISOString(),
|
||||
}),
|
||||
|
||||
enqueueTelegramOutbound: async ({ input }: { input: any }) => {
|
||||
const run = await enqueueTelegramOutboundTask({
|
||||
omniMessageId: String(input.omniMessageId ?? ""),
|
||||
chatId: String(input.chatId ?? ""),
|
||||
text: String(input.text ?? ""),
|
||||
businessConnectionId: input.businessConnectionId != null ? String(input.businessConnectionId) : null,
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
message: "enqueued",
|
||||
runId: run.runId,
|
||||
};
|
||||
},
|
||||
|
||||
sendTelegramMessage: async ({ input }: { input: any }) => {
|
||||
const result = await sendTelegramMessage({
|
||||
chatId: String(input.chatId ?? ""),
|
||||
text: String(input.text ?? ""),
|
||||
businessConnectionId: input.businessConnectionId != null ? String(input.businessConnectionId) : null,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
export function startServer() {
|
||||
const server = createServer(async (req, res) => {
|
||||
if (!req.url || !req.method) {
|
||||
writeJson(res, 404, { ok: false, error: "not_found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === "/health" && req.method === "GET") {
|
||||
writeJson(res, 200, {
|
||||
ok: true,
|
||||
service: "telegram_backend",
|
||||
now: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === "/webhooks/telegram/business" && req.method === "POST") {
|
||||
if (!isWebhookAuthorized(req)) {
|
||||
writeJson(res, 401, { ok: false, error: "invalid_webhook_secret" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const envelope = parseTelegramBusinessUpdate(body);
|
||||
const run = await enqueueTelegramInboundTask(envelope);
|
||||
|
||||
writeJson(res, 200, {
|
||||
ok: true,
|
||||
queued: true,
|
||||
runId: run.runId,
|
||||
providerEventId: envelope.providerEventId,
|
||||
idempotencyKey: envelope.idempotencyKey,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const statusCode = message.startsWith("payload_too_large:") ? 413 : 503;
|
||||
writeJson(res, statusCode, {
|
||||
ok: false,
|
||||
error: "receiver_enqueue_failed",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === "/graphql" && req.method === "POST") {
|
||||
if (!isGraphqlAuthorized(req)) {
|
||||
writeJson(res, 401, { errors: [{ message: "unauthorized" }] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = (await readJsonBody(req)) as {
|
||||
query?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
operationName?: string;
|
||||
};
|
||||
|
||||
const result = await graphql({
|
||||
schema,
|
||||
source: String(body.query || ""),
|
||||
rootValue: root,
|
||||
variableValues: body.variables || {},
|
||||
operationName: body.operationName,
|
||||
});
|
||||
|
||||
writeJson(res, 200, result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const statusCode = message.startsWith("payload_too_large:") ? 413 : 400;
|
||||
writeJson(res, statusCode, { errors: [{ message }] });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
writeJson(res, 404, { ok: false, error: "not_found" });
|
||||
});
|
||||
|
||||
server.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`[telegram_backend] listening on :${PORT}`);
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
310
telegram_backend/src/telegram.ts
Normal file
310
telegram_backend/src/telegram.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { JsonObject, OmniInboundEnvelopeV1 } from "./types";
|
||||
|
||||
const MAX_TEXT_LENGTH = 4096;
|
||||
const TELEGRAM_FILE_MARKER = "tg-file:";
|
||||
|
||||
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.read_business_message) return "read_business_message";
|
||||
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 normalizeNumber(value: unknown) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeId(value: unknown) {
|
||||
if (value == null) return null;
|
||||
const text = String(value).trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function pickTelegramChatPhotoFileId(source: JsonObject | null | undefined) {
|
||||
const photo = asObject(asObject(source).photo);
|
||||
return normalizeString(photo.small_file_id) ?? normalizeString(photo.big_file_id);
|
||||
}
|
||||
|
||||
type TelegramMediaInfo = {
|
||||
kind: "voice" | "audio" | "video_note" | null;
|
||||
fileId: string | null;
|
||||
durationSec: number | null;
|
||||
mimeType: string | null;
|
||||
title: string | null;
|
||||
};
|
||||
|
||||
function pickTelegramMedia(message: JsonObject): TelegramMediaInfo {
|
||||
const voice = asObject(message.voice);
|
||||
if (Object.keys(voice).length > 0) {
|
||||
return {
|
||||
kind: "voice",
|
||||
fileId: normalizeString(voice.file_id),
|
||||
durationSec: normalizeNumber(voice.duration),
|
||||
mimeType: normalizeString(voice.mime_type),
|
||||
title: "Voice message",
|
||||
};
|
||||
}
|
||||
|
||||
const audio = asObject(message.audio);
|
||||
if (Object.keys(audio).length > 0) {
|
||||
const performer = normalizeString(audio.performer);
|
||||
const title = normalizeString(audio.title) ?? normalizeString(audio.file_name);
|
||||
const combinedTitle = performer && title ? `${performer} - ${title}` : title ?? performer;
|
||||
return {
|
||||
kind: "audio",
|
||||
fileId: normalizeString(audio.file_id),
|
||||
durationSec: normalizeNumber(audio.duration),
|
||||
mimeType: normalizeString(audio.mime_type),
|
||||
title: combinedTitle,
|
||||
};
|
||||
}
|
||||
|
||||
const videoNote = asObject(message.video_note);
|
||||
if (Object.keys(videoNote).length > 0) {
|
||||
return {
|
||||
kind: "video_note",
|
||||
fileId: normalizeString(videoNote.file_id),
|
||||
durationSec: normalizeNumber(videoNote.duration),
|
||||
mimeType: null,
|
||||
title: "Video note",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: null,
|
||||
fileId: null,
|
||||
durationSec: null,
|
||||
mimeType: null,
|
||||
title: 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 receivedAt = new Date().toISOString();
|
||||
|
||||
// Handle read_business_message separately — different payload structure
|
||||
const readEvent = asObject(update.read_business_message);
|
||||
if (Object.keys(readEvent).length > 0) {
|
||||
const readChat = asObject(readEvent.chat);
|
||||
const threadExternalId = normalizeId(readChat.id);
|
||||
const businessConnectionId = normalizeString(readEvent.business_connection_id);
|
||||
const updateId = update.update_id;
|
||||
const providerEventId =
|
||||
(updateId != null && requireString(updateId, "")) || makeFallbackEventId(raw);
|
||||
const occurredAt = isoFromUnix(readEvent.date) ?? receivedAt;
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
idempotencyKey: ["telegram_business", providerEventId, "read"].join(":"),
|
||||
provider: "telegram_business",
|
||||
channel: "TELEGRAM",
|
||||
direction: "IN",
|
||||
providerEventId,
|
||||
providerMessageId: null,
|
||||
eventType: "read_business_message",
|
||||
occurredAt,
|
||||
receivedAt,
|
||||
payloadRaw: raw,
|
||||
payloadNormalized: {
|
||||
threadExternalId,
|
||||
contactExternalId: threadExternalId,
|
||||
text: null,
|
||||
businessConnectionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const message = pickMessage(update);
|
||||
|
||||
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 ownerChatId = normalizeId(businessConnection.user_chat_id);
|
||||
const chatId = normalizeId(chat.id);
|
||||
const fromId = normalizeId(from.id);
|
||||
|
||||
let contactSource: JsonObject | null = null;
|
||||
if (ownerChatId) {
|
||||
// Prefer the counterparty id/source (different from connected owner chat id).
|
||||
if (chatId && chatId !== ownerChatId) contactSource = chat;
|
||||
if (fromId && fromId !== ownerChatId) {
|
||||
if (!contactSource || direction === "IN") {
|
||||
contactSource = from;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!contactSource) {
|
||||
contactSource = direction === "OUT" && Object.keys(chat).length > 0 ? chat : from;
|
||||
}
|
||||
|
||||
const fallbackContactSource = contactSource === chat ? from : chat;
|
||||
const contactAvatarFileId = pickTelegramChatPhotoFileId(contactSource);
|
||||
|
||||
const threadExternalId =
|
||||
chat.id != null
|
||||
? String(chat.id)
|
||||
: businessConnection.user_chat_id != null
|
||||
? String(businessConnection.user_chat_id)
|
||||
: null;
|
||||
|
||||
const contactExternalId =
|
||||
normalizeId(contactSource?.id) ??
|
||||
normalizeId(fallbackContactSource?.id) ??
|
||||
null;
|
||||
|
||||
const media = pickTelegramMedia(message);
|
||||
const text =
|
||||
cropText(message.text) ??
|
||||
cropText(message.caption) ??
|
||||
(media.kind === "voice"
|
||||
? "[voice message]"
|
||||
: media.kind === "video_note"
|
||||
? "[video note]"
|
||||
: media.kind === "audio"
|
||||
? media.title
|
||||
? `[audio] ${media.title}`
|
||||
: "[audio]"
|
||||
: null);
|
||||
|
||||
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,
|
||||
mediaKind: media.kind,
|
||||
mediaFileId: media.fileId,
|
||||
mediaDurationSec: media.durationSec,
|
||||
mediaMimeType: media.mimeType,
|
||||
mediaTitle: media.title,
|
||||
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: contactAvatarFileId ? `${TELEGRAM_FILE_MARKER}${contactAvatarFileId}` : null,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
24
telegram_backend/src/types.ts
Normal file
24
telegram_backend/src/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type OmniInboundChannel = "TELEGRAM" | "WHATSAPP" | "INSTAGRAM" | "PHONE" | "EMAIL" | "INTERNAL";
|
||||
|
||||
export type OmniInboundEnvelopeV1 = {
|
||||
version: 1;
|
||||
idempotencyKey: string;
|
||||
provider: string;
|
||||
channel: OmniInboundChannel;
|
||||
direction: "IN" | "OUT";
|
||||
providerEventId: string;
|
||||
providerMessageId: string | null;
|
||||
eventType: string;
|
||||
occurredAt: string;
|
||||
receivedAt: string;
|
||||
payloadRaw: unknown;
|
||||
payloadNormalized: {
|
||||
threadExternalId: string | null;
|
||||
contactExternalId: string | null;
|
||||
text: string | null;
|
||||
businessConnectionId: string | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type JsonObject = Record<string, unknown>;
|
||||
Reference in New Issue
Block a user