Handling updates (new messages & other events)
Update handling can be done in different ways:
Webhook (for HTTP APIs)
getUpdates (only for Javascript APIs)
Noop (default)
Async Event driven
Plugins » are also supported!
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\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 admined 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\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
, instead
curl_exec
- Please use
https://github.com/amphp/http-client
, instead
mysqli_query
,
mysqli_connect
,
mysql_connect
,
PDO
,
mysqli
- Please use
https://github.com/amphp/mysql
or
https://github.com/amphp/postgres
, instead
fsockopen
- 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
));
Next section