Перейти к содержимому

Вторая система на той же ленте

Обновлено 17 октября 2026

Что получится: вторая система — аналитика, аудит, хранилище переписки — получает копию всех уведомлений канала по своему адресу. Основное приложение при этом продолжает читать очередь или вебхук как раньше: подписка ничего у него не забирает. Приёмник — на PHP, с проверкой подписи и защитой от повторов.

                     ┌──► очередь или вебхук ──► основное приложение (бот, CRM)
 события канала ─────┤
                     └──► подписка «analytics» ──► приёмник на PHP ──► events.db
                          подпись HMAC, свои повторы

Что понадобится

  • канал с ключом и переменные GRATIO_URL, GRATIO_ID, GRATIO_TOKEN;
  • PHP 8 с PDO SQLite на сервере с публичным HTTPS-адресом.

Шаг 1. Приёмник

Файл gratio.php. Он проверяет подпись по сырому телу, отсекает повторную доставку по X-Gratio-Delivery и сохраняет уведомление.

<?php
const TOLERANCE = 300;                         // подпись не старше 5 минут

function gratio_verify(string $secret, string $body, string $header): bool {
    $parts = [];
    foreach (explode(',', $header) as $p) {
        [$k, $v] = array_pad(explode('=', $p, 2), 2, '');
        $parts[$k] = $v;
    }
    $t = (int)($parts['t'] ?? 0);
    if (!$t || empty($parts['v1']) || abs(time() - $t) > TOLERANCE) {
        return false;
    }
    return hash_equals(hash_hmac('sha256', $t . '.' . $body, $secret), $parts['v1']);
}

$body = file_get_contents('php://input');
if (!gratio_verify(getenv('GRATIO_SUB_SECRET'), $body, $_SERVER['HTTP_X_GRATIO_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

$db = new PDO('sqlite:' . __DIR__ . '/events.db');
$db->exec('create table if not exists events (
    delivery text primary key, instance text, type text, chat_id text, body text, at int)');
$event = json_decode($body, true);
$stmt = $db->prepare('insert or ignore into events values (?, ?, ?, ?, ?, ?)');
$stmt->execute([
    $_SERVER['HTTP_X_GRATIO_DELIVERY'] ?? '',
    $_SERVER['HTTP_X_GRATIO_INSTANCE'] ?? '',
    $event['typeWebhook'] ?? '',
    $event['senderData']['chatId'] ?? ($event['chatId'] ?? ''),
    $body,
    $event['timestamp'] ?? time(),
]);
http_response_code(200);
echo 'ok';

Повтор приходит с тем же X-Gratio-Delivery — insert or ignore его пропустит, а ответ 200 скажет Gratio, что всё принято.

Шаг 2. Создайте подписку

Подписку создаёт ключ канала. Секрет не передаём — Gratio создаст его сам и покажет один раз:

curl

curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/subscriptions/$GRATIO_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "analytics",
    "url": "https://bi.example.ru/gratio.php"
  }'

Python

import os
import requests

BASE = f"{os.environ['GRATIO_URL']}/waInstance{os.environ['GRATIO_ID']}"
TOKEN = os.environ["GRATIO_TOKEN"]

url = f"{BASE}/subscriptions/{TOKEN}"
payload = {"name": "analytics", "url": "https://bi.example.ru/gratio.php"}
response = requests.post(url, json=payload, timeout=60)
print(response.status_code, response.json())

Node.js

const { GRATIO_URL, GRATIO_ID, GRATIO_TOKEN } = process.env;
const base = `${GRATIO_URL}/waInstance${GRATIO_ID}`;

const url = `${base}/subscriptions/${GRATIO_TOKEN}`;
const payload = { name: "analytics", url: "https://bi.example.ru/gratio.php" };
const response = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});
console.log(response.status, await response.json());

PHP

<?php
$base = getenv('GRATIO_URL') . '/waInstance' . getenv('GRATIO_ID');
$url = $base . '/subscriptions/' . getenv('GRATIO_TOKEN');
$payload = ['name' => 'analytics', 'url' => 'https://bi.example.ru/gratio.php'];

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 60,
]);
$response = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), PHP_EOL;
print_r($response);

Вы увидите:

{"id": "sub_9f2c41d0", "name": "analytics", "url": "https://bi.example.ru/gratio.php",
 "createdAt": 1792000000, "pending": 0, "oldestPendingAt": null, "dead": 0,
 "lastDeliveredAt": null, "lastError": null, "lastErrorAt": null, "system": false,
 "created": true, "secret": "5d0c7a…e81a"}

Сохраните secret в переменной GRATIO_SUB_SECRET на сервере приёмника. Больше его не покажут: потеряли — создайте подписку с тем же name и своим secret, адрес и очередь останутся.

Шаг 3. Проверьте

Напишите на номер канала с телефона и посмотрите, что пришло:

sqlite3 events.db "select type, chat_id, at from events order by at desc limit 5"

Вы увидите:

incomingMessageReceived|8695836|1791823658

Основное приложение получило то же сообщение своим путём — проверьте его журнал.

Шаг 4. Следите за доставкой

Список подписок показывает, сколько уведомлений ждут доставки и почему последняя не удалась:

curl "$GRATIO_URL/waInstance$GRATIO_ID/subscriptions/$GRATIO_TOKEN"
{"subscriptions": [{"id": "sub_9f2c41d0", "name": "analytics", "url": "https://bi.example.ru/gratio.php",
  "createdAt": 1792000000, "pending": 0, "oldestPendingAt": null, "dead": 0,
  "lastDeliveredAt": 1792000100, "lastError": null, "lastErrorAt": null, "system": false}]}
Признак Что значит Что делать
pending растёт, lastError — HTTP 500, тайм-аут приёмник недоступен; Gratio повторяет 1, 5, 15, 60, 300 с, затем каждые 5 минут — сутки поднять приёмник, уведомления дойдут сами
dead больше нуля, lastError — HTTP 401 или другой 4xx приёмник пять раз подряд отказал — уведомление отложено, очередь идёт дальше проверить секрет и код приёмника
system: true служебная подписка Gratio не трогать: менять её ключом канала нельзя

Шаг 5. Смените секрет или снимите подписку

Новый секрет — тот же POST с тем же name и своим secret (16–128 символов). Снять — DELETE с id подписки в конце пути:

curl -X DELETE "$GRATIO_URL/waInstance$GRATIO_ID/subscriptions/$GRATIO_TOKEN/sub_9f2c41d0"

Вы увидите: {"removed": true}.

Что дальше

Была ли статья полезна?