Интеграции розыгрышей¶
giveaway-worker читает нормализованные сообщения Twitch собственной Kafka consumer group. Постоянное IRC-подключение существующего Twitch gateway позволяет принимать команды между эфирами. Время сообщения сопоставляется с историей streams: бонусы активности начисляются только внутри эфира. Открытая страница кабинета или OBS для сбора не нужна.
Все обращения за секретами идут через Tokenator. Проверки Helix выполняет токен владельца проверяемого канала; подключение бота не заменяет OAuth-права или адресное разрешение проверяемого канала конкретному Twitch-каналу.
| Helix endpoint | Права владельца | Использование |
|---|---|---|
channels/followers?user_id=… |
moderator:read:followers |
фоллоу и минимальный срок |
subscriptions?user_id=… |
channel:read:subscriptions |
платная, Prime и подарочная подписка |
chat/chatters |
moderator:read:chatters |
все страницы, first=1000, moderator_id владельца; набор и время присутствия |
channels/vips?user_id=… |
существующие channel:read:vips или channel:manage:vips |
VIP-множитель |
users?id=… |
дополнительных scopes нет | аватары, до 100 ID в запросе |
Проверки, аватары и выбор делят лимит восемь Helix-запросов в секунду на канал внутри worker. При исчерпании бюджета учитывается Ratelimit-Reset. 401, 403, 429, сетевые ошибки и отсутствие прав означают недоступность проверки; они не превращаются в невыполненное условие. Выбранный кандидат сохраняется и повторно проверяется через 30 секунд. Подтверждение приза обслуживает независимый таймер.
Telegram-сообщество выбирается из подписок, принадлежащих владельцу Twitch-канала. Используется tg_spam_bot, либо tg_bot, и его lease из Tokenator. getMe и getChatMember проверяют административный статус бота, затем членство канонического Telegram-аккаунта из user_twitch_links → twitch_identities и user_telegram_links. Допускаются member, administrator, creator и restricted с is_member=true. Отсутствие привязки сохраняет заявку без допуска; участник связывает аккаунты и запрашивает повторную проверку на своей странице.
Сообщения о наборе, победителе и подтверждении публикуются через существующий actions outbox. Отметка доставки журнала и запись outbox выполняются одной транзакцией, ключом служит ID события. Публичные ответы и PostgreSQL NOTIFY не содержат Telegram ID, токенов или связей аккаунтов. OBS использует отдельную перевыпускаемую ссылку только для чтения.
API: Twitch Helix, Telegram Bot API. Настройки владельца: розыгрыши. Запуск и миграция: docs/operations/giveaways.md в репозитории.
Общая привязка аккаунтов через бота¶
Команда /twitch доступна в личных сообщениях Telegram-боту (включая City auth bot). internal/accountlink разрешает отправителя по user_telegram_links, затем по нативной Telegram-записи; существующая каноническая связь имеет приоритет. Код из криптографического генератора действует 10 минут. Повторная команда возвращает ещё действующий код, а после истечения создаёт новый. Код записывается в существующие twitch_identity_link_codes и city_user_twitch_link_codes одной транзакцией. Команда !code в Twitch просит повторное подтверждение той же командой в течение двух минут; после подтверждения удаляет одноразовый код и обновляет общую user_twitch_links. Розыгрыши, City и кабинет используют ту же идентичность. Код не выдаётся в группах; ошибки привязки журналируются по типу ошибки и пользовательскому/канальному идентификатору, без кода и текста ошибки БД.
Вход через Twitch и привязка к Telegram¶
Кнопка «Войти через Twitch» (OIDC, scope openid user:read:moderated_channels) открывает кабинет. Порядок выбора аккаунта при входе: аккаунт с Telegram-идентичностью, к которому уже привязан Twitch; иначе, если канал этого вещателя принадлежит ровно одному Telegram-аккаунту, у которого ещё нет Twitch, нет прав администратора бота и нет других каналов, Twitch автоматически привязывается к нему (linked_via='broadcaster', владельцу уходит уведомление в Telegram с напоминанием об «Отключить Twitch»), а ранее созданный пустой Twitch-аккаунт вливается в него; иначе — собственный Twitch-native аккаунт, где Telegram-разделы (подписки, зеркала) скрыты, а карточка подсказывает, в каком канале уже есть владелец.
Из открытого кабинета «Подключить Twitch» (OAuth в режиме привязки) только привязывает личность к текущему аккаунту: сессия не меняется, пустой аккаунт при отказе не создаётся. Редирект несёт wall_auth=success|merged|link_conflict|link_error и wall_auth_reason=other_twitch|identity_elsewhere. Обратное направление — Telegram-вход при активной Twitch-сессии (link_twitch) — выбирает существующий Telegram-аккаунт, поглощает в него Twitch-native аккаунт и отдаёт merged: true. Привязка через !code использует то же ядро слияния. Подробности переноса — в docs/database.md.
Присутствие и проверка участия¶
Для активности доступны activity_source=messages|presence|both|either (пустое значение старых настроек означает messages) и presence_minutes. Оба порога действуют в скользящем activity_window_minutes; порог присутствия должен помещаться в это окно. Учитываются только интервалы между соседними успешными снимками Chatters в одном эфире и окне открытого набора, с разрывом не более 135 секунд при периоде 90 секунд. Пропуск пользователя, ошибка API, пауза, смена эфира или долгий простой не добавляют время. Снимки и прогресс хранятся в PostgreSQL. Это присутствие в чате, не индивидуальное время просмотра видео. Билеты за активные интервалы по-прежнему требуют обычного сообщения.
!участие [id] и !giveaway-status [id] показывают только собственную заявку отправителя в текущем чате: допуск, билеты, вес или прогресс порогов. После анимации ответ учитывает подтверждение победы, срок !забрать, отклонение результата и отметку выдачи приза. Без номера показываются до двух розыгрышей; для остальных нужен номер. Ответ проходит через actions outbox, повторная доставка не дублирует ответ; ограничение — один ответ участнику на розыгрыш за 30 секунд. Команда не создаёт заявку и не начисляет билеты. Страница участника также возвращает entry.joined, текст entry.progress, entry.outcome, entry.excluded_from_draws и entry.draw_weight, без Twitch/Telegram ID и списка других участников. Вес для следующих выборов равен нулю при недопуске или исключении по результату текущего цикла; прежние билеты сохраняются.
Версия каталога прав Twitch: 2026-10-13.1: возможность moderation_messages теперь покрывает и закреп сообщений правилом pin_message (тот же moderator:manage:chat_messages, новых scopes нет). Возможность giveaway_chatters и пресет «Только нужное сейчас» учитывают активность с присутствием; используется существующий moderator:read:chatters, новые scopes не добавлены.
Ссылки зрителей, награды и исключение¶
GET /api/v1/public/giveaways/{viewer_token} и POST /recheck принимают отдельный случайный 256-битный ключ. Числовой ID и OBS credential не подходят; черновики и архив скрыты. В административном API /overlay-links возвращает ссылку страницы, действие rotate-viewer перевыпускает только её. DTO своей заявки содержит также purchase_status; он не раскрывает идентификаторы покупок, Telegram ID или чужие заявки. Страница не предоставляет список розыгрышей. Nginx исключает URL из access log и добавляет no-referrer, noindex, nofollow, noarchive.
Возможность giveaway_rewards использует существующий scope владельца channel:manage:redemptions; «Только нужное сейчас» учитывает включённые покупки в розыгрышах с открытым, приостановленным или закрытым набором. Новых названий scopes нет. Worker читает ingest.events второй группой <giveaway_worker.group_id>-rewards. Нормализованный redemption_id сохраняется один раз, затем проверяется через Helix с токеном владельца канала и тем же client ID: награда должна принадлежать приложению PulseCaster, иметь включённую очередь, а покупка — принадлежать указанному Twitch ID. Получение события само по себе не доказывает оплату.
Сервис сохраняет статус checking, проверяет условия, лимиты и открытый набор под блокировкой розыгрыша, начисляет полный пакет и ставит fulfill. PATCH FULFILLED подтверждает покупку; отклонённая покупка при разрешённом возврате получает refund и PATCH CANCELED. Статус перечитывается на следующем проходе: рестарт после успешного PATCH безопасен. Отмена Twitch снимает только билеты этой покупки, отдельной отрицательной записью журнала. API-сбой сохраняет запрос и повторяет проверку через 30 секунд; ручные запросы проверяются отдельно, не задерживая новые покупки.
Административный GET детали включает redemptions (последние 100); POST /api/v1/channels/{channel_id}/games/giveaways/{id}/refund с redemption_id запрашивает ручной возврат с правами зоны interactive и проверкой принадлежности розыгрышу. За цену, прогрессивный рост и лимиты награды Twitch отвечает существующий раздел наград. Используйте отдельную награду, без дополнительного правила начисления билетов за то же событие.
draw_mode=classic|elimination меняет представление выбора. Для исключения задаются elimination_min_seconds, elimination_max_seconds (1..60) и elimination_max_rounds (2..200). Победитель по-прежнему выбирается через crypto/rand с весами. Сервер сохраняет все исключённые ID, группы, длительности и reveal_at; OBS восстанавливает фазу по серверному времени. Пул 10 000 участвует целиком, число анимационных раундов ограничено настройкой.
Партнёрские проверки розыгрышей используют giveaway_partner_grants(grantor_channel_id, recipient_channel_id). Проверяющий розыгрыш должен быть получателем разрешения. Список условий и поиск ограничены разрешёнными партнёрами; отзыв доступа не считается невыполненным условием зрителя. Для «Только нужное сейчас» проверки чужого розыгрыша учитываются у владельца проверяемого канала только при действующем адресном разрешении. Набор OAuth scopes и версия каталога не меняются.
Telegram-сообщество подключается через существующую форму создания подписки с Chat ID и выбранным ботом. Публикации можно затем выключить, сохранив подключение для проверок. Для чужого сообщества его администратор должен согласиться на добавление бота с административными правами; владение Twitch-каналом не даёт прав на чужую Telegram-группу.
Общий чат (Shared Chat)¶
В сессии общего чата в комнату канала попадают сообщения участников соседних каналов. Их бейджи описывают роль
в чужом канале, поэтому при разборе сообщения (convertIRCMessage для IRC, chatMessageFromEventSubChatMessage
для EventSub) у сообщений из другой комнаты (source-room-id не совпадает с room-id, для EventSub —
source_broadcaster_user_id не совпадает с broadcaster_user_id) убираются бейджи broadcaster, moderator,
lead_moderator, vip, subscriber, founder. Дальше такой человек для команд, правил, статистики и роли
в users_stats — обычный зритель. Глобальные бейджи (staff, admin, global_mod, premium) остаются.
Дополнительно: серия просмотров (viewermilestone) и награды из IRC игнорируются, если пришли из другой комнаты,
а чат-команды владельца сверяют числовой Twitch ID автора, а не бейдж.
Обложки уведомлений Telegram и VK¶
Редактор обложки использует черновик: перемещение игры, логотипа, метки 18+ и маркера градиента не меняет рабочую подписку. Для кругового градиента маркер задаёт центр, для линейного — начало перехода. Существующие цвета, направление, радиус, размер области и маска сохраняются. При фоне слева координаты маркера учитывают смещение области градиента в генераторе.
У выбранной игры, логотипа и метки 18+ появляются четыре угловые ручки размера. Перетаскивание сохраняет пропорции и закрепляет противоположный угол; размер ограничен допустимыми значениями генератора. Стрелки позволяют править положение или размер с клавиатуры, Shift увеличивает шаг. Escape и отмена захвата указателя возвращают параметры до начала жеста; «Отменить действие» отменяет завершённый жест целиком. Ползунок размера и точные параметры остаются доступны. Метка 18+ на холсте показывается только при включённом возрастном статусе предпросмотра.
«Направляющие и привязка» выравнивает перемещаемый элемент по центру и краям холста, а также по границам и центрам других видимых элементов. Порог привязки — 6 экранных пикселей независимо от масштаба холста. Alt временно отключает привязку; на телефоне доступен выключатель. Стрелки и шесть кнопок выравнивания позволяют задавать точное положение. При перемещении маркера градиента учитывается смещение его области при фоне слева.
У выбранного кругового градиента ручка ↔ меняет радиус, у линейного ручка ↻ меняет угол. Для больших радиусов ручка остаётся внутри холста; её перемещение масштабируется относительно текущего радиуса. Shift при вращении округляет угол до 15°. Стрелки меняют радиус на 1 px либо угол на 1°; Shift увеличивает шаг. Эти жесты не меняют размеры области градиента и маску.
«Кадрировать фон» в режиме «Игра и ваш фон» включает кадрирование черновика и выбирает изображение для проверки. Фотография заполняет существующую рамку с сохранением пропорций; перетаскивание и масштаб 100–400% определяют видимую область. Координаты рамки, сторона фона и маска остаются прежними. Параметры относительные и применяются ко всем изображениям библиотеки; выбор конкретного файла меняет только предпросмотр. Выключение «Сохранять пропорции изображения» возвращает прежнее растяжение, сохраняя параметры кадрирования для повторного включения.
API изображений подписки (с существующей проверкой доступа к подписке):
GET /api/v1/subscriptions/{id}/image-config— сохранённые параметры;?preset=default&img_type=1|2— исходные параметры выбранного режима без сохранения.POST /api/v1/subscriptions/{id}/image-preview— предпросмотр черновика. JSON содержитimg_type: 1|2, соответствующий объектtype1илиtype2, необязательныеassetиmature. Параметры нормализуются так же, как при сохранении; ответ — JPEG сCache-Control: no-store. Черновик не записывается в настройки подписки.mature: falseпозволяет проверить обложку без метки 18+ независимо от текущего статуса стрима.PUT /api/v1/subscriptions/{id}/image-config— единое сохранение режима (img_type: 0|1|2) и соответствующих параметров. Для режима 0 объект параметров не нужен. Смена режима и параметры сохраняются в одной транзакции; ошибка откатывает оба изменения. Существующий PUT безimg_typeпродолжает обновлять параметры текущего режима.- Существующий
GET /image-previewпо-прежнему показывает сохранённую конфигурацию.
Объекты type2 в GET/PUT image-config и POST image-preview дополнены полями background_crop_enabled (по умолчанию false), background_crop_x и background_crop_y (0–1000, по умолчанию 500), background_crop_zoom (100–400, по умолчанию 100). X/Y задают долю доступного смещения обрезки по каждой оси: 0 — начало, 1000 — конец. Отсутствие поля или null в PUT сохраняет значение; явное false выключает кадрирование. Параметры кадрирования сохраняются sqlc-запросом в общей транзакции с остальными настройками. Ошибка откатывает также переключение режима. Генератор использует преобразование с дробными координатами без промежуточного увеличенного изображения.
GET/POST image-assets дополнены width, height исходного изображения и crop_data_url — JPEG с полной композицией, вписанный в 640 × 640 без обрезки. Прежний thumb_data_url остаётся миниатюрой 220 × 124 для библиотеки. Кадрирование доступно при наличии полного предпросмотра и размеров; исходный файл не изменяется.
Тела PUT и POST ограничены 64 КиБ. Выбранный asset используется только в предпросмотре; новые посты продолжают выбирать случайный фон из библиотеки подписки. Возрастная метка в рабочих постах зависит от статуса стрима, а флажок предпросмотра используется только для проверки.
Для кадрирования требуется миграция 20261007231302_add_subscription_background_crop.sql. Сначала применяется миграция, затем обновляются ownadmin-api и процессы, создающие изображения уведомлений через TwitchService (в разделённом runtime — event-hub), затем frontend Mini App. Существующие подписки сохраняют прежнее растяжение: миграция задаёт background_crop_enabled = false. Перемещение, привязка и ручки градиента используют существующие поля. Генератор режима 1 также исправлен: размер метки 18+ определяется её настройками, а не фиксированной рамкой 130 × 85.
Универсальный редактор обложки¶
Новый редактор Mini App открыт по /channels/{channelId}/subscription/{subscriptionId}/images/studio; страница прежних изображений предлагает переход, а сохранённый режим 3 сразу открывает студию. Режимы ½ продолжают использовать прежние параметры до явного сохранения новой композиции. Все правки остаются черновиком до «Сохранить обложку». Выключатель «Прикреплять картинку к посту» сохраняет композицию для повторного включения.
Документ scene версии 1 содержит width, height, background и массив layers от дальнего слоя к ближнему. Поддерживаются category, avatar, age, media, fill, text, rect, circle; одинаковые типы можно добавлять повторно. Слои перемещаются и меняют размер на холсте; доступны удаление, дублирование, порядок, видимость, закрепление, поворот, отражение и непрозрачность. Угловые ручки сохраняют пропорции, Shift позволяет менять стороны независимо. Привязка работает к центру и краям холста с порогом 6 экранных px; Alt временно выключает её. Кадрирование задаётся относительными X/Y 0–1 и увеличением 1–4 внутри рамки с fit: cover|contain; отмена возвращает весь сеанс кадрирования.
Текстовый слой поддерживает необязательные font_weight: normal|bold, text_align: left|center|right, min_font_size: 12..font_size, max_lines: 0..20 (0 — без отдельного ограничения) и text_overflow: shrink|wrap|ellipsis. Уменьшение останавливается на минимальном размере; перенос сохраняет выбранный размер, а сокращение добавляет многоточие в последнюю доступную строку. Высота рамки остаётся ограничением во всех режимах. Непоместившийся или сокращённый текст даёт предупреждение. Без этих полей сохраняется прежнее поведение: обычный шрифт, выравнивание слева, перенос с уменьшением до 12 px.
Настройка подложки находится в самом текстовом слое: backdrop_fit: content|frame, цвет, прозрачность и скругление. content рассчитывает ширину по самой длинной отображаемой строке и высоту по числу строк после подстановки данных стрима; размеры ограничены рамкой. padding_x и padding_y задают отступы отдельно, включая 0; без них используется прежний padding. Размер рамки и положение слоя при этом не меняются. Новая подложка включается в режиме «По тексту»; сохранённые композиции без backdrop_fit продолжают заполнять всю рамку. Обводка рисуется только в добавленной области вокруг альфа-силуэта и не осветляет полупрозрачную внутреннюю часть слоя.
GET image-assets дополняет каждый файл used_in_layers: [{id,name,hidden?,fallback?}] — ссылками из сохранённой композиции этой подписки, включая скрытые слои, варианты случайного набора и запасную картинку категории. Повторная ссылка в одном слое учитывается один раз. Библиотека студии показывает ссылки из текущего черновика; библиотека прежней страницы — сохранённые ссылки. Перед удалением используемого файла прежняя страница перечисляет слои и просит подтверждение. Удаление не заменяет файл и не переписывает композицию: при следующей генерации отсутствующее изображение пропускается с предупреждением. Удаление слоя или исключение файла из набора не удаляет сам файл из библиотеки.
Эти параметры расширяют JSON-документ версии 1 без изменения схемы БД и новой миграции. Для выпуска сначала обновляются ownadmin-api и процессы генерации обложек (в разделённом runtime — event-hub, при активном lifecycle fallback также stream-status-reconciler), затем frontend с пересобранным WASM той же версии. Быстрый Canvas загружает обычный и жирный шрифт из manifest; точный native/WASM рендер использует те же встроенные Go fonts. Уже опубликованный пост получает новое оформление только при следующей разрешённой публикации или обновлении.
Заливка поддерживает сплошной цвет, линейный, круговой, отражённый и градиент по кругу. Начальная и конечная видимость независимы, поэтому можно сделать плавный переход в прозрачность. Центр и переход редактируются ручками на холсте и точными параметрами. Поддерживаются скругления, подложка текста, обводка по альфа-контуру PNG, наружная/внутренняя тень и свечение. Для обоих эффектов направление и расстояние переводятся в существующие X/Y (до 128 px по каждой оси), которые также можно задать точно. Шрифт обложки — Go Regular, одинаковый в серверном и браузерном композиторе. Текст переносится и уменьшается до 12 px; если он всё ещё не помещается, редактор показывает предупреждение.
Текстовые слои подставляют {stream_title}, {category}, {channel} (также поддержаны {stream_name}, {game_name}). Категория получает актуальную картинку Twitch. Необязательный fallback_mode выбирает запасной вариант: standard — штатная картинка Twitch с геймпадом, asset — файл fallback из библиотеки, hide — пропустить слой. Новые категории используют standard; документы без fallback_mode сохраняют прежнюю семантику (свой fallback либо пропуск). Если запасное изображение недоступно, слой пропускается с предупреждением. Метка 18+ использует стандартный файл либо загруженную картинку и условие mature|always. Медиаслой выбирает фиксированный файл или случайный вариант из своего списка assets. Выбор основан на ID стрима и ID слоя, поэтому при изменении названия, категории и повторной доставке набор остаётся прежним. У разных стримов варианты могут повторяться. Poster override из медиа-пайплайна по-прежнему заменяет изображение категории, сохраняя остальные слои. Обновление существующего поста при разрешённом событии изменения названия пересоздаёт обложку режима 3 также в VK; Telegram уже пересоздавал её. Новые Twitch OAuth scopes и изменения каталога прав не требуются.
Вход «Обложка поста» расположен над редактором текста и выделен отдельной карточкой с предпросмотром и действием. Выбор медиа открывает модальное окно с миниатюрами, увеличением до доступного preview, поиском по имени, страницами по 12 файлов и подтверждением набора. Выбор на страницах сохраняется до применения; отмена не меняет слой. Инспектор показывает компактный набор с отдельной прокруткой. На широком экране панели студии ограничены высотой окна и прокручиваются независимо. Лимиты хранилища (50 файлов на подписку) и медиаслоя (12 файлов) не меняются.
Проверка будущих постов включает длинное/своё название, другую реальную категорию Twitch, отсутствие картинки категории, переключение возрастного статуса и каждого файла случайного набора. Тестовые данные и выбранный для проверки файл не записываются в рабочую композицию. Исходные PNG/JPEG не меняются при кадрировании; PNG сохраняет прозрачность. Загрузки ограничены 10 МБ и 24 млн исходных пикселей; ресурсы композитора приводятся к стороне не больше 1600 px. Холст ограничен 320–2560 px, кадр слоя — 5120 px по стороне и 8 млн пикселей, общая площадь кадров без заливок — 32 млн пикселей, документ — 64 КиБ.
API использует существующую авторизацию доступа к подписке:
GET /api/v1/subscriptions/{id}/image-configвозвращаетscene, в том числе сохранённую отключённую композицию;?preset=default&img_type=3возвращает начальную композицию без записи.PUT /api/v1/subscriptions/{id}/image-configпринимает{img_type: 3, scene}или{img_type: 0, scene}. Для выключения прежних режимов по-прежнему достаточно{img_type: 0}. Режим 3 безsceneотклоняется. Режим и документ сохраняются атомарно; ошибка откатывает оба.POST /api/v1/subscriptions/{id}/cover-resourcesпринимаетscene, необязательныеmissing_category, числовой Twitchgame_id,picks(ID слоя → файл из его списка),variant: 0..10000,sample: {title,category,channel,mature}. Ответdataсодержитresources(PNG base64 по ключу),bindings(ID слоя → ключ),values,warnings,fontиbold_font(обычный и жирный TTF base64). Файлы читаются только из хранилища этой подписки. Ответ имеетCache-Control: no-storeи не меняет настройки/публикации.- GET/POST
image-previewдля режима 3 возвращает PNG через тот же серверный композитор. Прежние режимы возвращают JPEG.
При перемещении Canvas показывает немедленный черновой результат. Через 250 мс после последней правки Worker выполняет общий Go-композитор src/cover в WASM и заменяет картинку каноническим PNG. Геометрия, текст, эффекты и прозрачность не требуют сетевого запроса; запрос ресурсов нужен при изменении источников или тестовой категории. Предварительный Canvas может слегка отличаться по сглаживанию/эффектам. Worker оставляет только последний ожидающий документ, устаревшие ответы не заменяют новый черновик. Если WASM не загрузился, показывается сообщение о недоступности точной проверки; серверное сохранение и генерация остаются доступны.
Сборка браузерного композитора и воспроизводимая проверка PNG (из каталога backend, нужны Go и Node.js):
Первый скрипт обновляет ../front-pulse-caster/tg-mini-app/public/cover-render/render.wasm и соответствующий wasm_exec.js. Путь вывода можно передать первым аргументом. Второй собирает временную копию, проверяет актуальность поставляемых WASM/runtime-файлов при наличии соседнего frontend, создаёт синтетическую композицию с текстом, прозрачным PNG, кадрированием, поворотом, обводкой и эффектами и сравнивает PNG native/WASM для всех пяти заливок. COVER_NODE позволяет указать путь к Node. После изменения композитора пересоберите WASM, обновите параметр runtime в coverCanvas.js для сброса HTTP-кеша и поставляйте frontend с backend одной версии. Модуль (~5 МБ без HTTP-сжатия) загружается только при открытии студии; WebAssembly и Worker нужны для канонического клиентского предпросмотра.
Для выпуска нужна миграция 20261008164752_add_subscription_cover_scene.sql. Порядок применения и отката описан в базе данных. На текущие опубликованные посты сохранение настроек само по себе не действует: новую композицию применяет следующая публикация или разрешённое обновление поста.
Редактор текстов уведомлений¶
Настройки подписки показывают одно выбранное событие и общий предпросмотр поста. Черновики текста, включения всех пяти событий и библиотеки кнопок сохраняются вместе кнопкой «Сохранить изменения». Переключение событий, потеря фокуса и предпросмотр не выполняют PUT. При переходе к обложке или отправке можно сохранить черновики и продолжить либо уйти без сохранения. Закрытие страницы с изменениями вызывает предупреждение браузера.
В обычном редакторе переменные показываются именованными метками; в API по-прежнему передаются {channel}, {stream_name}, {game_name}, {old_stream_name}, {old_game_name} и {history_game}. Форматирование выделенного текста создаёт существующую Telegram HTML-разметку. Режим HTML сохраняет доступ к расширенным шаблонам; переключение режимов само по себе не меняет исходный шаблон. Вставка из буфера обмена переносит обычный текст без внешней HTML-разметки.
«Ещё стили» добавляет спойлер, цитату, сворачиваемую цитату, блок кода с необязательным языком и снятие оформления выделенного текста. Замена стиля удаляет несовместимое оформление кода; цитаты не вкладываются друг в друга. В предпросмотре спойлеры и сворачиваемые цитаты раскрываются нажатием, Enter или пробелом. Сохранение и переключение редактора сохраняют HTML, а не отдельный массив entities: существующий отправитель использует parse_mode: HTML.
Обычные <span> браузерного редактора удаляются с сохранением текста и вложенного оформления. Спойлер сохраняется как <span class="tg-spoiler">, без посторонних классов и атрибутов. actions-executor выполняет такое же преобразование перед локальной или удалённой доставкой текста, подписи к фото и их обновлений: это исправляет также уже сохранённые шаблоны и действия в очереди без изменения базы. Преобразование касается только span и не исправляет произвольную ошибочную HTML-разметку. При обновлении медиа подпись кодируется JSON-сериализатором, чтобы кавычки в тегах и тексте, переводы строк и обратные слеши не повреждали запрос. Для выпуска обновляются frontend Mini App и actions-executor; при удалённом Telegram gateway обновляется также процесс telegram-gateway, использующий HTTP-клиент. Миграция не требуется.
Можно вставить tg-time с Unix-временем и форматом даты; пользователь выбирает время в своём часовом поясе, Telegram отображает его в часовом поясе зрителя. Предпросмотр показывает пример в часовом поясе браузера, относительная дата вычисляется при обновлении текста. tg-emoji вставляется по числовому ID с обычным эмодзи для замены. Предпросмотр показывает замену; для пользовательских эмодзи в каналах боту требуется соответствующий доступ Telegram (дополнительное имя пользователя, приобретённое на Fragment). Мини-апп не проверяет наличие этого доступа.
Поддержка сверяется с HTML style Bot API и форматами даты. Размер шрифта и цвет обычного текста сообщения через entities не задаются. Новый редактор стилей и угловые ручки используют существующие API, не требуют миграции или обновления backend; для их выпуска пересобирается frontend Mini App.
PUT /api/v1/subscriptions/{id}/settings обновляет только переданные поля. Отсутствующее поле или JSON null оставляет текущее значение в базе, включая режим и ID обложки; явные false и пустая строка применяются. Новый frontend отправляет только изменённые поля событий и не передаёт img_type. Это сохраняет изменения других событий и обложки, сделанные параллельно. Одновременная правка одного и того же поля остаётся с семантикой последнего сохранения; контроля версий в этом API нет.
Кнопки редактируются на месте в разделе «Кнопка под постом»: можно менять подписи, добавлять и удалять варианты. Нажатие кнопки в предпросмотре раскрывает этот раздел; старый маршрут /channels/{channelId}/subscription/{id}/buttons перенаправляет туда же. Пустая библиотека возвращает стандартную подпись «СМОТРЕТЬ СТРИМ».
Необязательное поле buttons в PUT settings содержит полный желаемый список объектов {id, value}: существующий положительный ID сохраняет идентификатор и обновляет подпись; отсутствующий ID или 0 создаёт вариант; отсутствующий в списке существующий вариант удаляется. [] очищает библиотеку; отсутствие поля или null сохраняет её. Подписи обрезаются по краям, пустые и повторяющиеся без учёта регистра подписи отклоняются с 400. ID проверяются в рамках настроек этой подписки до записи: чужой или уже удалённый ID даёт 409. Тексты, библиотека и явно переданный режим обложки записываются в одной транзакции под блокировкой строки settings. Ошибка откатывает всё. Ответ сохраняет прежние поля settings и добавляет buttons, если библиотека передана, включая пустой массив после очистки. Старые API создания и удаления кнопок остаются доступны. Контроля версий списка нет: одновременная правка подписей использует последнее сохранение.
Предпросмотр использует пример данных для текста и сохранённую обложку через GET image-preview. Он не отправляет пост в соцсеть. В Telegram показывается одна кнопка из библиотеки (выбор варианта меняет только пример); при завершении стрима — фиксированная «СТРИМ ЗАВЕРШИЛСЯ». В VK HTML-теги удаляются, сущности декодируются, а текст кнопки и адрес стрима добавляются в конец поста согласно существующему механизму доставки. Изменение названия и/или игры использует отдельное событие и обновляет пост; завершение также обновляет существующий пост. Событие одновременной смены названия и игры не объединяет настройки двух отдельных событий.
Миграция не требуется. Для выпуска редактора сначала обновляется backend частичного сохранения настроек, затем frontend Mini App. Сервисы доставки и Twitch OAuth scopes не меняются.
Получатели и отправка подписки¶
Страница «Куда отправлять посты» показывает отдельные карточки Telegram и VK со статусом и выключателем отправки. MAX на странице нет: настройки MAX хранятся в подписке, но сервер в MAX посты не отправляет. Поля подключения раскрываются по запросу. Обычное подключение VK проходит через VK ID с выбором сообщества, в котором есть право публикации; ручные поля ID и ключа доступны отдельно. Страница отправляет на сервер только те площадки, которые изменили, поэтому сохранённые настройки MAX она не трогает. Все правки применяются явной кнопкой «Сохранить отправку»; ошибки оставляют черновик, а выход с изменениями предлагает сохранить или отменить его.
GET /api/v1/subscriptions/{id}/delivery возвращает получателей также для площадок на паузе. Ключи доступа по-прежнему не выдаются, только признаки has_access_token. PUT /api/v1/subscriptions/{id}/delivery обновляет переданные поля переданных площадок; отсутствие поля или null сохраняет его. Frontend отправляет только изменения, не передаёт пустой ключ и не перезаписывает другие площадки. telegram.topic: 0 очищает номер темы; для личного чата тема также очищается. Изменения Telegram проверяют выбранного бота и доступ к получателю при включённой отправке; изменение другой площадки не требует повторного подключения Telegram. Существующая проверка дубликатов получателя остаётся в силе, в том числе для подписки на паузе.
enabled: false теперь приостанавливает отправку, сохраняя ID получателя, тему, выбранного бота и защищённые ключи. Возобновление использует эти параметры. Пауза и смена получателя не перезаписывают VK access/refresh token и сопутствующие OAuth-поля; они изменяются только при явно переданном новом ключе или готовом OAuth-подключении. Таким образом, изменение выключателя не затирает параллельное обновление токена. Для VK подтверждённое подключение и выбранное сообщество применяются при сохранении; отмена возвращает предыдущего получателя, а поздний ответ отменённого подключения игнорируется.
Получателей и ключи, которые старый механизм уже удалил при отключении, восстановить автоматически невозможно: их нужно указать или подключить заново. Старый Telegram disabled: ID скрывается в ответе как отсутствие подключения. Изменения не требуют миграции. При выпуске сначала обновляется backend API, затем frontend Mini App; рабочие посты по-прежнему обрабатывают существующие сервисы доставки.
Пауза Telegram также блокирует обновление существующих постов при смене названия/игры, завершении стрима и замене обложки. Перед новой попыткой pipeline и повторной отправки обложки выключатель перечитывается из базы. Уже переданное внешнему исполнителю действие не отзывается: пауза применяется к следующей отправке или обновлению. Twitch OAuth scopes и каталог возможностей не меняются. При выпуске обновляется также процесс, выполняющий Twitch notification pipelines.
Размер обложки и запасной текст Telegram¶
Kafka publisher перед сериализацией telegram.photo.send, telegram.message.edit_media, vk.wall.post и vk.wall.edit подготавливает слишком большие inline-фото: файлы до 600 КиБ остаются как есть, большие PNG/JPEG преобразуются в JPEG с качеством 92/85/75; при необходимости пропорционально уменьшается разрешение. Итоговые байты ограничены 600 КиБ, JSON envelope — 900 КиБ, оставляя место для base64 и служебных данных при стандартном actions_runtime.kafka.batch_bytes = 1 МиБ. kafka-go проверяет размер отдельного сообщения по Writer.BatchBytes ещё до обращения к broker; код 10 может означать этот клиентский отказ. В штатном compose broker разрешает 32 МиБ, но это не отменяет ограничения producer-а и индивидуальных настроек topic. Исходные файлы, документ сцены и точный PNG предпросмотра не меняются. Telegram также может повторно сжимать фотографию. При нестандартном лимите broker/topic меньше 900 КиБ его необходимо учитывать отдельно; повышение broker limits для обычного пути не требуется. Формат photo: []byte в wire-контракте не меняется. Это исправление относится к штатному Kafka transport; emergency outbox не перекодирует фото этим publisher-ом.
Подтверждённый отказ отправки фото, включая отказ очереди по размеру, допускает запасной текст. После успешного текста pipeline сохраняет его ID и повторяет edit_media того же поста. Новая ревизия заменяет старую, окончание стрима или пауза подписки прекращают доставку согласно существующим проверкам. Таймаут подтверждения либо успешный результат без валидного message_id остаются неопределённым исходом: автоматический второй стартовый пост не создаётся. В таком случае уведомление сообщает, что публикация не подтверждена, вместо утверждения, что текст точно не опубликован.
Если запасной текст тоже не отправлен, уведомление содержит обе причины. Число попыток берётся из actions.result.attempt_count, если оно доступно, а не из configured maximum; при отказе ещё до executor число попыток не выдумывается. Ошибки подготовки/валидации до enqueue и явный отказ Kafka помечаются как запрос, который не был опубликован, даже если подготовка завершилась таймаутом; они допускают запасной текст. Неопределённый таймаут самой записи такого признака не получает. Получатель-подписчик и административный чат не получают два одинаковых уведомления, если это один чат. Kafka readers ожидания результата закрываются и при отказе публикации задания. Структурный лог подготовки фото содержит action ID/type и размеры до/после, без текста поста, адресов и токенов.
Для выпуска пересоберите процессы, публикующие stream delivery actions (event-hub, а при активном lifecycle fallback также stream-status-reconciler) и использующие общий publisher, в частности actions-executor для собственных уведомлений. Фронтенд, WASM и схема базы этим исправлением не затронуты. Старый непринятый Kafka запрос не появится в очереди после рестарта сам: повторить отсутствующий пост нужно через штатный повтор/обновление, предварительно проверив, что пост действительно отсутствует.
Надёжность донатных интеграций и DonateX OAuth¶
DonateX поддерживает официальный OAuth Authorization Code + PKCE. Владелец выбирает OAuth либо существующий API-ключ. Подключение загружает последние донаты, затем использует SignalR и периодическую REST-сверку. Access/refresh token обновляются одной транзакцией под блокировкой подключения; настройки приложения и адрес возврата описаны в конфигурации.
Для всех подключений API дополнительно возвращает last_error_code и next_retry_at. В MemeAlerts и DonateX окончательная
ошибка OAuth (invalid_grant, invalid_client, unauthorized_client, invalid_scope) прекращает
автоматические запросы. При invalid_grant владелец повторно подтверждает доступ. Токены и ранее
собранные события при ошибке не удаляются.
DonatePay использует API-ключ. После первоначального получения профиля фоновая синхронизация запрашивает
только транзакции. REST и socket-token запросы одного процесса разнесены минимум на 15 секунд; это
консервативная настройка клиента, а не заявленный лимит площадки. При 429 соблюдается Retry-After
(секунды или HTTP-дата), без заголовка — пауза 15 минут. Пауза хранится в базе, действует и на ручную
проверку и переживает рестарт. Учтены также JSON-ответы status: 429 при HTTP 200. Работающий realtime
канал не обрывается только из-за ограничения REST. Иные приложения, использующие тот же ключ/IP, могут
продолжать расходовать общий лимит площадки.
Предметы роста¶
Кабинет /play/growth использует существующую Twitch identity-авторизацию и связку аккаунта; сохраняется возврат к выбранному каналу. Новых scopes и отдельного owner OAuth для зрителя нет. Бот публикует пакетные итоги через существующую возможность chat_interactions (channel:bot); каталог и preset «Только нужное сейчас» не расширяются. Правила и API.