Link Search Menu Expand Document

Filters

MadelineProto offers a very simple and intuitive message filtering system, based on PHP’s type system and attributes.

There are two filter types:

Simple filters

Simple filters are implemented using simple PHP types, for example:

use danog\MadelineProto\SimpleEventHandler;
use danog\MadelineProto\EventHandler\Attributes\Handler;
use danog\MadelineProto\EventHandler\Message;
use danog\MadelineProto\EventHandler\Message\PrivateMessage;
use danog\MadelineProto\EventHandler\Message\GroupMessage;
use danog\MadelineProto\EventHandler\Message\ChannelMessage;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\EventHandler\SimpleFilter\Outgoing;
use danog\MadelineProto\EventHandler\SimpleFilter\HasMedia;

class MyEventHandler extends SimpleEventHandler
{
    #[Handler]
    public function h1(Incoming & Message $message): void
    {
        // Handle all incoming messages (private+groups+channels).
    }

    #[Handler]
    public function h2(Outgoing & PrivateMessage $message): void
    {
        // Handle all outgoing messages (private).
    }

    #[Handler]
    public function h3((Incoming & GroupMessage & HasMedia) | (Incoming & ChannelMessage & HasMedia) $message): void
    {
        // Handle all incoming messages with media attached (groups+channels).
    }
}

MadelineProto will send updates about new messages to all methods marked with the Handler attribute, appropriately filtering them first according to the typehint.

A filter typehint is composed of:

  • A single concrete type: A
    • Concrete types are objects with useful bound methods and properties containing the fields of the message.
  • A single concrete type intersected with one or more filter interfaces: A&B&C (used to AND filters)
    • Filter interfaces are PHP interfaces that are automatically parsed using reflection.
      Unlike concrete types, they cannot be used for type assertions outside of a method marked by #[Handler] or #[Filter...] attributes.
  • A union of concrete types: A|B|C (used to OR filters)
  • A union of concrete types or intersections: (A&B)|C|(D&F) (used to OR filters in DNF form)

Single concrete type examples:

  • Message - Handle all incoming and outgoing messages (private or groups or channels).
  • ChannelMessage - Handle all incoming and outgoing messages (channels).
  • GroupMessage - Handle all incoming and outgoing messages (groups).
  • PrivateMessage - Handle all incoming and outgoing messages (private).
  • ServiceMessage - Handle all incoming and outgoing service messages (private or groups or channels).
  • AbstractMessage - Handle all incoming and outgoing service+normal messages (private or groups or channels).

Intersection examples:

  • Incoming & Message - Handle all incoming messages (private or groups or channels).
  • Incoming & GroupMessage & HasMedia - Handle all incoming media messages (groups).

Union/DNF examples:

  • GroupMessage|ChannelMessage - Handle all incoming and outgoing messages (groups or channels).
  • (Incoming&GroupMessage)|(Incoming&ChannelMessage) - Handle all incoming messages (groups or channels).
  • ServiceMessage|(ChannelMessage&HasMedia) - Handle all service messages or incoming and outgoing media channel messages.

Simple filters can optionally be combined with attribute filters, in which case they will be AND-ed together.

Here’s the full list of all concrete types:

Here’s the full list of simple filter interfaces (see attribute filters for more advanced filters like commands, regexes, and much more!):

Attribute filters

Attribute filters are implemented using PHP attributes, for example:

use danog\MadelineProto\SimpleEventHandler;
use danog\MadelineProto\EventHandler\Attributes\Handler;
use danog\MadelineProto\EventHandler\Message;
use danog\MadelineProto\EventHandler\Message\PrivateMessage;
use danog\MadelineProto\EventHandler\Message\GroupMessage;
use danog\MadelineProto\EventHandler\Message\ChannelMessage;
use danog\MadelineProto\EventHandler\Filter\FilterIncoming;

class MyEventHandler extends SimpleEventHandler
{
    #[FilterIncoming]
    public function h1(Message $message): void
    {
        // Handle all incoming messages (private+groups+channels).
    }

    #[FilterAnd(new FilterOutgoing, new FilterPrivate)]
    public function h2(Message $message): void
    {
        // Handle all outgoing messages (private).
    }

    #[FilterAnd(new FilterIncoming, new FilterOr(new FilterGroup, new FilterChannel), new FilterMedia)]
    public function h3(Message $message): void
    {
        // Handle all incoming messages with media attached (groups+channels).
    }
    
    #[FilterOr(new FilterGroup, new FilterChannel)]
    public function h4(Incoming&Message&HasMedia $message): void
    {
        // Same as h3, but combining simple filters with attribute filters.
    }
}

Attribute filters are usual combined with simple filters.
Attribute filters are ANDed with simple filters defined on the same method, for example this:

#[FilterOr(new FilterGroup, new FilterChannel)]
public function h4(Incoming&Message&HasMedia $message)

Is exactly the same as this:

#[FilterAnd(new FilterIncoming, new FilterMessage, new FilterMedia, new FilterOr(new FilterGroup, new FilterChannel))]
public function h3($message): void

Which can also be written using only simple filters:

#[Handler]
public function h3((Incoming & GroupMessage & HasMedia) | (Incoming & ChannelMessage & HasMedia) $message): void

Here’s the full list of filter attributes (see the MTProto filters » for even more low-level filters):

Creating custom attribute filters

To create a custom attribute filter, simply create a method attribute that extends the Filter class.

You must implement an apply(Update $update): bool method, that returns true or false according to the filter’s logic.

<?php declare(strict_types=1);

use Attribute;
use danog\MadelineProto\EventHandler\Update;

/**
 * Use with #[FilterBoolean(true/false)]
 */
#[Attribute(Attribute::TARGET_METHOD)]
final class FilterBoolean extends Filter
{
    public function __construct(private readonly bool $applyIf) {}
    public function apply(Update $update): bool
    {
        return $this->applyIf;
    }
}

You can also optionally implement the public function initialize(EventHandler $API): Filter function.

This function is useful to perform expensive one-time initialization tasks, to avoid performing them during filtering, for example:

<?php declare(strict_types=1);

/**
 * 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
 */

namespace danog\MadelineProto\EventHandler\Filter;

use danog\MadelineProto\EventHandler;
use danog\MadelineProto\EventHandler\AbstractMessage;
use danog\MadelineProto\EventHandler\AbstractStory;
use danog\MadelineProto\EventHandler\BotCommands;
use danog\MadelineProto\EventHandler\ChatInviteRequester\BotChatInviteRequest;
use danog\MadelineProto\EventHandler\InlineQuery;
use danog\MadelineProto\EventHandler\Query\ButtonQuery;
use danog\MadelineProto\EventHandler\Story\StoryReaction;
use danog\MadelineProto\EventHandler\Typing;
use danog\MadelineProto\EventHandler\Update;
use danog\MadelineProto\EventHandler\User\Blocked;
use danog\MadelineProto\EventHandler\User\BotStopped;
use danog\MadelineProto\EventHandler\User\Phone;
use danog\MadelineProto\EventHandler\User\Status;
use danog\MadelineProto\EventHandler\User\Username;

/**
 * Allow incoming or outgoing group messages made by a certain list of senders.
 *
 * @internal
 */
abstract class AbstractFilterFromSenders extends Filter
{
    /** @var array<string|int> */
    private readonly array $peers;
    /** @var list<int> */
    private readonly array $peersResolved;
    public function __construct(string|int ...$idOrUsername)
    {
        $this->peers = array_unique($idOrUsername);
    }
    public function initialize(EventHandler $API): Filter
    {
        if (\count($this->peers) === 1) {
            return (new FilterFromSender(array_values($this->peers)[0]))->initialize($API);
        }
        $res = [];
        foreach ($this->peers as $peer) {
            $res []= $API->getId($peer);
        }
        /** @psalm-suppress InaccessibleProperty */
        $this->peersResolved = $res;
        return $this;
    }
    public function apply(Update $update): bool
    {
        return $update instanceof AbstractMessage && \in_array($update->senderId, $this->peersResolved, true) ||
            ($update instanceof AbstractStory && \in_array($update->senderId, $this->peersResolved, true)) ||
            ($update instanceof StoryReaction && \in_array($update->senderId, $this->peersResolved, true)) ||
            ($update instanceof ButtonQuery && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof InlineQuery && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof Typing && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof Blocked && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof BotStopped && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof Phone && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof Status && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof Username && \in_array($update->userId, $this->peersResolved, true)) ||
            ($update instanceof BotCommands && \in_array($update->botId, $this->peersResolved, true)) ||
            ($update instanceof BotChatInviteRequest && \in_array($update->userId, $this->peersResolved, true));
    }
}

Usually you should return $this from initialize(), but if you want to replace the current filter with another filter, you can return the new filter, instead:

<?php declare(strict_types=1);

use Attribute;
use danog\MadelineProto\EventHandler;
use danog\MadelineProto\EventHandler\Filter\Filter;
use danog\MadelineProto\EventHandler\Filter\FilterPrivate;
use danog\MadelineProto\EventHandler\Filter\FilterFromAdmin;
use danog\MadelineProto\EventHandler\Update;

/**
 * A shorthand filter for FilterPrivate && FilterFromAdmin
 */
#[Attribute(Attribute::TARGET_METHOD)]
final class FilterPrivateAdmin extends Filter
{
    public function initialize(EventHandler $API): Filter
    {
        return (new FiltersAnd(new FilterPrivate, new FilterFromAdmin))->initialize($API);
    }

    public function apply(Update $update): bool
    {
        throw new AssertionError("Unreachable!");
    }
}

Another example:

<?php declare(strict_types=1);

/**
 * 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
 */

namespace danog\MadelineProto\EventHandler\Filter\Combinator;

use Attribute;
use danog\MadelineProto\EventHandler;
use danog\MadelineProto\EventHandler\Filter\Filter;
use danog\MadelineProto\EventHandler\Update;

/**
 * NOTs a filter.
 */
#[Attribute(Attribute::TARGET_METHOD)]
final class FilterNot extends Filter
{
    public function __construct(private readonly Filter $filter)
    {
    }
    public function initialize(EventHandler $API): Filter
    {
        $filter = $this->filter->initialize($API);
        if ($filter instanceof self) {
            // The nested filter is a FilterNot, optimize !!A => A
            return $filter->filter;
        }
        if ($filter === $this->filter) {
            // The nested filter didn't replace itself
            return $this;
        }
        // The nested filter replaced itself, re-wrap it
        return new self($filter);
    }

    public function apply(Update $update): bool
    {
        return !$this->filter->apply($update);
    }
}

MTProto filters

MTProto filters are used to obtain raw MTProto updates in the form of arrays.
Unlike simple updates, raw MTProto updates do not have bound methods, but MadelineProto offers a bunch of helper methods that can be used, instead.

Please note that MTProto filters are not covered by any backwards compatibility promise: they may change at any time, including in minor versions, because they’re related directly to the Telegram API schema.
Use simple filters for a stable object-oriented update API.

MTProto filters are defined by creating a function with the appropriate name, for example to handle updateNewMessage updates:

use danog\MadelineProto\SimpleEventHandler;

class MyEventHandler extends SimpleEventHandler
{
    /**
     * Handle updates from users.
     *
     * 100+ other types of onUpdate... method types are available, see https://docs.madelineproto.xyz/API_docs/types/Update.html for the full list.
     * You can also use onAny to catch all update types (only for debugging)
     * A special onUpdateCustomEvent method can also be defined, to send messages to the event handler from an API instance, using the sendCustomEvent method.
     *
     * @param array $update Update
     */
    public function onUpdateNewMessage(array $update): void
    {
        if ($update['message']['_'] === 'messageEmpty') {
            return;
        }

        $this->logger($update);

        // Chat ID
        $id = $this->getId($update);

        // Sender ID, not always present
        $from_id = isset($update['message']['from_id'])
            ? $this->getId($update['message']['from_id'])
            : null;
        

        // 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[$id])) {
            $this->notifiedChats[$id] = true;

            $this->messages->sendMessage(
                peer: $update,
                message: "This userbot is powered by [MadelineProto](https://t.me/MadelineProto)!",
                reply_to_msg_id: $update['message']['id'] ?? null,
                parse_mode: 'Markdown'
            );
        }
    }
}

Here’s a full list of all MTProto filters (click on each filter name to view the structure of the array that will be passed to it):

Next section