Handling updates (new messages & other events)
Update handling can be done in different ways:
Async Event driven
Simple example:
<?php declare(strict_types=1);
// Simple example bot.
// PHP 8.2.4+ is required.
// Run via CLI (recommended: `screen php bot.php`) or via web.
// To reduce RAM usage, follow these instructions: https://docs.madelineproto.xyz/docs/DATABASE.html
use danog\MadelineProto\EventHandler\Attributes\Handler;
use danog\MadelineProto\EventHandler\Message;
use danog\MadelineProto\EventHandler\Plugin\RestartPlugin;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\SimpleEventHandler;
// Load via composer (RECOMMENDED, see https://docs.madelineproto.xyz/docs/INSTALLATION.html#composer-from-scratch)
if (file_exists('vendor/autoload.php')) {
require_once 'vendor/autoload.php';
} else {
// Otherwise download an !!! alpha !!! version of MadelineProto via madeline.php
if (!file_exists('madeline.php')) {
copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
}
require_once 'madeline.php';
}
class BasicEventHandler extends SimpleEventHandler
{
// !!! Change this to your username !!!
public const ADMIN = "@me";
/**
* Get peer(s) where to report errors.
*/
public function getReportPeers()
{
return [self::ADMIN];
}
/**
* Returns a set of plugins to activate.
*
* See here for more info on plugins: https://docs.madelineproto.xyz/docs/PLUGINS.html
*/
public static function getPlugins(): array
{
return [
// Offers a /restart command to admins that can be used to restart the bot, applying changes.
// Make sure to run in a bash while loop when running via CLI to allow self-restarts.
RestartPlugin::class,
];
}
/**
* Handle incoming updates from users, chats and channels.
*/
#[Handler]
public function handleMessage(Incoming&Message $message): void
{
// Code that uses $message...
// See the following pages for more examples and documentation:
// - https://github.com/danog/MadelineProto/blob/v8/examples/bot.php
// - https://docs.madelineproto.xyz/docs/UPDATES.html
// - https://docs.madelineproto.xyz/docs/FILTERS.html
// - https://docs.madelineproto.xyz/
}
}
BasicEventHandler::startAndLoop('bot.madeline');
Advanced example:
<?php declare(strict_types=1);
/**
* Example bot.
*
* PHP 8.2.4+ is required.
*
* Copyright 2016-2020 Daniil Gentili
* (https://daniil.it)
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
use danog\MadelineProto\API;
use danog\MadelineProto\Broadcast\Progress;
use danog\MadelineProto\Broadcast\Status;
use danog\MadelineProto\EventHandler\Attributes\Cron;
use danog\MadelineProto\EventHandler\Attributes\Handler;
use danog\MadelineProto\EventHandler\Filter\FilterCommand;
use danog\MadelineProto\EventHandler\Filter\FilterRegex;
use danog\MadelineProto\EventHandler\Filter\FilterText;
use danog\MadelineProto\EventHandler\Filter\FilterTextCaseInsensitive;
use danog\MadelineProto\EventHandler\Message;
use danog\MadelineProto\EventHandler\Message\ChannelMessage;
use danog\MadelineProto\EventHandler\Message\Service\DialogPhotoChanged;
use danog\MadelineProto\EventHandler\Plugin\RestartPlugin;
use danog\MadelineProto\EventHandler\SimpleFilter\FromAdmin;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\EventHandler\SimpleFilter\IsReply;
use danog\MadelineProto\Logger;
use danog\MadelineProto\ParseMode;
use danog\MadelineProto\RemoteUrl;
use danog\MadelineProto\Settings;
use danog\MadelineProto\Settings\Database\Mysql;
use danog\MadelineProto\Settings\Database\Postgres;
use danog\MadelineProto\Settings\Database\Redis;
use danog\MadelineProto\SimpleEventHandler;
use danog\MadelineProto\VoIP;
use function Amp\Socket\SocketAddress\fromString;
// MadelineProto is already loaded
if (class_exists(API::class)) {
// Otherwise, if a stable version of MadelineProto was installed via composer, load composer autoloader
} elseif (file_exists('vendor/autoload.php')) {
require_once 'vendor/autoload.php';
} else {
// Otherwise download an !!! alpha !!! version of MadelineProto via madeline.php
if (!file_exists('madeline.php')) {
copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
}
require_once 'madeline.php';
}
/**
* Event handler class.
*
* NOTE: ALL of the following methods are OPTIONAL.
* You can even provide an empty event handler if you want.
*
* All properties returned by __sleep are automatically stored in the database.
*/
class MyEventHandler extends SimpleEventHandler
{
/**
* @var int|string Username or ID of bot admin
*/
public const ADMIN = "@me"; // !!! Change this to your username !!!
/**
* @var array<int, bool>
*/
private array $notifiedChats = [];
/**
* Returns a list of names for properties that will be automatically saved to the session database (MySQL/postgres/redis if configured, the session file otherwise).
*/
public function __sleep(): array
{
return ['notifiedChats'];
}
/**
* Get peer(s) where to report errors.
*
* @return int|string|array
*/
public function getReportPeers()
{
return [self::ADMIN];
}
/**
* Initialization logic.
*/
public function onStart(): void
{
$this->logger("The bot was started!");
$this->logger($this->getFullInfo('MadelineProto'));
$this->sendMessageToAdmins("The bot was started!");
}
/**
* Returns a set of plugins to activate.
*/
public static function getPlugins(): array
{
return [
// Offers a /restart command to admins that can be used to restart the bot, applying changes.
// Make sure to run in a bash while loop when running via CLI to allow self-restarts.
RestartPlugin::class,
];
}
/**
* This cron function will be executed forever, every 60 seconds.
*/
#[Cron(period: 60.0)]
public function cron1(): void
{
$this->sendMessageToAdmins("The bot is online, current time ".date(DATE_RFC850)."!");
}
/**
* Handle incoming updates from users, chats and channels.
*/
#[Handler]
public function handleMessage(Incoming&Message $message): void
{
// In this example code, send the "This userbot is powered by MadelineProto!" message only once per chat.
// Ignore all further messages coming from this chat.
if (!isset($this->notifiedChats[$message->chatId])) {
$this->notifiedChats[$message->chatId] = true;
$message->reply(
message: "This userbot is powered by [MadelineProto](https://t.me/MadelineProto)!",
parseMode: ParseMode::MARKDOWN
);
}
}
/**
* Reposts a media file as a Telegram story.
*/
#[FilterCommand('story')]
public function storyCommand(Message & FromAdmin $message): void
{
if ($this->isSelfBot()) {
$message->reply("Only users can post Telegram Stories!");
return;
}
$media = $message->getReply(Message::class)?->media;
if (!$media) {
$message->reply("You should reply to a photo or video to repost it as a story!");
return;
}
$this->stories->sendStory(
peer: 'me',
media: $media,
caption: "This story was posted using [MadelineProto](https://t.me/MadelineProto)!",
parse_mode: ParseMode::MARKDOWN,
privacy_rules: [['_' => 'inputPrivacyValueAllowAll']]
);
}
/**
* Automatically sends a comment to all new incoming channel posts.
*/
#[Handler]
public function makeComment(Incoming&ChannelMessage $message): void
{
if ($this->isSelfBot()) {
return;
}
$message->getDiscussion()->reply(
message: "This comment is powered by [MadelineProto](https://t.me/MadelineProto)!",
parseMode: ParseMode::MARKDOWN
);
}
#[FilterCommand('broadcast')]
public function broadcastCommand(Message & FromAdmin $message): void
{
// We can broadcast messages to all users with /broadcast
if (!$message->replyToMsgId) {
$message->reply("You should reply to the message you want to broadcast.");
return;
}
$this->broadcastForwardMessages(
from_peer: $message->senderId,
message_ids: [$message->replyToMsgId],
drop_author: true,
pin: true,
);
}
private int $lastLog = 0;
/**
* Handles updates to an in-progress broadcast.
*/
#[Handler]
public function handleBroadcastProgress(Progress $progress): void
{
if (time() - $this->lastLog > 5 || $progress->status === Status::FINISHED) {
$this->lastLog = time();
$this->sendMessageToAdmins((string) $progress);
}
}
#[FilterCommand('echo')]
public function echoCmd(Message $message): void
{
// Contains the arguments of the command
$args = $message->commandArgs;
$message->reply($args[0] ?? '');
}
#[FilterRegex('/.*(mt?proto)[^.]?.*/i')]
public function testRegex(Incoming & Message $message): void
{
$message->reply("Did you mean to write MadelineProto instead of ".$message->matches[1].'?');
}
#[FilterText('test')]
public function pingCommand(Message $message): void
{
$message->reply('test reply');
}
#[FilterCommand('react')]
public function reactCommand(Message&IsReply $message): void
{
$message->getReply(Message::class)->addReaction('👌');
}
#[FilterCommand('unreact')]
public function unreactCommand(Message&IsReply $message): void
{
$message->getReply(Message::class)->delReaction('👌');
}
#[FilterTextCaseInsensitive('hi')]
public function pingCommandCaseInsensitive(Message $message): void
{
$message->reply('hello');
}
/**
* Called when the dialog photo of a chat or channel changes.
*/
#[Handler]
public function logPhotoChanged(Incoming&DialogPhotoChanged $message): void
{
if ($message->photo) {
$message->reply("Nice! Here's a download link for the photo: ".$message->photo->getDownloadLink());
}
// The group photo was deleted
}
/**
* Gets a download link for any file up to 4GB!
*
* The bot must be started via web for this command to work.
*
* You can also start it via CLI but you'll have to specify a download script URL in the settings: https://docs.madelineproto.xyz/docs/FILES.html#getting-a-download-link-cli-bots.
*/
#[FilterCommand('dl')]
public function downloadLink(Incoming&Message $message): void
{
$reply = $message->getReply(Message::class);
if (!$reply?->media) {
$message->reply("This command must reply to a media message!");
return;
}
$reply->reply("Download link: ".$reply->media->getDownloadLink());
}
#[FilterCommand('call')]
public function callVoip(Incoming&Message $message): void
{
$this->requestCall($message->senderId)->play(new RemoteUrl('http://icestreaming.rai.it/1.mp3'));
}
#[Handler]
public function handleIncomingCall(VoIP&Incoming $call): void
{
$call->accept()->play(new RemoteUrl('http://icestreaming.rai.it/1.mp3'));
}
public static function getPluginPaths(): string|array|null
{
return 'plugins/';
}
}
$settings = new Settings;
$settings->getLogger()->setLevel(Logger::LEVEL_ULTRA_VERBOSE);
// You can also use Redis, MySQL or PostgreSQL.
// Data is migrated automatically.
//
// $settings->setDb((new Redis)->setDatabase(0)->setPassword('pony'));
// $settings->setDb((new Postgres)->setDatabase('MadelineProto')->setUsername('daniil')->setPassword('pony'));
// $settings->setDb((new Mysql)->setDatabase('MadelineProto')->setUsername('daniil')->setPassword('pony'));
// You can also enable collection of additional prometheus metrics.
// $settings->getMetrics()->setEnablePrometheusCollection(true);
// You can also enable collection of additional memory profiling metrics.
// Note: you must also set the MEMPROF_PROFILE=1 environment variable or GET parameter.
// $settings->getMetrics()->setEnableMemprofCollection(true);
// Metrics can be returned by an autoconfigured http://127.0.0.1:12345 HTTP server.
//
// Endpoints:
//
// /metrics - Prometheus metrics
// /debug/pprof - PProf memory profile for pyroscope
//
// $settings->getMetrics()->setMetricsBindTo(fromString("127.0.0.1:12345"));
// Metrics can also be returned by the current script via web, if called with a specific query string:
//
// ?metrics - Prometheus metrics
// ?pprof - PProf memory profile for pyroscope
//
// $settings->getMetrics()->setReturnMetricsFromStartAndLoop(true);
// For users or bots
MyEventHandler::startAndLoop('bot.madeline', $settings);
// For bots only
// MyEventHandler::startAndLoopBot('bot.madeline', 'bot token', $settings);
The example code above defines an event handler class MyEventHandler
, creates a MadelineProto session, and sets the event handler class to our newly created event handler.
The new startAndLoop
method automatically initializes MadelineProto, enables async, logs in the user/bot, initializes error reporting, catches and reports all errors surfacing from the event loop to the peers returned by the getReportPeers
method.
All events are handled concurrently thanks to async, here’s a full explanation.
All incoming events are always handled, including old events that occurred while the script was turned off.
To access the $MadelineProto
instance inside of the event handler, simply access $this
:
$this->messages->sendMessage(['peer' => '@danogentili', 'message' => 'hi']);
Bound methods
MadelineProto offers a large number of helper bound methods and properties, depending on the filter type you specify in the typehint of #[Handler]
methods.
See here » for more info on how to use bound methods, properties and filters.
Here’s a full list of the concrete object types on which bound methods and properties are defined:
- danog\MadelineProto\Broadcast\Progress » - Broadcast progress.
- danog\MadelineProto\EventHandler\AbstractMessage » - Represents an incoming or outgoing message.
- danog\MadelineProto\EventHandler\AbstractPrivateMessage » - Represents a private or secret chat message.
- danog\MadelineProto\EventHandler\AbstractStory » - Represents a Telegram Story.
- danog\MadelineProto\EventHandler\BotCommands » - The command set of a certain bot in a certain chat has changed.
- danog\MadelineProto\EventHandler\CallbackQuery » - Represents a query sent by the user by clicking on a button.
- danog\MadelineProto\EventHandler\Channel\ChannelParticipant » - A participant has left, joined, was banned or admin’d in a channel or supergroup.
- danog\MadelineProto\EventHandler\Channel\MessageForwards » - Indicates that the forward counter of a message in a channel has changed.
- danog\MadelineProto\EventHandler\Channel\MessageViewsChanged » - Indicates that the view counter of a message in a channel has changed.
- danog\MadelineProto\EventHandler\Channel\UpdateChannel » - A new channel is available, or info about an existing channel was changed.
- danog\MadelineProto\EventHandler\ChatInviteRequester » - Indicates someone has requested to join a chat or channel.
- danog\MadelineProto\EventHandler\ChatInviteRequester\BotChatInviteRequest » - Indicates someone has requested to join a chat or channel (bots only).
- danog\MadelineProto\EventHandler\ChatInviteRequester\PendingJoinRequests » - Someone has requested to join a chat or channel.
- danog\MadelineProto\EventHandler\Delete » - Indicates that some messages were deleted.
- danog\MadelineProto\EventHandler\Delete\DeleteChannelMessages » - Some messages in a supergroup/channel were deleted.
- danog\MadelineProto\EventHandler\Delete\DeleteMessages » - Some messages were deleted in a private chat or simple group.
- danog\MadelineProto\EventHandler\Delete\DeleteScheduledMessages » - Some scheduled messages were deleted from the schedule queue of a chat.
- danog\MadelineProto\EventHandler\InlineQuery » - An incoming inline query.
- danog\MadelineProto\EventHandler\Message » - Represents an incoming or outgoing message.
- danog\MadelineProto\EventHandler\Message\ChannelMessage » - Represents an incoming or outgoing channel message.
- danog\MadelineProto\EventHandler\Message\CommentReply » - Represents a reply to one of our messages in a channel comment group that we’re not a member of (i.e. received via
@replies
). - danog\MadelineProto\EventHandler\Message\GroupMessage » - Represents an incoming or outgoing group message.
- danog\MadelineProto\EventHandler\Message\PrivateMessage » - Represents an incoming or outgoing private message.
- danog\MadelineProto\EventHandler\Message\SecretMessage » - Represents New encrypted message.
- danog\MadelineProto\EventHandler\Message\ServiceMessage » - Represents info about a service message.
- danog\MadelineProto\EventHandler\Message\ServiceMessage » - Represents info about a service message.
- danog\MadelineProto\EventHandler\Message\Service\DialogBotAllowed » - We have given the bot permission to send us direct messages.
- danog\MadelineProto\EventHandler\Message\Service\DialogChannelCreated » - The channel was created.
- danog\MadelineProto\EventHandler\Message\Service\DialogChannelMigrateFrom » - Indicates the channel was migrated from the specified chat.
- danog\MadelineProto\EventHandler\Message\Service\DialogChatJoinedByLink » - A user joined the chat via an invite link.
- danog\MadelineProto\EventHandler\Message\Service\DialogChatMigrateTo » - Indicates the chat was migrated to the specified supergroup.
- danog\MadelineProto\EventHandler\Message\Service\DialogContactSignUp » - A contact just signed up to telegram.
- danog\MadelineProto\EventHandler\Message\Service\DialogCreated » - A chat or channel was created.
- danog\MadelineProto\EventHandler\Message\Service\DialogDeleteMessages » - Deleted messages.
- danog\MadelineProto\EventHandler\Message\Service\DialogGameScore » - Someone scored in a game.
- danog\MadelineProto\EventHandler\Message\Service\DialogGeoProximityReached » - A user of the chat is now in proximity of another user.
- danog\MadelineProto\EventHandler\Message\Service\DialogGiftPremium » - Info about a gifted Telegram Premium subscription.
- danog\MadelineProto\EventHandler\Message\Service\DialogGroupCall » - Represents a service message about a group call.
- danog\MadelineProto\EventHandler\Message\Service\DialogGroupCall\GroupCall » - The group call has started or ended.
- danog\MadelineProto\EventHandler\Message\Service\DialogGroupCall\GroupCallInvited » - A set of users was invited to the group call.
- danog\MadelineProto\EventHandler\Message\Service\DialogGroupCall\GroupCallScheduled » - A group call was scheduled.
- danog\MadelineProto\EventHandler\Message\Service\DialogHistoryCleared » - Chat history was cleared.
- danog\MadelineProto\EventHandler\Message\Service\DialogMemberJoinedByRequest » - A user was accepted into the group by an admin.
- danog\MadelineProto\EventHandler\Message\Service\DialogMemberLeft » - A member left the chat or channel.
- danog\MadelineProto\EventHandler\Message\Service\DialogMembersJoined » - Some members joined the chat or channel.
- danog\MadelineProto\EventHandler\Message\Service\DialogMessagePinned » - A message was pinned in a chat.
- danog\MadelineProto\EventHandler\Message\Service\DialogPeerRequested » - Contains info about a peer that the user shared with the bot after clicking on a keyboardButtonRequestPeer button.
- danog\MadelineProto\EventHandler\Message\Service\DialogPhoneCall » - A phone call.
- danog\MadelineProto\EventHandler\Message\Service\DialogPhotoChanged » - The photo of the dialog was changed or deleted.
- danog\MadelineProto\EventHandler\Message\Service\DialogReadMessages » - Messages marked as read.
- danog\MadelineProto\EventHandler\Message\Service\DialogScreenshotTaken » - A screenshot of the chat was taken.
- danog\MadelineProto\EventHandler\Message\Service\DialogSetChatTheme » - The chat theme was changed.
- danog\MadelineProto\EventHandler\Message\Service\DialogSetChatWallPaper » - The wallpaper of the current chat was changed.
- danog\MadelineProto\EventHandler\Message\Service\DialogSetTTL » - The Time-To-Live of messages in this chat was changed.
- danog\MadelineProto\EventHandler\Message\Service\DialogSuggestProfilePhoto » - A new profile picture was suggested using photos.uploadContactProfilePhoto.
- danog\MadelineProto\EventHandler\Message\Service\DialogTitleChanged » - The title of a channel or group has changed.
- danog\MadelineProto\EventHandler\Message\Service\DialogTopicCreated » - A forum topic was created.
- danog\MadelineProto\EventHandler\Message\Service\DialogTopicEdited » - Forum topic information was edited.
- danog\MadelineProto\EventHandler\Message\Service\DialogWebView » - Data from an opened reply keyboard bot web app was relayed to the bot that owns it (user & bot side service message).
- danog\MadelineProto\EventHandler\Pinned » - Indicates that some messages were pinned/unpinned.
- danog\MadelineProto\EventHandler\Pinned\PinnedChannelMessages » - Represents messages that were pinned/unpinned in a channel.
- danog\MadelineProto\EventHandler\Pinned\PinnedGroupMessages » - Represents messages that were pinned/unpinned in a chat/supergroup.
- danog\MadelineProto\EventHandler\Pinned\PinnedPrivateMessages » - Some messages were pinned in a private chat.
- danog\MadelineProto\EventHandler\Privacy » - Indicates some privacy rules for a user or set of users.
- danog\MadelineProto\EventHandler\Query\ButtonQuery » - Represents a query sent by the user by clicking on a button.
- danog\MadelineProto\EventHandler\Query\ChatButtonQuery » - Represents a query sent by the user by clicking on a button in a chat.
- danog\MadelineProto\EventHandler\Query\ChatGameQuery » - Represents a query sent by the user by clicking on a “Play game” button in a chat.
- danog\MadelineProto\EventHandler\Query\GameQuery » - Represents a query sent by the user by clicking on a “Play game” button.
- danog\MadelineProto\EventHandler\Query\InlineButtonQuery » - Represents a query sent by the user by clicking on a button in an inline message.
- danog\MadelineProto\EventHandler\Query\InlineGameQuery » - Represents a query sent by the user by clicking on a “Play game” button in an inline message.
- danog\MadelineProto\EventHandler\Story\Story » - Represents a Telegram story.
- danog\MadelineProto\EventHandler\Story\StoryDeleted » - Represents a deleted story.
- danog\MadelineProto\EventHandler\Story\StoryReaction » - Represents a reaction to a story.
- danog\MadelineProto\EventHandler\Typing » - A user is typing.
- danog\MadelineProto\EventHandler\Typing\ChatUserTyping » - The user is preparing a message in a group; typing, recording, uploading, etc. This update is valid for 6 seconds. If no further updates of this kind are received after 6 seconds, it should be considered that the user stopped doing whatever they were doing.
- danog\MadelineProto\EventHandler\Typing\SecretUserTyping » - The user is preparing a message in a secret chat; typing, recording, uploading, etc. This update is valid for 6 seconds. If no further updates of this kind are received after 6 seconds, it should be considered that the user stopped doing whatever they were doing.
- danog\MadelineProto\EventHandler\Typing\SupergroupUserTyping » - A user is typing in a supergroup.
- danog\MadelineProto\EventHandler\Typing\UserTyping » - The user is preparing a message; typing, recording, uploading, etc. This update is valid for 6 seconds. If no further updates of this kind are received after 6 seconds, it should be considered that the user stopped doing whatever they were doing.
- danog\MadelineProto\EventHandler\User\Blocked » - A peer was blocked.
- danog\MadelineProto\EventHandler\User\BotStopped » - A bot was stopped or re-started.
- danog\MadelineProto\EventHandler\User\Phone » - A user’s phone number was changed.
- danog\MadelineProto\EventHandler\User\Status » - Contains a status update.
- danog\MadelineProto\EventHandler\User\Status\Emoji » - The emoji status of a certain user has changed or was removed.
- danog\MadelineProto\EventHandler\User\Status\EmptyStatus » - User status has not been set yet.
- danog\MadelineProto\EventHandler\User\Status\LastMonth » - Online status: last seen last month.
- danog\MadelineProto\EventHandler\User\Status\LastWeek » - Online status: last seen last week.
- danog\MadelineProto\EventHandler\User\Status\Offline » - The user’s offline status.
- danog\MadelineProto\EventHandler\User\Status\Online » - Online status of the user.
- danog\MadelineProto\EventHandler\User\Status\Recently » - Online status: last seen recently.
- danog\MadelineProto\EventHandler\User\Username » - Changes were made to the user’s first name, last name or username.
- danog\MadelineProto\VoIP » - This update represents a VoIP Telegram call.
Filters
MadelineProto offers three different filter types, used to filter updates by type or other attributes, click on the following links for more info:
Plugins
Plugins are also supported, check out the plugin docs » for more info!
Cron
All event handler methods marked by the danog\MadelineProto\EventHandler\Attributes\Cron
attribute are periodically invoked by MadelineProto every period
seconds:
use danog\MadelineProto\EventHandler\Attributes\Cron;
class MyEventHandler extends SimpleEventHandler
{
/**
* This cron function will be executed forever, every 60 seconds.
*/
#[Cron(period: 60.0)]
public function cron1(): void
{
$this->sendMessageToAdmins("The bot is online, current time ".date(DATE_RFC850)."!");
}
}
You can also specify millisecond intervals like 0.5
(500 milliseconds).
MadelineProto’s crons are based on the danog/loop library: the associated PeriodicLoop instance that can be used to stop or restart the loop is passed as first parameter to the cron, and can also be fetched using $this->getPeriodicLoop('methodName')
, in this case $this->getPeriodicLoop('cron1')
.
Persisting data and IPC
All property names returned by the __sleep
method will be saved in the database/session file, and then automatically loaded when the bot is restarted.
<?php declare(strict_types=1);
namespace MadelinePlugin\Danogentili;
use danog\MadelineProto\EventHandler\Attributes\Cron;
use danog\MadelineProto\EventHandler\Filter\FilterText;
use danog\MadelineProto\EventHandler\Message;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\PluginEventHandler;
/**
* Plugin event handler class.
*
* All properties returned by __sleep are automatically stored in the database.
*/
class PingPlugin extends PluginEventHandler
{
private int $pingCount = 0;
private string $pongText = 'pong';
/**
* You can set a custom pong text from the outside of the plugin:.
*
* ```
* if (!file_exists('madeline.php')) {
* copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
* }
* include 'madeline.php';
*
* $a = new API('bot.madeline');
* $plugin = $a->getPlugin(PingPlugin::class);
*
* $plugin->setPongText('UwU');
* ```
*
* This will automatically connect to the running instance of the plugin and call the specified method.
*/
public function setPongText(string $pong): void
{
$this->pongText = $pong;
}
/**
* Returns a list of names for properties that will be automatically saved to the session database (MySQL/postgres/redis if configured, the session file otherwise).
*/
public function __sleep(): array
{
return ['pingCount', 'pongText'];
}
/**
* Initialization logic.
*/
public function onStart(): void
{
$this->logger("The bot was started!");
$this->logger($this->getFullInfo('MadelineProto'));
$this->sendMessageToAdmins("The bot was started!");
}
/**
* Plugins may be enabled or disabled at startup by returning true or false from this function.
*/
public function isPluginEnabled(): bool
{
return true;
}
/**
* This cron function will be executed forever, every 60 seconds.
*/
#[Cron(period: 60.0)]
public function cron1(): void
{
$this->sendMessageToAdmins("The ping plugin is online, total pings so far: ".$this->pingCount);
}
#[FilterText('ping')]
public function pingCommand(Incoming&Message $message): void
{
$message->reply($this->pongText);
$this->pingCount++;
}
}
You can read and write to those properties from the outside using getter and setter methods, for example:
use danog\MadelineProto\API;
use MadelinePlugin\Danogentili\PingPlugin;
if (!file_exists('madeline.php')) {
copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
}
include 'madeline.php';
$a = new API('bot.madeline');
$handler = $a->getEventHandler(PingPlugin::class);
$handler->setPongText('UwU');
Built-in ORM
You can also directly connect to any database using the same async MySQL/Postgres/Redis ORM used by MadelineProto internally, danog/AsyncOrm!
To do so, simply specify the database settings, and use the OrmMappedArray
attribute to initialize the async database mapper:
use danog\AsyncOrm\Annotations\OrmMappedArray;
use danog\AsyncOrm\DbArray;
use danog\AsyncOrm\KeyType;
use danog\AsyncOrm\ValueType;
use danog\MadelineProto\SimpleEventHandler;
class MyEventHandler extends SimpleEventHandler {
/**
* @var DbArray<string, int>
*
* This ORM property is also persisted to the database, and is *not* fully kept in RAM at all times.
*
* You can also provide more specific type parameters (i.e. <string, int>; <int, someClass> etc),
* as well as custom caching settings.
*
* See https://github.com/danog/AsyncOrm for full documentation and more examples.
*/
#[OrmMappedArray(KeyType::STRING, ValueType::INT)]
protected DbArray $ormProperty;
/**
* This raw property is also persisted to the database, but is always kept in RAM at all times.
*/
private array $rawProperty = [];
/**
* Returns a list of names for properties that will be automatically saved to the session database (MySQL/postgres/redis if configured, the session file otherwise).
*/
public function __sleep(): array
{
return ['ormProperty', 'rawProperty'];
}
// ...
}
And use the newly created $dataStoredOnDb
property to access the database:
// Can be anything serializable, an array, an int, an object
$myData = [];
// Use the isset method to check whether some data exists in the database
if (isset($this->dataStoredOnDb['yourKey'])) {
// Always when fetching data
$myData = $this->dataStoredOnDb['yourKey'];
}
$this->dataStoredOnDb['yourKey'] = 123;
$this->dataStoredOnDb['otherKey'] = 0;
unset($this->dataStoredOnDb['otherKey']);
$this->logger("Count: ".count($this->dataStoredOnDb));
foreach ($this->dataStoredOnDb as $key => $value) {
$this->logger($key);
$this->logger($value);
}
Psalm generic typing is supported.
Each element of the array is stored in a separate database row (MySQL, Postgres or Redis, configured as specified here »), and is only kept in memory for the number of seconds specified in the cache TTL setting; when the TTL of an element expires, it is individually flushed to the database (if its value was changed), and then the row is removed from RAM.
Pros of using ORM DbArray
properties instead of raw properties:
- Much lower RAM usage, as the entire array is not kept in RAM at all times, only the most frequently used elements, according to the configured TTL.
- Added possibility of storing even gigabytes of data in a single
DbArray
, without keeping it all in memory. - If caching is disabled, the array is never kept in RAM, significantly hindering performance but further reducing RAM usage for truly huge elements (gigabyte-level).
Cons of using ORM DbArray
properties:
- Reads and writes are not atomic. Since each handler is started in a concurrent green thread (fiber), race conditions may ensue, thus accesses must be syncronized where and if needed using amphp/sync.
- Slower than raw properties (much slower if caching is fully disabled).
Both raw properties and ORM DbArray
properties are ultimately persisted on the database.
If no database is configured in the global settings, ORM properties behave pretty much like raw array properties, kept entirely in RAM and persisted to the session file.
IPC
You can communicate with the event handler from the outside, by invoking methods on the proxy returned by getEventHandler:
bot.php:
<?php declare(strict_types=1);
use danog\MadelineProto\EventHandler\Attributes\Handler;
use danog\MadelineProto\EventHandler\Message;
use danog\MadelineProto\EventHandler\Plugin\RestartPlugin;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\SimpleEventHandler;
if (!file_exists('madeline.php')) {
copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
}
include 'madeline.php';
final class MyEventHandler extends SimpleEventHandler {
public function someMethod(): string {
return "Some data";
}
/**
* Handle incoming updates from users, chats and channels.
*/
#[Handler]
public function handleMessage(Incoming&Message $message): void
{
// Code that uses $message...
// See the following pages for more examples and documentation:
// - https://github.com/danog/MadelineProto/blob/v8/examples/bot.php
// - https://docs.madelineproto.xyz/docs/UPDATES.html
// - https://docs.madelineproto.xyz/docs/FILTERS.html
// - https://docs.madelineproto.xyz/
}
}
MyEventHandler::startAndLoop('bot.madeline');
script.php:
use danog\MadelineProto\API;
if (!file_exists('madeline.php')) {
copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
}
include 'madeline.php';
$a = new API('bot.madeline');
$handler = $a->getEventHandler(MyEventHandler::class);
$handler->someMethod();
Restarting
To forcefully restart and apply changes made to the event handler class, call $this->restart();
.
When running via cli, the bot must run in the official docker image with restart: always
or inside of a bash while true loop in order for restart()
to work.
Self-restart on webhosts
When running the event handler via web, MadelineProto will automatically enable a magical self-restart hack (callback ID restarter
), to keep the bot running even on webhosts with limited execution time.
Locking will also be handled automatically (as well as disconnection from the user that opened the page), so even if you start the script via web several times, only one instance will be running at a time (no need to do flocking manually!).
Please note that this self-restart logic may fail in case of a physical server reboot or web server/php-fpm restart, so it’s always a better idea to run via CLI, or use a cron to periodically ping the bot’s URL.
It relies on the shutdown function, so you must not set a custom shutdown function in your code, and instead use the MadelineProto shutdown static API:
use danog\MadelineProto\Shutdown;
$id = Shutdown::addCallback(static function () {
// This function will run on shutdown
});
$id = Shutdown::addCallback(static function () {
// This function will run on shutdown
}, 'custom id');
$id = Shutdown::addCallback(static function () {
// This function will overwrite the previously set function with custom id
}, 'custom id');
$ok = Shutdown::removeCallback($id);
You can of course pass non-static functions, any type of callable is accepted.
A second optional parameter can also be accepted, containing the ID of the callable: you can use this if you want to later overwrite the callable with another callback, or remove it altogether.
The removeCallback
will return true if the callback exists and it was removed correctly, false otherwise.
Multiaccount
use danog\MadelineProto\EventHandler;
use danog\MadelineProto\Tools;
use danog\MadelineProto\API;
use danog\MadelineProto\Logger;
// Normal event handler definition as above
$MadelineProtos = [];
foreach ([
'bot.madeline' => 'Bot Login',
'user.madeline' => 'Userbot login',
'user2.madeline' => 'Userbot login (2)'
] as $session => $message) {
$MadelineProtos []= new API($session);
}
API::startAndLoopMulti($MadelineProtos, MyEventHandler::class);
This will create an event handler class EventHandler
, create a combined MadelineProto session with session files bot.madeline
, user.madeline
, user2.madeline
, and set the event handler class to our newly created event handler.
Usage is the same as for the normal event handler, with the difference is that multiple accounts can receive and handle updates in parallel, each with its own event handler instance.
Errors thrown inside of the event loop will be reported to the report peers specified by each separate instance.
Note that for performance reasons, some internal or connection exceptions not thrown from the EventHandler and exceptions thrown from onStart
may still get reported (only to, or also to) the last started event handler.
To dynamically start a new event handler in the background, use EventLoop::queue(MyEventHandler::startAndLoop(...), 'session.madeline', $settings))
.
Warning: this can only be done with already logged-in sessions, if your sessions aren’t logged in yet use startAndLoopMulti
, or login first.
use danog\MadelineProto\EventHandler;
use danog\MadelineProto\Tools;
use Revolt\EventLoop;
// Normal event handler definition as above
foreach ([
'bot.madeline' => 'Bot Login',
'user.madeline' => 'Userbot login',
'user2.madeline' => 'Userbot login (2)'
] as $session => $message) {
EventLoop::queue(MyEventHandler::startAndLoop(...), $session);
}
EventLoop::run(); // Or continue using some other async code...
Automatic static analysis
MadelineProto will automatically analyze the event handler code, blocking execution if performance or security issues are detected!
For example, the following functions and classes are banned, and the specified async counterparts must be used, instead:
file_get_contents
,file_put_contents
,fopen
- Please use https://github.com/amphp/file or https://github.com/amphp/http-client, insteadcurl_exec
- Please use https://github.com/amphp/http-client, insteadmysqli_query
,mysqli_connect
,mysql_connect
,PDO
,mysqli
- Please use https://github.com/amphp/mysql or https://github.com/amphp/postgres, insteadfsockopen
- Please use https://github.com/amphp/socket, instead
Avoiding the use of filesystem functions
For performance reasons, it is heavily recommended you do not read files from the filesystem at all, even using async functions.
MadelineProto does not block the usage of async file functions, but 99% of the time they can be replaced with a much faster alternative.
Here’s a list of common uses for files, and what they can be replaced with:
Configuration
Configuration can be done entirely using persistent properties, for example DON’T do this:
<?php
class OnlinePlugin extends PluginEventHandler
{
#[Cron(period: 60.0)]
public function cron(): void
{
// WRONG!
if (file_get_contents('online.txt') === 'on') {
$this->account->updateStatus(offline: false);
} else {
$this->account->updateStatus(offline: true);
}
}
}
Do this, instead:
<?php declare(strict_types=1);
namespace MadelinePlugin\Danogentili;
use danog\MadelineProto\EventHandler\Attributes\Cron;
use danog\MadelineProto\EventHandler\Filter\FilterCommand;
use danog\MadelineProto\EventHandler\Message;
use danog\MadelineProto\EventHandler\SimpleFilter\FromAdmin;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\PluginEventHandler;
final class OnlinePlugin extends PluginEventHandler
{
private bool $isOnline = true;
/**
* Returns a list of names for properties that will be automatically saved to the session database (MySQL/postgres/redis if configured, the session file otherwise).
*/
public function __sleep(): array
{
return ['isOnline'];
}
public function setOnline(bool $online): void
{
$this->isOnline = $online;
}
public function isPluginEnabled(): bool
{
// Only users can be online/offline
return $this->getSelf()['bot'] === false;
}
#[Cron(period: 60.0)]
public function cron(): void
{
$this->account->updateStatus(offline: !$this->isOnline);
}
#[FilterCommand('online')]
public function toggleOnline(Incoming&Message&FromAdmin $message): void
{
$this->isOnline = true;
}
#[FilterCommand('offline')]
public function toggleOffline(Incoming&Message&FromAdmin $message): void
{
$this->isOnline = false;
}
}
And, to toggle the settings from the outside of the bot (for example using a helper bot, or another program):
<?php
$online = true;
//$online = false;
$API = new \danog\MadelineProto\API('session.madeline');
$API->getEventHandler(\MadelinePlugin\Danogentili\OnlinePlugin::class)->setOnline($online);
Creating and uploading text files
Instead of writing to a file and then uploading it, you can use a ReadableBuffer
to upload a file from a string, instead:
use Amp\ReadableBuffer;
$contents = "Something";
$this->sendDocument(
peer: 'danogentili',
file: new ReadableBuffer($contents)
);
Logging
Instead of logging to separate files, you can use MadelineProto’s built-in logger, which will write everything to MadelineProto.log
:
$this->logger("Some text");
You can also use the new openFileAppendOnly
function, to open a file in write-only append-only mode in onStart and use it in your bot.
You may also wrap the File
resource returned by openFileAppendOnly in a proper PSR logger using amphp/log.
use danog\MadelineProto\SimpleEventHandler;
use danog\MadelineProto\Tools;
use Amp\Log\ConsoleFormatter;
use Amp\Log\StreamHandler;
use Monolog\Logger;
class MyEventHandler extends SimpleEventHandler {
private Logger $customLogger;
public function onStart()
{
// As documented in https://github.com/amphp/log
$handler = new StreamHandler(Tools::openFileAppendOnly('file.log'));
$this->customLogger = new Logger('main');
$this->customLogger->pushHandler($handler);
}
public function someOtherMethod(): void {
$this->customLogger->debug("Hello, world!");
$this->customLogger->info("Hello, world!");
$this->customLogger->notice("Hello, world!");
$this->customLogger->error("Hello, world!");
$this->customLogger->alert("Hello, world!");
}
}
Noop
$MadelineProto = new \danog\MadelineProto\API('bot.madeline');
$MadelineProto->start();
$MadelineProto->setNoop();
When an Update is received, nothing is done. This is useful if you need to populate the internal peer database with peers to avoid This peer is not present in the internal peer database errors
, but don’t need to handle updates.
This is the default.
Webhook
Useful when consuming MadelineProto updates through an API, not recommended when directly writing MadelineProto bots.
Webhooks will greatly slow down your bot if used directly inside of PHP code.
Only use the event handler when writing a MadelineProto bot, because update handling in the event handler is completely parallelized and non-blocking.
Webhooks must only be used when consuming MadelineProto updates from another programming language, like for example Javascript.
If your bot is written in PHP, use the event handler, instead.
$MadelineProto = new \danog\MadelineProto\API('bot.madeline');
// NOT recommended when directly writing MadelineProto bots.
// ONLY use when exposing updates via an HTTP API to another language (like Javascript).
$MadelineProto->setWebhook('https://example.com');
getUpdates
Only useful when consuming MadelineProto updates through an API in another language (like Javascript), absolutely not recommended when directly writing MadelineProto bots.
getUpdates
will greatly slow down your bot if used directly inside of PHP code.
Only use the event handler when writing a MadelineProto bot, because update handling in the event handler is completely parallelized and non-blocking.
getUpdates
must only be used when consuming MadelineProto updates from another programming language, like for example Javascript.
If your bot is written in PHP, use the event handler, instead.
$MadelineProto = new \danog\MadelineProto\API('bot.madeline');
// NOT recommended when directly writing MadelineProto bots.
// ONLY use when exposing updates via an HTTP API to another language (like Javascript).
// DO NOT use this to handle updates in PHP code, it will cause crashes.
// Same parameters as for bot API getUpdates
echo json_encode($MadelineProto->getUpdates($_GET));