# Telegram-бот

Бот із двома командами: `/horoscope leo` повертає гороскоп знака на сьогодні, `/moon` поточну фазу Місяця. Він забирає повідомлення з Telegram сам (long polling), тож йому не потрібні вебхук, домен чи HTTPS: досить будь-якої машини з Node.js 22.

## 1. Бот і ключ

1. У Telegram напишіть [@BotFather](https://t.me/BotFather) команду `/newbot` і збережіть токен, який він видасть.
2. У дашборді створіть ключ **Тільки сервер**, як описано в розділі [Ключ для автоматизації](/integrations/). Область ендпоінтів: `public/horoscope/*` і `public/moon-phase`. Обидва ендпоінти кредитів не витрачають.
3. Задайте обидва значення як змінні середовища, а не в коді:

```bash frame="terminal"
export TELEGRAM_BOT_TOKEN="123456:ABC..."
export ASTROWAY_API_KEY="aw_live_..."
```

## 2. Код

Збережіть як `bot.mjs`:

```js title="bot.mjs"
// Node 22 or newer, no dependencies.
const TELEGRAM = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`;
const ASTROWAY = 'https://api.astroway.info/v1';
const LANG = 'uk';
const SIGNS = ['aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo',
  'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces'];

// A day's answer per path: 12 signs and one Moon are at most 13 calls a day.
const cache = new Map();

async function astroway(path) {
  const key = `${new Date().toISOString().slice(0, 10)} ${path}`;
  if (cache.has(key)) return cache.get(key);
  const res = await fetch(`${ASTROWAY}${path}`, {
    headers: { 'X-Api-Key': process.env.ASTROWAY_API_KEY },
  });
  const json = await res.json();
  if (!json.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
  cache.set(key, json.data);
  return json.data;
}

async function answer(text) {
  const [word, arg = ''] = text.trim().split(/\s+/);
  const command = word.split('@')[0]; // in a group the command arrives as /horoscope@YourBot
  if (command === '/horoscope') {
    const sign = arg.toLowerCase();
    if (!SIGNS.includes(sign)) return `Send a sign: /horoscope leo\n${SIGNS.join(', ')}`;
    const data = await astroway(`/public/horoscope/daily?sign=${sign}&lang=${LANG}`);
    return `${data.date}, ${sign}\n\n${data.horoscope}`.slice(0, 4000);
  }
  if (command === '/moon') {
    const m = await astroway(`/public/moon-phase?lang=${LANG}`);
    return `${m.localized.phaseName}, ${m.illuminationPercent}%, Moon in ${m.localized.moonSign}\n`
      + `${m.localized.meaning}\nNext new moon: ${m.next.newMoon}`;
  }
  return 'Commands: /horoscope <sign>, /moon';
}

async function telegram(method, body) {
  const res = await fetch(`${TELEGRAM}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  return res.json();
}

let offset = 0;
for (;;) {
  const updates = await telegram('getUpdates', { offset, timeout: 30 });
  for (const update of updates.result ?? []) {
    offset = update.update_id + 1;
    const message = update.message;
    if (!message?.text) continue;
    let reply;
    try {
      reply = await answer(message.text);
    } catch (err) {
      reply = `AstroWay: ${err.message}`;
    }
    await telegram('sendMessage', { chat_id: message.chat.id, text: reply });
  }
}
```

Запустіть `node bot.mjs` і напишіть ботові `/moon`. Відповідь має такий вигляд:

```text
Спадний серп, 7.85%, Moon in Терези
Завершення циклу: відпочинок, очищення й підготовка до нового.
Next new moon: 2026-11-09T07:02:07Z
```

`LANG` перемикає мову тексту гороскопу й назв: `uk`, `en` та інші мови з [переліку](/languages/). Підписи на кшталт `Moon in` у коді англійською, змініть їх під себе.

## Чому в коді кеш

Гороскоп знака однаковий для всіх, хто питає того дня, і фаза Місяця теж. Кеш на добу означає не більше за 13 запитів до API на день, скільки б людей не писало ботові.

Це важливо на Free: ключ Free на `/v1/public/*` рахується в той самий ліміт, що й запит без ключа, 30 запитів на годину з IP-адреси вашого сервера. Без кешу бот, у якого кілька десятків користувачів, упреться в нього за годину. Ключ платного тарифу рахується за [лімітом тарифу](/rate-limits-credits/).

**Caution:**
Неправильний ключ на `/v1/public/*` не дає `401`: запит обслуговується як анонімний і отримує анонімний ліміт. Якщо бот раптом відповідає `PUBLIC_RATE_LIMIT`, спершу перевірте, що `ASTROWAY_API_KEY` справді заданий у середовищі процесу.

## Що далі

- **Натальна карта.** Ендпоінти з розрахунком за моментом народження (`POST /v1/chart` коштує 20 кредитів) приймають ту саму пару дата-час-зона-координати, що описана в розділі [Інтеграції без коду](/integrations/). Боту знадобиться діалог із кількох кроків або [Mini App](https://core.telegram.org/bots/webapps) з формою.
- **Вебхук замість polling.** Коли бот переїде на сервер з HTTPS, `setWebhook` прибирає постійне з'єднання. Логіка `answer()` не змінюється.

**Note:**
Перевірено 2026-11-07: функція `answer()` і запити до AstroWay виконані проти продакшену з усіма п'ятьма видами повідомлень із прикладу. Сам Telegram у перевірці підмінено локальним сервером, який відповідає у форматі Bot API: справжнього бота ми не запускали.
