Skip to content

BroadcastManager

High-Performance Telegram Broadcast Manager for MadelineProto. Manage Telegram broadcasts efficiently: send text, media, albums, inline buttons, pin/unpin messages, delete previous broadcasts, edit the last broadcast, schedule broadcasts, run self-destruct deletion jobs, and track live progress.

AGPL LicenseMade with PHPPackagist VersionPackagist DownloadsCI


Requirements

  • PHP 8.2 or newer
  • danog/madelineproto^8.4
  • amphp/amp^3.0

Installation

composer require wizardloop/broadcastmanager

Include Composer autoload:

require'vendor/autoload.php';

Create the manager with your MadelineProto API instance:

useBroadcastTool\BroadcastManager;
$manager = newBroadcastManager($api);

Optional custom data directory:

BroadcastManager::setDataDir(__DIR__ . '/data'); // default: __DIR__ . '/../data'

Features

  • Concurrent broadcasts with safe concurrency clamping.
  • Live progress message updates.
  • Pause, resume, and cancel by broadcast id.
  • Text, media, albums, entities, and inline buttons.
  • Optional pinning of the last message sent to each peer.
  • Delete last broadcast or all saved broadcast messages.
  • Edit the last saved broadcast message for each peer.
  • Scheduled broadcasts persisted to disk.
  • Self-destruct broadcasts with automatic deletion after 0 to 48 hours.
  • Per-broadcast metadata in data/broadcasts/{broadcastId}.json.
  • FLOOD_WAIT retry handling and hard-fail handling.
  • Internal error log at data/broadcast-errors.log.

Basic Broadcast

$users = ['123456789', '987654321'];
$messages = [
[
'message' => '<b>Hello subscribers</b>',
'parse_mode' => 'HTML',
],
];
$broadcastId = $manager->broadcastWithProgress(
allUsers: $users,
messages: $messages,
chatId: $adminChatId,
pin: false,
concurrency: 10
);

broadcastWithProgress() returns a string id. Use it with progress(), pause(), resume(), and cancel().

$progress = $manager->progress($broadcastId);

Concurrency is clamped internally:

  • Minimum: 1
  • Maximum: 30
  • Default: 10
  • Recommended examples: 10

Live status messages are updated at most once every 5 seconds and once again when the operation finishes.


Message Payloads

Broadcast messages are passed directly to MadelineProto send methods with peer added internally.

Text message

$messages = [
[
'message' => '<b>Hello</b>',
'parse_mode' => 'HTML',
],
];

Inline buttons

$messages = [
[
'message' => 'Click a button below:',
'buttons' => [
[['text' => 'Visit Website', 'url' => 'https://example.com']],
[['text' => 'Start', 'callback_data' => 'start_action']],
],
],
];

buttons is converted to reply_markup internally for sending.

Media message

$messages = [
[
'message' => 'Photo caption',
'media' => [
'_' => 'inputMediaUploadedPhoto',
'file' => '/path/to/photo.jpg',
],
'parse_mode' => 'HTML',
],
];

When a message has media, BroadcastManager uses messages->sendMedia(). Otherwise it uses messages->sendMessage().

Album message

If a message contains albumFile, BroadcastManager reads that JSON file and sends it with messages->sendMultiMedia() in chunks of 10.

$messages = [
[
'albumFile' => __DIR__ . '/album.json',
],
];

Example album.json item:

[
{
"media": {
"type": "photo",
"botApiFileId": "AgACAgQAAxkBA..."
},
"caption": "Album caption",
"entities": []
}
]

Supported album media types are mapped to inputMediaPhoto and inputMediaDocument.


Progress

$progress = $manager->progress($broadcastId);
if ($progress !== null) {
echo"Progress: {$progress['progressPercent']}%\n";
echo"Success: {$progress['success']}\n";
echo"Failed: {$progress['failed']}\n";
echo"Pending: {$progress['pending']}\n";
}

progress() returns array|null:

[
'processed' => 0,
'success' => 0,
'failed' => 0,
'pending' => 0,
'flood' => 0,
'progressPercent' => 0.0,
'breakdown' => [
'sent' => 0,
'deleted' => 0,
'unpin' => 0,
'edited' => 0,
'unchanged' => 0,
'scheduled' => 0,
],
'edited' => 0,
'unchanged' => 0,
'scheduled' => 0,
'selfDestruct' => null,
'type' => 'send',
'total' => 0,
'elapsed' => 0.0,
'tps' => 0.0,
'done' => false,
'paused' => false,
'cancel' => false,
'startedAt' => null,
]

Control Running Operations

$manager->pause($broadcastId);
$manager->resume($broadcastId);
$manager->cancel($broadcastId);

cancel() only marks the operation as cancelled. It does not clear in-flight Telegram requests.

Check operation state:

$manager->isActive($broadcastId);
$manager->isPaused($broadcastId);
$manager->isCancelled($broadcastId);

Pause, resume, cancel, and progress work with send/edit/delete/unpin operations that have a live state id.


Pin And Unpin

Pin the last message sent to each peer:

$broadcastId = $manager->broadcastWithProgress(
allUsers: $users,
messages: $messages,
chatId: $adminChatId,
pin: true,
concurrency: 10
);

Unpin all messages for all peers:

$unpinId = $manager->unpinAllMessagesForAll($users, $adminChatId, 10);

Delete Broadcast Messages

Delete the last saved broadcast message for each peer using data/{peer}/lastBroadcast.txt:

$deleteId = $manager->deleteLastBroadcastForAll($users, $adminChatId, 10);

Delete the last message from a specific broadcast using data/broadcasts/{broadcastId}.json:

$deleteId = $manager->deleteLastBroadcastForAll(
allUsers: $users,
chatId: $adminChatId,
concurrency: 10,
broadcastId: $broadcastId
);

If you pass an empty allUsers array with broadcastId, peers are loaded from that broadcast metadata:

$deleteId = $manager->deleteLastBroadcastForAll(
allUsers: [],
chatId: $adminChatId,
concurrency: 10,
broadcastId: $broadcastId
);

When broadcastId is provided, the legacy lastBroadcast.txt file is not used and is not removed. This prevents a newer broadcast from being affected by an older delete request.

Delete all saved broadcast messages for each peer using data/{peer}/messages.txt:

$deleteAllId = $manager->deleteAllBroadcastsForAll($users, $adminChatId, 10);

Check whether legacy saved message files exist:

$hasLast = $manager->hasLastBroadcast();
$hasAll = $manager->hasAllBroadcast();

Edit Last Broadcast

editLastBroadcastForAll() reads each peer's data/{peer}/lastBroadcast.txt and edits that message id by default.

$editId = $manager->editLastBroadcastForAll(
allUsers: $users,
newText: '<b>Updated text</b>',
chatId: $adminChatId,
buttons: null,
media: null,
concurrency: 10,
parseMode: 'HTML'
);

Edit the last message from a specific broadcast using data/broadcasts/{broadcastId}.json:

$editId = $manager->editLastBroadcastForAll(
allUsers: $users,
newText: '<b>Updated text for a specific broadcast</b>',
chatId: $adminChatId,
buttons: null,
media: null,
concurrency: 10,
parseMode: 'HTML',
broadcastId: $broadcastId
);

If you pass an empty allUsers array with broadcastId, peers are loaded from that broadcast metadata:

$editId = $manager->editLastBroadcastForAll(
allUsers: [],
newText: '<b>Updated text for the stored broadcast peers</b>',
chatId: $adminChatId,
buttons: null,
media: null,
concurrency: 10,
parseMode: 'HTML',
broadcastId: $broadcastId
);

With buttons:

$editId = $manager->editLastBroadcastForAll(
allUsers: $users,
newText: 'Updated with a button',
chatId: $adminChatId,
buttons: [
[['text' => 'Open', 'url' => 'https://example.com']],
],
media: null,
concurrency: 10,
parseMode: 'HTML'
);

Edit counters include:

  • edited
  • unchanged
  • failed
  • flood

MESSAGE_NOT_MODIFIED is counted as unchanged, not failed.


Scheduled Broadcasts

Scheduled broadcasts are saved in data/scheduled-broadcasts.json.

$scheduleId = $manager->scheduleBroadcastForAll(
allUsers: $users,
messages: [
['message' => 'Scheduled hello'],
],
scheduledAt: time() + 3600,
chatId: $adminChatId,
pin: false,
concurrency: 10,
selfDestructHours: null
);

If scheduledAt is in the future, the broadcast is saved and not sent yet. If scheduledAt <= time(), it is marked running, executed immediately, and then marked done, cancelled, or failed.

Run due scheduled broadcasts periodically:

$results = $manager->runDueScheduledBroadcasts();

List scheduled broadcasts:

$scheduled = $manager->listScheduledBroadcasts();

Cancel a scheduled broadcast that has not started:

$cancelled = $manager->cancelScheduledBroadcast($scheduleId);

cancelScheduledBroadcast() returns false once the job is already running, done, cancelled, or failed.


Self-Destruct Broadcasts

Pass selfDestructHours as the sixth argument to broadcastWithProgress().

$broadcastId = $manager->broadcastWithProgress(
allUsers: $users,
messages: [
['message' => 'This message will be removed later.'],
],
chatId: $adminChatId,
pin: false,
concurrency: 10,
selfDestructHours: 6
);

Rules:

  • null means no automatic deletion.
  • 0 means delete immediately after the broadcast finishes.
  • 1 through 48 means delete after that many hours.

Invalid values below 0 or above 48 throw InvalidArgumentException.

Run due self-destruct jobs periodically:

$results = $manager->runDueSelfDestructJobs();

List self-destruct jobs:

$jobs = $manager->listSelfDestructJobs();

Cancel a self-destruct job that has not started:

$cancelled = $manager->cancelSelfDestructJob($jobId);

Self-destruct deletes by data/broadcasts/{broadcastId}.json, not by lastBroadcast.txt. If a newer broadcast was sent later, the self-destruct job still deletes only the messages from its own broadcast id.

If a broadcast is cancelled midway, the self-destruct job is created only for messages that were actually sent and saved in metadata.


Periodic Runners

Scheduled broadcasts and self-destruct jobs are durable, but they run only when you call the runners. Call them from your bot loop, event handler, or internal cron-style task:

$manager->runDueScheduledBroadcasts();
$manager->runDueSelfDestructJobs();

Filter Peers

$filterSub = $manager->filterPeers($users, 'users');
$targets = $filterSub['targets']; // array$failed = $filterSub['failed']; // int$total = $filterSub['total']; // int

Supported filter types:

  • users
  • groups
  • channels
  • all

Last Broadcast Data

lastBroadcastData() returns the latest saved status text from data/LastBrodDATA.txt, or false if it does not exist.

$lastData = $manager->lastBroadcastData();

Data Files

Legacy files are still written for backward compatibility:

  • data/{peer}/lastBroadcast.txt
  • data/{peer}/messages.txt
  • data/LastBrodDATA.txt

New files:

  • data/broadcasts/{broadcastId}.json
  • data/scheduled-broadcasts.json
  • data/self-destruct-jobs.json
  • data/broadcast-errors.log

Example broadcast metadata:

{
"id": "2b9a24c5ef2cbb10",
"type": "send",
"createdAt": 1710000000,
"status": "done",
"total": 100,
"sent": 90,
"failed": 10,
"peers": {
"12345": {
"lastMessageId": 111,
"messageIds": [111, 112],
"status": "sent"
}
},
"selfDestruct": {
"enabled": true,
"hours": 6,
"deleteAt": 1710021600,
"deleteJobId": "selfdestruct_..."
}
}

Public API Reference

publicfunction __construct(API$api);
publicfunction broadcastWithProgress(
array$allUsers,
array$messages,
$chatId = null,
bool$pin = false,
int$concurrency = 10,
?int$selfDestructHours = null
): string;
public function editLastBroadcastForAll(
array$allUsers,
string$newText,
$chatId = null,
?array$buttons = null,
$media = null,
int$concurrency = 10,
string$parseMode = 'HTML',
?string$broadcastId = null
): string;
public function scheduleBroadcastForAll(
array$allUsers,
array$messages,
int$scheduledAt,
$chatId = null,
bool$pin = false,
int$concurrency = 10,
?int$selfDestructHours = null
): string;
public function runDueScheduledBroadcasts(): array;
publicfunction cancelScheduledBroadcast(string$scheduleId): bool;
publicfunction listScheduledBroadcasts(): array;
publicfunction deleteLastBroadcastForAll(
array$allUsers,
$chatId = null,
int$concurrency = 10,
?string$broadcastId = null
): string;
public function deleteAllBroadcastsForAll(array$allUsers, $chatId = null, int$concurrency = 10): string;
publicfunction unpinAllMessagesForAll(array$allUsers, $chatId = null, int$concurrency = 10): string;
publicfunction runDueSelfDestructJobs(): array;
publicfunction cancelSelfDestructJob(string$jobId): bool;
publicfunction listSelfDestructJobs(): array;
publicfunction pause(string$id): void;
publicfunction resume(string$id): void;
publicfunction cancel(string$id): void;
publicfunction isPaused(string$id): bool;
publicfunction isCancelled(string$id): bool;
publicfunction isActive(?string$id = null): bool;
publicfunction hasLastBroadcast(): bool;
publicfunction hasAllBroadcast(): bool;
publicfunction progress(?string$id = null): ?array;
publicfunction lastBroadcastData(): string|false;
publicfunction filterPeers(array$allUsers, string$filterType = 'users'): array;
publicstaticfunction setDataDir(string$path): void;

Error Handling

Hard-fail Telegram RPC errors are counted as failed and are not retried. FLOOD_WAIT errors increase the job attempt count, set a future retry time, and are retried up to three attempts.

Internal logging is written to:

data/broadcast-errors.log

Log write failures are ignored so they do not crash the bot.


Changelog

See CHANGELOG.md for updates.


License

GNU AGPL-3.0 - see LICENSE.

About

High-performance Telegram broadcast manager for MadelineProto.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages