diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0c96721..bee3f0f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Introducing MadelineProto's biggest update yet, 8.0.0-beta100! This version introduces [plugins](https://docs.madelineproto.xyz/docs/PLUGINS.html), [bound methods](https://docs.madelineproto.xyz/docs/UPDATES.html#bound-methods), [filters](https://docs.madelineproto.xyz/docs/FILTERS.html), [a built-in cron system](https://docs.madelineproto.xyz/docs/UPDATES.html#cron), [IPC support for the event handler](https://docs.madelineproto.xyz/docs/UPDATES.html#persisting-data-and-ipc) and automatic static analysis for event handler code. +- [Plugins](https://docs.madelineproto.xyz/docs/PLUGINS.html) + To create a plugin, simply create an event handler that extends PluginEventHandler. For example, create a `plugins/Danogentili/PingPlugin.php` file: @@ -44,10 +46,16 @@ And use a [plugin base](https://raw.githubusercontent.com/danog/MadelineProto/v8 See the [documentation](https://docs.madelineproto.xyz/docs/PLUGINS.html) for more info on how to create MadelineProto plugins! +- [Message](https://docs.madelineproto.xyz/PHP/danog/MadelineProto/EventHandler/Message.html) objects with bound methods + Both plugins and normal bots can make use of [bound update methods](https://docs.madelineproto.xyz/docs/UPDATES.html#bound-methods) like `reply()`, `delete()`, `getReply()`, `getHTML()` and simplified properties like `chatId`, `senderId`, `command`, `commandArgs` and many more, see the [documentation](https://docs.madelineproto.xyz/docs/UPDATES.html#bound-methods) for more info! +- [Filters](https://docs.madelineproto.xyz/docs/FILTERS.html) + Plugins and bots can now use three different filtering systems, to easily receive only updates satisfying certain conditions (incoming/outgoing, from group, channel, private, from an admin or a specific peer, with an audio/sticker/..., satisfying a certain regex or a certain /command, and much more!), [see the documentation](https://docs.madelineproto.xyz/docs/FILTERS.html) for more info! +- [Built-in cron system](https://docs.madelineproto.xyz/docs/UPDATES.html#cron) + All event handler methods marked by the `Cron` attribute are now automatically invoked by MadelineProto every `period` seconds: ``` @@ -68,10 +76,17 @@ class MyEventHandler extends SimpleEventHandler See the [documentation](https://docs.madelineproto.xyz/docs/UPDATES.html#cron) for more info! +- [IPC support for the event handler](https://docs.madelineproto.xyz/docs/UPDATES.html#persisting-data-and-ipc) + +You can now call event handler and plugin methods from outside of the event handler, using `getEventHandler()` on an `API` instance, see [the docs for more info](https://docs.madelineproto.xyz/docs/PLUGINS.html#limitations)! + +- Automatic static analysis of event handler code + Finally, all new bots and plugins will be automatically analyzed by MadelineProto, blocking execution if performance or security issues are detected! Other features: - Thanks to the many translation contributors @ https://weblate.madelineproto.xyz/, MadelineProto is now localized in Hebrew, Persian, Kurdish, Uzbek, Russian, French and Italian! +- Added simplified `sendMessage`, `sendDocument`, `sendPhoto` methods that return abstract [Message](https://docs.madelineproto.xyz/PHP/danog/MadelineProto/EventHandler/Message.html) objects with simplified properties and bound methods! - You can now use `Tools::callFork` to fork a new green thread! - You can now automatically pin messages broadcasted using `broadcastMessages`, `broadcastForwardMessages` by using the new `pin: true` parameter! - You can now use `sendMessageToAdmins` to send messages to the bot's admin (the peers returned by `getReportPeers`). @@ -84,10 +99,6 @@ Other features: - You can now use `reportMemoryProfile()` to generate and send a `pprof` memory profile to all report peers to debug the causes of high memory usage. - Added support for `pay`, `login_url`, `web_app` and `tg://user?id=` buttons in bot API syntax! - Added a `getAdminIds` function that returns the IDs of the admin of the bot (equal to the peers returned by getReportPeers in the event handler). -- getEventHandler can now be used from IPC clients! -- Added `Cron` -- Added plugins, filters, simple filters -- `getReply`, `sendMessage`, `sendDocument`, `sendPhoto`, `reply`, `delete` Fixes: - Fixed file uploads with ext-uv! diff --git a/README.md b/README.md index 661b4e5c8..7243359cd 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Want to add your own open-source project to this list? [Click here!](https://doc * [Filters](https://docs.madelineproto.xyz/docs/FILTERS.html) * [Simple filters](https://docs.madelineproto.xyz/docs/FILTERS.html#simple-filters) * [Attribute filters](https://docs.madelineproto.xyz/docs/FILTERS.html#attribute-filters) + * [Creating custom attribute filters](https://docs.madelineproto.xyz/docs/FILTERS.html#creating-custom-attribute-filters) * [MTProto filters](https://docs.madelineproto.xyz/docs/FILTERS.html#mtproto-filters) * [Plugins](https://docs.madelineproto.xyz/docs/PLUGINS.html) * [Installing plugins](https://docs.madelineproto.xyz/docs/PLUGINS.html#installing-plugins) diff --git a/docs b/docs index d3eb81be0..64bf195ee 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit d3eb81be0cad837e7fd3f7f6d6e2f7b518b78b55 +Subproject commit 64bf195ee68d92bde6f49955ef735f58e9d6083c diff --git a/src/Broadcast/Progress.php b/src/Broadcast/Progress.php index 9815078f3..da241e4b1 100644 --- a/src/Broadcast/Progress.php +++ b/src/Broadcast/Progress.php @@ -45,6 +45,7 @@ final class Progress implements JsonSerializable ) { $this->percent = $pendingCount ? (int) (($successCount+$failCount)*100/$pendingCount) : 0; } + /** @internal */ public function jsonSerialize(): mixed { return \get_object_vars($this); diff --git a/src/EventHandler/CommandType.php b/src/EventHandler/CommandType.php index 22ff6cde8..c0e1c45a6 100644 --- a/src/EventHandler/CommandType.php +++ b/src/EventHandler/CommandType.php @@ -9,6 +9,7 @@ enum CommandType: string implements JsonSerializable case SLASH = '/'; case DOT = '.'; case BANG = '!'; + /** @internal */ public function jsonSerialize(): mixed { return $this->value; diff --git a/src/EventHandler/Filter/Combinator/FilterNot.php b/src/EventHandler/Filter/Combinator/FilterNot.php index 93099ae90..8d214d05a 100644 --- a/src/EventHandler/Filter/Combinator/FilterNot.php +++ b/src/EventHandler/Filter/Combinator/FilterNot.php @@ -20,11 +20,14 @@ final class FilterNot extends Filter { $filter = $this->filter->initialize($API); if ($filter === null) { + // The nested filter didn't replace itself return $this; } if ($filter instanceof self) { + // The nested filter is a FilterNot, optimize !!A => A return $filter->filter; } + // The nested filter replaced itself, re-wrap it return new self($filter); } diff --git a/src/EventHandler/Media.php b/src/EventHandler/Media.php index a94bcd99f..c0e0100a9 100644 --- a/src/EventHandler/Media.php +++ b/src/EventHandler/Media.php @@ -75,6 +75,7 @@ abstract class Media extends IpcCapable implements JsonSerializable $this->spoiler = $rawMedia['spoiler'] ?? false; } + /** @internal */ public function jsonSerialize(): mixed { $v = \get_object_vars($this); diff --git a/src/EventHandler/Media/MaskPosition.php b/src/EventHandler/Media/MaskPosition.php index c613aea4e..8f1fba371 100644 --- a/src/EventHandler/Media/MaskPosition.php +++ b/src/EventHandler/Media/MaskPosition.php @@ -12,6 +12,7 @@ enum MaskPosition: int implements JsonSerializable case Mouth = 2; case Chin = 3; + /** @internal */ public function jsonSerialize(): mixed { return $this->value; diff --git a/src/EventHandler/Message.php b/src/EventHandler/Message.php index a0a9c5402..f0980274b 100644 --- a/src/EventHandler/Message.php +++ b/src/EventHandler/Message.php @@ -47,10 +47,8 @@ abstract class Message extends AbstractMessage /** * Attached media. - * - * @var Audio|Document|DocumentPhoto|Gif|MaskSticker|Photo|RoundVideo|Sticker|Video|Voice|null */ - public readonly ?Media $media; + public readonly Audio|Document|DocumentPhoto|Gif|MaskSticker|Photo|RoundVideo|Sticker|Video|Voice|null $media; /** Whether this message is a sent scheduled message */ public readonly bool $fromScheduled; diff --git a/src/EventHandler/Update.php b/src/EventHandler/Update.php index 055e79d0e..6cd3f4ba7 100644 --- a/src/EventHandler/Update.php +++ b/src/EventHandler/Update.php @@ -10,6 +10,7 @@ use JsonSerializable; */ abstract class Update extends IpcCapable implements JsonSerializable { + /** @internal */ public function jsonSerialize(): mixed { $v = \get_object_vars($this); diff --git a/src/FileCallback.php b/src/FileCallback.php index e8b2c3987..867cddebe 100644 --- a/src/FileCallback.php +++ b/src/FileCallback.php @@ -28,14 +28,14 @@ final class FileCallback implements FileCallbackInterface /** * Callback. * - * @var callable(float, float, float) + * @var callable(float, float, int) */ public readonly mixed $callback; /** * Construct file callback. * * @param mixed $file File to download/upload - * @param callable(float, float, float) $callback Callback + * @param callable(float, float, int) $callback Callback */ public function __construct(public readonly mixed $file, callable $callback) { diff --git a/src/InternalDoc.php b/src/InternalDoc.php index ba966cd8b..04aaf43e8 100644 --- a/src/InternalDoc.php +++ b/src/InternalDoc.php @@ -767,7 +767,7 @@ abstract class InternalDoc /** * Get event handler (or plugin instance). * - * @param ?class-string + * @param ?class-string $class */ public function getEventHandler(?string $class = null): \danog\MadelineProto\EventHandler|\danog\MadelineProto\Ipc\EventHandlerProxy|\__PHP_Incomplete_Class|null { @@ -1497,7 +1497,7 @@ abstract class InternalDoc * @param Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream $file File to upload: can be a message to reuse media present in a message. * @param Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream|null $thumb Optional: Thumbnail to upload * @param string $caption Caption of document - * @param ?callable(float, float, float) $callback Upload callback (percent, speed in mpbs, time elapsed) + * @param ?callable(float, float, int) $callback Upload callback (percent, speed in mpbs, time elapsed) * @param ?string $fileName Optional file name, if absent will be extracted from the passed $file. * @param "html"|"markdown"|null $parseMode Parse mode * @param integer|null $replyToMsgId ID of message to reply to. @@ -1549,7 +1549,7 @@ abstract class InternalDoc * @param boolean $clearDraft Clears the draft field * @param boolean $noWebpage Set this flag to disable generation of the webpage preview * - * @return list + * @return list<\danog\Madelineproto\EventHandler\Message> */ public function sendMessageToAdmins(string $message, ?string $parseMode = null, ?array $replyMarkup = null, ?int $scheduleDate = null, bool $silent = false, bool $noForwards = false, bool $background = false, bool $clearDraft = false, bool $noWebpage = false): array { @@ -1563,7 +1563,7 @@ abstract class InternalDoc * @param integer|string $peer Destination peer or username. * @param Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream $file File to upload: can be a message to reuse media present in a message. * @param string $caption Caption of document - * @param ?callable(float, float, float) $callback Upload callback (percent, speed in mpbs, time elapsed) + * @param ?callable(float, float, int) $callback Upload callback (percent, speed in mpbs, time elapsed) * @param ?string $fileName Optional file name, if absent will be extracted from the passed $file. * @param "html"|"markdown"|null $parseMode Parse mode * @param integer|null $replyToMsgId ID of message to reply to. @@ -1958,7 +1958,7 @@ abstract class InternalDoc /** * Wrap a media constructor into an abstract Media object. */ - public function wrapMedia(array $media, bool $protected): ?\danog\MadelineProto\EventHandler\Media + public function wrapMedia(array $media, bool $protected = false): ?\danog\MadelineProto\EventHandler\Media { return $this->wrapper->getAPI()->wrapMedia($media, $protected); } diff --git a/src/MTProto.php b/src/MTProto.php index 4e8b3ab0b..608828cfb 100644 --- a/src/MTProto.php +++ b/src/MTProto.php @@ -1677,7 +1677,7 @@ final class MTProto implements TLCallback, LoggerGetter * @param boolean $clearDraft Clears the draft field * @param boolean $noWebpage Set this flag to disable generation of the webpage preview * - * @return list + * @return list<\danog\Madelineproto\EventHandler\Message> */ public function sendMessageToAdmins( string $message, diff --git a/src/MTProtoTools/Files.php b/src/MTProtoTools/Files.php index 56f87a5ad..0294af63f 100644 --- a/src/MTProtoTools/Files.php +++ b/src/MTProtoTools/Files.php @@ -899,7 +899,7 @@ trait Files $seekable = false; } if ($offset === $end) { - $cb(100, 0, 0); + $cb(100.0, 0.0, 0); return true; } $params = []; @@ -923,11 +923,11 @@ trait Files return true; } $count = \count($params); - $time = 0; - $speed = 0; + $time = 0.0; + $speed = 0.0; $origCb = $cb; $cb = static function () use ($cb, $count, &$time, &$speed): void { - static $cur = 0; + static $cur = 0.0; $cur++; $cb($cur * 100 / $count, $time, $speed); }; @@ -981,7 +981,7 @@ trait Files $this->clearCdnHashes($messageMedia['file_token']); } if (!isset($messageMedia['size'])) { - $origCb(100, $time, $speed); + $origCb(100.0, $time, $speed); } return true; } diff --git a/src/MTProtoTools/FilesAbstraction.php b/src/MTProtoTools/FilesAbstraction.php index 00abc5c1d..807e11005 100644 --- a/src/MTProtoTools/FilesAbstraction.php +++ b/src/MTProtoTools/FilesAbstraction.php @@ -54,7 +54,7 @@ trait FilesAbstraction /** * Wrap a media constructor into an abstract Media object. */ - public function wrapMedia(array $media, bool $protected): ?Media + public function wrapMedia(array $media, bool $protected = false): ?Media { if ($media['_'] === 'messageMediaPhoto') { if (!isset($media['photo'])) { @@ -117,7 +117,7 @@ trait FilesAbstraction * @param Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream $file File to upload: can be a message to reuse media present in a message. * @param Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream|null $thumb Optional: Thumbnail to upload * @param string $caption Caption of document - * @param ?callable(float, float, float) $callback Upload callback (percent, speed in mpbs, time elapsed) + * @param ?callable(float, float, int) $callback Upload callback (percent, speed in mpbs, time elapsed) * @param ?string $fileName Optional file name, if absent will be extracted from the passed $file. * @param "html"|"markdown"|null $parseMode Parse mode * @param integer|null $replyToMsgId ID of message to reply to. @@ -209,7 +209,7 @@ trait FilesAbstraction * @param integer|string $peer Destination peer or username. * @param Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream $file File to upload: can be a message to reuse media present in a message. * @param string $caption Caption of document - * @param ?callable(float, float, float) $callback Upload callback (percent, speed in mpbs, time elapsed) + * @param ?callable(float, float, int) $callback Upload callback (percent, speed in mpbs, time elapsed) * @param ?string $fileName Optional file name, if absent will be extracted from the passed $file. * @param "html"|"markdown"|null $parseMode Parse mode * @param integer|null $replyToMsgId ID of message to reply to. diff --git a/src/TL/Types/Bytes.php b/src/TL/Types/Bytes.php index b8bea7dec..30d7f2450 100644 --- a/src/TL/Types/Bytes.php +++ b/src/TL/Types/Bytes.php @@ -21,6 +21,7 @@ declare(strict_types=1); namespace danog\MadelineProto\TL\Types; use ArrayAccess; +use AssertionError; use JsonSerializable; /** @@ -69,11 +70,7 @@ final class Bytes implements JsonSerializable, ArrayAccess */ public function offsetSet(mixed $offset, mixed $value): void { - if ($offset === null) { - $this->bytes .= $value; - } else { - $this->bytes[$offset] = $value; - } + throw new AssertionError("Cannot modify nested bytes!"); } /** * Get char at offset. @@ -92,7 +89,7 @@ final class Bytes implements JsonSerializable, ArrayAccess */ public function offsetUnset(mixed $offset): void { - unset($this->bytes[$offset]); + throw new AssertionError("Cannot modify nested bytes!"); } /** * Check if char at offset exists. diff --git a/src/Tools.php b/src/Tools.php index b8ae45a62..613452b62 100644 --- a/src/Tools.php +++ b/src/Tools.php @@ -28,6 +28,7 @@ use Countable; use Exception; use Fiber; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\Include_; use PhpParser\Node\Expr\New_; use PhpParser\Node\Name; use PhpParser\Node\Scalar\LNumber; @@ -629,7 +630,8 @@ abstract class Tools extends AsyncTools { $code = (new ParserFactory)->create(ParserFactory::ONLY_PHP7)->parse($code); Assert::notNull($code); - $traverser = new NodeTraverser([new NameResolver()]); + $traverser = new NodeTraverser; + $traverser->addVisitor(new NameResolver()); $code = $traverser->traverse($code); $finder = new NodeFinder; @@ -640,7 +642,7 @@ abstract class Tools extends AsyncTools } $class = $class[0]->name->toString(); - /** @var DeclareDeclare|null $call */ + /** @var DeclareDeclare|null $declare */ $declare = $finder->findFirstInstanceOf($code, DeclareDeclare::class); if ($declare === null || $declare->key->name !== 'strict_types' diff --git a/src/Wrappers/Events.php b/src/Wrappers/Events.php index 8399aa9f7..8d49cf28d 100644 --- a/src/Wrappers/Events.php +++ b/src/Wrappers/Events.php @@ -141,7 +141,7 @@ trait Events /** * Get event handler (or plugin instance). * - * @param ?class-string + * @param ?class-string $class */ public function getEventHandler(?string $class = null): EventHandler|EventHandlerProxy|__PHP_Incomplete_Class|null { diff --git a/tools/build_docs.php b/tools/build_docs.php index cd7324a51..beca4de83 100755 --- a/tools/build_docs.php +++ b/tools/build_docs.php @@ -166,7 +166,7 @@ function printTypes(array $types, string $type): string foreach ($orderedfiles as $key => $filename) { $lines = file_get_contents($filename); - $lines = preg_replace_callback('/\<\!-- cut_here (\S+) -->.*\<\!-- cut_here_end \1 --\>/sim', function ($matches) { + $lines = preg_replace_callback('/\<\!--\s+cut_here\s+(\S+)\s+-->.*\<\!--\s+cut_here_end\s+\1\s+--\>/sim', function ($matches) { [, $match] = $matches; if ($match === "concretefilters") { $result = [Update::class, AbstractMessage::class, Message::class, ServiceMessage::class];