Skip to main content
This example demonstrates how to work with Telegram secret chats, which feature end-to-end encryption. Based on secret_bot.php.

Complete Code

<?php declare(strict_types=1);

use danog\MadelineProto\EventHandler\Attributes\Handler;
use danog\MadelineProto\EventHandler\Message\PrivateMessage;
use danog\MadelineProto\EventHandler\Message\SecretMessage;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\LocalFile;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings;
use danog\MadelineProto\SimpleEventHandler;

if (file_exists(__DIR__.'/../vendor/autoload.php')) {
    include 'vendor/autoload.php';
} else {
    if (!file_exists('madeline.php')) {
        copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
    }
    include 'madeline.php';
}

class SecretHandler extends SimpleEventHandler
{
    private array $sent = [];
    
    public const ADMIN = "danogentili"; // Change this
    
    public function getReportPeers()
    {
        return [self::ADMIN];
    }

    private $call;
    
    public function onStart(): void
    {
        $this->call = $this->requestCall(self::ADMIN)->play(
            new LocalFile('/home/daniil/Music/a.ogg')
        );
    }

    /**
     * Handle updates from users.
     */
    #[Handler]
    public function handleNormalMessage(Incoming&PrivateMessage $update): void
    {
        if ($update->message === 'request') {
            $this->requestSecretChat($update->senderId);
        }
        if ($update->message === 'ping') {
            $update->reply('pong');
        }
    }

    /**
     * Handle secret chat messages.
     */
    #[Handler]
    public function handle(Incoming&SecretMessage $update): void
    {
        if ($update->media) {
            $path = $update->media->downloadToDir('/tmp');
            $update->reply($path);
        }
        if (isset($this->sent[$update->chatId])) {
            return;
        }

        // Photo, secret chat
        $this->sendPhoto(
            peer: $update->chatId,
            file: new LocalFile('tests/faust.jpg'),
            caption: 'This file was uploaded using MadelineProto',
        );

        // Photo as document, secret chat
        $this->sendDocumentPhoto(
            peer: $update->chatId,
            file: new LocalFile('tests/faust.jpg'),
            caption: 'This file was uploaded using MadelineProto',
        );

        // GIF, secret chat
        $this->sendGif(
            peer: $update->chatId,
            file: new LocalFile('tests/pony.mp4'),
            caption: 'This file was uploaded using MadelineProto',
        );

        // Sticker, secret chat
        $this->sendSticker(
            peer: $update->chatId,
            file: new LocalFile('tests/lel.webp'),
            mimeType: "image/webp"
        );

        // Document, secret chat
        $this->sendDocument(
            peer: $update->chatId,
            file: new LocalFile('tests/60'),
            fileName: 'fairy'
        );

        // Video, secret chat
        $this->sendVideo(
            peer: $update->chatId,
            file: new LocalFile('tests/swing.mp4'),
        );

        // Audio, secret chat
        $this->sendAudio(
            peer: $update->chatId,
            file: new LocalFile('tests/mosconi.mp3'),
        );

        $this->sendVoice(
            peer: $update->chatId,
            file: new LocalFile('tests/mosconi.mp3'),
        );

        $i = 0;
        while ($i < 10) {
            $this->logger("SENDING MESSAGE $i TO ".$update->chatId);
            $this->sendMessage(peer: $update->chatId, message: (string) ($i++));
        }
        $this->sent[$update->chatId] = true;
    }
}

$settings = new Settings;
$settings->getLogger()->setLevel(Logger::ULTRA_VERBOSE);

SecretHandler::startAndLoop('user.madeline', $settings);

Key Features

Requesting Secret Chats

#[Handler]
public function handleNormalMessage(Incoming&PrivateMessage $update): void
{
    if ($update->message === 'request') {
        $this->requestSecretChat($update->senderId);
    }
}
Send “request” in a normal chat to initiate a secret chat with the bot.

Handling Secret Messages

Secret messages use the SecretMessage type:
#[Handler]
public function handle(Incoming&SecretMessage $update): void
{
    // Your secret chat logic here
}
Secret chats are end-to-end encrypted and only work between two users (not groups or channels).

Media in Secret Chats

Downloading Media

if ($update->media) {
    $path = $update->media->downloadToDir('/tmp');
    $update->reply($path);
}

Sending Photos

// Regular photo
$this->sendPhoto(
    peer: $update->chatId,
    file: new LocalFile('tests/faust.jpg'),
    caption: 'This file was uploaded using MadelineProto',
);

// Photo as document
$this->sendDocumentPhoto(
    peer: $update->chatId,
    file: new LocalFile('tests/faust.jpg'),
    caption: 'This file was uploaded using MadelineProto',
);

Sending GIFs

$this->sendGif(
    peer: $update->chatId,
    file: new LocalFile('tests/pony.mp4'),
    caption: 'This file was uploaded using MadelineProto',
);

Sending Stickers

$this->sendSticker(
    peer: $update->chatId,
    file: new LocalFile('tests/lel.webp'),
    mimeType: "image/webp"
);

Sending Documents

$this->sendDocument(
    peer: $update->chatId,
    file: new LocalFile('tests/60'),
    fileName: 'fairy'
);

Sending Videos

$this->sendVideo(
    peer: $update->chatId,
    file: new LocalFile('tests/swing.mp4'),
);

Sending Audio

// Audio file
$this->sendAudio(
    peer: $update->chatId,
    file: new LocalFile('tests/mosconi.mp3'),
);

// Voice message
$this->sendVoice(
    peer: $update->chatId,
    file: new LocalFile('tests/mosconi.mp3'),
);

LocalFile vs RemoteUrl

MadelineProto supports multiple file sources:
use danog\MadelineProto\LocalFile;
use danog\MadelineProto\RemoteUrl;

// Local file
$this->sendPhoto(
    peer: $chatId,
    file: new LocalFile('/path/to/photo.jpg')
);

// Remote URL
$this->sendPhoto(
    peer: $chatId,
    file: new RemoteUrl('https://example.com/photo.jpg')
);

Tracking Sent Messages

private array $sent = [];

public function handle(Incoming&SecretMessage $update): void
{
    if (isset($this->sent[$update->chatId])) {
        return; // Already sent to this chat
    }
    
    // Send media...
    
    $this->sent[$update->chatId] = true;
}
Use the __sleep() method to persist the $sent array across restarts:
public function __sleep(): array
{
    return ['sent'];
}

Important Notes

Secret chats require a user account. Bots cannot use secret chats. Use startAndLoop() instead of startAndLoopBot().
Secret chats are device-specific. They cannot be accessed from multiple devices simultaneously.
Secret chats use end-to-end encryption. Messages and media are never stored on Telegram servers.

Advanced: Voice Calls in Secret Chats

private $call;

public function onStart(): void
{
    $this->call = $this->requestCall(self::ADMIN)->play(
        new LocalFile('/home/daniil/Music/a.ogg')
    );
}
See Voice Calls Example for more details.

Next Steps

Build docs developers (and LLMs) love