How does the DelegatorBot work exactly in TelePot?
Asked Answered
A

1

5

I'm trying to study the python library Telepot by looking at the counter.py example available here: https://github.com/nickoala/telepot/blob/master/examples/chat/counter.py.
I'm finding a little bit difficult to understand how the DelegatorBot class actually works.

This is what I think I've understood so far:

1.

I see that initially this class (derived from "ChatHandler" class) is being defined:

class MessageCounter(telepot.helper.ChatHandler):

    def __init__(self, *args, **kwargs):
        super(MessageCounter, self).__init__(*args, **kwargs)
        self._count = 0

    def on_chat_message(self, msg):
        self._count += 1
        self.sender.sendMessage(self._count)

2.

Then a bot is created by instancing the class DelegatorBot:

bot = telepot.DelegatorBot(TOKEN, [
    pave_event_space()(
        per_chat_id(), create_open, MessageCounter, timeout=10
    ),
])

3.

I understand that a new instance of DelegatorBot is created and put in the variable bot. The first parameter is the token needed by telegram to authenticate this bot, the second parameter is a list that contains something I don't understand.

I mean this part:

pave_event_space()(
    per_chat_id(), create_open, MessageCounter, timeout=10
)

And then my question is..

Is pave_event_space() a method called that returns a reference to another method? And then this returned method is invoked with the parameters (per_chat_id(), create_open, MessageCounter, timeout=10) ?

Aerostat answered 29/7, 2017 at 9:45 Comment(2)
I have added some more in-depth discussions to the original answer below. Hope it's helpful.Collimator
Thank you very muchAerostat
C
12

Short answer

Yes, pave_event_space() returns a function. Let's call that fn. fn is then invoked with fn(per_chat_id(), create_open, ...), which returns a 2-tuple (seeder function, delegate-producing function).

If you want to study the code further, this short answer probably is not very helpful ...

Longer answer

To understand what pave_event_space() does and what that series of arguments means, we have to go back to basics and understand what DelegatorBot accepts as arguments.

DelegatorBot's constructor is explained here. Simply put, it accepts a list of 2-tuples (seeder function, delegate-producing function). To reduce verbosity, I am going to call the first element seeder and the second element delegate-producer.

A seeder has this signature seeder(msg) -> number. For every message received, seeder(msg) gets called to produce a number. If that number is new, the companion delegate-producer (the one that shares the same tuple with the seeder) will get called to produce a thread, which is used to handle the new message. If that number has been occupied by a running thread, nothing is done. In essence, the seeder "categorizes" the message. It spawns a new thread if it sees a message belong to a new "category".

A delegate-producer has this signature producer(cls, *args, **kwargs) -> Thread. It calls cls(*args, **kwargs) to instantiate a handler object (MessageCounter in your case) and wrap it in a thread, so the handler's methods are executed independently.

(Note: In reality, a seeder does not necessarily returns a number and a delegate-producer does not necessarily returns a Thread. I have simplified above for clarity. See the reference for a full explanation.)

In earlier days of telepot, a DelegatorBot was usually made by supplying a seeder and a delegate-producer transparently:

bot = DelegatorBot(TOKEN, [
        (per_chat_id(), create_open(MessageCounter, ...))])

Later, I added to handlers (e.g. ChatHandler) a capability to generate its own events (say, a timeout event). Each class of handlers get their own event space, so different classes' events won't mix. Within each event space, the event objects themselves also have a source id to identify which handler has emitted it. This architecture puts some extra requirements on seeders and delegate-producers.

Seeders have to be able to "categorize" events (in additional to external messages) and returns the same number that leads to the event emitter (because we don't want to spawn a thread for this event; it's supposed to be handled by the event emitter itself). Delegate-producers also have to pass the appropriate event space to the Handler class (because each Handler class gets a unique event space, generated externally).

For everything to work properly, the same event space has to be supplied to the seeder and its companion delegate-producer. And every pair of (seeder, delegate-producer) has to get a globally unique event space. pave_event_space() ensures these two conditions, basically patches some extra operations and parameters onto per_chat_id() and create_open() and making sure they are consistent.

Deeper still

Exactly how the "patching" is done? Why do I make you do pave_event_space()(...) instead of the more straight-forward pave_event_space(...)?

First, recall that our ultimate goal is to have a 2-tuple (per_chat_id(), create_open(MessageCounter, ...)). To "patch" it usually means (1) appending some extra operations to per_chat_id(), and (2) inserting some extra parameters to the call create_open(... more arguments here ...). That means I cannot let the user call create_open(...) directly because, once it is called, I cannot insert extra parameters. I need a more abstract construct in which the user specifies create_open but the call create_open(...) is actually made by me.

Imagine a function named pair, whose signature being pair(per_chat_id(), create_open, ...) -> (per_chat_id(), create_open(...)). In other words, it passes the first argument as the first tuple element, and creates the second tuple element by making an actual call to create_open(...) with remaining arguments.

Now, it reaches a point where I am unable to explain source code in words (I have been thinking for 30 minutes). The pseudo-code of pave_event_space looks like this:

def pave_event_space(fn=pair):
    def p(s, d, *args, **kwargs):
        return fn(append_event_space_seeder(s), 
                  d, *args, event_space=event_space, **kwargs)
    return p

It takes the function pair, and returns a pair-like function (signature identical to pair), but with a more complex seeder and more parameters tagged on. That's what I meant by "patching".

pave_event_space is the most often-seen "patcher". Other patchers include include_callback_query_chat_id and intercept_callback_query_origin. They all do basically the same kind of things: takes a pair-like function, returns another pair-like function, with a more complex seeder and more parameters tagged on. Because the input and output are alike, they can be chained to apply multiple patches. If you look into the callback examples, you will see something like this:

bot = DelegatorBot(TOKEN, [
    include_callback_query_chat_id(
        pave_event_space())(
            per_chat_id(), create_open, Lover, timeout=10),
])

It patches event space stuff, then patches callback query stuff, to enable the seeder (per_chat_id()) and handler (Lover) to work cohesively.

That's all I can say for now. I hope this throws some light on the code. Good luck.

Collimator answered 30/7, 2017 at 7:20 Comment(4)
It stemmed from my desire to allow arbitrary cross-section of messages. The seeder is essentially a "router" of messages, dividing messages into groups (to be handled by one target) by giving them different IDs. The seeder can be any functions, allowing you to divide messages however way you want (although, in practice, there are a few ways very commonly used, e.g. by chat_id or by from_id). On the other hand, the delegate-producer, being another arbitrary function, allows arbitrary ways to handle messages routed to it (although, in practice, there are a few ways very commonly used) ...Collimator
The price of leaving things so general is that the user has to plug in more things to make it work. I have built-in functions for those common seeders and delegate-producers, but the user still has to supply them. As Bot API evolved, there also arose the need to "coordinate" the function pair, giving rise to wrapping like this: pave_event_space()(...). I admit it is complicated. Can it be simpler while maintaining that generality? Possibly, but what has been made has been made. The project is not important enough for me to consider a re-design. I will leave it at that.Collimator
By the way I was not trying to be rude, it is just an observationNiveous
I know. Your comment is more an exclamation than an "accusation" or complaint. And I think others may share your observation. So I just want to lay out the rationale and hopefully ease some minds.Collimator

© 2022 - 2024 — McMap. All rights reserved.