javascript

n8n — масштабируем получение SMS и уведомлений с нескольких (десятков) SIM-карт одновременно

  • понедельник, 29 декабря 2025 г. в 00:00:08
https://habr.com/ru/articles/981276/

TL;DR Автор в прошлой статье настроил Telegram-чат, куда несколько смартфонов скидывают пуши с помощью MacroDroid и/или Tasker. Проблема в том, что смартфоны брали на себя слишком много работы. Что, если они будут тонкими клиентами, которые шлют сырые данные на сервер, где уже происходит вся обработка и рассылка? Автор делится workflow и конфигурацией для n8n, которые позволяют это реализовать в режиме "Быстрого старта".

Дисклеймеры

Общий дисклеймерО личности автораОтказ от ответственностиОб использовании ChatGPT

Аннотация

В статье рассматривается вопрос более гибкого и масштабируемого решения получения уведомлений с нескольких SIM-карт - за счёт некоего центрального сервера, в данном случае n8n. За счёт этого у администратора n8n-сервера появляется возможность полностью динамически настраивать всю бизнес-логику: от обработки поступающих данных и форматирования до рассылки уведомлений в Telegram (и не только). Рассказывается также об истории создания проекта, способах создания конфигурации для него и настройке не только сервера, но и конечных устройств. В данный момент автор использует данный сетап. Рассмотрены вопросы, проблемы и процесс создания всего конвейера - от MacroDroid/Tasker до n8n.

Введение

Предположим, что вы смогли-таки реализовать сетап с MacroDroid/Tasker из прошлой статьи. Или вы просто столкнулись с необходимостью масштабировать получение SMS и уведомлений с нескольких десятков SIM-карт одновременно. И, вдобавок, нужно пересылать эти уведомления в Telegram, причём не только себе. Вдобавок, периодически. Вдобавок, срочно, желательно в ту же секунду. Вручную подобным мазохизмом заниматься не хочется, да и не получится. А вот автоматизировать - можно, да и, положа руку на сердце - придётся.

В качестве затравки может прочитать предыдущую статью, где описаны подробные критерии выбора смартфонов и принцип работы с MacroDroid/Tasker. Здесь основной упор будет выполнен на централизацию существующего решения.

Почему n8n?

Рассматривал следующее:

  1. Написать код на TypeScript (NestJS), упаковать и запустить на сервере, который будет принимать HTTP-запросы от смартфонов и обрабатывать их;

  2. Индустриальный стек мониторинга и алертинга: Prometheus + AlertManager + Grafana + какой-нибудь экспортер SMS/уведомлений. Всё это дело у меня уже имеется, осталось только написать экспортеры или (что более вероятно) HTTP-запросы для PushGateway или VictoriaLogs или Vector;

  3. Третий вариант подкрался неожиданно - ребята раздавали экземпляры n8n бесплатно. Ну, что ж, а раз дело дошло до бесплатного, то почему бы и не попробовать?

n8n - это low-code платформа для автоматизации рабочих процессов. Она позволяет создавать сложные интеграции и автоматизации с минимальным количеством кода, используя визуальный интерфейс. В моём случае это было именно то, что нужно: быстрое создание рабочих процессов для обработки входящих данных и отправки уведомлений.

Что ж, пишем воркфлоу на нём, благо целый парк SIM-карт и телефонов у меня уже имеется.

А как получилось настроить смартфоны?

Кратко: на каждый смартфон поставил MacroDroid или Tasker с плагином AutoNotification, настроил группу с Telegram-ботом, созданным в @BotFather.

Какой конфиг придумал?

Концепция

Принцип работы следующий, подсмотренный у ядра XRay:

Есть единый конфиг, полностью и почти что в императивном стиле описывающий, что принимать, как обрабатывать, в каком виде рендерить и куда слать. Входящий webhook принимает HTTP-запросы от смартфонов (MacroDroid/Tasker), затем просто происходит цепочка маппингов и темплейтингов, заигрывания с сырыми данными и по конвейеру выдающему последней ноде список адресов и payload, куда и как слать запросы.

Будет несколько сущностей:

  • Recipients - получатели уведомлений (например, Telegram userId или chatId). Содержит метаданные о получателе, например, разрешённые deviceId (чтобы не слали посторонние смартфоны), предпочтительный язык, дополнительные плейсхолдеры и т.д. Также содержит секреты для API (например, Telegram Bot token) и, самое главное - объект rules. С помощью него можно очень гибко настраивать, какие именно уведомления и при каких условиях этот получатель хочет нужные данные. Например, только SMS с OTP-кодами, только от определённых номеров, только с определёнными ключевыми словами и т.д. Recipients связывает Templates и Endpoints;

  • Templates - шаблоны сообщений, которые будут рендериться с помощью плейсхолдеров. Например, шаблон для SMS с OTP-кодом может выглядеть так: "Получено SMS от {data.fromPhoneNumber}: {data.body}. Код: {data.otpCodes[0]}". Шаблоны могут быть разными для разных типов уведомлений (SMS, push-уведомления и т.д.);

  • Endpoints - конечные точки, куда будут отправляться уведомления. Например, для Telegram это будет URL вида https://api.telegram.org/bot{recipient.botToken}/sendMessage, с параметрами chat_id={recipient.chatId} и text={%text%}. Можно также добавить поддержку других платформ, например, Slack, Email и т.д.;

Собственно, всё. Этот конфиг задаёт всё поведение системы от начала и до конца. Но он будет не самым простым, и вам придётся его создать самостоятельно, пока я не написал веб-конструктов для него.

Проектирование модели

Как истинный ООПшник (кстати, что такое ООП?), решил я начать с проектирования. В моём случае я описал модельки полезной нагрузки (payload) для входящих HTTP-запросов от смартфонов:

Например, SMS-сообщение
export interface SmsReceivedDataModel {
  /** Discriminator. */
  type: "sms_received";

  /** Sender address/number (may be alphanumeric in some regions). */
  fromName?: string;
  fromNameInContacts?: string;
  fromPhoneNumber?: string;

  /** Message body text. */
  body: string;

  /** ISO 8601 timestamp when SMS was received (device time). */
  at?: string;

  /** SIM slot index if known (1/2). */
  simSlot?: number;

  /** Subscription/carrier name if available. */
  carrier?: string;

  /** Message center address if captured. */
  serviceCenter?: string;

  /** True if message looks like OTP (if your automation tags it). */
  isOtp?: boolean;

  /** Extracted OTP code(s) if automation parses it. */
  otpCodes?: string[];

  /** Optional thread id / conversation id from SMS provider. */
  threadId?: string;

  /** Optional app/package that captured the SMS (some automations rely on notifications). */
  capturedByPackage?: string;

  /** Optional keyword tags computed by automation app. */
  tags?: string[];
}
Конфиг для n8n workflow
/**
 * Core configuration for an n8n workflow that:
 * - receives "data" from an automation app via webhook
 * - selects recipients & templates
 * - renders messages (templating)
 * - sends to external HTTP endpoints (e.g., Telegram Bot API)
 *
 * All top-level entities are objects (maps) keyed by id to match your preference
 * (objects over arrays) and enable stable lookups in n8n.
 */
export interface ConfigModel {
  /** Schema version for migrations and backward compatibility. */
  version: string;
  tokens: Record<string, string>[];

  /** Optional human label for the configuration instance. */
  name?: string;

  /**
   * A map of recipients by `id`.
   *
   * Key MUST match `Recipient.id`.
   */
  recipients: Record<RecipientId, RecipientModel>;

  /**
   * A map of endpoints by `id`.
   *
   * Endpoints may reference `{recipient.*}` placeholders.
   */
  endpoints: Record<EndpointId, EndpointModel>;

  /**
   * A map of templates by `id`.
   *
   * Templates may reference placeholders from data/recipient/meta.
   */
  templates: Record<TemplateId, TemplateModel>;

  /**
   * Optional device registry. If omitted, "device checks" can rely solely on deviceId
   * strings embedded in incoming payloads and recipients.allowedDeviceIds.
   */
  devices?: Record<DeviceId, DeviceDefinition>;

  /**
   * Optional global defaults applied by n8n when building outbound requests.
   * Per-endpoint settings override these defaults.
   */
  defaults?: {
    /** Default timeout in milliseconds for outbound HTTP calls. */
    httpTimeoutMs?: number;

    /** Default headers applied to outbound HTTP calls unless overridden. */
    headers?: Record<string, string>;

    /**
     * If true, the system should reject any placeholder that cannot be resolved
     * (fail-fast). If false, unresolved placeholders may be left as-is or replaced
     * with empty string depending on runtime policy.
     */
    strictTemplating?: boolean;
  };
}


/**
 * Core configuration for an n8n workflow that:
 * - receives "data" from an automation app via webhook
 * - selects recipients & templates
 * - renders messages (templating)
 * - sends to external HTTP endpoints (e.g., Telegram Bot API)
 *
 * All top-level entities are objects (maps) keyed by id to match your preference
 * (objects over arrays) and enable stable lookups in n8n.
 */
export interface ConfigModel {
  /** Schema version for migrations and backward compatibility. */
  version: string;
  tokens: Record<string, string>[];

  /** Optional human label for the configuration instance. */
  name?: string;

  /**
   * A map of recipients by `id`.
   *
   * Key MUST match `Recipient.id`.
   */
  recipients: Record<RecipientId, RecipientModel>;

  /**
   * A map of endpoints by `id`.
   *
   * Endpoints may reference `{recipient.*}` placeholders.
   */
  endpoints: Record<EndpointId, EndpointModel>;

  /**
   * A map of templates by `id`.
   *
   * Templates may reference placeholders from data/recipient/meta.
   */
  templates: Record<TemplateId, TemplateModel>;

  /**
   * Optional device registry. If omitted, "device checks" can rely solely on deviceId
   * strings embedded in incoming payloads and recipients.allowedDeviceIds.
   */
  devices?: Record<DeviceId, DeviceDefinition>;

  /**
   * Optional global defaults applied by n8n when building outbound requests.
   * Per-endpoint settings override these defaults.
   */
  defaults?: {
    /** Default timeout in milliseconds for outbound HTTP calls. */
    httpTimeoutMs?: number;

    /** Default headers applied to outbound HTTP calls unless overridden. */
    headers?: Record<string, string>;

    /**
     * If true, the system should reject any placeholder that cannot be resolved
     * (fail-fast). If false, unresolved placeholders may be left as-is or replaced
     * with empty string depending on runtime policy.
     */
    strictTemplating?: boolean;
  };
}

/* ----------------------------- IDs / Aliases ----------------------------- */

export type RecipientId = string;
export type EndpointId = string;
export type TemplateId = string;
export type DeviceId = string;

/* -------------------------------- Recipient ------------------------------ */

/**
 * A "recipient" is a business-logic target that receives templated messages via one
 * or more endpoints. Example: a Telegram user/channel/chat ID plus extra vars.
 */
export interface RecipientModel {
  /** Optional display name for UI or logs. */
  name?: string;
  disabled?: boolean;

  /**
   * Recipient-scoped variables for templating, e.g.:
   * { "tgChatId": "123456", "lang": "ru", "timezone": "Asia/Bangkok" }
   *
   * Use `{recipient.vars.tgChatId}` (or `{recipient.vars.lang}`) in templates/endpoints.
   */
  vars: Record<string, string | number | boolean | null>;

  /**
   * Which incoming devices are allowed to trigger messages to this recipient.
   * If omitted or empty, all devices are allowed (unless your runtime enforces otherwise).
   */
  allowedDeviceIds?: DeviceId[];

  /**
   * Which endpoint(s) the recipient is eligible to send through.
   * If omitted or empty, runtime may allow all endpoints or require explicit mapping
   * depending on your policy.
   */
  endpointIds?: EndpointId[];

  /**
   * Which template(s) may be used for this recipient.
   * If omitted or empty, runtime may allow all templates or require explicit mapping
   * depending on your policy.
   */
  templateIds?: TemplateId[];

  /**
   * Optional filtering by data types for this recipient.
   * If omitted, all data types are allowed.
   */
  allowedDataTypes?: DataType[];

  /**
   * Optional per-recipient rendering preferences.
   * Useful for future UI and consistent formatting.
   */
  rendering?: {
    /** Language preference for template selection, if you implement i18n. */
    language?: string;

    /** Timezone for formatting timestamps. */
    timezone?: string;

    /** If true, escape HTML (Telegram HTML mode) or perform endpoint-specific escaping. */
    escapeMode?: "none" | "telegram_html" | "telegram_markdownv2";
  };

  /**
   * Optional "routing rules" to select specific template(s) and/or endpoint(s)
   * based on incoming data. This enables business logic in config instead of n8n code.
   */
  rules: RecipientRuleModel[];
}

/**
 * A rule evaluated in order; first match wins (typical policy).
 */
export interface RecipientRuleModel {
  /** Unique id of the rule (for UI, debugging, and test referencing). */
  id: string;

  /** Optional human description. */
  description?: string;
  type: DataType;

  /** Match conditions; all specified conditions must match (AND). */
  when: FieldPredicate[];

  /**
   * Actions to apply when the rule matches.
   * These override or constrain the recipient's default templateIds/endpointIds.
   */
  then: {
    /** Use these templates (in order). */
    templateIds?: TemplateId[];

    /** Send via these endpoints (in order). */
    endpointIds?: EndpointId[];
  };
}

/**
 * A simple field predicate for config-driven routing.
 */
export interface FieldPredicate {
  /** Path in the runtime object (you define actual resolver). */
  path: string;

  /** Comparison operator. */
  operator:
    | "equals"
    | "not_equals"
    | "includes"
    | "not_includes"
    | "starts_with"
    | "ends_with"
    | "matches_regex"
    | "gt"
    | "gte"
    | "lt"
    | "lte"
    | "exists";

  /** Comparison value; omitted for `exists`. */
  value?: string | number | boolean | null;
}

/* -------------------------------- Endpoint -------------------------------- */

/**
 * Defines how to send an HTTP request.
 * URLs, headers, query, and body may include `{recipient.*}` placeholders.
 *
 * NOTE: Actual auth secrets should ideally NOT be embedded directly here unless
 * encrypted or stored in n8n credentials; model allows it for completeness.
 */
export interface EndpointModel {
  /** Optional display name for UI or logs. */
  name?: string;

  /** HTTP method. */
  method: HttpMethod;

  /**
   * Fully qualified URL including path.
   * May contain templated placeholders from recipient vars, e.g.:
   *   "https://api.telegram.org/bot{recipient.vars.botToken}/sendMessage"
   */
  url: string;

  /**
   * Optional query string parameters.
   * Values may include placeholders.
   */
  searchParams?: Record<string, string>;

  /**
   * Optional headers.
   * Values may include placeholders.
   */
  headers?: Record<string, string>;

  /**
   * Optional request body settings.
   * If not provided, runtime can send no body.
   */
  body?: EndpointBody;

  /**
   * Timeout in milliseconds for this endpoint call.
   * Overrides config.defaults.httpTimeoutMs.
   */
  timeoutMs?: number;

  /**
   * Optional retry policy for transient failures.
   */
  retry?: {
    /** Maximum attempts including the first try. */
    maxAttempts: number;
    /** Backoff strategy. */
    backoff: "fixed" | "exponential";
    /** Base delay in milliseconds. */
    baseDelayMs: number;
    /** Only retry on these HTTP status codes, if specified. */
    retryOnStatusCodes?: number[];
  };

  /**
   * Optional expectation/validation on response for success determination.
   * Useful for endpoints like Telegram that always return JSON with ok=true/false.
   */
  successCriteria?: {
    /** Treat these status codes as success. If omitted, use 200-299. */
    statusCodes?: number[];
    /**
     * A JSON path to a boolean "success" field.
     * Example for Telegram: "ok"
     */
    jsonBooleanPath?: string;
  };
}

export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

export interface EndpointBody {
  /**
   * Body format.
   * - json: typical REST payload
   * - form_urlencoded: Telegram often supports application/x-www-form-urlencoded
   * - raw: string payload (e.g., pre-rendered JSON or text)
   */
  type: "json" | "form_urlencoded" | "raw";

  /**
   * Body content.
   * - For json/form_urlencoded: object where leaf values may contain placeholders
   * - For raw: a string which may contain placeholders
   */
  content: Record<string, unknown> | string;
}

/* -------------------------------- Templates ------------------------------- */

/**
 * A template is a named message payload with placeholders.
 * You can keep it generic, or add endpoint-specific "rendering targets" later.
 */
export interface TemplateModel {
  /** Optional human name for UI/logs. */
  name?: string;

  /**
   * Main template string. Placeholders use `{...}` and are resolved at runtime.
   *
   * Recommended placeholder namespaces:
   * - {data.*}      incoming data payload fields
   * - {recipient.*} recipient fields/vars
   * - {meta.*}      envelope info (deviceId, receivedAt, etc.)
   */
  text: string;

  /**
   * Optional endpoint-specific formatting metadata.
   * Example: Telegram sendMessage parse_mode.
   */
  format?: {
    /** Controls how the receiving endpoint should parse markup. */
    parseMode?: "plain" | "telegram_html" | "telegram_markdownv2";

    /** If true, strip unknown placeholders (otherwise fail or keep). */
    ignoreUnknownPlaceholders?: boolean;
  };

  /**
   * Optional additional fields that can be used as payload parts
   * (e.g., title, subject, caption), depending on endpoint mapping.
   */
  fields?: Record<string, string>;
}

/* --------------------------------- Devices -------------------------------- */

/**
 * Optional device registry entry to enrich logs and enable per-device gating.
 */
export interface DeviceDefinition {
  /** Optional friendly name. */
  name?: string;

  /** Optional platform description. */
  platform?: "android" | "ios" | "other";

  /** Optional additional metadata. */
  vars?: Record<string, string | number | boolean | null>;
}

Да, вышло заморочно-таки, особенно со второй моделькой. Да, писалось нейронкой, но затем на 60% отредактировано мной.

Первая версия workflow для n8n

Первая версия писалась очень долго. Решил я тогда однозначно всё-таки вкатиться в вайб-кодинг, причём по уму: настроить проект в VS Code, nx monorepo (ибо будет несколько проектов), copilot-instructions.md/AGENTS.md, MCP, Skills и т. д.

Кто не шарит за вайб-код, MCP - это типа как API внешних инструментов для ИИ-агентов на основе нейросетей. В данном случае оно поможет нейронке правильно создавать и редактировать workflow у n8n. Для MCP с n8n есть классный проект n8n-mcp, его и подключаем для Windows через npm:

npm i n8n-mcp -g

В mcp.json прописываем (не забудьте подставить свои значения):

{
  "mcp": {
    "servers": {
      "n8n-mcp": {
        "command": "npx",
        "args": ["n8n-mcp"],
        "env": {
          "N8N_API_URL": "http://localhost:5678",
          "N8N_API_KEY": "your-api-key"
        }
      }
    }
  }
}

После этого можно создавать и редактировать workflow в n8n с помощью нейронки. Грузанул её контекстом, моделями, привёл аналогии, простые примеры, чего я хочу. Промпт бы потянул на тонкую книжку!

Но, к сожалению, нейросети, даже хвалёные GPT-5.2 и Gemini 3 Pro, создавали лишь что-то типа такого:

Тот самый ИИ на пике формы. "Дайте нам ещё пять триллионов, семь АЭС и весь рынок оперативки, дальше точно будет лучше, мы обещаем, точно будет!.."
Тот самый ИИ на пике формы. "Дайте нам ещё пять триллионов, семь АЭС и весь рынок оперативки, дальше точно будет лучше, мы обещаем, точно будет!.."

Не, ну так можно было и на моём любимом TypeScript написать. Вся бизнес-логика в одной ноде, ей Б-гу! Отхохотавшись вволю, решил писать сам по-старинке, без вайб-кодинга, но местами с помощью нейронки.

P. S. Надо отдать должное - всё завелось с первого раза, но смысл тогда был в n8n, если мне от него нужна была одна нода со всей бизнес-логикой? Так не пойдёт. Дальше пишем сами.

Вторая версия

Нейросети не пользовались фичами n8n, поэтому начал делить 9К строк кода на отдельные ноды, каждая из которых выполняет одну функцию:

Ну, вот, другое дело...
Ну, вот, другое дело...

Добавим ветвящуюся логику, поддержку GET- и POST-запросов:

Ну, вот, ещё более другое дело...
Ну, вот, ещё более другое дело...

Выделим внутреннюю проверку ошибок в If-ноды, будем выкидывать разные ошибки - от авторизации до валидации и маппинга.

Итоговый workflow получился таким:

Ваще другое дело жестб
Ваще другое дело жестб

И что оно умеет?

Теперь можно играться как угодно с маппингом данных, их содержимым, фильтровать входящие уведомления об СМС или пушах и отправлять как угодно в каком угодно виде. Всё сделано так, что вам здесь нужно изменить всего лишь одну ноду:

Нода Config
Нода Config

- и оно будет делать как описано.

Каждое устройство имеет свой токен-ключ, без них n8n не пропустит левые запросы. Для дебага с HTTP-клиента можно добавить токен с именем debug. Токены придумываете сами, сохраняете в конфиге n8n. Смартфоны обязаны использовать ключи в HTTP-заголовке x-n8n-device-relay.

Ключевую роль играет шаблонизация. Реализована она в виде таких строк: {data.body} или {device.id} - они позволяют ссылаться куда и как угодно. По сути, вы можете не соблюдать мои гайдлайны и практически весь конфиг переделать под свои модельки и слать с мобилок любые JSON'ки - бизнес-логика сама позаботится о правильном рендеринге значений, если вы всё сделали правильно.

Честно, не хотел изобретать такой велосипед с шаблонизацией, но я не нашёл возможности прикрутить, например, Jinja в n8n (особенно учитывая, что он не мой). Поэтому попросил нейронку написать простую шаблонизацию на JavaScript, который и использую в одной из нод. Работает вполне себе нормально - можно ссылаться на вложенные объекты, но, кажется, массивы оно не поддерживает.

Итоговый конфиг выглядит примерно так:

Пример конфига (минимальный)
{
  "version": "1.0.0",
  "tokens": [
    {
      "name": "myphone",
      "key": "<THINK_OF_A_SECRET_KEY_YOURSELF>"
    },
    {
      "name": "debug",
      "key": "<THIS_TOKEN_WITH_DEBUG_NAME_BYPASSES_DEVICE_CHECK_USE_ONLY_FOR_TESTING>"
    }
  ],
  "recipients": {
    "me": {
      "vars": {
        "botToken": "<INSERT_TELEGRAM_BOT_TOKEN>",
        "tgChatId": "<INSERT_TELEGRAM_CHAT_ID>"
      },
      "rules": [
        {
          "id": "rule_sms_received",
          "type": "sms_received",
          "when": [
            {
              "path": "device.id",
              "operator": "equals",
              "value": "myphone"
            }
          ],
          "then": {
            "endpointIds": [
              "telegram_sendMessage_post"
            ],
            "templateIds": [
              "template_sms_1_ru"
            ]
          }
        },
        {
          "id": "rule_call_received",
          "type": "call_received",
          "when": [
            {
              "path": "device.id",
              "operator": "equals",
              "value": "myphone"
            }
          ],
          "then": {
            "endpointIds": [
              "telegram_sendMessage_post"
            ],
            "templateIds": [
              "template_call_1_ru"
            ]
          }
        },
        {
          "id": "rule_wifi_connected",
          "type": "wifi_connected",
          "when": [
            {
              "path": "device.id",
              "operator": "equals",
              "value": "myphone"
            }
          ],
          "then": {
            "endpointIds": [
              "telegram_sendMessage_post"
            ],
            "templateIds": [
              "template_wifi_1_ru"
            ]
          }
        },
        {
          "id": "rule_notification_received",
          "type": "notification_received",
          "when": [
            {
              "path": "device.id",
              "operator": "equals",
              "value": "myphone"
            }
          ],
          "then": {
            "endpointIds": [
              "telegram_sendMessage_post"
            ],
            "templateIds": [
              "template_app_1_ru"
            ]
          }
        },
        {
          "id": "rule_notification_received",
          "type": "notification_received",
          "when": [
            {
              "path": "device.id",
              "operator": "equals",
              "value": "myphone"
            },
            {
              "path": "data.body",
              "operator": "matches_regex",
              "value": "(?:.+)?SOME STRING(?:.+)?"
            }
          ],
          "then": {
            "endpointIds": [
              "telegram_sendMessage_post"
            ],
            "templateIds": [
              "template_app_1_ru"
            ]
          }
        }
      ]
    }
  },
  "endpoints": {
    "telegram_sendMessage_get": {
      "method": "GET",
      "url": "https://api.telegram.org/bot{recipient.vars.botToken}/sendMessage",
      "searchParams": {
        "chat_id": "{recipient.vars.tgChatId}",
        "text": "{%text%}"
      }
    },
    "telegram_sendMessage_post": {
      "method": "POST",
      "url": "https://api.telegram.org/bot{recipient.vars.botToken}/sendMessage",
      "body": {
        "type": "json",
        "content": {
          "chat_id": "{recipient.vars.tgChatId}",
          "text": "{%text%}"
        }
      }
    }
  },
  "templates": {
    "template_sms_1_ru": {
      "text": "📄 Входящее смс от: {data.fromName} (номер телефона: {data.fromPhoneNumber})\n\n📖 Текст:\n{data.body}\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%,\n📟 Имя SIM: {device.cellularCarrierName} (ID: {device.currentSimSlotIndex}),\n📶 Сеть Wi-Fi: {device.currentWifiSsid}"
    },
    "template_call_1_ru": {
      "text": "📞 Входящий звонок от: {data.fromName} (номер телефона: {data.fromPhoneNumber})\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%,\n📟 Имя SIM: {device.cellularCarrierName} (ID: {device.currentSimSlotIndex}),\n📶 Сеть Wi-Fi: {device.currentWifiSsid}"
    },
    "template_wifi_1_ru": {
      "text": "🛜 Сеть Wi-Fi подключена!\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%"
    },
    "template_app_1_ru": {
      "text": "🔔 Уведомление от приложения: {data.appName} ({data.packageName})\n\n📖 Заголовок:\n{data.title}\n\n📖 Текст:\n{data.body}\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%"
    }
  }
}

Как вам?) Давайте разберём его подробнее. Надеюсь, вы зацените результат.

Тяжёлое. Как написать такой конфиг?

Начинаем строить JSON'ку! Пишите:

{

}

- всё нижеперечисленное пихаем внутрь.

version

version - версия схемы конфига. Это не ваша версия, а служебное значение. Пока что 1.0.0. Конкретно сейчас роли не играет, но в будущем может понадобиться для миграций и обратной совместимости.

"version": "1.0.0",

tokens

tokens - список токенов-ключей для авторизации устройств. Каждый токен имеет имя и ключ. Каждый смартфон должен использовать свой ключ в заголовке x-n8n-device-relay. Для дебага предусмотрен токен с именем debug, который пропускает проверку устройства.

"tokens": [
  {
    "name": "myphone",
    "key": "<THINK_OF_A_SECRET_KEY_YOURSELF>"
  },
  {
    "name": "debug",
    "key": "<THIS_TOKEN_WITH_DEBUG_NAME_BYPASSES_DEVICE_CHECK_USE_ONLY_FOR_TESTING>"
  }
],

recipients

recipients - карта получателей уведомлений. Каждый получатель имеет свои переменные, правила и настройки. В примере есть получатель с id me, который хранит в себе переменные botToken и tgChatId для отправки им сообщений в Telegram.


"recipients": {
  "me": {
    "disabled": true,
    "vars": {
      "botToken": "<INSERT_TELEGRAM_BOT_TOKEN>",
      "tgChatId": "<INSERT_TELEGRAM_CHAT_ID>"
    },
    "allowedDeviceIds": [
      "myphone"
    ],
    "endpointIds": [
      "telegram_sendMessage_post"
    ],
    "templateIds": [
      "template_sms_1_ru",
      "template_call_1_ru",
      "template_wifi_1_ru",
      "template_app_1_ru"
    ],
    "rules": [
      {
        "id": "rule_sms_received",
        "type": "sms_received",
        "when": [
          {
            "path": "device.id",
            "operator": "equals",
            "value": "myphone"
          }
        ],
        "then": {
          "endpointIds": [
            "telegram_sendMessage_post"
          ],
          "templateIds": [
            "template_sms_1_ru"
          ]
        }
      }
    ]
  }
},

Разберём его подробнее:

  • vars - переменные получателя, которые можно использовать в шаблонах templates и конечных точках endpoints. В данном случае это токен бота и ID чата Telegram. Вы можете добавить любые другие переменные по своему усмотрению и использовать их затем в шаблонах и эндпоинтах (будут описаны нижу);

  • rules - список правил для данного получателя. Каждое правило имеет:

    • Уникальный в пределах recipient'а id;

    • Тип данных type (например, sms_received, call_received, wifi_connected, notification_received);

    • Условия when. В условиях можно указать путь к полю, оператор (equals, not_equals, includes, not_includes, starts_with, ends_with, matches_regex, gt, gte, lt, lte, exists) и значение. Особенно полезен оператор matches_regex для фильтрации уведомлений по ключевым словам, в значении (value) вы указываете регулярное выражение без косых черт //и модификаторов типа gmi;

    • Действия then. В действиях указываются идентификаторы шаблонов и конечных точек, которые будут использоваться при совпадении условия;

    Важно! Внутри правила всё в массиве when работает по логике И (AND) - все условия должны быть выполнены для срабатывания правила.

  • Опционально можно добавить т. н. "белые списки" устройств (allowedDeviceIds), шаблонов (templateIds) и конечных точек (endpointIds) на уровне получателя, чтобы ограничить доступ. Они переопределяют rules. Например, если в rules есть конечная точка telegram_sendMessage_post, но allowedEndpointIds существует и этого эндпоинта там нет, то правило не выполнится;

  • Иногда клиент нужен, но не сейчас. Тогда ставим disabled: true, и recipient будет отключён. Поле опционально, поэтому можно убрать его вообще.

endpoints

endpoints - карта конечных точек для отправки уведомлений. В примере есть две конечные точки для отправки HTTP-запросов (пока что по дефолту - только Telegram, но можете добавить что угодно своё). Поддерживаются методы GET и POST.

"endpoints": {
    "telegram_sendMessage_get": {
      "method": "GET",
      "url": "https://api.telegram.org/bot{recipient.vars.botToken}/sendMessage",
      "searchParams": {
        "chat_id": "{recipient.vars.tgChatId}",
        "text": "{%text%}"
      }
    },
    "telegram_sendMessage_post": {
      "method": "POST",
      "url": "https://api.telegram.org/bot{recipient.vars.botToken}/sendMessage",
      "body": {
        "type": "json",
        "content": {
          "chat_id": "{recipient.vars.tgChatId}",
          "text": "{%text%}"
        }
      }
    }
},

Сразу можно заметить, что в URL и параметрах используются плейсхолдеры вида {recipient.vars.botToken} и {%text%}. Первый - это переменная получателя, а второй - это текст, сгенерированный из шаблона. {%text%} - специальный "магический" плейсхолдер, смотрящий в подходящий шаблон (так как движок рендеринга значений смотрит лишь в data, а в templates у него вход заказан).

templates

templates - шаблоны (no shit Sherlock!) текста. В примере есть несколько шаблонов для разных типов уведомлений (SMS, звонки, подключение к Wi-Fi, уведомления приложений). Каждый шаблон имеет текст с плейсхолдерами для динамического заполнения.

"templates": {
    "template_sms_1_ru": {
      "text": "📄 Входящее смс от: {data.fromName} (номер телефона: {data.fromPhoneNumber})\n\n📖 Текст:\n{data.body}\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%,\n📟 Имя SIM: {device.cellularCarrierName} (ID: {device.currentSimSlotIndex}),\n📶 Сеть Wi-Fi: {device.currentWifiSsid}"
    },
    "template_call_1_ru": {
      "text": "📞 Входящий звонок от: {data.fromName} (номер телефона: {data.fromPhoneNumber})\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%,\n📟 Имя SIM: {device.cellularCarrierName} (ID: {device.currentSimSlotIndex}),\n📶 Сеть Wi-Fi: {device.currentWifiSsid}"
    },
    "template_wifi_1_ru": {
      "text": "🛜 Сеть Wi-Fi подключена!\n\nСеть Wi-Fi: {device.currentWifiSsid}\nДанные о локации:\n- {device.locationPoint}\n- {device.locationLink}\n- Ссылка на последнее известное местоположение: {device.locationLastTimestamp}\n- Скорость аппарата: {device.currentSpeed} км/ч.\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%"
    },
    "template_app_1_ru": {
      "text": "🔔 Уведомление от приложения: {data.appName} ({data.packageName})\n\n📖 Заголовок:\n{data.title}\n\n📖 Текст:\n{data.body}\n\nℹ️ Доп. данные:\n🔋 Заряд: {device.batteryLevelPercent}%"
    }
}

Тут уже довольно просто. Можете использовать переменные, самих шаблонов делаете сколько угодно. Используете их в recipient'ах, в объекте rules.

Как только конфиг будет готов, переходим к следующему шагу.

Я приготовил n8n, сделал конфиг - но это не workflow. Где его взять?

Всё сделано до вас: копипастите этот workflow (raw) в свой n8n, Ctrl + V в окне с новым workflow.

Видите этот участок:

Подсказка, где редактировать конфиг
Подсказка, где редактировать конфиг

- два раза там кликаете по ноде Config и вставляете свою JSON'ку.

С помощью HTTP-клиента и моей коллекции Postman можете протестировать работу workflow:

  • Выбираете нужный запрос в HTTP-клиенте;

  • В environment не забудьте вбить адрес вашего n8n-сервера и ключ устройства;

  • Во вкладке с n8n наведите на Webhook и жмите Execute workflow;

  • Жмёте Send в HTTP-клиенте;

  • Видите, что workflow отработал или нет, и почему.

И, если вздумается расширить логику, вы можете свободно можете менять содержимое нод, смотреть, как маппятся данные, что на входе и на выходе. Добавьте свои конечные точки после ноды Switch.

А как ср... слать данные?

Настраиваем смартфоны с MacroDroid или Tasker:

  1. Скачать макросы для MacroDroid или профили для Tasker (для последнего не забудьте установить плагин AutoNotification);

  2. Импортировать их в MacroDroid/Tasker на каждом смартфоне:

  • У MacroDroid это реализовано через главное меню - Импорт/Экспорт макросов - Импорт макросов;

  • У Tasker это реализовано через Профили. Удерживаете палец на вкладке Профили, выбираете Импорт;

  1. В настройках каждого макроса/профиля указать в header x-n8n-device-relay ключ устройства из вашего конфига n8n;

  2. В настройках каждого макроса/профиля указать URL вашего n8n-сервера (пример: http://your-n8n-domain.com/webhook/device-relay).

Тестируйте, проверяйте результат во вкладке n8n под названием executions.

Вы можете быстро сгенерировать MacroDroid макросы для каждого устройства скриптом PowerShell, для этого:

  • Клонируем репозиторий, в терминале переходим в его папку;

  • Копируем .\scripts\device-list.example.json или .\scripts\device-list-minimal.example.json с новым именем .\scripts\device-list.json:

Copy-Item .\scripts\device-list.example.json .\scripts\device-list.json

- редактируем его, добавляя свои устройства. Токены у устройств должны совпадать с токенами в конфиге n8n;

  • Пишем:

.\scripts\generate-macrodroid-macros-per-device.ps1
  • В папке .\scripts\devices\ появятся готовые макросы для MacroDroid, которые останется только импортировать в MacroDroid на каждом устройстве.

Оно работает! Это всё?

В общем-то, да. На самом деле, в модельке конфига много какие поля описаны, просто абсолютное большинство опциональны. Это всё потому, что те же приложения-автоматизаторы имеют очень слабо пересекающиеся множества данных, и привести их к общему знаменателю, единой модели, сложно. Как мог...

В будущем надо бы на опциональный bcrypt перейти и задуматься о поддержке iOS Shortcuts и Android Automate, но это уже другая история. Я лично счастлив с MacroDroid и Tasker.

Проблемы, вопросы и их решения

Проблема: Чому не самохостинг?

Решение: Я ждал этот вопрос. Знаете, это личное и, по-моему мнению, логичное решение, которое я описывал в статье "Чему учит постоянная релокация (или её ожидание) в контексте персональной инфраструктуры". Кратко говоря: когда ты на ходу и не нашёл свой дом, то лучше избегать самохостинга максимально. n8n в облаке или чей-то чужой n8n в данном случае - это просто в использовании. Хорошо, что оно подвернулось под руку.

Проблема: Ты палишь токены в открытом виде в конфиге! Шлёшь данные со смартфонов открытым текстом! Админ это видит, админ это сливает!

Решение: Ага. "А что ты мне сделаешь, я в другмо городе" ©. Это просто вопрос доверия к администратору n8n-сервера или надежде на принцип "неуловимого Джо". Админу с рутовыми правами ничего не мешает грепать логи n8n килограммами, или веб-сервер на verbose-логи настроить, или даже напрямую лезть в БД или var/log и кушать данные там, хех. Ладно, ладно! Давайте договоримся: в будущем планируется предварительная обработка данных bcrypt'ом - например, прямо на смартфоне (вряд ли), на своём сервере или на Cloudflare Worker'е. PR welcome.

Проблема: Так-то это можно реализовать на Vector/VictoriaLogs/PushGateway + Prometheus + AlertManager, зачем городить огород с n8n?

Решение: Я бы так и сделал). Правда, по моделькам и авторизации как у них - я не знаю. Вдобавок, я прям вижу, как вспухли бы правила алертинга у Прометея, там ведь фильтровать и маппить всё это надо. n8n мне попался чисто случайно, и я решил попробовать. Так-то подобное можно реализовать на любом бэкенде, было бы желание. У меня вышло неплохо, собой доволен.

Заключение

Таким образом, с помощью этой статьи вам удастся масштабировать получение SMS и уведомлений с парка смартфонов огромного размера (IMO до сотен или даже тысяч аппаратов), используя n8n в качестве центрального сервера для обработки и рассылки уведомлений. Вы сможете настроить гибкую бизнес-логику, шаблоны сообщений и конечные точки доставки, что позволит вам эффективно управлять большим количеством устройств и получать важные уведомления в нужное время и в нужном формате. Развитие системы планируется в сторону поддержки дополнительных платформ и улучшения безопасности передачи данных.