RU
DevGram Plugin SDK

Introduction

Python plugins running inside DevGram and using the native Telegram API.

DevGram plugins

The plugin inherits from BasePlugin and is executed by built-in Python 3.11. Root and external Xposed Framework are not needed.

Native format

Plugins are supplied as one file .plugin (or .py) or archive .dgplugin for complex extensions with resources and dependencies. The DevGram API does not copy the API of other clients.

SDK Features

Events

Messages, TL updates, requests and NotificationCenter.

Interface

Settings, bulletins, alerts and Pill Stack.

Client API

Media, formatting, accounts and controllers.

Native

Java class proxy and method hooks.

Reading order

  1. Preparation and first package
  2. BasePlugin and events
  3. Client API and UI
  4. Development, publishing and security
''

Execution model

The plugin is loaded as a separate runtime module, but runs inside the DevGram process. Therefore, a bug in the Java bridge, an infinite loop or heavy work on the UI thread can affect the client. Treat every callback like production application code: validate input data, limit runtime, and free up resources.

What is considered a public API

The stable layer is located in the modules devgram.*. Fields and methods with leading underscores, Telegram internal classes and specific layout IDs are not a contract. If you have to use a low-level point, add version checking and fallback.

Minimum plugin standard

  1. Unique ID and clear metadata.
  2. No secrets in the archive.
  3. Idempotent load/unload.
  4. Work in light, dark and system theme.
  5. Correct behavior after reload, disable and safe mode.
DevGram Builder

DevGram Builder

High-level layer on top of DevGram SDK for large plugins: project structure, modules, resources and declarative settings.

Why Builder

Builder separates business logic from lifecycle and Android bridge. One project can be compiled into a native one .dgplugin without manual management of imports, locales and resources.

Modules

Separate hooks, screens, commands and services.

Resources

Assets, localization and metadata have predictable paths.

Lifecycle

Init, enable, disable and dispose are controlled by the project.

Public API

Typed facades over client_utils and UI.

Important

DevGram Builder - DevGram-native layer. It does not run exteraGram plugins and does not change our package format.

DevGram Builder

Quick Start

Create a DevGram Builder project, add a module and build it into `.dgplugin`.

devgram-builder new hello.devgram
cd hello.devgram
devgram-builder add module messages
devgram-builder build
python3 tools/devgram_dev.py upload dist/hello.devgram

First module

from devgram.builder import Module

class Welcome(Module):
    id = "welcome"

    def on_message(self, event):
        if event.text == "/hello":
            event.reply("Hello from DevGram Builder")

If the CLI is not installed, the same structure can be created manually under the Project Structure section.

DevGram Builder

Project Structure

Each directory has one responsibility.

hello.devgram/
├── devgram-builder.json
├── main.py
├── modules/
│   ├── messages.py
│   └── appearance.py
├── services/
├── screens/
├── assets/
├── locales/
│   ├── ru.json
│   └── en.json
└── wheels/
CatalogPurpose
modules/Event modules and commands.
services/Background tasks and stateful services.
screens/UI and settings pages.
assets/Icons, pictures and templates.
DevGram Builder

Public API

Builder facades hide the low-level Java bridge but return regular DevGram objects.

APIPurpose
ModuleBasic lifecycle of the module.
module.on_message(event)Normalized message event.
module.settings()Declarative settings.
context.client(account)Account-aware Client API.
context.ui.bulletin()Native die.
context.storagePersistent module storage.

Lifecycle

def setup(context):
    context.register(Welcome())

def dispose(context):
    context.unregister_all()
DevGram Builder

Metadata

File devgram-builder.json describes the project before packaging it in manifest `.dgplugin`.

{
  "id": "author.plugin",
  "name": "Plugin name",
  "version": "1.2.0",
  "entrypoint": "main.py",
  "modules": ["modules.messages", "modules.appearance"],
  "minDevGram": "12.9.3",
  "permissions": ["network", "storage"]
}

Version rules

The version must be incremented for each archive published. minDevGram use if the project depends on a new API. Builder checks module IDs, entrypoint existence, and resource duplication before packaging.

DevGram Builder

Modules & Imports

A large plugin is divided into independent modules with an explicit lifecycle.

# modules/messages.py
from devgram.builder import Module

class Messages(Module):
    id = "messages"
    depends_on = ["storage"]

    def enable(self, context):
        self.handle = context.events.messages(self.on_message)

    def disable(self, context):
        self.handle.close()

Don't use wildcard imports. Place common functions in services/, rather than copying between modules. Circular dependencies are not allowed: Builder prints the module chain that created the cycle.

DevGram Builder

Assets & Localization

Builder indexes resources and checks references to them during the build.

icon = context.assets.path("icons/plugin.png")
title = context.i18n.get("settings.title")
message = context.i18n.format("welcome", name=user_name)

Fallback

Row selection order: current language → English → default → key. All JSON files must contain a strings object. Do not store binary data in locale files.

Dimensions assets

Make your catalog icons square. Compress large images in advance: `.dgplugin` is downloaded and checked entirely, so extra assets increase installation time.

DevGram Builder

Settings

Builder combines module settings into one native page and automatically adds namespace to keys.

class Appearance(Module):
    def settings(self):
        return [
            Header(text="Внешний вид"),
            Switch(key="glass", text="Стекло", default=False),
            Selector(key="style", text="Стиль", items=["System", "Compact"]),
        ]

    def on_setting_changed(self, key, value):
        if key == "glass": self.apply_glass(value == "1")

The physical key is stored as module_id.keyso two modules can share a logical name enabled without conflict.

DevGram Builder

Dependencies

There are two types of dependencies: project modules and Python wheels.

Module dependencies

depends_on defines the order of enable and the reverse order of disable. If the required module does not load, the dependent module does not start and receives an understandable error.

Python wheels

devgram-builder add wheel dist/library.whl
devgram-builder inspect dependencies

Builder packages wheels and creates a lock file with SHA-256. For Android, pure-Python dependencies are preferred.

DevGram Builder

Development

Assembly, verification and hot reload are performed in one sequence.

devgram-builder check
devgram-builder build --debug
devgram-builder upload --reload
devgram-builder logs --follow

Debug build

The debug package includes a source map of modules and an extended log. Before publishing, collect without --debugto not include local paths and diagnostic files.

Editor workflow

Run the check when saving, keep ADB connected and reload only the changed plugin ID. A full restart is only needed when the native Java bridge changes.

DevGram Builder

Troubleshooting

Diagnosis of structure, import and lifecycle problems.

Module not found

Check the name in metadata for availability __init__.py and character case. Android filesystem is case sensitive.

Circular dependency

Move the general logic into a service module on which both sides depend. Don't create mutual depends_on.

Works after restart, but not after reload

The module retained the global singleton or Java listener. Move creation to enable(), and deleting in disable().

Settings are lost

Do not change the project ID or module ID. To rename a key, add an explicit schema version migration.

Getting started

Preparation

A minimal environment for writing and installing extensions.

Requirements

  • current DevGram on Android;
  • Python 3 for packaging;
  • ADB for hot reload;
  • editor with Python support.

Developer mode

Open Plugins → Plugin System and enable developer mode. Copy the local token.

export DEVGRAM_TOKEN="token_from_app"
python3 tools/devgram_dev.py status
Local access

Dev server is only accessible via ADB forwarding and requires a token.

Preparing the working environment

Separate the source directory and the built package directory. In the sources, store manifest, Python code, resources and test data; In the release archive, do not include the virtual environment, logs, IDE metadata and local keys.

Checking the environment

python3 --version
adb devices
unzip -t plugin.dgplugin

Before starting, make sure that the device is visible through ADB, DevGram is installed from the same branch for which the plugin was written, and Developer mode is enabled. If ADB shows unauthorized, confirm with fingerprint on your phone.

Repeatable assembly

Commit dependency versions and build the archive from a clean directory. A repeated launch should create the same list of files and the same manifest, except for the deliberately changed checksum.

Getting started

First plugin

A minimal package that can be installed in one tap.

from devgram import BasePlugin
from devgram.ui import Header, Switch, Button

class Hello(BasePlugin):
    id = "hello.devgram"
    name = "Hello DevGram"
    version = "1.0.0"
    author = "Your name"
    description = "Мой первый плагин для DevGram"
    icon = "https://example.com/plugin-icon.png"

    def settings(self):
        return [Header(text="Hello"), Switch(key="enabled", text="Включено"), Button(key="test", text="Проверить")]

    def on_setting_click(self, key):
        if key == "test": self.bulletin("Плагин работает", kind="success")

plugin = Hello()
Plugin avatar

Field icon accepts a direct HTTPS link to a PNG or JPG. DevGram will show a picture in the installation card and the list of plugins. Details are in a separate item “Plugin Avatar” in the Package menu.

Save the file with the extension .plugin, send it to Telegram and click “Install”. The package with resources and dependencies is packaged in .dgplugin.

Analysis of the first plugin

Entry point contains only metadata and small logic. When importing, do not call the UI or send messages: the loader may not yet have an active Fragment. Register observer, menu and pill in on_load().

Step by step check

  1. Install the package and make sure it appears in the manager.
  2. Open settings and change the value.
  3. Reload only this plugin.
  4. Send a test message from the second account.
  5. Disable and remove the plugin, then check for callback missing.

Typical errors

If the plugin is not visible, check the extension, archive root and ID uniqueness. If it is visible but does not run, see the first import error: subsequent messages are often a consequence.

Package

Manifest

Metadata defines the identity and entry point of the extension.

{
  "id": "hello.devgram",
  "name": "Hello DevGram",
  "version": "1.0.0",
  "author": "Your name",
  "entrypoint": "main.py",
  "description": "Example plugin",
  "icon": "https://example.com/plugin-icon.png"
}
FieldRule
idUnique stable ID.
entrypointPython entry point file.
versionUpdate version.
descriptionBrief description for the catalog and installation card.
iconDirect HTTPS link to a PNG or JPG avatar.
Atomic installation

The package is validated and unpacked into a temporary folder. A failed update does not break the active version.

manifest fields

Manifest describes the identity of the package before Python is imported. Use UTF-8 strings, don't duplicate keys, and don't store custom tokens there. The version and min_app_version values ​​are compared as strings according to the bootloader rules, so follow the format major.minor.patch.

entrypoint compatibility

The entrypoint path must be relative and point to an existing Python file. The module name must not be the same as a system package or another installed plugin. After renaming the file, update the manifest and check the clean installation.

Icon and description

For icon use a direct HTTPS link to the PNG or JPG. The description is written for the directory: first the purpose, then the restrictions and required permissions.

Package

Plugin avatar

How to add an icon to the installation card, list of plugins and directory.

Regular .plugin

Add a field icon to the class next to name, author And description:

class MyPlugin(BasePlugin):
    id = "my.plugin"
    name = "My Plugin"
    author = "@you"
    description = "Краткое описание"
    icon = "https://example.com/plugin-icon.png"

Package .dgplugin

Please provide the same link in manifest.json:

{
  "id": "my.plugin",
  "name": "My Plugin",
  "icon": "https://example.com/plugin-icon.png"
}

Requirements

  • Use a direct HTTPS link to the image file rather than a link to an HTML page.
  • Format - PNG or JPG; A small square image is better.
  • Check the contrast on a light and dark theme.
  • If icon is not specified or the image did not load, DevGram will leave the standard icon.
Where will she appear

DevGram loads an avatar when opening the installation card and caches it for the list of plugins.

Package

Assets and languages

Package resources are available without absolute paths.

background = self.asset_path("background.png")
title = self.string("title", default="DevGram")

Translation files are stored in locales/en.json And locales/ru.json. Path asset_path() limited to folder assets/.

Package

Dependencies

Compatible Python wheels can be included in the package.

Place pure-Python wheels in wheels/. When hot reloading, old modules and paths are removed, so the update is not mixed with the old version.

ABI

Native wheels must match the ABI and Python version of the DevGram build.

SDK

BasePlugin

Main extension class and its lifecycle.

MethodPurpose
on_load()Initialization.
on_unload()Freeing observers, hooks and pills.
on_setting_changed(key,value)Change the setting.
get_setting / set_settingPersistent plugin storage.

BasePlugin Contract

The loader creates one instance per installed plugin ID. Don't count on a new object for every event. Clear status fields in unload, and store resource registration in instance fields.

Safe lifecycle

def on_load(self):
    self._closed = False
    self._handles = []

def on_unload(self):
    self._closed = True
    for handle in self._handles:
        try: handle.close()
        except Exception: pass
    self._handles.clear()

The callback that came after unload checks self._closed and stops working. This is especially important for background queue and network responses.

SDK

Multi-account

Each callback can work with a specific Telegram account, and not with a globally selected one.

Getting a client

from devgram import get_selected_account, get_client

account = get_selected_account()
client = get_client(account)
client.send_text(dialog_id, "Hello")
controller = client.get_messages_controller()

AccountClient

MethodDescription
get_messages_controller()MessagesController of the desired account.
get_user_config()UserConfig of the desired account.
get_connections_manager()ConnectionsManager of the desired account.
send_text(dialog_id,text)Sending from the selected account.
Rule

In account-aware callback use the passed account. Do not rely on the account that is open in the interface at this moment.

Account-aware rule

The account index is passed from the event to each subsequent call. Do not save the selected account to a global variable; the user can switch profiles while the request is running.

Object Consistency

Peer, controller, storage and request must be obtained for one account. You cannot mix InputPeer from one cache with ConnectionsManager of another account. For a background task, store account in a closure and pass it to the UI notification only as a context.

Examination

Test two accounts, logout of one profile and switching accounts during the request. If your account is deleted, show the error correctly and do not resend automatically.

SDK

Events

Hooks for messages, updates and TL requests with account-aware options.

def on_send_message(self, text): return text
def on_receive_message(self, text): pass
def on_update_hook(self, update_name, account, update): pass
def on_send_request_hook(self, account, name, request): pass
def on_receive_response_hook(self, account, name, response, error): pass

NotificationCenter

from devgram.client_utils import observe
handle = observe(notification_id, callback, account=account)
handle.close()

Order of events

Before sending, the message goes through a send hook, then a Telegram request. Incoming updates may arrive before the visible list of dialogs is updated. Don't make the assumption that the UI already contains a message or that the callback will be called exactly once.

Filtration

First check account, update type, dialog ID and the presence of the required field. Early return reduces the load. For multiple similar updates, use debounce and re-ID protection.

observer errors

The callback exception should not stop NotificationCenter. Log the event name and plugin ID, then return. Close Handle even if the handler is partially initialized.

SDK

Message menu

The plugin can add commands to the long press menu on a message.

def menu_items(self):
    return ["Скопировать как Markdown", "Проверить текст"]

def on_menu_click(self, label, message_text, dialog_id):
    if label == "Скопировать как Markdown":
        self.copy(message_text)
        self.bulletin("Скопировано", kind="success")

callback parameters

ParameterMeaning
labelPressed item from menu_items().
message_textText of the selected message.
dialog_idID of the chat where the menu is open.

Long press menu

Bring back stable human-readable labels and keep them short. The handler must re-check message and dialog before taking action: between building the menu and clicking, the message may have been deleted or changed.

Several plugins

Don't use too general names or override other people's commands. The plugin ID is added to the internal key by the loader, but the text displayed must explain the action.

Errors

If the action requires a network, immediately show a bulletin about the launch, execute the request in the queue and update the result on the UI thread. Don't block the menu by waiting for a response.

SDK

Client API

Sending, editing, formatting and controllers.

from devgram.client_utils import send_text, send_formatted_text
from devgram.text_formatting import Entity

send_text(peer_id, "Hello", account=account)
send_formatted_text(peer_id, "DevGram", [Entity("bold", 0, 7)], account=account)
self.send_photo(peer_id, "/path/photo.jpg", "Caption")
self.send_file(peer_id, "/path/file.zip")
self.edit_message(peer_id, message_id, "Updated")
HelperResult
get_messages_controller(account)MessagesController
get_notification_center(account)NotificationCenter
get_file_loader(account)FileLoader
get_download_controller(account)DownloadController

Dispatch and controllers

High-level helpers are suitable for normal actions and take into account account. Controllers are needed for scenarios where the native Telegram model is required. Do not call storage and connections manager methods from the UI thread.

Callback and result

Handle the request error separately from the Python exception. For a repeatable action, store the request ID and prevent double clicks. Before sending, check the peer and user permission for the action.

Media

For large files, use FileLoader and DownloadController rather than reading the entire byte array into memory. Show download status and handle cancellation.

SDK

Formatting text

DevGram creates real TLRPC.MessageEntityrather than sending Markdown as plain text.

Supported entities

Bold, italic, underline, strike, spoiler, code, pre, blockquote and text URL.

from devgram.client_utils import send_formatted_text
from devgram.text_formatting import Entity, parse_text, parse_markdown, parse_html

text, entities = parse_markdown("**DevGram** _plugin_")
send_formatted_text(peer_id, text, entities, account=account)

text, entities = parse_html("<b>DevGram</b>")

UTF-16 offsets

Telegram calculates entity offsets in UTF-16. Use utf16_length(), add_surrogates() And remove_surrogates(), if you create entities manually.

HelperDescription
parse_text(text,parse_mode)Single point for HTML or Markdown.
escape_markdown(text)Escapes control characters.
escape_html(text)Escapes HTML.
to_tlrpc_entities(list)Java ArrayList with Telegram entities.

UTF-16 ranges

Telegram entities use UTF-16 code units, while Python slicing uses Unicode code points. Emoji, skin tone modifiers and some compound characters make these indexes different. Use utf16_length() and ready-made parse helpers.

Overlaid entities

Check range intersections and order before submitting. HTML and Markdown parser return parsed text; do not add entity at source indexes after removing markup.

User input

Escape text before substitution in HTML/Markdown. Check the URL separately and do not turn any text from the message into a clickable link without an explicit action.

SDK

Settings

Declarative strings in the native settings page.

from devgram.ui import Header, Switch, Input, Selector, Button

def settings(self):
    return [Header(text="Основное"), Switch(key="enabled", text="Включено"), Input(key="name", text="Имя"), Selector(key="mode", text="Режим", items=["A", "B"]), Button(key="reset", text="Сбросить")]

value = self.get_setting("name", "")
self.set_setting("name", "DevGram")

Settings and status

Keys must be stable and have a plugin namespace. Changing the value causes a callback, but does not guarantee that the current screen has already been redrawn. If the new setting requires a restart, show a bulletin with a clear action.

Value types

Switch is stored as a boolean-compatible value, Input is a string, Selector is the index or value of the selected item. Transform data at the API edge and maintain schema version for migrations.

UI errors

A long label is moved or shortened, but should not overlap the control. Check dark mode, font scale, screen rotation and re-enter settings.

Utilities

Intents

Registering Android Intent handlers and opening links safely.

from devgram.intents import register, open_uri

handle = register(on_link, action="android.intent.action.VIEW", scheme="https", host="example.org")
open_uri("https://devgram.space")

def on_unload(self):
    handle.unhandle()

Filters register()

ParameterDescription
actionAndroid action.
schemeURI scheme: https, tg and others.
hostHostname.
pathPath URI.

IntentContext provides action, data, scheme, host, path and the original Java Intent.

Routing

Handlers are checked by priority, then by action, scheme, host, path, query and category. The narrower the matcher, the fewer random triggers. Callback returns True only if the Intent is actually processed.

URI Security

The URI from messages and network is not trusted. Compare scheme and host using a white list, limit the query length, and do not pass an arbitrary URI to the system resolver without warning.

Closing

Save HandlerHandle and call unhandle() in unload. When reloading, the old handler should not remain in the global list.

Utilities

File utilities

Text and binary file operations, as well as private plugin storage.

from devgram.file_utils import ensure_dir_exists, list_dir, read_file, write_file

ensure_dir_exists(path)
write_file(path, "text")
items = list_dir(path, extensions=[".json"], recursive=True)
data = read_file(path)
MethodPurpose
read_file_bytes / write_file_bytesBinary content.
delete_file(path)Deleting a file.
self.read_file(name)Reading from the plugin's private folder.
self.write_file(name,data)Write to a private folder.

File model

Pass an absolute path only after checking that it is inside a permitted directory. Don't use a custom filename directly: normalize the path, disable traversal and limit the size.

Text and bytes

Text functions use explicit encoding, binary helpers store bytes without conversion. Don't read a large file entirely if streaming or checking the size is enough.

Removal

Before deleting, check the object's existence, type, and directory membership. For important data, use temporary backup and clear confirmation in the UI.

Utilities

Android utilities

Streams, clipboard, logging and ready-made Java listeners.

from devgram.android_utils import run_on_ui_thread, run_on_queue, log, copy_to_clipboard

run_on_ui_thread(lambda: update_view(), delay=250)
run_on_queue(lambda: load_data())
copy_to_clipboard("DevGram")
log("plugin loaded")

Listeners

from devgram.android_utils import OnClickListener, OnLongClickListener, R

view.setOnClickListener(OnClickListener(lambda view: clicked(view)))
view.setOnLongClickListener(OnLongClickListener(lambda view: True))

R(fn) creates a Java Runnable. Always send long operations to the queue, and change the View only on the UI thread.

UI and background

run_on_ui_thread() doesn't make the heavy function safe: it just schedules it in the UI queue. run_on_queue() does not give access to View. Perform the intersection of two worlds through a small immutable result.

Listeners

Save the listener handle or View reference for screen time only. Remove the listener when closing, otherwise the Activity may be held after navigation.

Clipboard and logs

Do not copy secrets without explicit user action and do not log private text. Bulletin should confirm action but not show sensitive content.

UI and runtime

Bulletins and alerts

Native Telegram notifications with callback buttons.

self.bulletin("Скопировано", kind="success", duration=5000, button="Отменить", callback=undo)
self.alert("Удалить файл?", "Действие нельзя отменить", positive="Удалить", on_positive=remove, negative="Отмена")

Bulletin and alert

Bulletin is designed for short-term results and disappears on its own. Alert is used for a decision that requires a choice. Each destructive action must have a cancellation, and the callback must be safe when the Fragment is closed.

Duration

Don't turn the bulletin into a permanent notification. For a long operation, show the status on the screen and briefly confirm the result when completed.

Topics

Use native colors and check the contrast of text, icons and disabled states in a light and dark theme.

UI and runtime

Pill Stack

Interactive compact widgets belonging to a specific plugin.

self.register_pill("status", "Status", text="Online", value=42, on_click=self.open)

def on_unload(self): self.unregister_pills()
Possession

Another plugin cannot overwrite or delete your pills.

Ownership of Pill Stack

Each pill belongs to a plugin ID. Use a stable local key, update the value instead of creating a new pill, and delete all pills in unload.

Interactivity

Click callback should be fast and account-aware. If the action is network, change the state to loading and allow retry after error. Don't store an Activity object in a pill.

Size and Availability

The text must fit on a small screen and in a larger font. Add a clear description and don't just color code the condition.

UI and runtime

Effects and View

High-level helpers for glass, blur, tint, sizing and embedding Android View.

panel = self.glass_panel(context_view, corner=22, blur=18, tint=0x26FFFFFF, border=0.6)
self.add_view(parent, panel, width=-1, height=-2, left=16, right=16, gravity=80)
self.round_corners(panel, 18)
self.tint(panel, self.rgba(30, 30, 34, 210))
self.remove_view(panel)
MethodDescription
glass(view,...)Wraps an existing View in a glass panel.
blur / unblurSets or removes the RenderEffect.
dp(value)dp to pixels.
rgba(r,g,b,a)Android ARGB integer.
Android limitation

RenderEffect blurs the contents of the View itself, rather than an arbitrary background behind a separate Surface.

UI and runtime

Java hooks

Direct Android runtime and Xposed-style method hooks for advanced scenarios.

from java import jclass
AndroidUtilities = jclass("org.telegram.messenger.AndroidUtilities")
# Hook engine — AliuHook (Xposed API over LSPlant): reliable on all Android,
# catches even inlined methods. You can hook ANY class (profile, chats, settings).

# 1) Per-hook callback — your own before/after/replace for a SPECIFIC hook:
self.hook("org.telegram.ui.ProfileActivity", "onResume", after=self.on_profile)

# 2) Full method replacement (original is NOT called, return the result from fn):
self.hook("org.telegram.messenger.MessagesController", "getUser", "long", replace=self.fake_user)

# 3) All overloads of a method (or all constructors, method="<init>"):
self.hook_all("org.telegram.ui.ChatActivity", "onResume", after=self.tweak)

# 4) Classic mode — one before_hook/after_hook for ALL plugin hooks:
self.hook("org.telegram.ui.ChatActivity", "onResume")

def on_profile(self, frame):
    profile = frame.thisObject          # the ProfileActivity itself
    args = frame.args                   # arguments (mutable)
    res  = frame.getResult()            # result (in after)
    # frame.setResult(x) — replace result; in before it SKIPS the original
    original = self.invoke_original(frame)   # call the original bypassing hooks

view = self.java_class("android.view.View", logic, arg_types=["android.content.Context"], args=[context])
Don't hook hot methods

Frame-by-frame callbacks like onDraw via Python may cause an ANR.

Hook utilities (AliuHook)

hook(cls, m, ...types, before=, after=, replace=, priority=) — per-hook callbacks; hook_all(cls, m, ...) — all overloads/constructors; deoptimize(cls, m, ...types) — deoptimize a method; invoke_original(frame) — call the original bypassing hooks; unhook_all() — remove your hooks; make_class_inheritable(cls), allocate_instance(cls), is_hooked(cls, m, ...types). Existing plugins using before_hook/after_hook keep working unchanged.

Low cost

Java hook and proxy link the plugin with specific implementation details. They require class checking, overload, constructor and return type. First look for a public event or controller.

Fallback

Optional hook is set to try/except and disables only one function. The critical hook declares the minimum version and an understandable error.

Cleanup

Store the handle, remove the hook in unload and do not leave a static reference for the plugin or Context.

UI and runtime

Reflection

Access to private and static private fields of Java classes.

from devgram.hook_utils import find_class, get_private_field, set_private_field
from devgram.hook_utils import get_static_private_field, set_static_private_field

clazz = find_class("org.telegram.ui.ChatActivity")
value = get_private_field(instance, "chatActivityEnterView")
set_private_field(instance, "field", value)

Reflection depends on internal Telegram names and may require updating after changing the base version of the client. Always wrap such operations in error handling.

Private fields

The name private field and its type are not a stable contract. Check for presence before reading, don't change critical fields without fallback behavior, and log only safe information.

Types

The value must match the Java type, including primitive wrappers and nullability. The reflection error can only appear on a specific version of the device.

When to refuse

If the task is solved through the Client API, settings or NotificationCenter, reflection is not needed. The less private access, the easier it is to update the plugin.

Publication

Dev server

Hot reload and debugging without restarting the application.

export DEVGRAM_TOKEN="..."
python3 tools/devgram_dev.py upload plugin.dgplugin
python3 tools/devgram_dev.py reload --plugin hello.devgram
python3 tools/devgram_dev.py debugger-start --platform vscode --port 5678

The tool uses ADB forward/reverse. The network port does not open to the outside.

Hot reload

Reload first calls unload, then clears the modules and imports the new version. If the old callback continues to run, the problem is an unclosed handle, timer, thread, or Java listener.

ADB workflow

adb devices
adb logcat | grep DevGram
adb push plugin.dgplugin /sdcard/Download/

Test logcat only on a test device and remove personal data from the report.

Release check

Hot reload does not replace a clean installation: separately check install, update, disable, enable, uninstall and launch after safe mode.

Publication

Safety

The plugin code runs inside the client process, so trust the package source.

  • do not keep secrets in the archive;
  • do not block the UI thread;
  • shoot observers and pills in on_unload;
  • consider account in multi-account callbacks.
Safe mode

After a fall in callback, DevGram disables callbacks of plugins until manual recovery.

Secure Border

Safe mode helps restore the client after a callback crash, but does not check the author's intentions. Installation remains the user's decision. The directory and changelog must honestly describe the network, hooks, and access to messages and files.

Error isolation

One invalid update should not disable all observers. One optional API shouldn't break loading. Use local try/except and short operations.

Secrets

Do not store bot token, Firebase credentials, signing keys and private URLs in the source code or archive. Scan the entire package and build history before publishing.

Publication

Problem Solving

Typical installation, download and execution errors.

The plugin did not appear

  • check the extension (.plugin or .dgplugin);
  • for package .dgplugin make sure that manifest.json and entrypoint are at the root of the archive;
  • check the uniqueness of the ID;
  • open the plugin system log.

Changes not applied

Use reload specific plugin ID. DevGram clears its modules from the Python cache; if the code is still old, check the entrypoint and archive contents.

After the fall nothing works

The client could enable safe mode. Remove or fix the problematic package, click Reload Plugins, then turn off Safe Mode.

UI freezes

Move networking, large file reading and computing to run_on_queue(). Don't execute Python code in frame-by-frame methods.

Diagnostic order

  1. Play on a clean run.
  2. Make a note of the plugin ID/version and exact screen.
  3. Check the first line of traceback.
  4. Disable only the problematic plugin.
  5. Repeat after reload and after a complete restart.

The directory is empty

Separate the network error from the render error: show loading, empty and error separately, check the callback on the UI thread and make sure filters are not applied before the data is loaded.

Crash after update

Compare manifest, schema migration, imports and optional hooks. With an incompatible API, it is better to disable the function and show a warning than to crash every time you open it.

Publication

API reference

A short list of public DevGram SDK modules.

ModulePurpose
devgramBasePlugin, AccountClient, hooks, UI helpers and lifecycle.
devgram.client_utilsAccount-aware controllers, sending and NotificationCenter.
devgram.text_formattingEntities, HTML, Markdown and UTF-16.
devgram.android_utilsStreams, listeners, clipboard and log.
devgram.file_utilsText and binary files.
devgram.intentsRegistering and sending Android Intent.
devgram.hook_utilsReflection helpers.
devgram.uiSettings, AlertDialogBuilder and BulletinHelper.

BasePlugin shortcuts

toast, me, user_name, chat_name, copy, clipboard, send_message, send_photo, send_file, edit_message, send_request, tl, implement, java_class, hook, bulletin, alert, register_pill.

API Map

Start with BasePlugin and lifecycle, then move on to account-aware client_utils, formatting and UI. Connect Utilities as needed, and use native sections only after checking the public API.

Return Value Contract

None usually means no result or an action without a synchronous return; boolean callbacks report whether the event has been processed; handles have explicit close/unhandle. Always check the specific module table.

Version strategy

The stable plugin ID is preserved, the version is increased, and the minimum app version limits the launch. Breaking changes are accompanied by migration and changelog.

Publication

Cookbook

Practical recipes for common tasks. Each example can be inserted into a separate `.plugin` or `.dgplugin` and adapted to your scenario.

1. Full lifecycle

Use on_load() only for registering resources and observers. Move long operations to a queue. IN on_unload() remove everything that was registered.

from devgram import BasePlugin
from devgram.client_utils import observe
from devgram.android_utils import run_on_queue, log

class LifecyclePlugin(BasePlugin):
    id = "sample.lifecycle"
    _observer = None

    def on_load(self):
        self._observer = observe(42, self._notification, account=self.current_account())
        run_on_queue(self._warm_cache)

    def _warm_cache(self):
        log("cache warmed")

    def _notification(self, notification_id, account, args):
        self.bulletin("Событие получено")

    def on_unload(self):
        if self._observer:
            self._observer.close()
            self._observer = None
Why is this important

If you do not remove the observer, after hot reload the callback may fire twice. If you don't clear the pill, the old UI will remain registered.

2. Command in the message menu

The menu returns only a list of strings. Keep the logic of action in on_menu_click(); Don't change Java View directly from callback without UI transition.

def menu_items(self):
    return ["Посчитать слова", "Сохранить текст"]

def on_menu_click(self, label, message_text, dialog_id):
    if label == "Посчитать слова":
        self.bulletin(f"Слов: {len(message_text.split())}")
    elif label == "Сохранить текст":
        self.write_file("last.txt", message_text)
        self.bulletin("Сохранено", kind="success")

3. Sending a response with entities

First get clean text and list Entity, then pass them to send_formatted_text(). Do not send Markdown tags to Telegram directly.

from devgram.text_formatting import Entity
from devgram.client_utils import send_formatted_text

text = "DevGram: официальный клиент"
entities = [Entity("bold", 0, 7), Entity("italic", 9, 10)]
send_formatted_text(dialog_id, text, entities, account=account)

4. Multi-account without errors

Any callback that receives account, should pass it on to helpers. Otherwise, the message may leave the account selected in the UI, and not the event account.

def on_update_hook(self, update_name, account, update):
    client = self.client(account)
    client.send_text(self.chat_id, "Ответ от правильного аккаунта")

5. UI and background tasks

from devgram.android_utils import run_on_ui_thread, run_on_queue

def load(self):
    run_on_queue(lambda: self._load_data())

def _load_data(self):
    value = read_remote_data()
    run_on_ui_thread(lambda: self.bulletin("Готово"))
Thread Rule

Java View, bulletin, alert, clipboard and changing settings - UI. Network, JSON, files and heavy computing - queue.

6. Settings with migration

def on_load(self):
    if self.get_setting("schema", "0") == "0":
        old = self.get_setting("old_name", "")
        if old: self.set_setting("name", old)
        self.set_setting("schema", "1")

def settings(self):
    return [Switch(key="enabled", text="Включено", default=True), Input(key="name", text="Имя")]

7. Errors and logging

Wrap the outer code in try/except, record the context via log() and show the user a clear message. Don't show stack trace in bulletin.

try:
    self.send_request(request, self._on_result)
except Exception as error:
    log("request failed: " + repr(error))
    self.bulletin("Не удалось выполнить запрос", kind="error")

8. Review before publication

  1. The ID has the author's namespace.
  2. Enlarged version.
  3. The archive contains no secrets or debug logs.
  4. The plugin runs in safe mode and after hot reload.
  5. All observers, hooks and pills are released.
  6. Network and files are not executed on the UI thread.
Verified API

SDK contracts

The exact module contracts that are included in the current DevGram build. The signatures on this page are verified against the Python runtime client.

Source of Truth

If the example from the experimental DevGram Builder is inconsistent with this page, use the API from here. Builder is still a high-level design layer, and modules devgram.* are already available to plugins.

devgram

The root module provides BasePlugin, AccountClient, HookResult, HookStrategy, get_selected_account() And get_client(account=None). A plugin instance is created by the loader. Don't create BasePlugin manually and do not store Android Activity in a global variable: after changing the screen, the link will become obsolete.

BasePlugin Metadata

FieldTypePurpose
idstrPermanent unique ID. Used for settings, files, hooks and updates.
namestrDisplay name.
versionstrPackage version; enlarge before publishing.
authorstrAuthor or team.
descriptionstrBrief description without markup.
iconstrPNG/JPG URL for a list of plugins.
min_app_versionstrMinimum compatible version of DevGram.

AccountClient

get_client() returns the facade of the current account, and get_client(account) binds operations to the specified index. Available get_messages_controller(), get_user_config(), get_connections_manager() And send_text(dialog_id, text). Account index and Telegram user ID are different values.

from devgram import get_client

def send_for_event(account, dialog_id):
    client = get_client(account)
    client.send_text(dialog_id, "Сообщение из нужного аккаунта")

devgram.client_utils

All account-aware helpers accept account=None. Without the parameter, the account selected in the interface is used. In event handlers, always pass the received account, otherwise the action may be performed by another profile.

FunctionResultApplication
send_text(peer, text, account=None)NoneSends plain text.
send_formatted_text(peer, text, entities, account=None)NoneSends text from native entities.
get_messages_controller(account=None)Java objectUsers, chats and dialogues.
get_connections_manager(account=None)Java objectTL requests and network state.
get_messages_storage(account=None)Java objectLocal base; perform heavy operations in queue.
get_file_loader(account=None)Java objectPaths and downloads of Telegram media.
get_last_fragment()BaseFragment/NoneCurrent screen. Always check for None.

NotificationCenter

observe(notification_id, callback, account=None) returns ObserverHandle. Save handle and call close() when unloading. Re-registering after a hot reload without closing the old handle results in double callbacks.

from devgram.client_utils import observe

def on_load(self):
    self._observer = observe(1, self._on_notification)

def _on_notification(self, notification_id, account, args):
    self.log("notification account=" + str(account))

def on_unload(self):
    if self._observer:
        self._observer.close()
        self._observer = None

devgram.text_formatting

Entity(type, offset, length, url=None, language=None) describes the range in UTF-16. Available parse_html(), parse_markdown(), parse_text(), to_tlrpc_entities(), utf16_length(), escape_markdown() And escape_html(). Do not calculate the length using len()if the string contains emoji or non-BMP characters.

from devgram.text_formatting import Entity, utf16_length
from devgram.client_utils import send_formatted_text

text = "🔥 DevGram"
entities = [Entity("bold", 0, utf16_length(text))]
send_formatted_text(dialog_id, text, entities, account=account)

Parsing custom text

parse_text(text, parse_mode) returns cleared text and entities. Use parse_mode="html" or parse_mode="markdown". Escape user data before adding it to the template, otherwise the entered characters will become markup.

from devgram.text_formatting import escape_html, parse_text

source = "Автор: " + escape_html(user_name)
plain, entities = parse_text(source, "html")

devgram.file_utils

The module contains ensure_dir_exists(), list_dir(), read_file(), write_file(), binary options and delete_file(). list_dir(path, extensions=None, recursive=False, include_files=True, include_dirs=False) can filter extensions and traverse the tree.

from devgram.file_utils import ensure_dir_exists, write_file, read_file

folder = ensure_dir_exists(self.storage_path("cache"))
path = folder + "/state.json"
write_file(path, '{"enabled": true}')
state = read_file(path)

Check paths received from Intent or message. Prohibit .., absolute paths and leaving the plugin directory. Write JSON atomically if the file is important for state recovery.

devgram.android_utils

FunctionContract
run_on_ui_thread(func, delay=0)Runs a callback in the UI queue; delay is specified in milliseconds.
run_on_queue(func)Moves files, network and computation to the background queue.
log(data)Writes a line with the DevGram prefix to FileLog.
copy_to_clipboard(text)Copies text via the system clipboard.
OnClickListener(fn)Creates a Java listener for Android View.
OnLongClickListener(fn)Callback should return boolean.
from devgram.android_utils import run_on_queue, run_on_ui_thread

def refresh(self):
    run_on_queue(self._load)

def _load(self):
    result = read_remote_data()
    run_on_ui_thread(lambda: self.bulletin("Данные обновлены"))

devgram.intents

register(callback, action=None, scheme=None, host=None, path=None, query=None, category=None, flags=0, priority=0) registers an incoming route. The higher priority is checked first. Callback receives IntentContext with margins intent, action, uri, parsed And query. Return strictly Trueto stop dispatch.

from devgram.intents import register, open_uri

def on_load(self):
    self._route = register(self._open, scheme="devgram", host="plugin", priority=100)

def _open(self, context):
    item = context.query.get("id", [None])[0]
    if not item:
        return False
    self.bulletin("Открыт объект " + item)
    return True

def on_unload(self):
    self._route.unhandle()

open_uri(uri) passes the URI to Android resolver, and send(intent) launches an explicitly created Intent. Don't run untested schemas from remote data.

devgram.hook_utils

Reflection API includes find_class(), get_private_field(), set_private_field(), as well as static options. This is a low-level API: the names of private fields change between Telegram versions. Wrap lookup in error handling and declare the minimum version of DevGram.

devgram.ui

Settings are built from Header, Switch, Input, Selector, Text And Button. Dialogs are created by a chain of methods AlertDialogBuilder: title, message, positive, negative and neutral buttons.

from devgram.ui import AlertDialogBuilder

AlertDialogBuilder() \
    .set_title("Удалить данные?") \
    .set_message("Действие нельзя отменить") \
    .set_positive_button("Удалить", self.clear_data) \
    .set_negative_button("Отмена") \
    .show()

Threads and lifecycle

  1. IN on_load() only register resources and run short operations.
  2. Change all Android Views and notifications to UI thread.
  3. Execute files, JSON, network and large loops in queue.
  4. IN on_unload() close observers, intent handles, hooks and pills.
  5. Cleanup must be idempotent: calling it again should not fail.
  6. Callback after unloading should check that the plugin is still active.

Compatibility

The public Python API is more stable than Java reflection, but is also being developed. Do not import private names with underscores. Before publishing, test the installation, updating over the old version, disabling, re-enabling, hot reload, deleting and launching after safe mode.

Verified API · android_utils

Android Utils

Detailed help on the module android_utils, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Functions

NameSignaturePurpose
run_on_ui_threadrun_on_ui_thread(func, delay=0)Working function of the module; check the return value and execution flow.
run_on_queuerun_on_queue(func)Working function of the module; check the return value and execution flow.
loglog(data)Working function of the module; check the return value and execution flow.
copy_to_clipboardcopy_to_clipboard(text)Working function of the module; check the return value and execution flow.
RR(fn)Working function of the module; check the return value and execution flow.
OnClickListenerOnClickListener(fn)Working function of the module; check the return value and execution flow.
OnLongClickListenerOnLongClickListener(fn)Working function of the module; check the return value and execution flow.

Import example

from android_utils import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · client_utils

Client Utils

Detailed help on the module client_utils, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Functions

NameSignaturePurpose
send_textsend_text(peer, text, account=None)Working function of the module; check the return value and execution flow.
send_formatted_textsend_formatted_text(peer, text, entities, account=None)Working function of the module; check the return value and execution flow.
get_messages_controllerget_messages_controller(account=None)Working function of the module; check the return value and execution flow.
get_user_configget_user_config(account=None)Working function of the module; check the return value and execution flow.
get_connections_managerget_connections_manager(account=None)Working function of the module; check the return value and execution flow.
get_account_instanceget_account_instance(account=None)Working function of the module; check the return value and execution flow.
get_send_messages_helperget_send_messages_helper(account=None)Working function of the module; check the return value and execution flow.
get_media_data_controllerget_media_data_controller(account=None)Working function of the module; check the return value and execution flow.
get_contacts_controllerget_contacts_controller(account=None)Working function of the module; check the return value and execution flow.
get_messages_storageget_messages_storage(account=None)Working function of the module; check the return value and execution flow.
get_notification_centerget_notification_center(account=None)Working function of the module; check the return value and execution flow.
get_file_loaderget_file_loader(account=None)Working function of the module; check the return value and execution flow.
get_media_controllerget_media_controller()Working function of the module; check the return value and execution flow.
get_notifications_controllerget_notifications_controller(account=None)Working function of the module; check the return value and execution flow.
get_notifications_settingsget_notifications_settings(account=None)Working function of the module; check the return value and execution flow.
get_location_controllerget_location_controller(account=None)Working function of the module; check the return value and execution flow.
get_secret_chat_helperget_secret_chat_helper(account=None)Working function of the module; check the return value and execution flow.
get_download_controllerget_download_controller(account=None)Working function of the module; check the return value and execution flow.
get_last_fragmentget_last_fragment()Working function of the module; check the return value and execution flow.
observeobserve(notification_id, callback, account=None)Observe one NotificationCenter event and return a removable ObserverHandle.
_account_account(account)Working function of the module; check the return value and execution flow.

NotificationCenterDelegate

Subclass and override didReceivedNotification(id, account, args).

Methods

MethodSignatureContract
didReceivedNotificationdidReceivedNotification(self, notification_id, account, args)Check argument types and release created resources when unloading the plugin.

ObserverHandle

Public class DevGram SDK.

Methods

MethodSignatureContract
__init____init__(self, center, delegate, notification_id)Check argument types and release created resources when unloading the plugin.
closeclose(self)Check argument types and release created resources when unloading the plugin.

Import example

from client_utils import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · dev_server

Dev Server

Detailed help on the module dev_server, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Functions

NameSignaturePurpose
_connect_connect(platform, host, port, wait)Working function of the module; check the return value and execution flow.
start_debuggerstart_debugger(platform='vscode', host='127.0.0.1', port=5678, wait=False)Connect to a debugger exposed by the computer through adb reverse.
stop_debuggerstop_debugger()Working function of the module; check the return value and execution flow.
debugger_statusdebugger_status()Working function of the module; check the return value and execution flow.

Import example

from dev_server import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · file_utils

File Utils

Detailed help on the module file_utils, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Functions

NameSignaturePurpose
ensure_dir_existsensure_dir_exists(path)Working function of the module; check the return value and execution flow.
list_dirlist_dir(path, extensions=None, recursive=False, include_files=True, include_dirs=False)Working function of the module; check the return value and execution flow.
read_fileread_file(path)Working function of the module; check the return value and execution flow.
write_filewrite_file(path, content)Working function of the module; check the return value and execution flow.
read_file_bytesread_file_bytes(path)Working function of the module; check the return value and execution flow.
write_file_byteswrite_file_bytes(path, content)Working function of the module; check the return value and execution flow.
delete_filedelete_file(path)Working function of the module; check the return value and execution flow.

Import example

from file_utils import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · hook_utils

Hook Utils

Detailed help on the module hook_utils, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Functions

NameSignaturePurpose
find_classfind_class(class_name)Working function of the module; check the return value and execution flow.
_field_field(clazz, name)Working function of the module; check the return value and execution flow.
get_private_fieldget_private_field(obj, name)Working function of the module; check the return value and execution flow.
set_private_fieldset_private_field(obj, name, value)Working function of the module; check the return value and execution flow.
get_static_private_fieldget_static_private_field(clazz, name)Working function of the module; check the return value and execution flow.
set_static_private_fieldset_static_private_field(clazz, name, value)Working function of the module; check the return value and execution flow.

Import example

from hook_utils import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · intents

Intents

Detailed help on the module intents, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Functions

NameSignaturePurpose
registerregister(callback, *, action=None, scheme=None, host=None, path=None, query=None, category=None, flags=0, priority=0)Register an incoming-intent handler. Return True from callback to consume it.
_matches_matches(entry, context)Working function of the module; check the return value and execution flow.
dispatchdispatch(intent)Working function of the module; check the return value and execution flow.
open_uriopen_uri(uri)Open a URI using Android's resolver.
sendsend(intent)Launch an explicitly constructed Android Intent.

IntentContext

Public class DevGram SDK.

Methods

MethodSignatureContract
__init____init__(self, intent)Check argument types and release created resources when unloading the plugin.

HandlerHandle

Public class DevGram SDK.

Methods

MethodSignatureContract
__init____init__(self, entry)Check argument types and release created resources when unloading the plugin.
unhandleunhandle(self)Check argument types and release created resources when unloading the plugin.

Import example

from intents import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · text_formatting

Text Formatting

Detailed help on the module text_formatting, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Functions

NameSignaturePurpose
to_tlrpc_entitiesto_tlrpc_entities(entities)Convert Entity objects/dicts to Java ArrayList<TLRPC.MessageEntity>.
add_surrogatesadd_surrogates(text)Working function of the module; check the return value and execution flow.
remove_surrogatesremove_surrogates(text)Working function of the module; check the return value and execution flow.
utf16_lengthutf16_length(text)Working function of the module; check the return value and execution flow.
parse_htmlparse_html(text)Working function of the module; check the return value and execution flow.
parse_markdownparse_markdown(text)Working function of the module; check the return value and execution flow.
_python_index_for_utf16_python_index_for_utf16(text, offset)Working function of the module; check the return value and execution flow.
parse_textparse_text(text, parse_mode=None)Working function of the module; check the return value and execution flow.
escape_markdownescape_markdown(text)Working function of the module; check the return value and execution flow.
escape_htmlescape_html(text)Working function of the module; check the return value and execution flow.

Entity

Public class DevGram SDK.

_HTMLToEntities

Public class DevGram SDK.

Methods

MethodSignatureContract
__init____init__(self)Check argument types and release created resources when unloading the plugin.
lengthlength(self)Check argument types and release created resources when unloading the plugin.
handle_datahandle_data(self, data)Check argument types and release created resources when unloading the plugin.
handle_starttaghandle_starttag(self, tag, attrs)Check argument types and release created resources when unloading the plugin.
handle_endtaghandle_endtag(self, tag)Check argument types and release created resources when unloading the plugin.

Import example

from text_formatting import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · ui.alert

Ui.Alert

Detailed help on the module ui.alert, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

AlertDialogBuilder

Public class DevGram SDK.

Methods

MethodSignatureContract
__init____init__(self, context=None, progress_style=0, resources_provider=None)Check argument types and release created resources when unloading the plugin.
set_titleset_title(self, value)Check argument types and release created resources when unloading the plugin.
set_messageset_message(self, value)Check argument types and release created resources when unloading the plugin.
set_positive_buttonset_positive_button(self, text, listener=None)Check argument types and release created resources when unloading the plugin.
set_negative_buttonset_negative_button(self, text, listener=None)Check argument types and release created resources when unloading the plugin.
set_neutral_buttonset_neutral_button(self, text, listener=None)Check argument types and release created resources when unloading the plugin.
createcreate(self)Check argument types and release created resources when unloading the plugin.
showshow(self)Check argument types and release created resources when unloading the plugin.
dismissdismiss(self)Check argument types and release created resources when unloading the plugin.

Import example

from ui.alert import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · ui.bulletin

Ui.Bulletin

Detailed help on the module ui.bulletin, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

BulletinHelper

Public class DevGram SDK.

Methods

MethodSignatureContract
_show_show(kind, message, duration, button, callback)Check argument types and release created resources when unloading the plugin.
show_infoshow_info(cls, message, fragment=None, duration=DURATION_LONG, button=None, callback=None)Check argument types and release created resources when unloading the plugin.
show_successshow_success(cls, message, fragment=None, duration=DURATION_SHORT, button=None, callback=None)Check argument types and release created resources when unloading the plugin.
show_errorshow_error(cls, message, fragment=None, duration=DURATION_LONG, button=None, callback=None)Check argument types and release created resources when unloading the plugin.
showshow(cls, message, kind='info', duration=DURATION_LONG, button=None, callback=None, fragment=None)Check argument types and release created resources when unloading the plugin.

Import example

from ui.bulletin import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Verified API · ui.settings

Ui.Settings

Detailed help on the module ui.settings, verified with the sources of the current DevGram runtime.

Compatibility

The following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.

Setting

Public class DevGram SDK.

Methods

MethodSignatureContract
__init____init__(self, key='', text='', default=None, on_change=None, items=None, **kwargs)Check argument types and release created resources when unloading the plugin.
as_rowas_row(self)Check argument types and release created resources when unloading the plugin.

Header

Public class DevGram SDK.

Methods

MethodSignatureContract
__init____init__(self, text='', **kwargs)Check argument types and release created resources when unloading the plugin.

Switch

Public class DevGram SDK.

Input

Public class DevGram SDK.

Selector

Public class DevGram SDK.

Text

Public class DevGram SDK.

Button

Public class DevGram SDK.

Import example

from ui.settings import *

# Выполняйте файловые и сетевые операции вне UI-потока.
# Все handles и observers закрывайте в on_unload().

Errors and diagnostics

Wrap the call in try/except, record the context via devgram.android_utils.log() and show the user a short message via bulletin. Do not send stack traces to chat and do not leave secrets in logs.

How to read signatures

Parameters with default do not need to be passed. For Java objects check None before accessing the fields. Python exceptions mean a local preparation error, and Telegram error comes as a separate callback argument.

Flow of execution

Don't run network, large files, JSON, or heavy calculations during import or on the UI thread. Use run_on_queue(), and return the result via run_on_ui_thread(). After unloading, the callback must check that the instance is still active.

Resource life cycle

Any registration object belongs to the calling plugin. Save it in the field, close it in on_unload() and reset the link. Cleanup should work after partially completed on_load() and when called again.

Error Handling

def safe_call(self, operation):
    try:
        return operation()
    except Exception as error:
        self.log("DevGram operation failed: " + repr(error))
        self.bulletin("Операция недоступна", kind="error")
        return None

The user message explains the next action, but does not reveal the traceback. Leave the details in the debug log without tokens or personal data.

Compatibility

Public helper is preferable to Java lookup. For optional APIs, use feature detection and disable only the dependent feature. Don't rely on layout and View indexes that change between versions.

Examination

  1. Default and explicit account.
  2. Empty, long and Unicode values.
  3. Call again after reload.
  4. Call after closing the screen.
  5. Network, file and RPC error.
Developer guide

Plugin architecture

How to divide a plugin into lifecycle, services, event handlers, storage and interface so that reload and updates are predictable.

Limits of responsibility

Entry point should remain small: it links the lifecycle and delegates work to individual objects. Don't put network client, parser, cache and UI in one class. Event handlers must quickly check the condition and leave the hard work to the service.

class Plugin(BasePlugin):
    def on_load(self):
        self.storage = Storage(self)
        self.service = MessageService(self.storage)
        self.routes = RouteRegistry(self)
        self.routes.start()

    def on_unload(self):
        self.routes.close()
        self.service.close()

State

Separate persistent state, session cache and references to Android objects. Persistent data is recorded in private storage; The cache can be lost when reloading; View, Fragment and Activity cannot be saved between screens. Get the current Fragment just before showing the UI.

Dependencies

Pass services through the constructor. This makes the code testable and eliminates hidden global singletons. Circular dependencies usually mean that the overall contract needs to be put into a separate module.

Closing

Every object that registers a listener, observer, hook, intent route, or pill must have close(). The closure is executed in the reverse order of creation and can be called again.

Developer guide

.plugin format

The simplest plugin format is one file with code. Ideal for small plugins without resources or dependencies.

What is this

.plugin (or .py) is a regular Python file with a descendant class BasePlugin and the line plugin = MyPlugin() at the end. None manifest.json and archive: metadata (id, name, version, author, description, icon) are taken directly from the class attributes. The code is executed by built-in Python 3.11 inside the client - without root and without external Xposed.

Minimal example

from devgram import BasePlugin

class Hello(BasePlugin):
    id = "hello.devgram"
    name = "Hello"
    version = "1.0.0"
    author = "@you"
    description = "Мой первый плагин"
    icon = "https://example.com/plugin-icon.png"

    def on_load(self):
        self.toast("Hello from DevGram")

plugin = Hello()

Save the file with the extension .plugin (eg. hello.plugin). Any text editor will do; Python standard library is available, third party packages are not.

Installation and distribution

  1. Send a file .plugin to chat or channel.
  2. Tap on the file → installation card (icon, name, version, author, verification icon) → “Install”.
  3. Management: Settings → DevGram → Plugins (toggle switches, settings, “Share”, deletion).

Plugins from the developer channel with the 🧩 icon are considered automatically verified. The installation card is the same for .plugin And .dgplugin.

.plugin or .dgplugin?

  • .plugin — one file, without resources and dependencies. Just write and send. Suitable for most plugins.
  • .dgplugin - archive with manifest.json, modules, assets/, locales/ And wheels/. Needed when there is localization, images, several modules or pip dependencies (see the next section).
Developer guide

.dgplugin format

The complete structure of the package to be installed, naming rules and checking the contents before publication. For a simple one file plugin use .plugin (section above).

Archive root

plugin.dgplugin
├── manifest.json
├── main.py
├── modules/
├── assets/
├── locales/
└── wheels/

.dgplugin is an archive, but the extension cannot be changed: DevGram recognizes it by the installer. Manifest and entrypoint should be in the root, and not in a subfolder that appears after archiving the directory.

Identifier

Use reverse-domain or username namespace: space.devgram.example. The ID is not a display name and does not change after the first release. Changing the ID will create a new set of settings and will be perceived by the directory as a different plugin.

Resources

File names are case sensitive. Use ASCII, hyphens or underscores. Do not include the original PSD, APK, keystore, tokens or temporary files. Optimize large images in advance.

Checking the archive

  1. Unpack the package into a temporary directory.
  2. Check manifest and import entrypoint.
  3. Make sure that wheels are compatible with the built-in Python version and ABI.
  4. Check for absence of absolute paths and traversal entries.
  5. Install the package over the previous version and separately on a clean installation.
Developer guide

Threads and Performance

Rules for working with UI thread, background queue, network, files and frequent Telegram callbacks.

UI thread

Short changes to the interface are allowed on the UI thread: creating a bulletin, updating the View, and showing a dialog. Directory reading, JSON parsing, HTTP, hashing and large message processing should be done via run_on_queue().

Return result

def load(self):
    run_on_queue(self._load_background)

def _load_background(self):
    try:
        data = read_and_parse()
    except Exception as error:
        run_on_ui_thread(lambda: self.bulletin("Ошибка загрузки", kind="error"))
        return
    run_on_ui_thread(lambda: self.render(data))

Frequent callbacks

The message sending hook can be called frequently, and UI callbacks can be called several times per frame. Do not perform reflection lookup or disk access in them. Cache immutable class references, apply debounce to lookups, and coalesce to a series of identical updates.

Cancel

Background callback may complete after the plugin is unloaded. Store a generation token or activity flag and check it before updating the UI. Don't keep strong links to a closed Fragment.

Measurement

Log time only around the suspicious block. Remove detailed timing logs before release. If the callback takes more than one frame, move the work out of the UI thread.

Developer guide

Data and Migrations

How to store plugin settings and files, survive updates and recover from a corrupted state.

Settings

get_setting() And set_setting() suitable for small scalar values. Convert booleans and numbers explicitly because bridge can return a string representation. For structured data, use a JSON file in private storage.

Schema version

def migrate(self):
    version = int(self.get_setting("schema", "0"))
    if version < 1:
        self.set_setting("enabled", "1")
        version = 1
    if version < 2:
        migrate_cache_file(self.storage_path("state.json"))
        version = 2
    self.set_setting("schema", str(version))

Migrations are performed sequentially and must be idempotent. Do not write a new schema version until the step has completed successfully.

Atomic write

First write new JSON to a temporary file, sync and replace the main file. If there is a read error, save the damaged file for diagnostics and run with safe defaults.

Secrets

The package and private storage are not protected vault. Do not put bot tokens and persistent keys in the plugin. If the service requires authorization, use short-lived user tokens and the ability to revoke them.

Developer guide

TL API and requests

Creating Telegram TL objects, sending requests and handling errors without being tied to the selected account.

Create a request

Get class via self.tl() or Java bridge, create an object and fill in the required fields. Check the type of each field with the current Telegram schema.

request = self.tl("TL_messages_getHistory")
request.peer = input_peer
request.offset_id = 0
request.offset_date = 0
request.add_offset = 0
request.limit = 20
request.max_id = 0
request.min_id = 0
request.hash = 0
self.send_request(request, self._result, account=account)

Callback

Callback receives response and error. Telegram RPC error is not a Python exception, so check error first. Do not access response fields until you have checked its type.

Flood wait

Do not repeat the request immediately. Show a clear message or queue the operation with a specified delay. Automatic loops must have a page limit and a backoff.

Multi-account

InputPeer, controller and request must belong to the same account. You cannot get a peer from the cache of the first account and send it through the ConnectionsManager of the second.

Developer guide

Interface design

Native settings, bulletins, dialogs and interactive elements without Activity leaks or conflicts with the theme.

Component selection

Bulletin is suitable for a short action result and an optional button. Alert is used for confirmation or selection that cannot be made by chance. Permanent parameters are placed in settings rows. Don't show dialog for normal successful action.

Subject

Do not specify black or white text directly. Use theme keys or native DevGram components. Check day, night and system theme, larger font and long localization.

States

The screen must have loading, empty, error and content states. After an action, update the current model immediately, without forcing the user to close and open the screen. For a network, an optimistic update is only allowed if the rollback is correct.

Availability

Icon-only buttons must have a content description. Touch target should not become less than the system minimum. Don't just color code your status: add text or a familiar symbol.

Lifecycle

Before showing the UI, get the current Fragment and check for its presence. The callback of a button should not capture a closed Activity. After the plugin is unloaded, registered widgets are deleted.

Developer guide

Reliable Java hooks

Practice of low-level hooks and reflection, taking into account Telegram updates and the risk of process crashes.

When you need a hook

Use public events and the Client API first. Hook is justified if the required extension point is not available. It binds the plugin to a specific Java class implementation and requires stricter compatibility checking.

Search method

Check the full class name, parameter types, static/instance and return type. Overload cannot be selected by name alone. After updating the client, lookup may return a different method or fail.

Before and after

Before-hook can change the arguments or override the original; after-hook can replace the result. The returned object must be compatible with the Java type. Don't replace primitive with a value None.

Protection

def install(self):
    try:
        clazz = find_class("org.telegram.example.Target")
        self._hook = self.hook(clazz, "method", self._before)
    except Exception as error:
        self.log("hook unavailable: " + repr(error))
        self._hook = None

Unloading

Unhook should always be executed. Callback checks for plugin activity and does not hold View. If the hook is critical, declare a minimal version; if optional, disable only the related feature.

Developer guide

Testing the plugin

Matrix of checks before publication: clean installation, update, accounts, themes, lifecycle and error recovery.

Minimum matrix

ScenarioWhat to check
Clean installManifest, defaults, first launch, permissions.
UpdateMigrations, saving settings, replacing resources.
Disable/enableObservers are not duplicated, the UI disappears and comes back.
Hot reloadPython modules have been cleaned, hooks and handles have been replaced.
UninstallNo callbacks or registered pills.
Safe modeThe client starts after an intentional plugin error.

Accounts

Check at least two accounts, switching during a background operation, and an event from an unselected account. No operation should silently use the current UI account instead of the account event.

Interface

Check light and dark theme, small and large screen, system font scale, long Russian and English text, empty lists, offline and repeated request.

Negative cases

Corrupt the JSON, remove the asset, return an RPC error, close the screen before the callback completes, and call cleanup twice. The plugin should degrade locally without breaking the directory and application.

Developer guide

Publication and updates

Versioning, changelog, compatibility, archive verification and secure release to the DevGram directory.

Version

Use a sequential scheme and increment the version for each package downloaded. Do not publish different archives under the same version: users and moderators will not be able to unambiguously check the content.

Changelog

Write only user changes: new actions, changed behavior, bug fixes and important restrictions. Separately specify breaking changes and the required minimum client version.

Examination

  1. Build a package from a clean checkout.
  2. View the list of archive files.
  3. Scan tokens, keys and local paths.
  4. Install the release package on a clean copy of DevGram.
  5. Check the signature or checksum of the published file.

Resubmission

After rejection, correct the reason given and increase the version. Don't change the plugin ID. If the plugin uses dangerous hooks, describe the purpose and fallback for the incompatible version.

Rollback

Keep the previous stable package and rollback-compatible migrations whenever possible. Never delete user data just because a new version couldn't parse it.

Developer guide

Security model

The plugin runs inside the DevGram process and has access to sensitive context, so security starts with minimal permissions and transparent behavior.

Trusted Boundary

Python sandbox does not make unknown code safe. The plugin can access the Java bridge and client data. Install packages only from a clear source and show the user why the network, files, or message interception are needed.

Deleted data

Any data from Telegram, Intent, HTTP and files are considered untrusted. Limit sizes, validate JSON schema, normalize paths, and escape markup. Do not pass a custom string to reflection lookup.

Net

Use HTTPS, timeouts and response limiting. Do not disable certificate verification. Do not send messages, user IDs or tokens to analytics without an explicit purpose.

Logs

Do not log access tokens, cookies, full text of private messages and file contents. For diagnostics, use the short request ID, operation type and safe error code.

Fault tolerance

An error in one function disables it locally. Do not create an endless retry and do not fall into the callback of the main thread. Safe mode should allow the user to remove the problematic package.

Developer guide

Compatibility and versions

How to support multiple versions of DevGram and Telegram core without random crashes due to missing methods.

Stability levels

Public functions devgram.* are the preferred level. Java controllers change along with Telegram. Private reflection and method hooks are the most fragile level and require a version gate.

Feature detection

Check for the presence of a class, method or attribute, not just the version string. The version is useful for a clear message, but different builds may have a different set of backports.

try:
    target = find_class(CLASS_NAME)
except Exception:
    target = None

if target is None:
    self.bulletin("Функция недоступна в этой версии")

Graceful fallback

Optional integrations are disabled separately, but other features continue to work. If the plugin is meaningless without an API, stop loading with clear diagnostics and the minimum supported version.

Deprecated API

Once a replacement appears, maintain the old path for a limited time. Do not remove a setting or change its meaning without migration. Indicate deprecated methods in documentation and changelog.

Developer guide

Diagnostics and debugging

A systematic approach to import errors, blank screens, freezes, double callbacks and crashes after updating.

Import

First check the full traceback and the first frame inside the plugin. Typical reasons: the file is missing from the archive, the case of the name does not match, wheel is incompatible, or a circular import has occurred.

Empty interface

Separate data loading and render. Show loading before the operation begins, then content, empty or error. If the data has arrived and the screen is empty, log the generation of the screen and check that the callback has returned to the UI thread.

Double action

Almost always there remains an observer, hook or listener from the previous reload. Add registration log and cleanup with instance ID. Make sure on_unload() closes each handle.

Freeze

Remove the callback execution time and find the I/O on the main thread. Long-pressing, scrolling, or highlighting will often call hook multiple times; there should be no hard work inside it.

Crash after closing the screen

The Background callback holds the old Context or View. Before rendering, get the current Fragment, check the activity and cancel the update if the generation has changed.

Bug Report

Specify the DevGram version, Android SDK, device model, plugin ID/version, sequence of actions and full stack trace. Delete secrets and personal messages.

Developer guide

Glossary

Key terms of DevGram Plugin SDK and differences between similar objects.

Account index

Local account number in the application. Not equal to Telegram user ID and is used to select controller.

Dialog ID

A conversation ID that can represent a user, group, or channel. Don't treat the number sign as your only type check.

TL object

Java type representation from Telegram schema. Fields and subclasses depend on the current version of core.

Observer

Subscribe to NotificationCenter. The returned handle belongs to the plugin and is closed upon unload.

Hook

Intercepting a Java method before or after the original call. It differs from the public plugin event in that it is more strictly dependent on the implementation.

Entity

Telegram text formatting range. Offset and length are measured in UTF-16 code units.

Safe mode

Recovery mode, in which problematic callbacks are not launched until manual action by the user.

Hot reload

Unloading instance and Python modules and then loading a new version without completely restarting the client.

Pill

A compact interactive Pill Stack element registered with a specific plugin ID.

Builder

Experimental DevGram-native specification of a high-level project structure. Not all of the described facades are included in the stable runtime.

Reference index

Deep reference

Navigation page for the complete DevGram SDK reference.

Stable Python API

Start with SDK contracts, then open the page for the desired verified module. It lists the actual classes, functions, signatures, and methods of the current assembly.

How-To Guides

For a large plugin, use the pages about architecture, flows, migrations, testing, and security.

Low-level runtime

Java hooks, reflection, class proxy and TL API depend on Telegram core. Check for API availability and provide fallback.

Experimental Builder

Builder describes the target structure of the project. For executable code, verified modules remain the source of truth.

BasePlugin: contract and state

BasePlugin created by the loader once per package. Fields id, name, version, author, description And icon used by the directory and plugin manager. Don't change id after publication: it is the key for settings, private storage and registered hooks.

MethodCallContract
on_load()After importRegistration hooks, observers, pills. Don't block the UI.
on_unload()Before reload/removeRemove everything created by the plugin. Recall is acceptable.
on_send_message(text)Before outgoing textReturn new text, None without change or False to cancel.
on_receive_message(text)After incoming textObservation and logic; message change is not guaranteed.
on_update(update)TL updateJava object TLRPC.Update. Check the type via getClass().getSimpleName().
on_setting_changed(key,value)From native settingsThe value comes as a string; convert explicitly.

HookResult and strategy

For account-aware callback you can return HookResult. PASS leaves original behavior, REPLACE replaces the result, and the passed parameters replace the arguments before calling the original. Don't return a random Python object: the type must match the Java method.

from devgram import HookResult, HookStrategy

def on_send_message_hook(self, account, params):
    if self.get_setting("block_links", "0") == "1":
        return HookResult(HookStrategy.CANCEL)
    return HookResult(HookStrategy.PASS)

client_utils: account and controllers

All functions accept optional account. If it is not transmitted, the selected account is used. For events, always pass account manually.

FunctionReturnsWhen to use
send_text(peer,text,account)NoneSimple text.
send_formatted_text(peer,text,entities,account)NoneText with native MessageEntity.
get_messages_controller(account)MessagesControllerDialogues, users and chats.
get_connections_manager(account)ConnectionsManagerLow-level TL queries.
get_messages_storage(account)MessagesStorageReading the local cache. Don't block main.
get_media_data_controller(account)MediaDataControllerMedia and stickers.
get_contacts_controller(account)ContactsControllerContacts and username.
get_file_loader(account)FileLoaderLoading local media files.
get_download_controller(account)DownloadControllerDownload queue.

Request callbacks

request = self.tl("TL_messages_getHistory")
request.peer = peer
request.limit = 20

def result(response, error):
    if error:
        self.log("Telegram error: " + str(error))
        return
    self.run_on_ui(lambda: self.bulletin("История получена"))

self.send_request(request, result)

NotificationCenter

observe() returns ObserverHandle. Store handle in an object field, close it in on_unload(). Callback receives a numerical notification ID, account and a Java array of arguments.

text_formatting

Entity has fields type, offset, length, and for links and pre - url And language. Offset and length are always UTF-16, not Python code points. This is fundamental for emoji.

from devgram.text_formatting import Entity, utf16_length

text = "🔥 DevGram"
entity = Entity("bold", 0, utf16_length(text))
send_formatted_text(peer, text, [entity], account=account)

Settings rows

ClasskindFields
Header(text)headerSection title.
Switch(key,text,default,on_change)switchBoolean is stored as 1/0.
Input(key,text,default,on_change)textString value.
Selector(key,text,items,on_change)selectorList of options via separator.
Button(key,text)buttonCalling on_setting_click.

android_utils

run_on_ui_thread(fn,delay) puts a callback in the Telegram UI queue. run_on_queue(fn) uses global background queue. copy_to_clipboard() and bulletin should be called after returning to the UI.

file_utils and private storage

Global file helpers work with the absolute path passed in. Methods self.read_file() And self.write_file() work only inside the private folder of the current plugin ID. Don't use path from user input without validation.

intents

register() sorts handlers by priority. Callback should return Trueif the Intent is processed; otherwise dispatch will continue searching. HandlerHandle.unhandle() returns False if handle has already been removed.

Java class proxy

java_class(base,logic,arg_types,args) creates DexMaker subclass. Names of callable methods in logic become overrides. Inside override use BasePlugin.java_super(). The class must not be final and must have a compatible constructor.

Publication checklist

  1. Manifest is valid, ID is stable, version has been increased.
  2. No bot tokens, Firebase keys, signing keys and debug logs.
  3. Verified Android 7+ and at least two accounts.
  4. Install, update, disable, enable, reload and uninstall have been checked.
  5. After a crash, safe mode restores the application.
  6. Observer, hook, intent and pill are cleared in unload.
Extended reference

Advanced reference

Materials from the old documentation have been transferred to a modern structure: practical Android scripts, CallFrame, visual effects, chat folders and ways to explore Telegram classes.

CallFrame object

A CallFrame represents a specific intercepted Java method call. It provides a receiver, an array of arguments, and control over the result. Before changing the argument, check the index, Java type and nullability. For overload, match the full method signature first, otherwise the hook might apply to a different implementation.

def before(frame):
    args = frame.args
    if args and args[0] is not None:
        args[0] = str(args[0]).strip()

def after(frame):
    result = frame.result
    if result is None:
        return
    # Подмена допустима только совместимым Java-типом.

Types of hook arguments

Primitive types require an exact boolean/int/long-compatible value. Java String can be created from Python str via bridge, and complex Telegram objects cannot be replaced with a dictionary or arbitrary Python class. For a nullable parameter None only valid if the Java contract actually allows null.

Substitution of argument and result

Change the screen title before the original method, and process the returned object after. Do not override premium flags, authorization state, or server restrictions: a local value does not grant server rights and may break the interface.

Useful Telegram classes

RegionExamplesPurpose
ScreensLaunchActivity, BaseFragment, ChatActivityNavigation and current context.
CellsDialog, message and settings cellsDrawing list strings; Depends heavily on the version.
DataMessagesController, MessagesStorageCache, users, chats and messages.
NetConnectionsManagerTL requests and connection state.

How to examine a class and method

  1. Find a custom action that triggers the desired behavior.
  2. Find the corresponding Java class in the sources of the current version.
  3. Check overload, parameter types and return type.
  4. Install a temporary diagnostic hook without changing the behavior.
  5. Remove verbose logging after signature confirmation.

Do not transfer private field names from the old APK without checking: Telegram regularly changes its class structure.

Visual effects

Blur, glass panel, tint and border should take into account the theme, device performance and screen lifecycle. The effect is created after the View appears, and the resources are released when detached. On a weak GPU, reduce the blur radius and disable frame-by-frame updating.

Floating card

panel = self.glass_panel(
    source_view,
    corner=20,
    blur=14,
    tint=0x22FFFFFF,
    border=0.5,
)
# Добавьте panel в контейнер экрана на UI thread.
# Удалите panel при закрытии Fragment.

GPU shader AGSL

RuntimeShader is not available on all Android APIs. Check the SDK, shader source compilation and provide the usual drawable fallback. Don't pass custom text to the shader source and don't create a new shader every frame.

Chat folders and tabs

Cyclic swipe of folders requires knowledge of the currently selected filter ID and the number of available tabs. Do not calculate the next tab by View position: the order may change after synchronization. Store the ID, not the index.

def next_filter(filters, current_id):
    ids = [item.id for item in filters]
    if not ids:
        return None
    try:
        index = ids.index(current_id)
    except ValueError:
        return ids[0]
    return ids[(index + 1) % len(ids)]

Chat screen: background and overlays

Overlay should not intercept touch events, close system insets, or hold ChatActivity after exit. Add the View to the correct container, respect the keyboard, and remove it when destroying.

Videophone MP4

For a looping video, use a system player with muted audio, surface lifecycle and stop when the application goes into the background. Don't manually decode videos in Python. Add static fallback, resolution limit and shutdown setting to save battery.

TL request hooks

The Request hook can observe the request name and object before sending. Do not store the full request with private data. Changing peer, random_id or authorization fields can lead to duplicates and server errors. For analytics, a query type and a safe result are enough.

Java interfaces from Python

For listener, use ready-made proxy helpers or class proxy with the exact method signature. The Python callback reference should live as long as the Java listener. When unloaded, remove the listener and clear both sides of the link.

Full production checklist

  1. Low-level functions have feature detection.
  2. Every hook and listener is removed.
  3. View works in both themes and does not overlap input.
  4. Videos and shaders have fallback.
  5. Two accounts do not mix controllers.
  6. Logs do not contain messages or tokens.
  7. The plugin survives reload and safe mode.