// src/telegram/webhook.ts
var SECRET_HEADER = "x-telegram-bot-api-secret-token";
function readHeader(headers, name) {
  const lower = name.toLowerCase();
  for (const [key, value] of Object.entries(headers)) {
    if (key.toLowerCase() === lower) return value;
  }
  return void 0;
}
function validateTelegramWebhook(headers, _body, secret) {
  if (!secret) return false;
  const provided = readHeader(headers, SECRET_HEADER);
  if (!provided) return false;
  return provided === secret;
}

// src/telegram/bot.ts
var DEFAULT_API_BASE = "https://api.telegram.org";
function buildReplyMarkup(markup) {
  return markup ? JSON.stringify(markup) : void 0;
}
var TelegramApiError = class extends Error {
  method;
  errorCode;
  description;
  constructor(method, errorCode, description) {
    super(`Telegram API ${method} failed: ${description ?? "unknown error"}${errorCode ? ` (code ${errorCode})` : ""}`);
    this.name = "TelegramApiError";
    this.method = method;
    this.errorCode = errorCode;
    this.description = description;
  }
};
function createTelegramBot(config) {
  if (!config.token) throw new Error("createTelegramBot: token is required");
  const apiBase = config.apiBase ?? DEFAULT_API_BASE;
  const fetchImpl = config.fetch ?? fetch;
  async function callApi(method, payload) {
    const url = `${apiBase}/bot${config.token}/${method}`;
    const init = payload instanceof FormData ? { method: "POST", body: payload } : {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(payload ?? {})
    };
    const res = await fetchImpl(url, init);
    const json = await res.json();
    if (!json.ok || json.result === void 0) {
      throw new TelegramApiError(method, json.error_code, json.description);
    }
    return json.result;
  }
  function bufferToBlob(data, mimeType) {
    const view = data instanceof Uint8Array ? data : new Uint8Array(data);
    return new Blob([view], mimeType ? { type: mimeType } : void 0);
  }
  return {
    token: config.token,
    webhookSecret: config.webhookSecret,
    apiBase,
    callApi: (method, payload) => callApi(method, payload),
    async sendMessage(chatId, text, opts = {}) {
      const payload = {
        chat_id: chatId,
        text
      };
      if (opts.parseMode) payload.parse_mode = opts.parseMode;
      if (opts.replyMarkup) payload.reply_markup = opts.replyMarkup;
      if (opts.replyToMessageId !== void 0) payload.reply_to_message_id = opts.replyToMessageId;
      if (opts.disableWebPagePreview !== void 0) payload.disable_web_page_preview = opts.disableWebPagePreview;
      if (opts.disableNotification !== void 0) payload.disable_notification = opts.disableNotification;
      return callApi("sendMessage", payload);
    },
    async sendVoice(chatId, voice, opts = {}) {
      const form = new FormData();
      form.append("chat_id", String(chatId));
      form.append(
        "voice",
        bufferToBlob(voice, opts.mimeType ?? "audio/ogg"),
        opts.fileName ?? "voice.ogg"
      );
      if (opts.caption) form.append("caption", opts.caption);
      if (opts.parseMode) form.append("parse_mode", opts.parseMode);
      if (opts.duration !== void 0) form.append("duration", String(opts.duration));
      if (opts.replyToMessageId !== void 0) form.append("reply_to_message_id", String(opts.replyToMessageId));
      const rm = buildReplyMarkup(opts.replyMarkup);
      if (rm) form.append("reply_markup", rm);
      return callApi("sendVoice", form);
    },
    async sendPhoto(chatId, photo, opts = {}) {
      const form = new FormData();
      form.append("chat_id", String(chatId));
      form.append(
        "photo",
        bufferToBlob(photo, opts.mimeType ?? "image/jpeg"),
        opts.fileName ?? "photo.jpg"
      );
      if (opts.caption) form.append("caption", opts.caption);
      if (opts.parseMode) form.append("parse_mode", opts.parseMode);
      if (opts.replyToMessageId !== void 0) form.append("reply_to_message_id", String(opts.replyToMessageId));
      const rm = buildReplyMarkup(opts.replyMarkup);
      if (rm) form.append("reply_markup", rm);
      return callApi("sendPhoto", form);
    },
    async editMessage(chatId, messageId, text, opts = {}) {
      const payload = {
        chat_id: chatId,
        message_id: messageId,
        text
      };
      if (opts.parseMode) payload.parse_mode = opts.parseMode;
      if (opts.replyMarkup) payload.reply_markup = opts.replyMarkup;
      if (opts.disableWebPagePreview !== void 0) payload.disable_web_page_preview = opts.disableWebPagePreview;
      return callApi("editMessageText", payload);
    },
    async answerCallbackQuery(callbackQueryId, opts = {}) {
      const payload = { callback_query_id: callbackQueryId };
      if (opts.text) payload.text = opts.text;
      if (opts.showAlert !== void 0) payload.show_alert = opts.showAlert;
      if (opts.url) payload.url = opts.url;
      if (opts.cacheTime !== void 0) payload.cache_time = opts.cacheTime;
      return callApi("answerCallbackQuery", payload);
    },
    async getFile(fileId) {
      return callApi("getFile", { file_id: fileId });
    }
  };
}

// src/telegram/file-download.ts
var MIME_BY_EXT = {
  oga: "audio/ogg",
  ogg: "audio/ogg",
  mp3: "audio/mpeg",
  m4a: "audio/mp4",
  wav: "audio/wav",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  png: "image/png",
  gif: "image/gif",
  webp: "image/webp",
  pdf: "application/pdf"
};
async function downloadTelegramFile(bot, fileId, fetchImpl = fetch) {
  const file = await bot.getFile(fileId);
  if (!file.file_path) {
    throw new Error(`downloadTelegramFile: getFile returned no file_path for ${fileId}`);
  }
  const url = `${bot.apiBase}/file/bot${bot.token}/${file.file_path}`;
  const res = await fetchImpl(url);
  if (!res.ok) {
    throw new Error(`downloadTelegramFile: fetch ${file.file_path} failed with ${res.status}`);
  }
  const arrayBuffer = await res.arrayBuffer();
  const data = Buffer.from(arrayBuffer);
  const fileName = file.file_path.split("/").pop();
  const ext = fileName?.split(".").pop()?.toLowerCase();
  const headerMime = res.headers.get("content-type") ?? void 0;
  const mimeType = headerMime ?? (ext && MIME_BY_EXT[ext]) ?? "application/octet-stream";
  return {
    data,
    mimeType,
    fileName,
    filePath: file.file_path
  };
}

// src/telegram/router.ts
var SLASH_COMMAND = /^\/(\w+)(?:@\w+)?(?:\s+([\s\S]+))?$/;
function defaultLogger() {
  return {
    info: (msg, meta) => console.info(`[telegram-router] ${msg}`, meta ?? {}),
    warn: (msg, meta) => console.warn(`[telegram-router] ${msg}`, meta ?? {}),
    error: (msg, meta) => console.error(`[telegram-router] ${msg}`, meta ?? {})
  };
}
function parseCallback(data) {
  const idx = data.indexOf("|");
  if (idx === -1) return { action: data, payload: "" };
  return { action: data.slice(0, idx), payload: data.slice(idx + 1) };
}
var TelegramRouter = class {
  bot;
  logger;
  fallbackSlashToText;
  slashHandlers = /* @__PURE__ */ new Map();
  callbackHandlers = /* @__PURE__ */ new Map();
  textHandler;
  voiceHandler;
  photoHandler;
  documentHandler;
  constructor(bot, opts = {}) {
    this.bot = bot;
    this.logger = opts.logger ?? defaultLogger();
    this.fallbackSlashToText = opts.fallbackSlashToText ?? true;
  }
  onSlashCommand(name, handler) {
    const key = name.startsWith("/") ? name.slice(1) : name;
    this.slashHandlers.set(key.toLowerCase(), handler);
  }
  onCallback(action, handler) {
    this.callbackHandlers.set(action, handler);
  }
  onText(handler) {
    this.textHandler = handler;
  }
  onVoice(handler) {
    this.voiceHandler = handler;
  }
  onPhoto(handler) {
    this.photoHandler = handler;
  }
  onDocument(handler) {
    this.documentHandler = handler;
  }
  async dispatch(update) {
    try {
      if (update.callback_query) {
        await this.dispatchCallback(update, update.callback_query);
        return;
      }
      const message = update.message ?? update.edited_message;
      if (!message) {
        this.logger.info("update has no message or callback_query \u2014 ignoring", {
          update_id: update.update_id
        });
        return;
      }
      if (message.voice) {
        await this.runHandler(
          "voice",
          this.voiceHandler,
          () => this.voiceHandler({
            bot: this.bot,
            chatId: message.chat.id,
            userId: message.from?.id ?? 0,
            messageId: message.message_id,
            voice: message.voice,
            message,
            from: message.from,
            update
          })
        );
        return;
      }
      if (message.photo && message.photo.length > 0) {
        await this.runHandler(
          "photo",
          this.photoHandler,
          () => this.photoHandler({
            bot: this.bot,
            chatId: message.chat.id,
            userId: message.from?.id ?? 0,
            messageId: message.message_id,
            photo: message.photo,
            caption: message.caption,
            message,
            from: message.from,
            update
          })
        );
        return;
      }
      if (message.document) {
        await this.runHandler(
          "document",
          this.documentHandler,
          () => this.documentHandler({
            bot: this.bot,
            chatId: message.chat.id,
            userId: message.from?.id ?? 0,
            messageId: message.message_id,
            document: message.document,
            caption: message.caption,
            message,
            from: message.from,
            update
          })
        );
        return;
      }
      if (typeof message.text === "string") {
        await this.dispatchText(update, message);
        return;
      }
      this.logger.info("message kind unsupported \u2014 ignoring", {
        update_id: update.update_id,
        chat_id: message.chat.id
      });
    } catch (err) {
      this.logger.error("dispatch crashed", { error: serializeError(err), update_id: update.update_id });
    }
  }
  async dispatchCallback(update, query) {
    const data = query.data ?? "";
    const { action, payload } = parseCallback(data);
    const handler = this.callbackHandlers.get(action);
    if (!handler) {
      this.logger.info("no callback handler \u2014 ignoring", { action, data });
      return;
    }
    const ctx = {
      bot: this.bot,
      chatId: query.message?.chat.id ?? 0,
      userId: query.from.id,
      messageId: query.message?.message_id,
      callbackQueryId: query.id,
      action,
      payload,
      data,
      query,
      update
    };
    await this.runHandler(`callback:${action}`, handler, () => handler(ctx));
  }
  async dispatchText(update, message) {
    const text = message.text ?? "";
    const match = SLASH_COMMAND.exec(text);
    if (match) {
      const command = match[1].toLowerCase();
      const args = match[2] ?? "";
      const handler = this.slashHandlers.get(command);
      if (handler) {
        await this.runHandler(
          `slash:/${command}`,
          handler,
          () => handler({
            bot: this.bot,
            chatId: message.chat.id,
            userId: message.from?.id ?? 0,
            messageId: message.message_id,
            command,
            args,
            message,
            from: message.from,
            update
          })
        );
        return;
      }
      if (!this.fallbackSlashToText) {
        this.logger.info("no slash handler \u2014 ignoring", { command });
        return;
      }
    }
    if (this.textHandler) {
      await this.runHandler(
        "text",
        this.textHandler,
        () => this.textHandler({
          bot: this.bot,
          chatId: message.chat.id,
          userId: message.from?.id ?? 0,
          messageId: message.message_id,
          text,
          message,
          from: message.from,
          update
        })
      );
      return;
    }
    this.logger.info("no text handler \u2014 ignoring", { chat_id: message.chat.id });
  }
  async runHandler(label, handler, invoke) {
    if (!handler) {
      this.logger.info(`no handler \u2014 ignoring`, { kind: label });
      return;
    }
    try {
      await invoke();
    } catch (err) {
      this.logger.error(`handler ${label} threw`, { error: serializeError(err) });
    }
  }
};
function serializeError(err) {
  if (err instanceof Error) {
    return { name: err.name, message: err.message, stack: err.stack };
  }
  return { value: String(err) };
}

// src/telegram/formatting.ts
var HTML_ENTITIES = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;"
};
function escapeHtml(text) {
  return text.replace(/[&<>]/g, (ch) => HTML_ENTITIES[ch] ?? ch);
}
function formatBold(text) {
  return `<b>${escapeHtml(text)}</b>`;
}
function formatItalic(text) {
  return `<i>${escapeHtml(text)}</i>`;
}
function formatCode(text) {
  return `<code>${escapeHtml(text)}</code>`;
}
function formatPre(text) {
  return `<pre>${escapeHtml(text)}</pre>`;
}
function formatLink(text, url) {
  return `<a href="${escapeHtmlAttribute(url)}">${escapeHtml(text)}</a>`;
}
function formatTierSeparator(label) {
  return `\u2501\u2501\u2501 ${label} \u2501\u2501\u2501`;
}
var INBOX_CATEGORY_EMOJI = {
  kids_school: "\u{1F392}",
  kids_sports: "\u26BD",
  kids_medical: "\u{1F3E5}",
  family_personal: "\u{1F468}\u200D\u{1F469}\u200D\u{1F467}",
  finance_bills: "\u{1F4B0}",
  finance_receipts: "\u{1F9FE}",
  security_alert: "\u26A0\uFE0F",
  subscription: "\u{1F4B8}",
  promotional: "\u{1F4F0}",
  default: "\u{1F4E7}"
};
function formatEmoji(category) {
  return INBOX_CATEGORY_EMOJI[category] ?? INBOX_CATEGORY_EMOJI.default;
}
function escapeHtmlAttribute(value) {
  return value.replace(/[&<>"]/g, (ch) => {
    if (ch === "&") return "&amp;";
    if (ch === "<") return "&lt;";
    if (ch === ">") return "&gt;";
    return "&quot;";
  });
}

// src/telegram/inline-keyboard.ts
function createInlineKeyboard(rows) {
  return { inline_keyboard: rows };
}
function actionButton(text, action, payload = "") {
  const data = payload ? `${action}|${payload}` : action;
  if (data.length > 64) {
    throw new Error(
      `actionButton: callback_data exceeds Telegram's 64-byte limit (got ${data.length})`
    );
  }
  return { text, callback_data: data };
}
function urlButton(text, url) {
  return { text, url };
}

// src/telegram/pending-proposal.ts
function createApprovalKeyboard(proposalId, opts = {}) {
  const yesAction = opts.yesAction ?? "approve";
  const editAction = opts.editAction ?? "edit";
  const discardAction = opts.discardAction ?? "discard";
  return createInlineKeyboard([
    [
      actionButton(opts.yesLabel ?? "\u2705 Yes", yesAction, proposalId),
      actionButton(opts.editLabel ?? "\u270F\uFE0F Edit", editAction, proposalId),
      actionButton(opts.discardLabel ?? "\u{1F5D1} Discard", discardAction, proposalId)
    ]
  ]);
}
export {
  INBOX_CATEGORY_EMOJI,
  TelegramApiError,
  TelegramRouter,
  actionButton,
  createApprovalKeyboard,
  createInlineKeyboard,
  createTelegramBot,
  downloadTelegramFile,
  escapeHtml,
  formatBold,
  formatCode,
  formatEmoji,
  formatItalic,
  formatLink,
  formatPre,
  formatTierSeparator,
  urlButton,
  validateTelegramWebhook
};
//# sourceMappingURL=index.mjs.map