Skip to content

Transformers & hooks

TGPy API allows you to use TGPy internal features in your messages and modules.

import tgpy.api

Code transformers, AST transformers, and exec hooks are TGPy API features that can control code evaluation.

Code transformers are most commonly used.

Code transformers

With code transformers, you can transform the code before TGPy runs it. This is useful for setting up prefix commands, syntax changes, and more.

Transformers are functions that take message text and return some modified text. Whenever you send a message, TGPy tries to apply your code transformers to its text. If the final text is the valid Python code, it runs.

To create a transformer, you should define a function which takes a string and returns a new string — let’s call it func. Then you should register it as following:

tgpy.api.code_transformers.add(name, func)

Example

Say you want to run shell commands by starting your message with .sh, for example:

Your message
.sh ls

You can implement this feature by saving a code transformer to a module:

Your module
import os
import subprocess
import tgpy.api

def shell(code):
    proc = subprocess.run([os.getenv("SHELL") or "/bin/sh", "-c", code], encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return proc.stdout + (f"\n\nReturn code: {proc.returncode}" if proc.returncode != 0 else "")

def sh_trans(cmd):
    if cmd.lower().startswith(".sh "):
        return f"shell({repr(cmd[4:])})"
    return cmd

tgpy.api.code_transformers.add("shell", sh_trans)

Code by @purplesyringa

AST transformers

AST transformers are similar to code transformers, but operate with abstract syntax trees instead of text strings.

Add an AST transformer:

tgpy.api.ast_transformers.add(name, func)

First, TGPy applies code transformers. If the transformation result is valid Python code, AST transformers are then applied.

Exec hooks

Exec hooks are functions that run before the message is parsed and handled. Unlike transformers, they may edit the message, delete it, and so on.

Exec hooks must have the following signature:

async hook(message: Message, is_edit: bool) -> Message | bool | None

is_edit is True if you have edited the TGPy message

An exec hook may edit the message using Telegram API methods or alter the message in place.

If a hook returns Message object or alters it in place, the object is used instead of the original one during the rest of handling (including calling other hook functions). If a hook returns True or None, execution completes normally. If a hook returns False, the rest of hooks are executed and then the handling stops without further message parsing or evaluating.

Add a hook:

tgpy.api.exec_hooks.add(name, func)

Complete flow

flowchart TB
    start(New outgoing message) --> hooks
    start2(Outgoing message edited) -- is_edit=True --> hooks
    hooks(Exec hooks applied) -- If hooks didn't stop execution --> code
    code(Code transformers applied) -- If syntax is correct --> ast
    ast(AST transformers applied) -- If not a too simple expression --> run((Code runs))

    click hooks "#exec-hooks"
    click ast "#ast-transformers"
    click code "#code-transformers"

Managing

TGPy stores transformers and exec hooks in TransformerStore objects: tgpy.api.code_transformers, tgpy.api.ast_transformers and tgpy.api.exec_hooks.

Each of them represents a list of tuples (name, func) or a dict in the form of {name: func}.

While TGPy applies exec hooks in the same order they are listed, transformers are applied in reverse order. It's done so that the newly added transformers can emit code that uses features of an older transformer.

Some examples:

tgpy.api.code_transformers

TransformerStore({'postfix_await': <function tmp.<locals>.code_trans at 0x7f2db16cd1c0>})
tgpy.api.code_transformers.remove('postfix_await')
del tgpy.api.code_transformers['postfix_await']
tgpy.api.code_transformers['test'] = func
tgpy.api.code_transformers.add('test', func)
tgpy.api.code_transformers.append(('test', func))
for name, func in tgpy.api.code_transformers:
    ...
list(tgpy.api.code_transformers) -> list[tuple[str, function]]
dict(tgpy.api.code_transformers) -> dict[str, function]

Manual usage

Apply all your code transformers to a custom text:

tgpy.api.code_transformers.apply(text)

Apply all your AST transformers to a custom AST:

await tgpy.api.ast_transformers.apply(tree)

Apply all your exec hooks to a message:

await tgpy.api.exec_hooks.apply(message, is_edit)

Returns False if any of the hooks returned False or a Message object that should be used instead of the original one otherwise