From 856561ebe7b9b7ce06c140f3bd8d9230aa45ecef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:01:19 +0300
Subject: [PATCH 01/11] Add .gitignore file with common exclusions
---
.gitignore | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 .gitignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..15e5acc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+.env
+.env.local
+.env.*.local
+/vendor/
+/node_modules/
+/.idea/
+/.vscode/
+.DS_Store
+*.log
+*.cache
+
+docker-compose.override.yml
+*.pid
+
+composer.lock
+
+.phpunit.result.cache
+/tests/output/
+
+/build/
+/dist/
+
+*.bak
+*.swp
+*.swo
+
+sessions
+docs_md
+session.mad
+*.madeline
+*.madeline.*
+madeline.phar
+madeline.phar.version
+madeline.php
+
+*.save
+*.save.1
+
+*.save.*
From 91cde40e0c46bc81d2f9348622f25b298b285374 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:02:34 +0300
Subject: [PATCH 02/11] Add PHP CS Fixer configuration file
---
.php-cs-fixer.dist.php | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
create mode 100644 .php-cs-fixer.dist.php
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..7ba95ac
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,18 @@
+in(__DIR__)
+ ->exclude('vendor')
+ ->exclude('data');
+
+return (new PhpCsFixer\Config())
+ ->setRiskyAllowed(true)
+ ->setRules([
+ '@PSR12' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'no_unused_imports' => true,
+ 'single_quote' => true,
+ 'no_empty_phpdoc' => true,
+ 'not_operator_with_successor_space' => false,
+ ])
+ ->setFinder($finder);
From 2903e69b7349f74966c593daa7dadd1a926901fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:04:58 +0300
Subject: [PATCH 03/11] Create phpcs.xml for PHP CodeSniffer setup
Add PHP CodeSniffer configuration for BroadcastManager
---
phpcs.xml | 7 +++++++
1 file changed, 7 insertions(+)
create mode 100644 phpcs.xml
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..691a623
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,7 @@
+
+
+ PHP CodeSniffer configuration
+ src
+ vendor/*
+
+
From 0afb6d15494258700302591770304f27b46238b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:05:42 +0300
Subject: [PATCH 04/11] Add .gitattributes to manage line endings and exports
---
.gitattributes | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 .gitattributes
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ac9acf9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,20 @@
+# Force LF line endings
+* text=auto eol=lf
+
+# Set file types
+*.php text
+*.sh text eol=lf
+*.md text
+*.yml text
+*.json text
+*.xml text
+*.env text
+
+# Exclude tests and dev files from export
+phpcs.xml export-ignore
+php-cs-fixer.dist.php export-ignore
+.dockerignore export-ignore
+composer.lock export-ignore
+
+README.md export-ignore
+CHANGELOG.md export-ignore
From 610067fdb1bd914b29575c539e83fd60b2100f5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:10:54 +0300
Subject: [PATCH 05/11] Add PHPStan configuration
---
phpstan.neon | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 phpstan.neon
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..db37782
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,4 @@
+parameters:
+ level: 6
+ paths:
+ - src
From d23bf1a4ee2a777b01d606ed816b7489c14e5195 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:15:00 +0300
Subject: [PATCH 06/11] Refactor BroadcastManager for improved functionality
## [3.1.0] - 2026-04-13
# Refactor BroadcastManager for improved functionality
- Updated BroadcastManager class to improve functionality and code structure.
- Added support for handling broadcast IDs, enhanced error handling, and refactored methods to accept optional chat IDs.
### Added:
- added isActive() to check active
- added option to set chatId as null
### Fixed:
- fixed progress() to update progress state from all methods
---
src/BroadcastManager.php | 282 +++++++++++++++++++++++++++++++--------
1 file changed, 223 insertions(+), 59 deletions(-)
diff --git a/src/BroadcastManager.php b/src/BroadcastManager.php
index e8c2a63..18b605f 100644
--- a/src/BroadcastManager.php
+++ b/src/BroadcastManager.php
@@ -3,7 +3,7 @@
declare(strict_types=1);
/**
- * Broadcast Manager
+ * Broadcast Manager Tool
*
* @author - WizardLoop
* @copyright - WizardLoop
@@ -22,7 +22,6 @@ class BroadcastManager
{
private API $api;
private ?array $currentBroadcastState = null;
- private array $albumTimers = [];
private static string $dataDir = '';
public function __construct(API $api)
@@ -32,14 +31,14 @@ public function __construct(API $api)
/**
* Set data dir
- */
+ */
public static function setDataDir(string $path): void {
self::$dataDir = rtrim($path, '/');
}
/**
* Get data dir
- */
+ */
private static function getDataDir(): string {
if (!self::$dataDir) {
self::$dataDir = __DIR__ . '/../data';
@@ -48,29 +47,39 @@ private static function getDataDir(): string {
}
/**
- * Broadcast messages to users/channels with progress tracking
+ * Send broadcast.
+ *
+ * @return integer ID that can be used
*/
public function broadcastWithProgress(
array $allUsers,
array $messages,
- $chatId,
+ $chatId = null,
bool $pin = false,
int $concurrency = 20
- ): array {
+ ): string {
$api = $this->api;
+ $statusId = null;
+ if ($chatId) {
+ try {
/* ===== INIT STATUS ===== */
- $status = $api->messages->sendMessage([
+ $status = $api->messages->sendMessage([
'peer' => $chatId,
'message' => 'โ GATHERING PEERS...',
'parse_mode' => 'HTML'
]);
$statusId = $api->extractMessageId($status);
+ } catch (\Throwable) { }
+ }
$total = count($allUsers);
+ $broadcastId = bin2hex(random_bytes(8));
+
/* ===== STATE ===== */
$state = [
+ 'type' => 'send',
'sent' => 0,
'failed' => 0,
'queue' => new \SplQueue(),
@@ -82,8 +91,7 @@ public function broadcastWithProgress(
'startedAt' => microtime(true),
];
-/* ===== SET CURRENT BROADCAST STATE FOR PAUSE/CANCEL ===== */
- $this->currentBroadcastState = &$state;
+ $this->currentBroadcastState[$broadcastId] = $state;
foreach ($allUsers as $peer) {
$state['queue']->enqueue([
@@ -113,7 +121,7 @@ public function broadcastWithProgress(
($state['paused'] ? "\nโธ Paused" : '').
($state['cancel'] ? "\n๐ Cancelled" : '');
- if ($text !== $last) {
+ if ($chatId && $statusId && $text !== $last) {
try {
$api->messages->editMessage([
'peer' => $chatId,
@@ -270,14 +278,17 @@ public function broadcastWithProgress(
"โก TPS: {$tps} msg/s\n".
($state['cancel'] ? "๐ Cancelled" : "โ
Finished");
- try { $api->messages->editMessage(['peer'=>$chatId,'id'=>$statusId,'message'=>$finalText,'parse_mode'=>'HTML']); } catch (\Throwable) {}
- $dir1= self::getDataDir();
- if(!is_dir($dir1))@mkdir($dir1,0777,true);
+ if ($chatId) {
+ try { $api->messages->editMessage(['peer'=>$chatId,'id'=>$statusId,'message'=>$finalText,'parse_mode'=>'HTML']); } catch (\Throwable) {}
+ }
+
+ $dir1 = self::getDataDir();
+ try { if(!is_dir($dir1))@mkdir($dir1,0777,true); } catch (\Throwable) {}
try { \Amp\File\write("$dir1/LastBrodDATA.txt",$finalText); } catch (\Throwable) {}
foreach ($state['lastMessageIds'] as $peer=>$id) {
$dir = self::getDataDir() . "/$peer";
- if(!is_dir($dir))@mkdir($dir,0777,true);
+ try { if(!is_dir($dir))@mkdir($dir,0777,true); } catch (\Throwable) {}
try {
$fh = \Amp\File\openFile("$dir/messages.txt", "a");
$fh->write((string)$id . "\n");
@@ -286,22 +297,28 @@ public function broadcastWithProgress(
try{\Amp\File\write("$dir/lastBroadcast.txt",$id);}catch(\Throwable){}
}
- return $state;
+ $this->currentBroadcastState[$broadcastId] = $state;
+ return $broadcastId;
}
/**
- * Delete last broadcast message for all users
+ * Deletes the last broadcast message for all users.
+ *
+ * @return integer ID that can be used
*/
public function deleteLastBroadcastForAll(
array $allUsers,
- $chatId,
+ $chatId = null,
int $concurrency = 20
- ): array {
+ ): string {
$api = $this->api;
$total = count($allUsers);
+ $broadcastId = bin2hex(random_bytes(8));
+
/* ===== STATE ===== */
$state = [
+ 'type' => 'deletelast',
'deleted' => 0,
'failed' => 0,
'flood' => 0,
@@ -312,6 +329,8 @@ public function deleteLastBroadcastForAll(
'startedAt' => microtime(true),
];
+ $this->currentBroadcastState[$broadcastId] = $state;
+
foreach ($allUsers as $peer) {
$state['queue']->enqueue([
'peer' => (string)$peer,
@@ -321,6 +340,9 @@ public function deleteLastBroadcastForAll(
]);
}
+ $statusId = null;
+ if ($chatId) {
+ try {
/* ===== STATUS MESSAGE ===== */
$status = $api->messages->sendMessage([
'peer' => $chatId,
@@ -328,6 +350,8 @@ public function deleteLastBroadcastForAll(
'parse_mode' => 'HTML'
]);
$statusId = $api->extractMessageId($status);
+ } catch (\Throwable) { }
+ }
/* ===== PROGRESS LOOP ===== */
\Amp\async(function () use ($api, $chatId, $statusId, &$state, $total) {
@@ -347,7 +371,7 @@ public function deleteLastBroadcastForAll(
"โก TPS: {$tps} msg/s".
($state['cancel'] ? "\n๐ Cancelled" : '');
- if ($text !== $last) {
+ if ($chatId && $statusId && $text !== $last) {
try {
$api->messages->editMessage([
'peer' => $chatId,
@@ -482,6 +506,7 @@ public function deleteLastBroadcastForAll(
"โ Failed: {$state['failed']}\n".
($state['cancel'] ? "๐ Cancelled" : "โ
Finished");
+ if ($chatId) {
try {
$api->messages->editMessage([
'peer' => $chatId,
@@ -490,22 +515,29 @@ public function deleteLastBroadcastForAll(
'parse_mode' => 'HTML'
]);
} catch (\Throwable) {}
+ }
- return $state;
+ $this->currentBroadcastState[$broadcastId] = $state;
+ return $broadcastId;
}
/**
- * Delete all broadcasts message for all users
+ * Deletes all broadcast message for all users.
+ *
+ * @return integer ID that can be used
*/
public function deleteAllBroadcastsForAll(
array $allUsers,
- $chatId,
+ $chatId = null,
int $concurrency = 20
- ): array {
+ ): string {
$api = $this->api;
$total = count($allUsers);
+ $broadcastId = bin2hex(random_bytes(8));
+
$state = [
+ 'type' => 'deleteall',
'deleted' => 0,
'failed' => 0,
'flood' => 0,
@@ -516,6 +548,8 @@ public function deleteAllBroadcastsForAll(
'startedAt' => microtime(true),
];
+ $this->currentBroadcastState[$broadcastId] = $state;
+
foreach ($allUsers as $peer) {
$state['queue']->enqueue([
'peer' => (string)$peer,
@@ -525,12 +559,17 @@ public function deleteAllBroadcastsForAll(
]);
}
+ $statusId = null;
+ if ($chatId) {
+ try {
$status = $api->messages->sendMessage([
'peer' => $chatId,
'message' => "โ Deleting all broadcasts...",
'parse_mode' => 'HTML'
]);
$statusId = $api->extractMessageId($status);
+ } catch (\Throwable) { }
+ }
\Amp\async(function () use (&$state) {
while (!$state['done']) {
@@ -623,8 +662,8 @@ public function deleteAllBroadcastsForAll(
else $state['failed']++;
$file2 = self::getDataDir() ."/$peer/lastBroadcast.txt";
- @unlink($file2);
- @unlink($file);
+ try { @unlink($file2); } catch (\Throwable) { }
+ try { @unlink($file); } catch (\Throwable) { }
unset($state['inFlight'][$peer]);
$processed = $state['deleted'] + $state['failed'];
@@ -640,6 +679,7 @@ public function deleteAllBroadcastsForAll(
"โณ Pending: $pending\n".
"โก TPS: {$tps} msg/s";
+ if ($chatId) {
try {
$api->messages->editMessage([
'peer' => $chatId,
@@ -648,6 +688,7 @@ public function deleteAllBroadcastsForAll(
'parse_mode' => 'HTML'
]);
} catch (\Throwable) {}
+ }
} catch (\Throwable) {
unset($state['inFlight'][$peer]);
@@ -678,6 +719,7 @@ public function deleteAllBroadcastsForAll(
"โ Failed: {$state['failed']}\n".
($state['cancel'] ? "๐ Cancelled" : "โ
Finished");
+ if ($chatId) {
try {
$api->messages->editMessage([
'peer' => $chatId,
@@ -686,20 +728,27 @@ public function deleteAllBroadcastsForAll(
'parse_mode' => 'HTML'
]);
} catch (\Throwable) {}
+ }
- return $state;
+ $this->currentBroadcastState[$broadcastId] = $state;
+ return $broadcastId;
}
/**
* Unpin all messages for all users
+ *
+ * @return integer ID that can be used
*/
public function unpinAllMessagesForAll(
array $allUsers,
- $chatId,
+ $chatId = null,
int $concurrency = 20
- ): array {
+ ): string {
$api = $this->api;
+ $statusId = null;
+ if ($chatId) {
+ try {
/* ===== STATUS MESSAGE ===== */
$status = $api->messages->sendMessage([
'peer' => $chatId,
@@ -707,11 +756,16 @@ public function unpinAllMessagesForAll(
'parse_mode' => 'HTML'
]);
$statusId = $api->extractMessageId($status);
+ } catch (\Throwable) { }
+ }
$total = count($allUsers);
+ $broadcastId = bin2hex(random_bytes(8));
+
/* ===== STATE ===== */
$state = [
+ 'type' => 'unpin',
'unpin' => 0,
'failed' => 0,
'flood' => 0,
@@ -722,6 +776,8 @@ public function unpinAllMessagesForAll(
'startedAt' => microtime(true),
];
+ $this->currentBroadcastState[$broadcastId] = $state;
+
foreach ($allUsers as $peer) {
$state['queue']->enqueue([
'peer' => $peer,
@@ -731,12 +787,16 @@ public function unpinAllMessagesForAll(
]);
}
+ if ($chatId) {
+ try {
$api->messages->editMessage([
'peer' => $chatId,
'id' => $statusId,
'message' => "๐โ Starting unpin for all subscribers...",
'parse_mode' => 'HTML'
]);
+ } catch (\Throwable) { }
+ }
/* ===== PROGRESS LOOP ===== */
\Amp\async(function () use ($api, $chatId, $statusId, &$state, $total) {
@@ -757,7 +817,7 @@ public function unpinAllMessagesForAll(
"โก TPS: {$tps} msg/s".
($state['cancel'] ? "\n๐ Cancelled" : '');
- if ($text !== $last) {
+ if ($chatId && $statusId && $text !== $last) {
try {
$api->messages->editMessage([
'peer' => $chatId,
@@ -867,6 +927,7 @@ public function unpinAllMessagesForAll(
"โ Failed: {$state['failed']}\n".
($state['cancel'] ? "๐ Cancelled" : "โ
Finished");
+ if ($chatId) {
try {
$api->messages->editMessage([
'peer' => $chatId,
@@ -875,8 +936,10 @@ public function unpinAllMessagesForAll(
'parse_mode' => 'HTML'
]);
} catch (\Throwable) {}
+ }
- return $state;
+ $this->currentBroadcastState[$broadcastId] = $state;
+ return $broadcastId;
}
/**
@@ -891,43 +954,62 @@ private function progressBar(int $current, int $total): string {
/**
* Pause running broadcast
*/
- public function pause(): void {
- if ($this->currentBroadcastState) {
- $this->currentBroadcastState['paused'] = true;
+ public function pause(string $id): void {
+ if (isset($this->currentBroadcastState[$id])) {
+ $this->currentBroadcastState[$id]['paused'] = true;
}
}
/**
* Resume running broadcast
*/
- public function resume(): void {
- if ($this->currentBroadcastState) {
- $this->currentBroadcastState['paused'] = false;
+ public function resume(string $id): void {
+ if (isset($this->currentBroadcastState[$id])) {
+ $this->currentBroadcastState[$id]['paused'] = false;
}
}
/**
* cancel running broadcast
*/
- public function cancel(): void {
- if ($this->currentBroadcastState) {
- $this->currentBroadcastState['cancel'] = true;
- $this->currentBroadcastState['inFlight'] = [];
+ public function cancel(string $id): void {
+ if (isset($this->currentBroadcastState[$id])) {
+ $this->currentBroadcastState[$id]['cancel'] = true;
+ $this->currentBroadcastState[$id]['inFlight'] = [];
}
}
/**
* Check if broadcast is paused
*/
- public function isPaused(): bool {
- return $this->currentBroadcastState['paused'] ?? false;
+ public function isPaused(string $id): bool {
+ return $this->currentBroadcastState[$id]['paused'] ?? false;
}
/**
* Check if broadcast is cancelled
*/
- public function isCancelled(): bool {
- return $this->currentBroadcastState['cancel'] ?? false;
+ public function isCancelled(string $id): bool {
+ return $this->currentBroadcastState[$id]['cancel'] ?? false;
+ }
+
+ /**
+ * Check if broadcast is active
+ */
+ public function isActive(?string $id = null): bool {
+ if (!$id || !isset($this->currentBroadcastState[$id])) {
+ return false;
+ }
+
+ $state = $this->currentBroadcastState[$id];
+
+ if (!$state) return false;
+
+ return (
+ empty($state['done']) &&
+ empty($state['cancel']) &&
+ empty($state['paused'])
+ );
}
/**
@@ -955,23 +1037,97 @@ public function hasAllBroadcast(): bool {
}
/**
- * Get current broadcast progress
+ * normalize broadcast state
*/
- public function progress(): ?array {
- if (!$this->currentBroadcastState) return null;
+ private function normalizeBroadcastState(array $state): array {
+ return [
+ 'sent' => $state['sent'] ?? 0,
+ 'deleted' => $state['deleted'] ?? 0,
+ 'unpin' => $state['unpin'] ?? 0,
+ 'failed' => $state['failed'] ?? 0,
+ 'flood' => $state['flood'] ?? 0,
+
+ 'queue' => $state['queue'] ?? null,
+ 'inFlight' => $state['inFlight'] ?? [],
- $state = $this->currentBroadcastState;
- $processed = $state['sent'] + $state['failed'];
- $pending = ($state['queue'] ? $state['queue']->count() : 0);
+ 'done' => $state['done'] ?? false,
+ 'paused' => $state['paused'] ?? false,
+ 'cancel' => $state['cancel'] ?? false,
+
+ 'startedAt' => $state['startedAt'] ?? null,
+ ];
+ }
+
+ /**
+ * Get current broadcast progress
+ *
+ * @return array|null {
+ * processed: int, // total processed items (sent + deleted + unpin + failed)
+ * success: int, // successful operations (sent + deleted + unpin)
+ * failed: int, // failed operations count
+ * pending: int, // remaining items in queue
+ * flood: int, // FLOOD_WAIT occurrences
+ *
+ * progressPercent: float, // completion percentage (processed / total)
+ *
+ * breakdown: array {
+ * sent: int,
+ * deleted: int,
+ * unpin: int
+ * },
+ *
+ * done: bool, // process finished
+ * paused: bool, // process paused
+ * cancel: bool, // process cancelled
+ *
+ * startedAt: float // microtime start timestamp
+ * }
+ */
+ public function progress(?string $id = null): ?array {
+ if (!$id || !isset($this->currentBroadcastState[$id])) {
+ return null;
+ }
+
+ $state = $this->normalizeBroadcastState($this->currentBroadcastState[$id]);
+
+ $sent = (int)($state['sent'] ?? 0);
+ $deleted = (int)($state['deleted'] ?? 0);
+ $unpin = (int)($state['unpin'] ?? 0);
+ $failed = (int)($state['failed'] ?? 0);
+ $flood = (int)($state['flood'] ?? 0);
+
+ $processed = $sent + $deleted + $unpin + $failed;
+ $success = $sent + $deleted + $unpin;
+
+ $pending = ($state['queue'] instanceof \SplQueue)
+ ? $state['queue']->count()
+ : 0;
+
+ $total = $processed + $pending;
+
+ $progressPercent = $total > 0
+ ? round(($processed / $total) * 100, 2)
+ : 0;
return [
- 'sent' => $state['sent'],
- 'failed' => $state['failed'],
- 'flood' => $state['flood'] ?? 0,
- 'pending' => $pending,
- 'done' => $state['done'] ?? false,
- 'paused' => $state['paused'] ?? false,
- 'cancelled' => $state['cancel'] ?? false,
+ 'processed' => $processed,
+ 'success' => $success,
+ 'failed' => $failed,
+ 'pending' => $pending,
+ 'flood' => $flood,
+
+ 'progressPercent' => $progressPercent,
+
+ 'breakdown' => [
+ 'sent' => $sent,
+ 'deleted' => $deleted,
+ 'unpin' => $unpin,
+ ],
+
+ 'done' => (bool)($state['done'] ?? false),
+ 'paused' => (bool)($state['paused'] ?? false),
+ 'cancel' => (bool)($state['cancel'] ?? false),
+ 'startedAt' => $state['startedAt'] ?? null,
];
}
@@ -991,11 +1147,18 @@ public function lastBroadcastData(): string|false {
return false;
}
-return \Amp\File\read($path);
+ return \Amp\File\read($path);
}
/**
* Filter peers
+ * allowedTypes: all / users / groups / channels
+ *
+ * @return array {
+ * targets: array, // filtered peers
+ * failed: int, // count of failed
+ * total: int, // count filtered peers
+ * }
*/
public function filterPeers(
array $allUsers,
@@ -1034,4 +1197,5 @@ public function filterPeers(
];
}
+
}
From dddfdd1a12b0e00ecd998ee865833bde188723fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:16:58 +0300
Subject: [PATCH 07/11] Refactor BroadcastManager and update CHANGELOG
Refactor BroadcastManager to improve functionality and code structure, including enhanced error handling and support for optional chat IDs.
---
CHANGELOG.md | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 823ae63..f0ab89e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -165,3 +165,20 @@ Added functionality to send initial status messages when gathering peers and sta
### Added & Fixed:
* Handle additional RPCErrorException cases
+
+---
+
+## [3.1.0] - 2026-04-13
+
+# Refactor BroadcastManager for improved functionality
+- Updated BroadcastManager class to improve functionality and code structure.
+- Added support for handling broadcast IDs, enhanced error handling, and refactored methods to accept optional chat IDs.
+
+### Added:
+- added `isActive()` to check active
+- added option to set chatId as null
+
+### Fixed:
+- fixed `progress()` to update progress state from all methods
+
+---
From 0e0acb2c089c8cb5bd09023079263c86456ccabd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:18:47 +0300
Subject: [PATCH 08/11] Revise README with updated broadcast methods
Updated README to reflect changes in broadcast management methods and usage examples.
---
README.md | 159 +++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 110 insertions(+), 49 deletions(-)
diff --git a/README.md b/README.md
index bdbab61..7a99e44 100644
--- a/README.md
+++ b/README.md
@@ -58,29 +58,6 @@ Version](https://img.shields.io/packagist/v/wizardloop/broadcastmanager)](https:
---
-## ๐ Repository Structure
-
-```
-BroadcastManager/
-โโโ src/
-โ โโโ BroadcastManager.php
-โโโ data/
-โ โโโ .gitkeep
-โโโ composer.json
-โโโ README.md
-โโโ LICENSE
-โโโ CHANGELOG.md
-```
-
----
-
-## ๐ป Requirements
-
-* [MadelineProto](https://docs.madelineproto.xyz/)
-* [amphp/amp](https://amphp.org/)
-
----
-
## โก Installation
```bash
@@ -97,6 +74,10 @@ require 'vendor/autoload.php';
## ๐ Usage Example
+---
+## Send Broadcast
+
+### 1) live progress update in message to admin:
```php
use BroadcastTool\BroadcastManager;
@@ -105,10 +86,90 @@ $manager = new BroadcastManager($api);
$manager->broadcastWithProgress($users, $messages, $adminChatId, true, 20);
```
+### 2) track on progress without message:
+This method returns an integer ID that can be used.
+
+```php
+use BroadcastTool\BroadcastManager;
+
+$manager = new BroadcastManager($api);
+
+$broadcastId = $manager->broadcastWithProgress($users, $messages, null, true, 20);
+
+/**
+ * Get progress (can be polled)
+ */
+$progress = $manager->progress($broadcastId);
+
+if ($progress !== null) {
+
+ // ๐ Core stats
+ $processed = $progress['processed'];
+ $success = $progress['success'];
+ $failed = $progress['failed'];
+ $pending = $progress['pending'];
+ $flood = $progress['flood'];
+
+ // ๐ Progress %
+ $progressPercent = $progress['progressPercent'];
+
+ // ๐ฆ Breakdown
+ $sent = $progress['breakdown']['sent'];
+ $deleted = $progress['breakdown']['deleted'];
+ $unpin = $progress['breakdown']['unpin'];
+
+ // โ๏ธ State
+ $done = $progress['done'];
+ $paused = $progress['paused'];
+ $cancel = $progress['cancel'];
+
+ // โฑ Timing
+ $startedAt = $progress['startedAt'];
+
+ /**
+ * Example usage
+ */
+ echo "Progress: {$progressPercent}%\n";
+ echo "Sent: {$sent}\n";
+ echo "Failed: {$failed}\n";
+
+ if ($done) {
+ echo "Broadcast finished!";
+ }
+
+ if ($paused) {
+ echo "Broadcast paused...";
+ }
+}
+```
+
+```php
+* progress return array|null {
+* processed: int, // total processed items (sent + deleted + unpin + failed)
+* success: int, // successful operations (sent + deleted + unpin)
+* failed: int, // failed operations count
+* pending: int, // remaining items in queue
+* flood: int, // FLOOD_WAIT occurrences
+*
+* progressPercent: float, // completion percentage (processed / total)
+*
+* breakdown: array {
+* sent: int,
+* deleted: int,
+* unpin: int
+* },
+*
+* done: bool, // process finished
+* paused: bool, // process paused
+* cancel: bool, // process cancelled
+*
+* startedAt: float // microtime start timestamp
+* }
+```
+
---
## Filer Peers
-
```php
$filterSub = $manager->filterPeers($users, 'users');
$targets = $filterSub['targets']; # array
@@ -121,32 +182,32 @@ $total = $filterSub['total']; # int
## โธ Control Broadcasts
```php
-$manager->pause();
-$manager->resume();
-$manager->cancel();
+$manager->pause($broadcastId);
+$manager->resume($broadcastId);
+$manager->cancel($broadcastId);
```
-Check state:
+### Check state:
```php
-if ($manager->isPaused()) echo "Paused";
-if ($manager->isCancelled()) echo "Cancelled";
-if (!$manager->hasLastBroadcast()) echo "No last Broadcast do delete";
-if (!$manager->hasAllBroadcast()) echo "No all Broadcast to delete";
-print_r($manager->progress());
+if ($manager->isActive($broadcastId));
+if ($manager->isPaused($broadcastId));
+if ($manager->isCancelled($broadcastId));
+if (!$manager->hasLastBroadcast($broadcastId));
+if (!$manager->hasAllBroadcast($broadcastId));
+print_r($manager->progress($broadcastId));
```
-Set data dir:
+### Set data dir:
```php
-BroadcastManager::setDataDir(__DIR__ . '/data');
+BroadcastManager::setDataDir(__DIR__ . '/data'); // default: __DIR__ . '/../data'
```
-_default is: __DIR__ . '/../data'_
---
## ๐งน Delete Last Broadcast
```php
-$manager->deleteLastBroadcastForAll($users, $adminChatId, 20);
+$broadcastId = $manager->deleteLastBroadcastForAll($users, $adminChatId, 20);
```
---
@@ -154,7 +215,7 @@ $manager->deleteLastBroadcastForAll($users, $adminChatId, 20);
## โป๏ธ Delete All Broadcast
```php
-$manager->deleteAllBroadcastsForAll($users, $adminChatId, 20);
+$broadcastId = $manager->deleteAllBroadcastsForAll($users, $adminChatId, 20);
```
---
@@ -162,23 +223,23 @@ $manager->deleteAllBroadcastsForAll($users, $adminChatId, 20);
## ๐ Get Last Broadcast Data
```php
-$manager->lastBroadcastData();
+$broadcastId = $manager->lastBroadcastData();
```
---
## ๐ Pin / Unpin Messages
-Pin last broadcast automatically:
+## Pin last broadcast automatically:
```php
-$manager->broadcastWithProgress(..., pin: true);
+$broadcastId = $manager->broadcastWithProgress(..., pin: true);
```
-Unpin all messages:
+## Unpin all messages:
```php
-$manager->unpinAllMessagesForAll(...);
+$broadcastId = $manager->unpinAllMessagesForAll(...);
```
---
@@ -199,11 +260,11 @@ $message = [
## โ๏ธ Advanced Options
-* **Concurrency** โ Number of parallel workers.
-* **Filter Types** โ 'users', 'groups', 'channels', 'all'
-* **Album Handling** โ JSON-based albums with multiple media files.
-* **Retries & Delays** โ Automatic retries with backoff.
-* **Progress Tracking** โ Real-time broadcast stats with `progress()`.
+* **Concurrency** - Number of parallel workers.
+* **Filter Types** - 'users', 'groups', 'channels', 'all'
+* **Album Handling** - JSON-based albums with multiple media files.
+* **Retries & Delays** - Automatic retries with backoff.
+* **Progress Tracking** - Real-time broadcast stats with `progress()`.
---
@@ -219,7 +280,7 @@ $message = [
## ๐ License
-**GNU AGPL-3.0** โ see [LICENSE](LICENSE).
+**GNU AGPL-3.0** - see [LICENSE](LICENSE).
---
From 326cf795d6eb3a7e9b367d68e6c4af5f89f5857f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:23:49 +0300
Subject: [PATCH 09/11] Adjust PHPStan configuration parameters
Lower PHPStan level from 6 to 5 and add new parameters.
---
phpstan.neon | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/phpstan.neon b/phpstan.neon
index db37782..e0142f9 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,4 +1,10 @@
parameters:
- level: 6
+ level: 5
paths:
- src
+
+ checkMissingIterableValueType: true
+ checkGenericClassInNonGenericObjectType: true
+
+ ignoreErrors:
+ - '#Call to an undefined method#'
From be54df8435abe094dc87d5bd3385a1dcd3549e36 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:32:17 +0300
Subject: [PATCH 10/11] Add CI workflow for PHP project
---
.github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
create mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..704a97f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,31 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.2
+ tools: composer:v2
+
+ - name: Validate composer.json
+ run: composer validate --strict
+ continue-on-error: true
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress --no-interaction
+ continue-on-error: true
+
+ - name: PHP Syntax check
+ run: find src -name "*.php" -exec php -l {} \;
+ continue-on-error: true
From 4c9d9f9d655e2ac13dad097462c5b166427a7b4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wizard=20Loop=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?=
<67387949+WizardLoop@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:35:33 +0300
Subject: [PATCH 11/11] Add CI badge to README
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 7a99e44..a02bea4 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ Manage broadcasts efficiently: send messages, media albums, pin/unpin messages,
[](https://packagist.org/packages/wizardloop/broadcastmanager)
[](https://packagist.org/packages/wizardloop/broadcastmanager)
+
---