# Slack: команда /horoscope

Хтось у каналі пише `/horoscope leo`, і в канал приходить гороскоп Лева на сьогодні. Slack надсилає команду POST-запитом на ваш сервер і чекає відповіді не довше за 3 секунди, тож серверу потрібна публічна адреса з HTTPS. Бібліотека Bolt не потрібна: увесь обмін це один запит і одна відповідь.

## 1. Застосунок Slack

1. На [api.slack.com/apps](https://api.slack.com/apps) натисніть **Create New App → From scratch** і виберіть робочий простір.
2. **Slash Commands → Create New Command**: команда `/horoscope`, **Request URL** `https://bot.example.com/slack/horoscope`, **Usage Hint** `leo`.
3. **Basic Information → App Credentials**: скопіюйте **Signing Secret**.
4. **Install App → Install to Workspace**.
5. Ключ AstroWay **Тільки сервер** з областю `public/horoscope/*`, як описано в розділі [Ключ для автоматизації](/integrations/).

```bash frame="terminal"
export ASTROWAY_API_KEY="aw_live_..."
export SLACK_SIGNING_SECRET="..."
```

## 2. Код

Збережіть як `slack.mjs` і запустіть `node slack.mjs`:

```js title="slack.mjs"
// Node 22 or newer, no dependencies.
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

const ASTROWAY = 'https://api.astroway.info/v1';
const LANG = 'uk';
// One text per sign per day: at most 12 calls a day, whatever the traffic.
const cache = new Map();
const SIGNS = ['aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo',
  'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces'];

function signedBySlack(raw, timestamp, signature) {
  // Older than five minutes reads as a replay.
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = 'v0=' + createHmac('sha256', process.env.SLACK_SIGNING_SECRET)
    .update(`v0:${timestamp}:${raw}`).digest('hex');
  return signature?.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

async function horoscope(sign) {
  const key = `${new Date().toISOString().slice(0, 10)} ${sign}`;
  if (!cache.has(key)) {
    const res = await fetch(`${ASTROWAY}/public/horoscope/daily?sign=${sign}&lang=${LANG}`, {
      headers: { 'X-Api-Key': process.env.ASTROWAY_API_KEY },
    });
    const json = await res.json();
    if (!json.ok) return { response_type: 'ephemeral', text: `AstroWay: ${json.error.message}` };
    cache.set(key, `*${sign}, ${json.data.date}*\n${json.data.horoscope}`);
  }
  return { response_type: 'in_channel', text: cache.get(key) };
}

createServer(async (req, res) => {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const raw = Buffer.concat(chunks).toString();
  if (!signedBySlack(raw, req.headers['x-slack-request-timestamp'], req.headers['x-slack-signature'])) {
    res.writeHead(401).end();
    return;
  }

  const sign = new URLSearchParams(raw).get('text')?.trim().toLowerCase() ?? '';
  const reply = SIGNS.includes(sign)
    ? await horoscope(sign)
    : { response_type: 'ephemeral', text: `Usage: /horoscope leo\n${SIGNS.join(', ')}` };
  res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(reply));
}).listen(process.env.PORT ?? 3000);
```

## Як це працює

- **Підпис.** Slack підписує кожен запит: HMAC-SHA256 від `v0:<timestamp>:<тіло>` вашим Signing Secret. Код перевіряє підпис і відкидає запит, старший за п'ять хвилин, тож чужий або повторений запит отримує `401`.
- **`in_channel` чи `ephemeral`.** Гороскоп бачать усі в каналі, а підказку чи помилку бачить лише той, хто набрав команду.
- **3 секунди.** Перший запит знака за день іде в API, наступні беруться з кешу за кілька мілісекунд. Якщо замінити гороскоп на щось повільніше, наприклад PDF-звіт, відповідайте одразу, а результат надсилайте пізніше на `response_url` із тіла запиту.
- **Ліміт.** Ключ Free на `/v1/public/*` рахується як запит без ключа, 30 на годину з IP сервера; кеш тримає бот у межах 12 запитів на добу. Неправильний ключ тут не дає `401`, а мовчки отримує той самий анонімний ліміт.

**Note:**
Перевірено 2026-11-07: сервер із прикладу запущено локально і викликано запитами з тим самим набором полів і підписом, що надсилає Slack; гороскоп прийшов із продакшену AstroWay, повторний запит знака відповів із кешу за 2 мс, підроблений і застарілий підписи отримали `401`. У справжньому робочому просторі Slack команду ми не встановлювали.
