Skip to content

Announcements ​

Current version: v0.0.12

telegram-bot-essentials/announcements (namespace TelegramBotEssentials\Announcements) lets admins compose a message once and broadcast it to every bot user, or a filtered subset. Broadcasts run with live progress, per-target delivery tracking, and can be retracted (deleted) afterwards — all without blocking the bot's normal interactive traffic.

Installation ​

bash
composer require telegram-bot-essentials/announcements
php artisan migrate

Publish config to change the queue name or batch size:

bash
php artisan vendor:publish --tag=tbe-announcements-config
php
// config/tbe-announcements.php
return [
    'queue' => env('TBE_ANNOUNCEMENTS_QUEUE', 'announcements'),
    'batch_size' => (int) env('TBE_ANNOUNCEMENTS_BATCH_SIZE', 100),
];

Make sure a worker actually processes that queue, alongside your default one:

bash
php artisan queue:work --queue=default,announcements

Dedicated queue by design

Announcements deliberately dispatch onto their own (low-priority) queue rather than default, so a broadcast to thousands of users can't starve interactive bot responses (button presses, state answers) that share the same worker pool.

Data model ​

ModelPurpose
AnnouncementThe composed message: method (html, copy, or forward), message_text/message_id/from_chat_id depending on method, sent_at (set when the whole broadcast finishes), plus action_status_chat_id/action_status_message_id for the live progress message
AnnouncementTargetOne row per recipient BotUser, with status: null (pending) → one of sent / skipped / forbidden / failed, then deleted after retraction

AnnouncementTarget.status since v0.0.11:

StatusMeaning
sentDelivered; message_id stored
skippedNever attempted — the user was already known unreachable, so the broadcast didn't spend an API call on them
forbiddenThe send failed with an error Telegram attributes to the user (blocked, deleted account, can't-initiate) — fed back to botUserStatus()->reportFailure()
failedThe send failed transiently — rate limit, Telegram outage, markup error — not the user's fault, so their reachability is left untouched

Announcement::sendTo(int $chatId) dispatches by method:

php
'copy'    => wHook()->api()->copyMessage([...]);      // re-sends as a new message, no "Forwarded from" tag
'forward' => wHook()->api()->forwardMessage([...]);   // keeps the "Forwarded from" tag
'html'    => wHook()->api()->sendMessage([..., 'parse_mode' => 'HTML']); // freeform composed text

Sending flow ​

Admin composes & previews the announcement (sent to the admin's own chat first, to catch mistakes)
      ▼
"Start Sending" → targets are chunked into config('tbe-announcements.batch_size') groups
      ▼
SendAnnouncementJob (queued, one per chunk, exports/imports WebhookContext across the queue boundary)
      │  for each target:
      │    known unreachable (checked per target at send time, not when the list was built) → status = 'skipped'
      │    else Announcement::sendTo($peerId)
      │      success → status = 'sent', message_id stored, botUserStatus()->reportSuccess()
      │      user-attributed failure → status = 'forbidden', botUserStatus()->reportFailure()
      │      transient failure (429, outage, markup) → status = 'failed', reachability untouched
      ▼
Progress message (action_status_chat_id/message_id) is edited live with an ASCII progress bar,
throttled via a 3-second Cache::add() lock per announcement so concurrent job completions
don't hammer editMessageText() into a 429 rate limit

Reachability is re-checked per target at send time, not frozen when the target list was built — so a user who unblocked the bot in between still gets the message, and a forced send from the target list overrules whatever the bot currently believes. This also breaks a feedback loop: under the old catch-all, a 429 storm would have branded reachable users as blocked, and they'd then be skipped from every future broadcast too.

Retraction flow ​

Symmetric to sending: DeleteAnnouncementJob walks the same sent targets, calls deleteMessage() per recipient, and flips status to deleted (or forbidden if the delete call fails), updating the same throttled progress message.

Why WebhookContext matters here ​

Both jobs are constructed with WebhookContext $context and call wHook()->importContext($context) as the very first line of handle() — because a batch job runs on a queue worker with no incoming Telegram webhook of its own, wHook() would otherwise have no bot/API/tenant to operate with. This is the same export/import pattern documented in Reference → Webhook Context Export; Announcements is the primary real-world consumer of it in this ecosystem.

Admin UI ​

AnnouncementsQuery / AnnouncementsAnswer / AnnouncementsFeature / AnnouncementsKey provide the full admin flow: list, detail (with live sent / pending / skipped / forbidden / failed / deleted counts), compose, preview, send, and retract.

Localization ​

Ships English (en) and Persian (fa) translations out of the box:

bash
php artisan vendor:publish --tag=tbe-announcements-translations