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

DevGramBuilder

The official tool for creating a project, checking sources and building the installable .dgplugin.

Installation

python -m pip install --upgrade "git+https://github.com/DevGramOfficial/DevGramBuilder.git"
dgb --version

On Windows you can use py -3. If Linux prohibits system installation, create a virtualenv or use pipx.

First build

mkdir my-plugin
cd my-plugin
dgb new
dgb build -a -v -nf

The finished file will appear in builds/<id>-<version>.dgplugin. It can be opened in DevGram or uploaded through Dev Server.

Creation

dgb new creates a working structure and example.

Validation

-a checks Python and JSON locales.

Build

A package with open .py sources.

Device

dgb upload installs the package through Dev Server.

DevGramBuilder Repository · Latest release

DevGram Builder

Quick start

From an empty folder to a finished file in a few commands.

1. Install Builder

# Windows
py -3 -m pip install --upgrade "git+https://github.com/DevGramOfficial/DevGramBuilder.git"

# Linux / macOS
python3 -m pip install --upgrade "git+https://github.com/DevGramOfficial/DevGramBuilder.git"

2. Create a project

mkdir hello-devgram
cd hello-devgram
dgb new

Builder will ask for the title, author, ID, version, description and link to the icon. A non-interactive mode is available for CI:

dgb new . --gen   --name "Hello DevGram"   --author "@username"   --id "username.hello"   --plugin-version "1.0.0"   --description "Первый плагин"

3. Collect

dgb build -a -v -nf

-a includes syntax checking, -v - detailed output, -nf excludes the Builder service folder from the release.

4. Install

Send a file from builds/ in Telegram, open it in DevGram and click “Install”. For fast development use dgb upload.

DevGram Builder

Project structure

The original project and the finished archive have a predictable structure.

Project

my-plugin/
├── devgram-builder.json
├── .devgrambuilder/config.json
├── src/
│   ├── main.py
│   └── helpers.py
├── assets/
├── locales/
│   ├── en.json
│   └── ru.json
├── wheels/
└── builds/

Ready .dgplugin

manifest.json
main.py
helpers.py
assets/...
locales/en.json
locales/ru.json
wheels/...

Files from src/ go to the root of the archive. Builder creates manifest from devgram-builder.json. Git, IDE files, builds/, Python cache and logs are excluded automatically.

.devgrambuilder/config.json

{
  "source": "src",
  "ignoreAll": [".git/**", "builds/**", "**/__pycache__/**"],
  "optionalAssets": []
}
FieldPurpose
sourcePython source directory.
ignoreAllFiles which are never included.
optionalAssetsExcluded by --no-assets.
DevGram Builder

CLI Reference

DevGramBuilder 1.4 commands.

CommandPurpose
dgb new [directory]Create a project.
dgb buildBuild a source-only .dgplugin.
dgb watch secondsRebuild when files change.
dgb add-ignore / del-ignoreConfigure exclusions.
dgb statsShow build, file, line and size statistics.
dgb uploadInstall through Dev Server.

Build flags

FlagMeaning
-a, --astValidate Python and locales.
-vVerbose output.
-nfDo not include the service config.
-niDo not record Builder information.
--no-assetsExclude optionalAssets.
-o PATHOutput file.

Ignore

dgb add-ignore "assets/source.psd" --all
dgb add-ignore "assets/large/**" --no-assets
dgb del-ignore 0 --all

Statistics

dgb stats builds
dgb stats files
dgb stats lines
dgb stats size

Full help: dgb --help And dgb build --help.

DevGram Builder

Project metadata

devgram-builder.json converted to batch manifest.json.

{
  "id": "username.my_plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "author": "@username",
  "description": "Что делает плагин",
  "icon": "https://example.com/icon.png",
  "main": "main.py",
  "min_app_version": "12.10.3",
  "min_devgram": "3",
  "requirements": []
}
FieldRule
idStable ID up to 64 characters: letters, numbers, ., _, -.
name, version, authorRequired for Builder.
mainEntry point relative src/; usually main.py.
description, iconDescription and direct HTTPS link to the image.
min_app_versionMinimum version of DevGram.
min_devgramMinimum Plugin API level; current - 3.
requirementsUp to 32 dependency names from wheels/.
Do not change ID after publication

Another ID is treated as a new plugin and receives separate settings.

DevGram Builder

Modules and Imports

The project shares regular Python files inside src/.

# src/helpers.py
def greeting(name):
    return f"Привет, {name}!"

# src/main.py
from devgram import BasePlugin
from .helpers import greeting

class HelloPlugin(BasePlugin):
    id = "username.hello"
    name = "Hello"
    version = "1.0.0"
    author = "@username"

    def on_load(self):
        self.bulletin(greeting(self.name), kind="success")

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

The loader itself will find the successor class BasePlugin and will create an instance. Line plugin = HelloPlugin() not required. For subfolders add __init__.py and use relative imports.

Lifecycle

Create listeners, hooks, observers and pills in on_load(), and close in on_unload().

DevGram Builder

Resources and localization

Builder transfers assets/ And locales/ while maintaining the structure.

Assets

# assets/icons/cat.png
path = self.asset_path("icons/cat.png")

asset_path() returns a safe absolute path only to the existing file inside assets/. The case of the name is important.

Localization

# locales/ru.json
{"welcome": "Привет, {name}!"}

text = self.string("welcome", "Hello!", name="FireDragoq")

Optional Resources

Add templates to optionalAssets dgb build --no-assets.

DevGram Builder

Plugin settings

Builder packages the code, and the settings screen is created by the public DevGram Plugin SDK.

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

class Plugin(BasePlugin):
    id = "username.settings"

    def settings(self):
        return [
            Header(text="Основное"),
            Switch(key="enabled", text="Включено", default=True),
            Input(key="name", text="Имя", default="DevGram"),
            Selector(key="style", text="Стиль", items=["system", "compact"]),
            Button(key="test", text="Проверить"),
            Text(text="Версия 1.0.0"),
        ]

    def on_setting_changed(self, key, value):
        self.log(f"{key}={value}")

    def on_setting_click(self, key):
        if key == "test":
            self.bulletin("Работает", kind="success")

There is no separate runtime API Builder: components and callbacks are taken from devgram.ui.

DevGram Builder

Dependencies

Dependencies are supplied within the package: DevGram does not download anything during installation.

  1. Download compatible .whl.
  2. wheels/.
  3. Add distribution name to requirements.
# devgram-builder.json
"requirements": ["requests"]

# файл
wheels/requests-2.32.3-py3-none-any.whl

Builder checks that names and files match. A maximum of 32 dependencies are allowed.

Compatibility

Prefer py3-none-any. Wheel with native code must be built for Android, device ABI and Python 3.11.

Python Standard Library and devgram.* are not added to requirements.

DevGram Builder

Build and development

Source validation, watch mode and reproducible builds.

Build

dgb build -a -v -nf

-a validates Python through AST and checks JSON locales. Every .py file stays in the package; .pyc and .pyo are unsupported.

Watch

dgb watch 2 --args "-a -v -nf"

Changes are checked every two seconds.

Custom output path

dgb build -a -o dist/plugin.dgplugin
DevGram Builder

Builder troubleshooting

CLI installation, configuration, validation and upload.

dgb not found

Run python -m devgram_builder --version. If the module works, add Python Scripts to PATH. On Windows use py -3 -m devgram_builder.

externally-managed-environment

python3 -m venv .venv
source .venv/bin/activate
python -m pip install "git+https://github.com/DevGramOfficial/DevGramBuilder.git"

devgram-builder.json not found

Run the command inside the project. For a new project run dgb new.

Entry point not found

main is relative to src/. Check letter case and ignore rules.

Package contains .pyc

DevGram accepts only open .py sources. Rebuild the project with the current Builder.

Missing dependency wheel

The name in requirements must match a file in wheels/.

Dev Server returned an error

Check ADB, developer mode, port and the current token. invalid package means the package structure is invalid.

Getting started

Preparation

An environment for developing, building and quickly installing plugins.

Install

  • the current DevGram release;
  • Python 3.10 or newer for Builder;
  • ADB for Dev Server;
  • an editor with Python support.

DevGramBuilder

python -m pip install --upgrade "git+https://github.com/DevGramOfficial/DevGramBuilder.git"
dgb --version

Developer Mode

adb devices
adb forward tcp:42690 tcp:42690
Safety

Getting started

First .dgplugin

Create a project, modify the example and get an installable package.

mkdir hello-devgram
cd hello-devgram
dgb new

Open src/main.py. Minimum entry point:

from devgram import BasePlugin

class Hello(BasePlugin):
    id = "username.hello"
    name = "Hello DevGram"
    version = "1.0.0"
    author = "@username"
    description = "Мой первый плагин"

    def on_load(self):
        self.bulletin("Плагин загружен", kind="success")

    def on_unload(self):
        pass

Enter the same values ​​in devgram-builder.json

Assembly

dgb build -a -v -nf

Open the finished file from builds/ in DevGram. After installation, check to enable, disable, re-download and uninstall.

Package .dgplugin

manifest.json

Manifest is located strictly at the root of the archive and describes the package before running the code.

{
  "id": "username.my_plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "author": "@username",
  "description": "Описание плагина",
  "icon": "https://example.com/icon.png",
  "main": "main.py",
  "min_app_version": "12.10.3",
  "min_devgram": "3",
  "requirements": []
}
FieldPurpose
idMandatory ID up to 64 characters. Letters, numbers, periods, hyphens and underscores are allowed.
main main.py.
name, version, authorCard and catalog metadata.
description, iconDescription and HTTPS icon.
min_app_versionMinimum application version.
min_devgram / min_sdkMinimum Plugin API level.
requirementsNames of dependencies present in wheels/.
Correct field name

Used main entrypoint. DevGramBuilder generates manifest automatically.

Priority

id, name, version, author, description And icon from manifest replace class attributes.

Package .dgplugin

Plugin icon

The icon is displayed in the installation card, manager and directory.

In the Builder project

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

Add icon V devgram-builder.json; Builder will transfer the link to manifest.

Requirements

  • direct HTTPS link to the image, not the HTML page;
  • PNG or JPG, preferably square;
  • contrast on light and dark theme.

If there is no loading or an error occurs, DevGram displays a standard icon.

Package .dgplugin

Assets and localization

The resources are extracted along with the package into the plugin's private directory.

Assets

assets/background.png
assets/icons/action.png

background = self.asset_path("background.png")
action_icon = self.asset_path("icons/action.png")

The method returns an empty string if the file is missing or the path is out of bounds assets/.

Locales

# locales/en.json
{"welcome": "Hello, {name}!"}

# locales/ru.json
{"welcome": "Привет, {name}!"}

text = self.string("welcome", "Hello!", name="FireDragoq")

English is used as fallback, after which the current language strings are applied. Keys and values ​​must be strings.

Changeable data

assets/ intended for delivery files. Store user data via plugin_files_dir(), write_file() And read_file().

Package .dgplugin

Wheel dependencies

External libraries are provided inside the package.

my-plugin.dgplugin/
├── manifest.json
├── main.py
└── wheels/
    └── requests-2.32.3-py3-none-any.whl
"requirements": ["requests"]

.whl from wheels/ in Python path. The installer does not access PyPI and does not run pip.

Rules

  • no more than 32 simple names per requirements;
  • URLs, local paths and pip flags are prohibited;
  • each name must have a corresponding wheel;
  • _ And - are normalized equally.
Native libraries

Desktop wheel does not fit Android. Use py3-none-any or an assembly for the desired Android ABI and Python 3.11.

SDK

BasePlugin

Main extension class: lifecycle, metadata, storage and entry point to all DevGram APIs.

Plugin is a descendant class BasePlugin. The loader creates to the installed plugin ID and calls on_load(). The same object lives until unloaded, so store the state in the fields of the instance, and clear it in on_unload().

from devgram import BasePlugin

class MyPlugin(BasePlugin):
    id = "my.plugin"           # уникальный ID (обязателен)
    name = "Мой плагин"
    version = "1.0.0"
    author = "@you"
    description = "Что делает плагин"
    icon = "https://.../icon.png"   # необязательно
    min_app_version = "12.9"        # необязательно

    def on_load(self):
        self.log("загружен")

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

FieldPurpose
idUnique identifier. It stores settings, hooks and files.
name / version / author / descriptionInstallation card and list of plugins.
iconURL or asset path for the plugin avatar.
min_app_versionMinimum version of DevGram.

In the package .dgplugin the same fields can (and better) be set in manifest.json — it takes precedence over class attributes.

Life cycle

MethodWhen
on_load(self)Once at boot/power on. Register hooks, pills, observers.
on_unload(self)When unloading/disabling/reload. Remove EVERYTHING you registered.
on_setting_changed(self, key, value)A user has changed your setting.
on_setting_click(self, key)The setting button is pressed (type='button').

Plugin storage

self.set_setting("count", 5)          # переживает перезапуск
n = int(self.get_setting("count", 0)) # значение по умолчанию — 0
self.write_file("data.json", text)    # личная папка плагина
text = self.read_file("data.json")

API map (what to look for where)

RegionMethods
Eventson_send_message, on_update, on_send_request, menu_items → section “Events”
Dispatch/clientsend_message, edit_message, send_photo, send_request, controllers → “Client API”
Interfacebulletin, alert, toast, register_pill, register_panel_tab, settings
Java Hookshook, hook_all, invoke_original, java_class, implement → «Java hooks»
Appearance/Effectsglass, blur, tint, add_view, dp, canvas_* → “Effects and View”
Threads/utilitiesrun_on_ui, run_on_queue, copy, open_dialog, log
Idempotency

Everything you create in on_load, should be filmed in on_unload: unhook_all(), unregister_pills(), unregister_panel_tab(), taken by observers. Otherwise, after reload there will be “double” hooks and leaks.

SDK

Multi-account

A user can have multiple accounts. Work with a specific account, not just the active one.

from devgram import get_selected_account, get_client

acc = get_selected_account()      # индекс активного аккаунта
client = get_client(acc)          # клиент этого аккаунта
client.send_text(dialog_id, "hi")

Account-aware callbacks

Instead of “global” callbacks, override options with an account - they work for EVERY account:

def on_send_request_hook(self, account, name, request): ...
def on_receive_response_hook(self, account, name, response, error): ...

Controllers by account

from devgram.client_utils import get_messages_controller, get_user_config
mc  = get_messages_controller(account)   # без аргумента — активный аккаунт
cfg = get_user_config(account)
my_id = cfg.getClientUserId()
Don't close your active account

If an event arrived for account B, and you send a response through active account A, the message will go to the wrong place. Always forward account from callback to controllers and dispatch.

SDK

Events

Callbacks for messages, TL updates and TL requests. Override the methods you need BasePlugin.

Messages

def on_send_message(self, text):
    # перед отправкой. Вернуть: новый текст / None (без изменений) / False (отменить)
    return text.replace(":shrug:", "¯\\_(ツ)_/¯")

def on_receive_message(self, text):
    # входящее текстовое сообщение (только чтение/логика)
    pass

TL updates and requests

def on_update(self, update):
    # сырой TLRPC.Update (Java-объект). Поля читай рефлексией. Только чтение.
    pass

def on_send_request(self, name, request):
    # перед отправкой запроса. name = 'TL_messages_sendMessage' и т.п.
    # request — Java TLObject (можно читать/менять поля)
    pass

def on_receive_response(self, name, response, error):
    # ответ сервера. response — Java-объект (или None), error — TL_error (или None)
    pass

Account-aware options

In a multi-account, override these methods - they get a specific account:

def on_send_request_hook(self, account, name, request): ...
def on_receive_response_hook(self, account, name, response, error): ...

Messages and links menu

def menu_items(self):
    return ["Перевести"]                 # пункты в меню долгого тапа по сообщению

def on_menu_click(self, label, message_text, dialog_id):
    if label == "Перевести":
        self.bulletin(translate(message_text))

def link_menu_items(self, url):
    return ["Открыть в браузере"]         # меню долгого тапа по ССЫЛКЕ

def on_link_menu_click(self, label, url, dialog_id):
    from devgram.intents import open_uri
    open_uri(url)
Don't block callbacks

Event callbacks are called on client hot paths. Take hard work to run_on_queue(), and UI updates are in run_on_ui(). Wrap the body in try/except: one exception should not crash the flow.

Return values

on_send_message - the only callback that changes the outgoing text: the string replaces, None leaves as is, False cancels sending. The remaining event callbacks are for observation; To change the behavior of queries/methods on the fly, use Java hooks.

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 and editing messages, raw TL requests and access to all Telegram controllers.

Dispatch

self.send_message(dialog_id, "Привет", reply_to=0, entities=None)
send_text(dialog_id, "Быстрый текст")         # from devgram.client_utils import send_text
self.edit_message(dialog_id, message_id, "Новый текст")
self.send_photo(dialog_id, "/path/img.jpg", caption="", entities=None)
self.send_file(dialog_id, "/path/file")      # тип определится сам
self.send_video(dialog_id, "/path/v.mp4")
self.send_audio(dialog_id, "/path/a.mp3")
self.send_document(dialog_id, "/path/doc.pdf")
self.open_dialog(dialog_id)                    # открыть чат
self.update_profile_about("Новое био")         # текущий аккаунт

dialog_id: user = his id, group/channel = −id, secret chat - just like in the client.

Raw TL queries

req = self.tl("TL_messages_getHistory")   # пустой TLObject по короткому имени
req.peer = self.messages_controller().getInputPeer(dialog_id)
req.limit = 20
self.send_request(req, callback=lambda resp, err: self.log(resp))

Controllers

Return standard Telegram Java controllers (account-aware):

MethodJava
self.messages_controller()MessagesController
self.connections_manager()ConnectionsManager
self.user_config()UserConfig
self.send_messages_helper()SendMessagesHelper
self.media_data_controller()MediaDataController
self.contacts_controller()ContactsController
self.messages_storage()MessagesStorage
self.notification_center()NotificationCenter
self.file_loader()FileLoader
self.account_instance()AccountInstance (all at once)

client_utils module

from devgram.client_utils import (
    send_text, send_formatted_text, observe, get_last_fragment,
    get_messages_controller, get_media_controller, get_download_controller,
    get_location_controller, get_notifications_controller)

# подписка на NotificationCenter с авто-отпиской
handle = observe(NotificationCenter.messagesDidLoad, my_callback)
handle.close()   # в on_unload

Auxiliary: self.me() — account id, self.user_name(uid), self.chat_name(cid).

Work with real objects

Controllers and TL objects are real Telegram Java classes, not wrappers. See the fields in the sources; fields/methods with leading underscores and internal layout IDs are not a contract.

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 links (custom signature for the link).

From Markdown/HTML

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

text, entities = parse_markdown("**жирный** и [ссылка](https://t.me)")
send_formatted_text(dialog_id, text, entities)

text, entities = parse_html("<b>жирный</b> <i>курсив</i>")
text, entities = parse_text(raw, parse_mode="markdown")   # или "html"

Hand entities

ents = self.new_entities_list()
ents.add(self.bold_entity(0, 6))                 # offset/length в UTF-16
ents.add(self.text_link_entity(7, 6, "https://t.me/DevGram"))
self.send_message(dialog_id, "Жирный ссылка", entities=ents)

Shielding

from devgram.text_formatting import escape_markdown, escape_html
safe = escape_markdown(user_input)
Offsets in UTF-16

offset/length are calculated in UTF-16 code units (as in Telegram). For text with emoji/surrogates, use ready-made ones parse_* - they think correctly.

SDK

Settings

Declarative settings page: describe the lines - DevGram will draw a native screen and save the values.

from devgram.ui import Header, Switch, Input, Selector, Text, Button, Card, Custom

class MyPlugin(BasePlugin):
    def create_settings(self):
        return [
            Header("Общее"),
            Switch("enabled", "Включить", default=True),
            Input("api_key", "API-ключ", hint="вставьте токен"),
            Selector("mode", "Режим", ["Авто", "Ручной"], default=0),
            Text("Подсказка снизу серым"),
            Button("reset", "Сбросить"),
        ]

    def on_setting_changed(self, key, value):
        if key == "enabled":
            self.log("enabled = %s" % value)

    def on_setting_click(self, key):
        if key == "reset":
            self.bulletin("Сброшено")

Widgets

ClassPurpose
Header(text)Section title.
Switch(key, text, default=False)Toggle switch (bool).
Input(key, text, hint="")Text field (str).
Selector(key, text, items, default=0)Select from a list (index).
Text(text)
Button(key, text)Button → on_setting_click(key).
Card(...)Composite card.
Custom(view)Custom Android View in the settings bar.

Reading and writing values

on = self.get_setting("enabled", True)
self.set_setting("mode", 1)
Keys are stable

Do not rename keys between versions - the saved values ​​will be lost. To rename, do a migration: read the old key, write the new one, delete the old one.

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 and dialogs with callback buttons.

Bulletin

self.bulletin("Скопировано")                                   # info
self.bulletin("Готово", kind="success", duration=2000)
self.bulletin("Ошибка", kind="error")
self.bulletin("Удалено", button="Отменить", callback=self.undo) # с кнопкой

kind: info / success / error. duration

Alert (dialogue)

self.alert(
    "Заголовок", "Текст сообщения",
    positive="OK", on_positive=self.ok,
    negative="Отмена", on_negative=None,
    neutral="Позже", on_neutral=self.later,
)

Toast

self.toast("Короткое сообщение")
From the UI thread

Blocks/dialogs need to be shown on the UI thread. If calling from a background callback, wrap: self.run_on_ui(lambda: self.bulletin("...")).

UI and runtime

Pill Stack

Compact interactive plugin widgets in the line above the chat list. Each “pill” belongs to the plugin and is removed upon unloading.

def on_load(self):
    self.register_pill(
        "clock", "Часы",
        text="—",                    # что показывать
        value=self._now,             # str или функция() -> str (живое значение)
        on_click=self._open,
        on_long_click=self._copy,
        enabled=True,
    )

def on_unload(self):
    self.unregister_pills()          # обязательно
ParameterPurpose
pill_idThe unique id of the pill inside the plugin.
nameSignature.
textStatic text.
valueA string or function with no arguments that returns a string (the value to be updated).
on_click / on_long_clickClick handlers.
enabledShould I show the pill?
Reuse

Recall register_pill on_setting_changed. Always take your pills off on_unload.

UI and runtime

Panel tab

The plugin can add its own tab to the Emoji / GIF / Stickers panel - with any Android View, a corner action button and an icon on the chat emoticon button.

def on_load(self):
    self.register_panel_tab(
        "DevWave",                    # подпись вкладки
        self._build_tab_view,         # (context, dialog_id) -> View
        on_action=self._on_action,    # (anchor_view, dialog_id) -> None
        icon_path=self.asset_path("tab.png"),  # иконка на кнопке-смайлике
    )

def on_unload(self):
    self.unregister_panel_tab()

def _build_tab_view(self, context, dialog_id):
    from java import jclass
    LinearLayout = jclass("android.widget.LinearLayout")
    col = LinearLayout(context)
    col.setOrientation(LinearLayout.VERTICAL)
    # ... наполняем вкладку своими View, шлём в dialog_id
    return col
ParameterPurpose
titleTab text in the Emoji/GIF/Stickers strip.
build_view(context, dialog_id)Builds the contents of a tab. It is called again every time it is opened - do not cache the View itself.
on_action(anchor, dialog_id)Optional. Enables an action button in the corner of the panel (next to Emoji/GIF/Stickers). anchor — for positioning the pop-up.
icon_path
dialog_id

build_view And on_action get the ID of the current chat - build the content and send the content specifically to it.

Action Popup Menu

IN on_action It’s convenient to open the native Telegram menu (like the “arrow” in reSwaga): ActionBarPopupWindow + ActionBarMenuSubItem, positioning by anchor.

def _on_action(self, anchor, dialog_id):
    from java import jclass
    ctx = anchor.getContext()
    Popup  = jclass("org.telegram.ui.ActionBar.ActionBarPopupWindow")
    Layout = jclass("org.telegram.ui.ActionBar.ActionBarPopupWindow$ActionBarPopupWindowLayout")
    Item   = jclass("org.telegram.ui.ActionBar.ActionBarMenuSubItem")
    Click  = jclass("android.view.View$OnClickListener")
    Gravity = jclass("android.view.Gravity")

    layout = Layout(ctx)
    row = Item(ctx, False, False)        # 3-арг конструктор — без неоднозначности перегрузок
    row.setTextAndIcon("Отправить", 0)
    lst = self.implement(Click, onClick=lambda p, v: self._send(dialog_id))
    self._refs.append(lst)               # держим ссылку на прокси, иначе GC
    row.setOnClickListener(lst)
    layout.addView(row)

    popup = Popup(layout, -2, -2)        # -2 = WRAP_CONTENT литералом (не константой)
    popup.setInputMethodMode(2)          # INPUT_METHOD_NOT_NEEDED литералом
    popup.showAsDropDown(anchor, 0, -self.dp(48), Gravity.TOP | Gravity.RIGHT)

From Python, specify sizes/flags as numeric literals (-1/-2, 2), and not through LayoutHelper.WRAP_CONTENT/INPUT_METHOD_NOT_NEEDED: such static final int are inlined by the compiler and cut out by R8 in release, which is why reflection gives AttributeError. LayoutHelper.createFrame/createLinear use it themselves dp() to the number.

Tab life cycle

The tab belongs to the plugin ID. build_view is called every time a tab is selected, so build the View from the current state and do not hold a reference to the Activity. Links to Java proxy listeners (self.implement(...)) stored in a plugin field, otherwise the GC will collect them. IN on_unload call unregister_panel_tab().

Icon on the smiley button

If set icon_path, while your tab is open, the chat emoticon button shows this icon instead of the usual animation, and tapping again opens your tab (not the emoji). The icon is saved when the panel is minimized.

UI and runtime

Effects and View

Embed and place View

# width/height в dp; -1 = на весь размер (MATCH), -2 = по контенту (WRAP)
self.add_view(parent, child, width=-1, height=-2,
              left=16, top=0, right=16, bottom=0, gravity=80)  # 80 = снизу
self.remove_view(child)
px = self.dp(12)                       # dp → пиксели
color = self.rgba(30, 30, 34, 210)     # ARGB int

Liquid glass / blur / tint

panel = self.glass_panel(context_view, corner=22, blur=18, tint=0x26FFFFFF, border=0.6)
self.glass(existing_view)              # обернуть существующую View стеклом
self.tint(view, self.rgba(20,20,24,200))
self.round_corners(view, 18)
self.blur(view, radius=40.0)           # заморозить содержимое View (Android 12+)
self.unblur(view)
MethodDescription
glass_panel(context_view,...)New empty glass container panel (blurs the background behind).
glass(view,...)Wrap the existing View in glass in the same location.
blur / unblur(view)
tint(view, argb)
round_corners(view, dp)Round the corners (cut along the contour).
add_view / remove_viewAdd/remove child to ViewGroup.

Drawing on canvas

IN onDraw his java_class-View draw with native helpers (without going to Python for each primitive):

self.paint_color(paint, 0xFFFF3B30)
self.canvas_draw_round_rect(canvas, rect, rx, ry, paint)
self.canvas_draw_circle(canvas, cx, cy, r, paint)
self.canvas_draw_text(canvas, "DevGram", x, y, paint)
self.canvas_draw_bitmap(canvas, bmp, left, top, paint)
self.canvas_save(canvas); self.canvas_clip_path(canvas, path); self.canvas_restore(canvas)
cv = self.new_canvas(bitmap)           # Canvas по Bitmap (без hidden-API граблей)

Direct creation Canvas(bitmap)/calls drawRect

UI and runtime

Java hooks

Full Xposed-style hooks of any method/constructor and creation of real Java objects from Python. The engine is AliuHook on top of LSPlant: reliable on all Androids, even catches inline methods.

Method Hook

# before / after / replace + приоритет
self.hook("org.telegram.ui.ProfileActivity", "onResume", after=self.on_profile)
self.hook("org.telegram.messenger.MessagesController", "getUser", "long",
          before=self.pre, after=self.post, priority=0)

# полная подмена: оригинал НЕ вызывается, результат берётся из fn
self.hook("...MessagesController", "getUser", "long", replace=self.fake_user)

# все перегрузки метода (или все конструкторы: method="<init>")
self.hook_all("org.telegram.ui.ChatActivity", "onResume", after=self.tweak)

Argument types - strings: "int", "long", "boolean", "float", "java.lang.String", nested classes via $ ("a.b.Outer$Inner"). For a constructor, the method name is "<init>".

frame - call object (MethodHookParam)

Field/MethodWhat is this
frame.thisObjectnull for static).
frame.argsArray of arguments. Changes: frame.args[0] = x (in before).
frame.getResult()Current result (in after).
frame.setResult(x)Replace the result. IN before
frame.methodWhich method worked (for hook_all).
def on_profile(self, frame):
    activity = frame.thisObject
    uid = frame.args[0]                    # первый аргумент
    frame.setResult(42)                    # подменить результат
    orig = self.invoke_original(frame)     # вызвать оригинал в обход хуков

Hook management

MethodPurpose
invoke_original(frame)
unhook_all()Remove ALL plugin hooks (required in on_unload).
deoptimize(cls, method, ...types)Deoptimize the method (for “elusive” inline ones).
is_hooked(cls, method, ...types)Is the method now abandoned?
make_class_inheritable(cls)Take off final from class (for subclasses).
allocate_instance(cls)Create an object WITHOUT calling a constructor.

java_class creates a REAL subclass (e.g. its View With onDraw). Logic object methods override Java methods; signature - (self, this, *java_args).

class Logic:
    def onDraw(self, this, canvas):
        BasePlugin.java_super()            # super.onDraw(canvas)
        p = jclass("android.graphics.Paint")()
        self_plugin.paint_color(p, 0x8800FF88)
        self_plugin.canvas_draw_circle(canvas, 40, 40, 30, p)

view = self.java_class("android.view.View", Logic(),
                       arg_types=["android.content.Context"], args=[context])
view.setWillNotDraw(False)

ocl = self.on_click(lambda v: self.bulletin("тап"))          # View.OnClickListener
olc = self.on_long_click(lambda v: True)                     # View.OnLongClickListener
# произвольный интерфейс:
runnable = self.implement(jclass("java.lang.Runnable"), run=lambda s: self.tick())
Don't hook hot methods in Python

Time-lapse onDraw/measure/layout, called hundreds of times per second, gives an ANR when exiting in Python. For drawing, do one java_class-View and draw native canvas_*.

Compatibility

Old plugins with globals before_hook(frame)/after_hook(frame) continue to work. jclass/dynamic_proxy - standard ones from Chaquopy.

Low cost

The hook connects the plugin with implementation details: class, overload, constructor, result type. First look for a public event or controller. Place an optional hook in try/except, a critical hook with a version check and an understandable error. Store hooks only through the SDK (removable unhook_all()

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.

Development

Dev Server

Local installation .dgplugin via ADB without sending via Telegram.

Preparation

  1. Copy the port and token.
  2. Connect your phone and confirm ADB.

Loading via Builder

# Windows PowerShell
$env:DEVGRAM_TOKEN="ваш_токен"
dgb upload

# Linux / macOS
export DEVGRAM_TOKEN="ваш_токен"
dgb upload

Without an argument, Builder first builds the project. The finished archive can be specified explicitly:

dgb upload builds/my.plugin-1.0.0.dgplugin --token "ваш_токен"

How does this work

/upload and passes the token in the header. The server checks the package and sets it enabled.

Diagnostics

adb devices
adb forward tcp:42690 tcp:42690
curl -H "X-DevGram-Token: ваш_токен" http://127.0.0.1:42690/status
Private token

Don't publish it. If leaked, reset the token in the application.

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

.dgplugin problems

Errors in build, verification, installation and download.

The package does not open

  • check the extension .dgplugin;
  • make sure it's a ZIP;
  • manifest.json and file from field main at the root;
  • reassemble via dgb build -a -v -nf.

invalid package manifest

There is no ID, the ID is longer than 64 characters, contains a prohibited character, or main indicates a missing or unsafe path.

missing bundled wheel

For a name from requirements no matching file in wheels/.

Find the first error package load or load in the plugin logs. This is usually an import, an incompatible wheel, or a module-level exception.

The update has rolled back

HTTP 401

Copy the current token. Reset it and update if necessary DEVGRAM_TOKEN.

HTTP 500

Check ADB forwarding, multipart format and Dev Server up-to-date.

After reload the event is called twice

on_unload()

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

Change the "DevGram" title on the chat screen

The title gives away DialogsActivity.devgramActionBarTitle() and reinstalls it on each onResume - so just setTitle rolls back. Let's hook the method itself:

def on_load(self):
    self.hook("org.telegram.ui.DialogsActivity", "devgramActionBarTitle",
              after=lambda f: f.setResult("Мой заголовок"))

from java import jclass
MC = jclass("org.telegram.messenger.MessagesController")
MC.getGlobalMainSettings().edit().putInt("dg_actionBarTitle", 0) \
  .putString("forkCustomTitle", "Мой заголовок").apply()

Intercept outgoing request

def on_load(self):
    self.hook("org.telegram.tgnet.ConnectionsManager", "sendRequest",
              "org.telegram.tgnet.TLObject",
              "org.telegram.tgnet.RequestDelegate",
              before=self._pre)

def _pre(self, frame):
    req = frame.args[0]
    self.log("запрос: " + req.getClass().getSimpleName())

plugin = self
class Logic:
    def onDraw(self, this, canvas):
        p = jclass("android.graphics.Paint")(1)   # ANTI_ALIAS
        plugin.paint_color(p, 0xFFFF3B30)
        plugin.canvas_draw_circle(canvas, this.getWidth()/2, this.getHeight()/2,
                                  plugin.dp(12), p)
v = self.java_class("android.view.View", Logic(),
                    arg_types=["android.content.Context"], args=[ctx])
v.setWillNotDraw(False)

from java import jclass
BF = jclass("org.telegram.ui.Components.BulletinFactory")
BF.of(fragment).createEmojiBulletin(document_id, "Текст", None).show()

Profile is RecyclerListView with adapter and DiffUtil. The row is added, increasing rowCount V updateRowsIds and registering the position in DiffCallback.fillPositions (otherwise DiffUtil will start a phantom animation → crash). For a detailed analysis, see the “Reliable Java hooks” section.

Any recipe with a /View hook requires removal in on_unload: unhook_all(), remove_view(), unregister_pills().

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.

Sharing of Responsibility

DevGramBuilder creates the archive, and the modules devgram.* provide runtime API within the application.

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.plugin_files_dir() + "/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.alert 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).
Package .dgplugin

.dgplugin format

The exact structure, verification, installation and update of the package plugin.

What is this

.dgplugin — ZIP archive with mandatory manifest.json and Python entry point. The extension cannot be changed.

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

Loader validation

  • a valid ZIP and JSON object in manifest.json;
  • a valid id and an existing .py entry point;
  • no .pyc or .pyo files;
  • no absolute paths, .. components or duplicate names;
  • at most 4096 files;
  • every requirement has a matching bundled wheel;
  • minimum versions are compatible with the installed DevGram.

Installation and updates

DevGram validates the package, copies it to a temporary file, and only then replaces the installed version. If loading fails, the previous archive is restored. Files are extracted to the private .devgram/<id> directory.

Correct build

dgb build -a -v -nf
# builds/username.my_plugin-1.0.0.dgplugin

Do not create the archive manually: Builder creates the correct root, manifest and reproducible file order.

Open sources only

Builder keeps Python code in .py files. DevGram rejects packages containing .pyc or .pyo during installation and moderation submission.

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.plugin_files_dir() + "/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

Publishing .dgplugin

Preparing a version for users and the DevGram directory.

dgb build -a -v -nf

Use -c 2 only if necessary; the original assembly is easier to review and moderate.

Before publication

  1. Enlarge version, without changing id.
  2. Check the description, author, HTTPS icon and minimum versions.
  3. Check the list of archive files.
  4. Install cleanly and over the previous version.
  5. Check enable, disable, reload, delete and safe mode.
  6. Send the file exactly from builds/.

Updates

The same ID updates an existing plugin. Replacement is atomic; If the download fails, the previous archive is restored.

Changelog

Describe custom changes, new permissions or network calls, minimum version, and incompatible changes.

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

Official CLI for creating a project, checking sources and building .dgplugin. Runtime APIs provide regular modules devgram.*.

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.

DevGramBuilder

Builder is responsible for the structure and packaging of the project. Executable code uses public modules devgram.*.

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.