Appearance
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 migratePublish config to change the queue name or batch size:
bash
php artisan vendor:publish --tag=tbe-announcements-configphp
// 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,announcementsDedicated 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
| Model | Purpose |
|---|---|
Announcement | The 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 |
AnnouncementTarget | One row per recipient BotUser, with status: null (pending) → one of sent / skipped / forbidden / failed, then deleted after retraction |
AnnouncementTarget.status since v0.0.11:
| Status | Meaning |
|---|---|
sent | Delivered; message_id stored |
skipped | Never attempted — the user was already known unreachable, so the broadcast didn't spend an API call on them |
forbidden | The send failed with an error Telegram attributes to the user (blocked, deleted account, can't-initiate) — fed back to botUserStatus()->reportFailure() |
failed | The 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 textSending 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 limitReachability 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