PHPackages                             wizardloop/broadcastmanager - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. wizardloop/broadcastmanager

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

wizardloop/broadcastmanager
===========================

High-performance Telegram broadcast manager for MadelineProto

3.2.2(2w ago)1208AGPL-3.0-onlyPHPPHP ^8.2CI passing

Since Jan 6Pushed 2w agoCompare

[ Source](https://github.com/WizardLoop/BroadcastManager)[ Packagist](https://packagist.org/packages/wizardloop/broadcastmanager)[ Docs](https://github.com/WizardLoop/BroadcastManager)[ Fund](https://wizardloop.t.me/)[ GitHub Sponsors](https://github.com/WizardLoop)[ RSS](/packages/wizardloop-broadcastmanager/feed)WikiDiscussions main Synced today

READMEChangelog (10)Dependencies (6)Versions (23)Used By (0)

BroadcastManager
================

[](#broadcastmanager)

**High-Performance Telegram Broadcast Manager** for [MadelineProto](https://docs.madelineproto.xyz/). 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 License](https://camo.githubusercontent.com/0fb4f7634808706feb5d9bf669e078e2da524534575a39f2f08c74de3d8a1135/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4147504c2d2d332e302d626c75652e737667)](LICENSE)[![Made with PHP](https://camo.githubusercontent.com/c66a00369fa6bd52375992ae49c2544bdde33f8adc43f04b720faa95206ee5d2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d616465253230776974682d5048502d626c7565)](https://github.com/WizardLoop/BroadcastManager)[![Packagist Version](https://camo.githubusercontent.com/1d035b762c86ddda0e6f69a27c2d13de416b035999006a724f34b4ccfd1fa027/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f77697a6172646c6f6f702f62726f6164636173746d616e61676572)](https://packagist.org/packages/wizardloop/broadcastmanager)[![Packagist Downloads](https://camo.githubusercontent.com/65506add941efecd8fc010fa81be75759c839c6acbd588c818a872bb4090585e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f77697a6172646c6f6f702f62726f6164636173746d616e616765723f636f6c6f723d626c7565)](https://packagist.org/packages/wizardloop/broadcastmanager)[![CI](https://github.com/WizardLoop/BroadcastManager/actions/workflows/ci.yml/badge.svg)](https://github.com/WizardLoop/BroadcastManager/actions/workflows/ci.yml/badge.svg)

---

Requirements
------------

[](#requirements)

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

---

Installation
------------

[](#installation)

```
composer require wizardloop/broadcastmanager
```

Include Composer autoload:

```
require 'vendor/autoload.php';
```

Create the manager with your MadelineProto API instance:

```
use BroadcastTool\BroadcastManager;

$manager = new BroadcastManager($api);
```

Optional custom data directory:

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

---

Features
--------

[](#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
---------------

[](#basic-broadcast)

```
$users = ['123456789', '987654321'];

$messages = [
    [
        'message' => 'Hello subscribers',
        '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
----------------

[](#message-payloads)

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

### Text message

[](#text-message)

```
$messages = [
    [
        'message' => 'Hello',
        'parse_mode' => 'HTML',
    ],
];
```

### Inline buttons

[](#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

[](#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

[](#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)

```
$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
--------------------------

[](#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-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-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
-------------------

[](#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: 'Updated text',
    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: 'Updated text for a specific broadcast',
    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: 'Updated text for the stored broadcast peers',
    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)

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 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
------------------------

[](#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
----------------

[](#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
------------

[](#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
-------------------

[](#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
----------

[](#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
--------------------

[](#public-api-reference)

```
public function __construct(API $api);

public function 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;
public function cancelScheduledBroadcast(string $scheduleId): bool;
public function listScheduledBroadcasts(): array;

public function deleteLastBroadcastForAll(
    array $allUsers,
    $chatId = null,
    int $concurrency = 10,
    ?string $broadcastId = null
): string;
public function deleteAllBroadcastsForAll(array $allUsers, $chatId = null, int $concurrency = 10): string;
public function unpinAllMessagesForAll(array $allUsers, $chatId = null, int $concurrency = 10): string;

public function runDueSelfDestructJobs(): array;
public function cancelSelfDestructJob(string $jobId): bool;
public function listSelfDestructJobs(): array;

public function pause(string $id): void;
public function resume(string $id): void;
public function cancel(string $id): void;

public function isPaused(string $id): bool;
public function isCancelled(string $id): bool;
public function isActive(?string $id = null): bool;

public function hasLastBroadcast(): bool;
public function hasAllBroadcast(): bool;
public function progress(?string $id = null): ?array;
public function lastBroadcastData(): string|false;
public function filterPeers(array $allUsers, string $filterType = 'users'): array;

public static function setDataDir(string $path): void;
```

---

Error Handling
--------------

[](#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
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for updates.

---

License
-------

[](#license)

**GNU AGPL-3.0** - see [LICENSE](LICENSE).

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance91

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Recently: every ~37 days

Total

21

Last Release

18d ago

Major Versions

1.0.4 → 2.0.02026-01-07

2.0.5 → 3.0.02026-01-11

PHP version history (2 changes)1.0.0PHP &gt;=8.2

2.0.5PHP ^8.2

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/67387949?v=4)[Wizard Loop 🧙‍♂️](/maintainers/WizardLoop)[@WizardLoop](https://github.com/WizardLoop)

---

Top Contributors

[![WizardLoop](https://avatars.githubusercontent.com/u/67387949?v=4)](https://github.com/WizardLoop "WizardLoop (111 commits)")

---

Tags

broadcastmadelineprotomtprotophptelegramphpbottelegramBroadcastmadelineproto

### Embed Badge

![Health badge](/badges/wizardloop-broadcastmanager/health.svg)

```
[![Health](https://phpackages.com/badges/wizardloop-broadcastmanager/health.svg)](https://phpackages.com/packages/wizardloop-broadcastmanager)
```

###  Alternatives

[danog/madelineproto

Async PHP client API for the telegram MTProto protocol.

3.5k902.0k23](/packages/danog-madelineproto)[php-telegram-bot/inline-keyboard-pagination

PHP Telegram Bot InlineKeyboard Pagination

299.7k1](/packages/php-telegram-bot-inline-keyboard-pagination)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
