DevGram Plugin SDKIntroduction
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 formatPlugins 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
EventsMessages, TL updates, requests and NotificationCenter.
InterfaceSettings, bulletins, alerts and Pill Stack.
Client APIMedia, formatting, accounts and controllers.
NativeJava class proxy and method hooks.
Reading order
- Preparation and first package
- BasePlugin and events
- Client API and UI
- 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
- Unique ID and clear metadata.
- No secrets in the archive.
- Idempotent load/unload.
- Work in light, dark and system theme.
- Correct behavior after reload, disable and safe mode.
DevGram BuilderDevGram Builder
High-level layer on top of DevGram SDK for large plugins: project structure, modules, resources and declarative settings.
Why Builder
Builder separates business logic from lifecycle and Android bridge. One project can be compiled into a native one .dgplugin without manual management of imports, locales and resources.
ModulesSeparate hooks, screens, commands and services.
ResourcesAssets, localization and metadata have predictable paths.
LifecycleInit, enable, disable and dispose are controlled by the project.
Public APITyped facades over client_utils and UI.
ImportantDevGram Builder - DevGram-native layer. It does not run exteraGram plugins and does not change our package format.
DevGram BuilderQuick Start
Create a DevGram Builder project, add a module and build it into `.dgplugin`.
devgram-builder new hello.devgram
cd hello.devgram
devgram-builder add module messages
devgram-builder build
python3 tools/devgram_dev.py upload dist/hello.devgram
First module
from devgram.builder import Module
class Welcome(Module):
id = "welcome"
def on_message(self, event):
if event.text == "/hello":
event.reply("Hello from DevGram Builder")
If the CLI is not installed, the same structure can be created manually under the Project Structure section.
DevGram BuilderProject Structure
Each directory has one responsibility.
hello.devgram/
├── devgram-builder.json
├── main.py
├── modules/
│ ├── messages.py
│ └── appearance.py
├── services/
├── screens/
├── assets/
├── locales/
│ ├── ru.json
│ └── en.json
└── wheels/
| Catalog | Purpose |
|---|
modules/ | Event modules and commands. |
services/ | Background tasks and stateful services. |
screens/ | UI and settings pages. |
assets/ | Icons, pictures and templates. |
DevGram BuilderPublic API
Builder facades hide the low-level Java bridge but return regular DevGram objects.
| API | Purpose |
|---|
Module | Basic lifecycle of the module. |
module.on_message(event) | Normalized message event. |
module.settings() | Declarative settings. |
context.client(account) | Account-aware Client API. |
context.ui.bulletin() | Native die. |
context.storage | Persistent module storage. |
Lifecycle
def setup(context):
context.register(Welcome())
def dispose(context):
context.unregister_all()
DevGram BuilderMetadata
File devgram-builder.json describes the project before packaging it in manifest `.dgplugin`.
{
"id": "author.plugin",
"name": "Plugin name",
"version": "1.2.0",
"entrypoint": "main.py",
"modules": ["modules.messages", "modules.appearance"],
"minDevGram": "12.9.3",
"permissions": ["network", "storage"]
}
Version rules
The version must be incremented for each archive published. minDevGram use if the project depends on a new API. Builder checks module IDs, entrypoint existence, and resource duplication before packaging.
DevGram BuilderModules & Imports
A large plugin is divided into independent modules with an explicit lifecycle.
# modules/messages.py
from devgram.builder import Module
class Messages(Module):
id = "messages"
depends_on = ["storage"]
def enable(self, context):
self.handle = context.events.messages(self.on_message)
def disable(self, context):
self.handle.close()
Don't use wildcard imports. Place common functions in services/, rather than copying between modules. Circular dependencies are not allowed: Builder prints the module chain that created the cycle.
DevGram BuilderAssets & Localization
Builder indexes resources and checks references to them during the build.
icon = context.assets.path("icons/plugin.png")
title = context.i18n.get("settings.title")
message = context.i18n.format("welcome", name=user_name)
Fallback
Row selection order: current language → English → default → key. All JSON files must contain a strings object. Do not store binary data in locale files.
Dimensions assets
Make your catalog icons square. Compress large images in advance: `.dgplugin` is downloaded and checked entirely, so extra assets increase installation time.
DevGram BuilderSettings
Builder combines module settings into one native page and automatically adds namespace to keys.
class Appearance(Module):
def settings(self):
return [
Header(text="Внешний вид"),
Switch(key="glass", text="Стекло", default=False),
Selector(key="style", text="Стиль", items=["System", "Compact"]),
]
def on_setting_changed(self, key, value):
if key == "glass": self.apply_glass(value == "1")
The physical key is stored as module_id.keyso two modules can share a logical name enabled without conflict.
DevGram BuilderDependencies
There are two types of dependencies: project modules and Python wheels.
Module dependencies
depends_on defines the order of enable and the reverse order of disable. If the required module does not load, the dependent module does not start and receives an understandable error.
Python wheels
devgram-builder add wheel dist/library.whl
devgram-builder inspect dependencies
Builder packages wheels and creates a lock file with SHA-256. For Android, pure-Python dependencies are preferred.
DevGram BuilderDevelopment
Assembly, verification and hot reload are performed in one sequence.
devgram-builder check
devgram-builder build --debug
devgram-builder upload --reload
devgram-builder logs --follow
Debug build
The debug package includes a source map of modules and an extended log. Before publishing, collect without --debugto not include local paths and diagnostic files.
Editor workflow
Run the check when saving, keep ADB connected and reload only the changed plugin ID. A full restart is only needed when the native Java bridge changes.
DevGram BuilderTroubleshooting
Diagnosis of structure, import and lifecycle problems.
Module not found
Check the name in metadata for availability __init__.py and character case. Android filesystem is case sensitive.
Circular dependency
Move the general logic into a service module on which both sides depend. Don't create mutual depends_on.
Works after restart, but not after reload
The module retained the global singleton or Java listener. Move creation to enable(), and deleting in disable().
Settings are lost
Do not change the project ID or module ID. To rename a key, add an explicit schema version migration.
Getting startedPreparation
A minimal environment for writing and installing extensions.
Requirements
- current DevGram on Android;
- Python 3 for packaging;
- ADB for hot reload;
- editor with Python support.
Developer mode
Open Plugins → Plugin System and enable developer mode. Copy the local token.
export DEVGRAM_TOKEN="token_from_app"
python3 tools/devgram_dev.py status
Local accessDev server is only accessible via ADB forwarding and requires a token.
Preparing the working environment
Separate the source directory and the built package directory. In the sources, store manifest, Python code, resources and test data; In the release archive, do not include the virtual environment, logs, IDE metadata and local keys.
Checking the environment
python3 --version
adb devices
unzip -t plugin.dgplugin
Before starting, make sure that the device is visible through ADB, DevGram is installed from the same branch for which the plugin was written, and Developer mode is enabled. If ADB shows unauthorized, confirm with fingerprint on your phone.
Repeatable assembly
Commit dependency versions and build the archive from a clean directory. A repeated launch should create the same list of files and the same manifest, except for the deliberately changed checksum.
Getting startedFirst plugin
A minimal package that can be installed in one tap.
from devgram import BasePlugin
from devgram.ui import Header, Switch, Button
class Hello(BasePlugin):
id = "hello.devgram"
name = "Hello DevGram"
version = "1.0.0"
author = "Your name"
description = "Мой первый плагин для DevGram"
icon = "https://example.com/plugin-icon.png"
def settings(self):
return [Header(text="Hello"), Switch(key="enabled", text="Включено"), Button(key="test", text="Проверить")]
def on_setting_click(self, key):
if key == "test": self.bulletin("Плагин работает", kind="success")
plugin = Hello()
Plugin avatarField icon accepts a direct HTTPS link to a PNG or JPG. DevGram will show a picture in the installation card and the list of plugins. Details are in a separate item “Plugin Avatar” in the Package menu.
Save the file with the extension .plugin, send it to Telegram and click “Install”. The package with resources and dependencies is packaged in .dgplugin.
Analysis of the first plugin
Entry point contains only metadata and small logic. When importing, do not call the UI or send messages: the loader may not yet have an active Fragment. Register observer, menu and pill in on_load().
Step by step check
- Install the package and make sure it appears in the manager.
- Open settings and change the value.
- Reload only this plugin.
- Send a test message from the second account.
- Disable and remove the plugin, then check for callback missing.
Typical errors
If the plugin is not visible, check the extension, archive root and ID uniqueness. If it is visible but does not run, see the first import error: subsequent messages are often a consequence.
PackageManifest
Metadata defines the identity and entry point of the extension.
{
"id": "hello.devgram",
"name": "Hello DevGram",
"version": "1.0.0",
"author": "Your name",
"entrypoint": "main.py",
"description": "Example plugin",
"icon": "https://example.com/plugin-icon.png"
}
| Field | Rule |
|---|
id | Unique stable ID. |
entrypoint | Python entry point file. |
version | Update version. |
description | Brief description for the catalog and installation card. |
icon | Direct HTTPS link to a PNG or JPG avatar. |
Atomic installationThe package is validated and unpacked into a temporary folder. A failed update does not break the active version.
manifest fields
Manifest describes the identity of the package before Python is imported. Use UTF-8 strings, don't duplicate keys, and don't store custom tokens there. The version and min_app_version values are compared as strings according to the bootloader rules, so follow the format major.minor.patch.
entrypoint compatibility
The entrypoint path must be relative and point to an existing Python file. The module name must not be the same as a system package or another installed plugin. After renaming the file, update the manifest and check the clean installation.
Icon and description
For icon use a direct HTTPS link to the PNG or JPG. The description is written for the directory: first the purpose, then the restrictions and required permissions.
PackagePlugin avatar
How to add an icon to the installation card, list of plugins and directory.
Regular .plugin
Add a field icon to the class next to name, author And description:
class MyPlugin(BasePlugin):
id = "my.plugin"
name = "My Plugin"
author = "@you"
description = "Краткое описание"
icon = "https://example.com/plugin-icon.png"
Package .dgplugin
Please provide the same link in manifest.json:
{
"id": "my.plugin",
"name": "My Plugin",
"icon": "https://example.com/plugin-icon.png"
}
Requirements
- Use a direct HTTPS link to the image file rather than a link to an HTML page.
- Format - PNG or JPG; A small square image is better.
- Check the contrast on a light and dark theme.
- If
icon is not specified or the image did not load, DevGram will leave the standard icon.
Where will she appearDevGram loads an avatar when opening the installation card and caches it for the list of plugins.
PackageAssets and languages
Package resources are available without absolute paths.
background = self.asset_path("background.png")
title = self.string("title", default="DevGram")
Translation files are stored in locales/en.json And locales/ru.json. Path asset_path() limited to folder assets/.
PackageDependencies
Compatible Python wheels can be included in the package.
Place pure-Python wheels in wheels/. When hot reloading, old modules and paths are removed, so the update is not mixed with the old version.
ABINative wheels must match the ABI and Python version of the DevGram build.
SDKBasePlugin
Main extension class and its lifecycle.
| Method | Purpose |
|---|
on_load() | Initialization. |
on_unload() | Freeing observers, hooks and pills. |
on_setting_changed(key,value) | Change the setting. |
get_setting / set_setting | Persistent plugin storage. |
BasePlugin Contract
The loader creates one instance per installed plugin ID. Don't count on a new object for every event. Clear status fields in unload, and store resource registration in instance fields.
Safe lifecycle
def on_load(self):
self._closed = False
self._handles = []
def on_unload(self):
self._closed = True
for handle in self._handles:
try: handle.close()
except Exception: pass
self._handles.clear()
The callback that came after unload checks self._closed and stops working. This is especially important for background queue and network responses.
SDKMulti-account
Each callback can work with a specific Telegram account, and not with a globally selected one.
Getting a client
from devgram import get_selected_account, get_client
account = get_selected_account()
client = get_client(account)
client.send_text(dialog_id, "Hello")
controller = client.get_messages_controller()
AccountClient
| Method | Description |
|---|
get_messages_controller() | MessagesController of the desired account. |
get_user_config() | UserConfig of the desired account. |
get_connections_manager() | ConnectionsManager of the desired account. |
send_text(dialog_id,text) | Sending from the selected account. |
RuleIn account-aware callback use the passed account. Do not rely on the account that is open in the interface at this moment.
Account-aware rule
The account index is passed from the event to each subsequent call. Do not save the selected account to a global variable; the user can switch profiles while the request is running.
Object Consistency
Peer, controller, storage and request must be obtained for one account. You cannot mix InputPeer from one cache with ConnectionsManager of another account. For a background task, store account in a closure and pass it to the UI notification only as a context.
Examination
Test two accounts, logout of one profile and switching accounts during the request. If your account is deleted, show the error correctly and do not resend automatically.
SDKEvents
Hooks for messages, updates and TL requests with account-aware options.
def on_send_message(self, text): return text
def on_receive_message(self, text): pass
def on_update_hook(self, update_name, account, update): pass
def on_send_request_hook(self, account, name, request): pass
def on_receive_response_hook(self, account, name, response, error): pass
NotificationCenter
from devgram.client_utils import observe
handle = observe(notification_id, callback, account=account)
handle.close()
Order of events
Before sending, the message goes through a send hook, then a Telegram request. Incoming updates may arrive before the visible list of dialogs is updated. Don't make the assumption that the UI already contains a message or that the callback will be called exactly once.
Filtration
First check account, update type, dialog ID and the presence of the required field. Early return reduces the load. For multiple similar updates, use debounce and re-ID protection.
observer errors
The callback exception should not stop NotificationCenter. Log the event name and plugin ID, then return. Close Handle even if the handler is partially initialized.
SDKMessage 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
| Parameter | Meaning |
|---|
label | Pressed item from menu_items(). |
message_text | Text of the selected message. |
dialog_id | ID 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.
SDKClient API
Sending, editing, formatting and controllers.
from devgram.client_utils import send_text, send_formatted_text
from devgram.text_formatting import Entity
send_text(peer_id, "Hello", account=account)
send_formatted_text(peer_id, "DevGram", [Entity("bold", 0, 7)], account=account)
self.send_photo(peer_id, "/path/photo.jpg", "Caption")
self.send_file(peer_id, "/path/file.zip")
self.edit_message(peer_id, message_id, "Updated")
| Helper | Result |
|---|
get_messages_controller(account) | MessagesController |
get_notification_center(account) | NotificationCenter |
get_file_loader(account) | FileLoader |
get_download_controller(account) | DownloadController |
Dispatch and controllers
High-level helpers are suitable for normal actions and take into account account. Controllers are needed for scenarios where the native Telegram model is required. Do not call storage and connections manager methods from the UI thread.
Callback and result
Handle the request error separately from the Python exception. For a repeatable action, store the request ID and prevent double clicks. Before sending, check the peer and user permission for the action.
Media
For large files, use FileLoader and DownloadController rather than reading the entire byte array into memory. Show download status and handle cancellation.
SDKFormatting text
DevGram creates real TLRPC.MessageEntityrather than sending Markdown as plain text.
Supported entities
Bold, italic, underline, strike, spoiler, code, pre, blockquote and text URL.
from devgram.client_utils import send_formatted_text
from devgram.text_formatting import Entity, parse_text, parse_markdown, parse_html
text, entities = parse_markdown("**DevGram** _plugin_")
send_formatted_text(peer_id, text, entities, account=account)
text, entities = parse_html("<b>DevGram</b>")
UTF-16 offsets
Telegram calculates entity offsets in UTF-16. Use utf16_length(), add_surrogates() And remove_surrogates(), if you create entities manually.
| Helper | Description |
|---|
parse_text(text,parse_mode) | Single point for HTML or Markdown. |
escape_markdown(text) | Escapes control characters. |
escape_html(text) | Escapes HTML. |
to_tlrpc_entities(list) | Java ArrayList with Telegram entities. |
UTF-16 ranges
Telegram entities use UTF-16 code units, while Python slicing uses Unicode code points. Emoji, skin tone modifiers and some compound characters make these indexes different. Use utf16_length() and ready-made parse helpers.
Overlaid entities
Check range intersections and order before submitting. HTML and Markdown parser return parsed text; do not add entity at source indexes after removing markup.
User input
Escape text before substitution in HTML/Markdown. Check the URL separately and do not turn any text from the message into a clickable link without an explicit action.
SDKSettings
Declarative strings in the native settings page.
from devgram.ui import Header, Switch, Input, Selector, Button
def settings(self):
return [Header(text="Основное"), Switch(key="enabled", text="Включено"), Input(key="name", text="Имя"), Selector(key="mode", text="Режим", items=["A", "B"]), Button(key="reset", text="Сбросить")]
value = self.get_setting("name", "")
self.set_setting("name", "DevGram")
Settings and status
Keys must be stable and have a plugin namespace. Changing the value causes a callback, but does not guarantee that the current screen has already been redrawn. If the new setting requires a restart, show a bulletin with a clear action.
Value types
Switch is stored as a boolean-compatible value, Input is a string, Selector is the index or value of the selected item. Transform data at the API edge and maintain schema version for migrations.
UI errors
A long label is moved or shortened, but should not overlap the control. Check dark mode, font scale, screen rotation and re-enter settings.
UtilitiesIntents
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()
| Parameter | Description |
|---|
action | Android action. |
scheme | URI scheme: https, tg and others. |
host | Hostname. |
path | Path 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.
UtilitiesFile 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)
| Method | Purpose |
|---|
read_file_bytes / write_file_bytes | Binary 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.
UtilitiesAndroid 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 runtimeBulletins and alerts
Native Telegram notifications with callback buttons.
self.bulletin("Скопировано", kind="success", duration=5000, button="Отменить", callback=undo)
self.alert("Удалить файл?", "Действие нельзя отменить", positive="Удалить", on_positive=remove, negative="Отмена")
Bulletin and alert
Bulletin is designed for short-term results and disappears on its own. Alert is used for a decision that requires a choice. Each destructive action must have a cancellation, and the callback must be safe when the Fragment is closed.
Duration
Don't turn the bulletin into a permanent notification. For a long operation, show the status on the screen and briefly confirm the result when completed.
Topics
Use native colors and check the contrast of text, icons and disabled states in a light and dark theme.
UI and runtimePill Stack
Interactive compact widgets belonging to a specific plugin.
self.register_pill("status", "Status", text="Online", value=42, on_click=self.open)
def on_unload(self): self.unregister_pills()
PossessionAnother plugin cannot overwrite or delete your pills.
Ownership of Pill Stack
Each pill belongs to a plugin ID. Use a stable local key, update the value instead of creating a new pill, and delete all pills in unload.
Interactivity
Click callback should be fast and account-aware. If the action is network, change the state to loading and allow retry after error. Don't store an Activity object in a pill.
Size and Availability
The text must fit on a small screen and in a larger font. Add a clear description and don't just color code the condition.
UI and runtimeEffects and View
High-level helpers for glass, blur, tint, sizing and embedding Android View.
panel = self.glass_panel(context_view, corner=22, blur=18, tint=0x26FFFFFF, border=0.6)
self.add_view(parent, panel, width=-1, height=-2, left=16, right=16, gravity=80)
self.round_corners(panel, 18)
self.tint(panel, self.rgba(30, 30, 34, 210))
self.remove_view(panel)
| Method | Description |
|---|
glass(view,...) | Wraps an existing View in a glass panel. |
blur / unblur | Sets or removes the RenderEffect. |
dp(value) | dp to pixels. |
rgba(r,g,b,a) | Android ARGB integer. |
Android limitationRenderEffect blurs the contents of the View itself, rather than an arbitrary background behind a separate Surface.
UI and runtimeJava hooks
Direct Android runtime and Xposed-style method hooks for advanced scenarios.
from java import jclass
AndroidUtilities = jclass("org.telegram.messenger.AndroidUtilities")
# Hook engine — AliuHook (Xposed API over LSPlant): reliable on all Android,
# catches even inlined methods. You can hook ANY class (profile, chats, settings).
# 1) Per-hook callback — your own before/after/replace for a SPECIFIC hook:
self.hook("org.telegram.ui.ProfileActivity", "onResume", after=self.on_profile)
# 2) Full method replacement (original is NOT called, return the result from fn):
self.hook("org.telegram.messenger.MessagesController", "getUser", "long", replace=self.fake_user)
# 3) All overloads of a method (or all constructors, method="<init>"):
self.hook_all("org.telegram.ui.ChatActivity", "onResume", after=self.tweak)
# 4) Classic mode — one before_hook/after_hook for ALL plugin hooks:
self.hook("org.telegram.ui.ChatActivity", "onResume")
def on_profile(self, frame):
profile = frame.thisObject # the ProfileActivity itself
args = frame.args # arguments (mutable)
res = frame.getResult() # result (in after)
# frame.setResult(x) — replace result; in before it SKIPS the original
original = self.invoke_original(frame) # call the original bypassing hooks
view = self.java_class("android.view.View", logic, arg_types=["android.content.Context"], args=[context])
Don't hook hot methodsFrame-by-frame callbacks like onDraw via Python may cause an ANR.
Hook utilities (AliuHook)hook(cls, m, ...types, before=, after=, replace=, priority=) — per-hook callbacks; hook_all(cls, m, ...) — all overloads/constructors; deoptimize(cls, m, ...types) — deoptimize a method; invoke_original(frame) — call the original bypassing hooks; unhook_all() — remove your hooks; make_class_inheritable(cls), allocate_instance(cls), is_hooked(cls, m, ...types). Existing plugins using before_hook/after_hook keep working unchanged.
Low cost
Java hook and proxy link the plugin with specific implementation details. They require class checking, overload, constructor and return type. First look for a public event or controller.
Fallback
Optional hook is set to try/except and disables only one function. The critical hook declares the minimum version and an understandable error.
Cleanup
Store the handle, remove the hook in unload and do not leave a static reference for the plugin or Context.
UI and runtimeReflection
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.
PublicationDev server
Hot reload and debugging without restarting the application.
export DEVGRAM_TOKEN="..."
python3 tools/devgram_dev.py upload plugin.dgplugin
python3 tools/devgram_dev.py reload --plugin hello.devgram
python3 tools/devgram_dev.py debugger-start --platform vscode --port 5678
The tool uses ADB forward/reverse. The network port does not open to the outside.
Hot reload
Reload first calls unload, then clears the modules and imports the new version. If the old callback continues to run, the problem is an unclosed handle, timer, thread, or Java listener.
ADB workflow
adb devices
adb logcat | grep DevGram
adb push plugin.dgplugin /sdcard/Download/
Test logcat only on a test device and remove personal data from the report.
Release check
Hot reload does not replace a clean installation: separately check install, update, disable, enable, uninstall and launch after safe mode.
PublicationSafety
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 modeAfter 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.
PublicationProblem Solving
Typical installation, download and execution errors.
The plugin did not appear
- check the extension (
.plugin or .dgplugin); - for package
.dgplugin make sure that manifest.json and entrypoint are at the root of the archive; - check the uniqueness of the ID;
- open the plugin system log.
Changes not applied
Use reload specific plugin ID. DevGram clears its modules from the Python cache; if the code is still old, check the entrypoint and archive contents.
After the fall nothing works
The client could enable safe mode. Remove or fix the problematic package, click Reload Plugins, then turn off Safe Mode.
UI freezes
Move networking, large file reading and computing to run_on_queue(). Don't execute Python code in frame-by-frame methods.
Diagnostic order
- Play on a clean run.
- Make a note of the plugin ID/version and exact screen.
- Check the first line of traceback.
- Disable only the problematic plugin.
- Repeat after reload and after a complete restart.
The directory is empty
Separate the network error from the render error: show loading, empty and error separately, check the callback on the UI thread and make sure filters are not applied before the data is loaded.
Crash after update
Compare manifest, schema migration, imports and optional hooks. With an incompatible API, it is better to disable the function and show a warning than to crash every time you open it.
PublicationAPI reference
A short list of public DevGram SDK modules.
| Module | Purpose |
|---|
devgram | BasePlugin, AccountClient, hooks, UI helpers and lifecycle. |
devgram.client_utils | Account-aware controllers, sending and NotificationCenter. |
devgram.text_formatting | Entities, HTML, Markdown and UTF-16. |
devgram.android_utils | Streams, listeners, clipboard and log. |
devgram.file_utils | Text and binary files. |
devgram.intents | Registering and sending Android Intent. |
devgram.hook_utils | Reflection helpers. |
devgram.ui | Settings, 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.
PublicationCookbook
Practical recipes for common tasks. Each example can be inserted into a separate `.plugin` or `.dgplugin` and adapted to your scenario.
1. Full lifecycle
Use on_load() only for registering resources and observers. Move long operations to a queue. IN on_unload() remove everything that was registered.
from devgram import BasePlugin
from devgram.client_utils import observe
from devgram.android_utils import run_on_queue, log
class LifecyclePlugin(BasePlugin):
id = "sample.lifecycle"
_observer = None
def on_load(self):
self._observer = observe(42, self._notification, account=self.current_account())
run_on_queue(self._warm_cache)
def _warm_cache(self):
log("cache warmed")
def _notification(self, notification_id, account, args):
self.bulletin("Событие получено")
def on_unload(self):
if self._observer:
self._observer.close()
self._observer = None
Why is this importantIf you do not remove the observer, after hot reload the callback may fire twice. If you don't clear the pill, the old UI will remain registered.
2. Command in the message menu
The menu returns only a list of strings. Keep the logic of action in on_menu_click(); Don't change Java View directly from callback without UI transition.
def menu_items(self):
return ["Посчитать слова", "Сохранить текст"]
def on_menu_click(self, label, message_text, dialog_id):
if label == "Посчитать слова":
self.bulletin(f"Слов: {len(message_text.split())}")
elif label == "Сохранить текст":
self.write_file("last.txt", message_text)
self.bulletin("Сохранено", kind="success")
3. Sending a response with entities
First get clean text and list Entity, then pass them to send_formatted_text(). Do not send Markdown tags to Telegram directly.
from devgram.text_formatting import Entity
from devgram.client_utils import send_formatted_text
text = "DevGram: официальный клиент"
entities = [Entity("bold", 0, 7), Entity("italic", 9, 10)]
send_formatted_text(dialog_id, text, entities, account=account)
4. Multi-account without errors
Any callback that receives account, should pass it on to helpers. Otherwise, the message may leave the account selected in the UI, and not the event account.
def on_update_hook(self, update_name, account, update):
client = self.client(account)
client.send_text(self.chat_id, "Ответ от правильного аккаунта")
5. UI and background tasks
from devgram.android_utils import run_on_ui_thread, run_on_queue
def load(self):
run_on_queue(lambda: self._load_data())
def _load_data(self):
value = read_remote_data()
run_on_ui_thread(lambda: self.bulletin("Готово"))
Thread RuleJava View, bulletin, alert, clipboard and changing settings - UI. Network, JSON, files and heavy computing - queue.
6. Settings with migration
def on_load(self):
if self.get_setting("schema", "0") == "0":
old = self.get_setting("old_name", "")
if old: self.set_setting("name", old)
self.set_setting("schema", "1")
def settings(self):
return [Switch(key="enabled", text="Включено", default=True), Input(key="name", text="Имя")]
7. Errors and logging
Wrap the outer code in try/except, record the context via log() and show the user a clear message. Don't show stack trace in bulletin.
try:
self.send_request(request, self._on_result)
except Exception as error:
log("request failed: " + repr(error))
self.bulletin("Не удалось выполнить запрос", kind="error")
8. Review before publication
- The ID has the author's namespace.
- Enlarged version.
- The archive contains no secrets or debug logs.
- The plugin runs in safe mode and after hot reload.
- All observers, hooks and pills are released.
- Network and files are not executed on the UI thread.
Verified APISDK contracts
The exact module contracts that are included in the current DevGram build. The signatures on this page are verified against the Python runtime client.
Source of TruthIf the example from the experimental DevGram Builder is inconsistent with this page, use the API from here. Builder is still a high-level design layer, and modules devgram.* are already available to plugins.
devgram
The root module provides BasePlugin, AccountClient, HookResult, HookStrategy, get_selected_account() And get_client(account=None). A plugin instance is created by the loader. Don't create BasePlugin manually and do not store Android Activity in a global variable: after changing the screen, the link will become obsolete.
BasePlugin Metadata
| Field | Type | Purpose |
|---|
id | str | Permanent unique ID. Used for settings, files, hooks and updates. |
name | str | Display name. |
version | str | Package version; enlarge before publishing. |
author | str | Author or team. |
description | str | Brief description without markup. |
icon | str | PNG/JPG URL for a list of plugins. |
min_app_version | str | Minimum 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.
| Function | Result | Application |
|---|
send_text(peer, text, account=None) | None | Sends plain text. |
send_formatted_text(peer, text, entities, account=None) | None | Sends text from native entities. |
get_messages_controller(account=None) | Java object | Users, chats and dialogues. |
get_connections_manager(account=None) | Java object | TL requests and network state. |
get_messages_storage(account=None) | Java object | Local base; perform heavy operations in queue. |
get_file_loader(account=None) | Java object | Paths and downloads of Telegram media. |
get_last_fragment() | BaseFragment/None | Current screen. Always check for None. |
NotificationCenter
observe(notification_id, callback, account=None) returns ObserverHandle. Save handle and call close() when unloading. Re-registering after a hot reload without closing the old handle results in double callbacks.
from devgram.client_utils import observe
def on_load(self):
self._observer = observe(1, self._on_notification)
def _on_notification(self, notification_id, account, args):
self.log("notification account=" + str(account))
def on_unload(self):
if self._observer:
self._observer.close()
self._observer = None
devgram.text_formatting
Entity(type, offset, length, url=None, language=None) describes the range in UTF-16. Available parse_html(), parse_markdown(), parse_text(), to_tlrpc_entities(), utf16_length(), escape_markdown() And escape_html(). Do not calculate the length using len()if the string contains emoji or non-BMP characters.
from devgram.text_formatting import Entity, utf16_length
from devgram.client_utils import send_formatted_text
text = "🔥 DevGram"
entities = [Entity("bold", 0, utf16_length(text))]
send_formatted_text(dialog_id, text, entities, account=account)
Parsing custom text
parse_text(text, parse_mode) returns cleared text and entities. Use parse_mode="html" or parse_mode="markdown". Escape user data before adding it to the template, otherwise the entered characters will become markup.
from devgram.text_formatting import escape_html, parse_text
source = "Автор: " + escape_html(user_name)
plain, entities = parse_text(source, "html")
devgram.file_utils
The module contains ensure_dir_exists(), list_dir(), read_file(), write_file(), binary options and delete_file(). list_dir(path, extensions=None, recursive=False, include_files=True, include_dirs=False) can filter extensions and traverse the tree.
from devgram.file_utils import ensure_dir_exists, write_file, read_file
folder = ensure_dir_exists(self.storage_path("cache"))
path = folder + "/state.json"
write_file(path, '{"enabled": true}')
state = read_file(path)
Check paths received from Intent or message. Prohibit .., absolute paths and leaving the plugin directory. Write JSON atomically if the file is important for state recovery.
devgram.android_utils
| Function | Contract |
|---|
run_on_ui_thread(func, delay=0) | Runs a callback in the UI queue; delay is specified in milliseconds. |
run_on_queue(func) | Moves files, network and computation to the background queue. |
log(data) | Writes a line with the DevGram prefix to FileLog. |
copy_to_clipboard(text) | Copies text via the system clipboard. |
OnClickListener(fn) | Creates a Java listener for Android View. |
OnLongClickListener(fn) | Callback should return boolean. |
from devgram.android_utils import run_on_queue, run_on_ui_thread
def refresh(self):
run_on_queue(self._load)
def _load(self):
result = read_remote_data()
run_on_ui_thread(lambda: self.bulletin("Данные обновлены"))
devgram.intents
register(callback, action=None, scheme=None, host=None, path=None, query=None, category=None, flags=0, priority=0) registers an incoming route. The higher priority is checked first. Callback receives IntentContext with margins intent, action, uri, parsed And query. Return strictly Trueto stop dispatch.
from devgram.intents import register, open_uri
def on_load(self):
self._route = register(self._open, scheme="devgram", host="plugin", priority=100)
def _open(self, context):
item = context.query.get("id", [None])[0]
if not item:
return False
self.bulletin("Открыт объект " + item)
return True
def on_unload(self):
self._route.unhandle()
open_uri(uri) passes the URI to Android resolver, and send(intent) launches an explicitly created Intent. Don't run untested schemas from remote data.
devgram.hook_utils
Reflection API includes find_class(), get_private_field(), set_private_field(), as well as static options. This is a low-level API: the names of private fields change between Telegram versions. Wrap lookup in error handling and declare the minimum version of DevGram.
devgram.ui
Settings are built from Header, Switch, Input, Selector, Text And Button. Dialogs are created by a chain of methods AlertDialogBuilder: title, message, positive, negative and neutral buttons.
from devgram.ui import AlertDialogBuilder
AlertDialogBuilder() \
.set_title("Удалить данные?") \
.set_message("Действие нельзя отменить") \
.set_positive_button("Удалить", self.clear_data) \
.set_negative_button("Отмена") \
.show()
Threads and lifecycle
- IN
on_load() only register resources and run short operations. - Change all Android Views and notifications to UI thread.
- Execute files, JSON, network and large loops in queue.
- IN
on_unload() close observers, intent handles, hooks and pills. - Cleanup must be idempotent: calling it again should not fail.
- 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_utilsAndroid Utils
Detailed help on the module android_utils, verified with the sources of the current DevGram runtime.
CompatibilityThe following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.
Functions
| Name | Signature | Purpose |
|---|
run_on_ui_thread | run_on_ui_thread(func, delay=0) | Working function of the module; check the return value and execution flow. |
run_on_queue | run_on_queue(func) | Working function of the module; check the return value and execution flow. |
log | log(data) | Working function of the module; check the return value and execution flow. |
copy_to_clipboard | copy_to_clipboard(text) | Working function of the module; check the return value and execution flow. |
R | R(fn) | Working function of the module; check the return value and execution flow. |
OnClickListener | OnClickListener(fn) | Working function of the module; check the return value and execution flow. |
OnLongClickListener | OnLongClickListener(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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · client_utilsClient Utils
Detailed help on the module client_utils, verified with the sources of the current DevGram runtime.
CompatibilityThe following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.
Functions
| Name | Signature | Purpose |
|---|
send_text | send_text(peer, text, account=None) | Working function of the module; check the return value and execution flow. |
send_formatted_text | send_formatted_text(peer, text, entities, account=None) | Working function of the module; check the return value and execution flow. |
get_messages_controller | get_messages_controller(account=None) | Working function of the module; check the return value and execution flow. |
get_user_config | get_user_config(account=None) | Working function of the module; check the return value and execution flow. |
get_connections_manager | get_connections_manager(account=None) | Working function of the module; check the return value and execution flow. |
get_account_instance | get_account_instance(account=None) | Working function of the module; check the return value and execution flow. |
get_send_messages_helper | get_send_messages_helper(account=None) | Working function of the module; check the return value and execution flow. |
get_media_data_controller | get_media_data_controller(account=None) | Working function of the module; check the return value and execution flow. |
get_contacts_controller | get_contacts_controller(account=None) | Working function of the module; check the return value and execution flow. |
get_messages_storage | get_messages_storage(account=None) | Working function of the module; check the return value and execution flow. |
get_notification_center | get_notification_center(account=None) | Working function of the module; check the return value and execution flow. |
get_file_loader | get_file_loader(account=None) | Working function of the module; check the return value and execution flow. |
get_media_controller | get_media_controller() | Working function of the module; check the return value and execution flow. |
get_notifications_controller | get_notifications_controller(account=None) | Working function of the module; check the return value and execution flow. |
get_notifications_settings | get_notifications_settings(account=None) | Working function of the module; check the return value and execution flow. |
get_location_controller | get_location_controller(account=None) | Working function of the module; check the return value and execution flow. |
get_secret_chat_helper | get_secret_chat_helper(account=None) | Working function of the module; check the return value and execution flow. |
get_download_controller | get_download_controller(account=None) | Working function of the module; check the return value and execution flow. |
get_last_fragment | get_last_fragment() | Working function of the module; check the return value and execution flow. |
observe | observe(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
| Method | Signature | Contract |
|---|
didReceivedNotification | didReceivedNotification(self, notification_id, account, args) | Check argument types and release created resources when unloading the plugin. |
ObserverHandle
Public class DevGram SDK.
Methods
| Method | Signature | Contract |
|---|
__init__ | __init__(self, center, delegate, notification_id) | Check argument types and release created resources when unloading the plugin. |
close | close(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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · dev_serverDev Server
Detailed help on the module dev_server, verified with the sources of the current DevGram runtime.
CompatibilityThe following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.
Functions
| Name | Signature | Purpose |
|---|
_connect | _connect(platform, host, port, wait) | Working function of the module; check the return value and execution flow. |
start_debugger | start_debugger(platform='vscode', host='127.0.0.1', port=5678, wait=False) | Connect to a debugger exposed by the computer through adb reverse. |
stop_debugger | stop_debugger() | Working function of the module; check the return value and execution flow. |
debugger_status | debugger_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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · file_utilsFile Utils
Detailed help on the module file_utils, verified with the sources of the current DevGram runtime.
CompatibilityThe following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.
Functions
| Name | Signature | Purpose |
|---|
ensure_dir_exists | ensure_dir_exists(path) | Working function of the module; check the return value and execution flow. |
list_dir | list_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_file | read_file(path) | Working function of the module; check the return value and execution flow. |
write_file | write_file(path, content) | Working function of the module; check the return value and execution flow. |
read_file_bytes | read_file_bytes(path) | Working function of the module; check the return value and execution flow. |
write_file_bytes | write_file_bytes(path, content) | Working function of the module; check the return value and execution flow. |
delete_file | delete_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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · hook_utilsHook Utils
Detailed help on the module hook_utils, verified with the sources of the current DevGram runtime.
CompatibilityThe following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.
Functions
| Name | Signature | Purpose |
|---|
find_class | find_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_field | get_private_field(obj, name) | Working function of the module; check the return value and execution flow. |
set_private_field | set_private_field(obj, name, value) | Working function of the module; check the return value and execution flow. |
get_static_private_field | get_static_private_field(clazz, name) | Working function of the module; check the return value and execution flow. |
set_static_private_field | set_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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · intentsIntents
Detailed help on the module intents, verified with the sources of the current DevGram runtime.
CompatibilityThe following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.
Functions
| Name | Signature | Purpose |
|---|
register | register(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. |
dispatch | dispatch(intent) | Working function of the module; check the return value and execution flow. |
open_uri | open_uri(uri) | Open a URI using Android's resolver. |
send | send(intent) | Launch an explicitly constructed Android Intent. |
IntentContext
Public class DevGram SDK.
Methods
| Method | Signature | Contract |
|---|
__init__ | __init__(self, intent) | Check argument types and release created resources when unloading the plugin. |
HandlerHandle
Public class DevGram SDK.
Methods
| Method | Signature | Contract |
|---|
__init__ | __init__(self, entry) | Check argument types and release created resources when unloading the plugin. |
unhandle | unhandle(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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · text_formattingText Formatting
Detailed help on the module text_formatting, verified with the sources of the current DevGram runtime.
CompatibilityThe following symbols are considered public. Before publishing, check for API availability in the target version of DevGram and handle Java bridge errors.
Functions
| Name | Signature | Purpose |
|---|
to_tlrpc_entities | to_tlrpc_entities(entities) | Convert Entity objects/dicts to Java ArrayList<TLRPC.MessageEntity>. |
add_surrogates | add_surrogates(text) | Working function of the module; check the return value and execution flow. |
remove_surrogates | remove_surrogates(text) | Working function of the module; check the return value and execution flow. |
utf16_length | utf16_length(text) | Working function of the module; check the return value and execution flow. |
parse_html | parse_html(text) | Working function of the module; check the return value and execution flow. |
parse_markdown | parse_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_text | parse_text(text, parse_mode=None) | Working function of the module; check the return value and execution flow. |
escape_markdown | escape_markdown(text) | Working function of the module; check the return value and execution flow. |
escape_html | escape_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
| Method | Signature | Contract |
|---|
__init__ | __init__(self) | Check argument types and release created resources when unloading the plugin. |
length | length(self) | Check argument types and release created resources when unloading the plugin. |
handle_data | handle_data(self, data) | Check argument types and release created resources when unloading the plugin. |
handle_starttag | handle_starttag(self, tag, attrs) | Check argument types and release created resources when unloading the plugin. |
handle_endtag | handle_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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · ui.alertUi.Alert
Detailed help on the module ui.alert, verified with the sources of the current DevGram runtime.
CompatibilityThe 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
| Method | Signature | Contract |
|---|
__init__ | __init__(self, context=None, progress_style=0, resources_provider=None) | Check argument types and release created resources when unloading the plugin. |
set_title | set_title(self, value) | Check argument types and release created resources when unloading the plugin. |
set_message | set_message(self, value) | Check argument types and release created resources when unloading the plugin. |
set_positive_button | set_positive_button(self, text, listener=None) | Check argument types and release created resources when unloading the plugin. |
set_negative_button | set_negative_button(self, text, listener=None) | Check argument types and release created resources when unloading the plugin. |
set_neutral_button | set_neutral_button(self, text, listener=None) | Check argument types and release created resources when unloading the plugin. |
create | create(self) | Check argument types and release created resources when unloading the plugin. |
show | show(self) | Check argument types and release created resources when unloading the plugin. |
dismiss | dismiss(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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · ui.bulletinUi.Bulletin
Detailed help on the module ui.bulletin, verified with the sources of the current DevGram runtime.
CompatibilityThe 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
| Method | Signature | Contract |
|---|
_show | _show(kind, message, duration, button, callback) | Check argument types and release created resources when unloading the plugin. |
show_info | show_info(cls, message, fragment=None, duration=DURATION_LONG, button=None, callback=None) | Check argument types and release created resources when unloading the plugin. |
show_success | show_success(cls, message, fragment=None, duration=DURATION_SHORT, button=None, callback=None) | Check argument types and release created resources when unloading the plugin. |
show_error | show_error(cls, message, fragment=None, duration=DURATION_LONG, button=None, callback=None) | Check argument types and release created resources when unloading the plugin. |
show | show(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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Verified API · ui.settingsUi.Settings
Detailed help on the module ui.settings, verified with the sources of the current DevGram runtime.
CompatibilityThe 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
| Method | Signature | Contract |
|---|
__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_row | as_row(self) | Check argument types and release created resources when unloading the plugin. |
Header
Public class DevGram SDK.
Methods
| Method | Signature | Contract |
|---|
__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
- Default and explicit account.
- Empty, long and Unicode values.
- Call again after reload.
- Call after closing the screen.
- Network, file and RPC error.
Developer guidePlugin 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
- Send a file
.plugin to chat or channel. - Tap on the file → installation card (icon, name, version, author, verification icon) → “Install”.
- Management: Settings → DevGram → Plugins (toggle switches, settings, “Share”, deletion).
Plugins from the developer channel with the 🧩 icon are considered automatically verified. The installation card is the same for .plugin And .dgplugin.
.plugin or .dgplugin?
- .plugin — one file, without resources and dependencies. Just write and send. Suitable for most plugins.
- .dgplugin - archive with
manifest.json, modules, assets/, locales/ And wheels/. Needed when there is localization, images, several modules or pip dependencies (see the next section).
Developer guide.dgplugin format
The complete structure of the package to be installed, naming rules and checking the contents before publication. For a simple one file plugin use .plugin (section above).
Archive root
plugin.dgplugin
├── manifest.json
├── main.py
├── modules/
├── assets/
├── locales/
└── wheels/
.dgplugin is an archive, but the extension cannot be changed: DevGram recognizes it by the installer. Manifest and entrypoint should be in the root, and not in a subfolder that appears after archiving the directory.
Identifier
Use reverse-domain or username namespace: space.devgram.example. The ID is not a display name and does not change after the first release. Changing the ID will create a new set of settings and will be perceived by the directory as a different plugin.
Resources
File names are case sensitive. Use ASCII, hyphens or underscores. Do not include the original PSD, APK, keystore, tokens or temporary files. Optimize large images in advance.
Checking the archive
- Unpack the package into a temporary directory.
- Check manifest and import entrypoint.
- Make sure that wheels are compatible with the built-in Python version and ABI.
- Check for absence of absolute paths and traversal entries.
- Install the package over the previous version and separately on a clean installation.
Developer guideThreads 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 guideData and Migrations
How to store plugin settings and files, survive updates and recover from a corrupted state.
Settings
get_setting() And set_setting() suitable for small scalar values. Convert booleans and numbers explicitly because bridge can return a string representation. For structured data, use a JSON file in private storage.
Schema version
def migrate(self):
version = int(self.get_setting("schema", "0"))
if version < 1:
self.set_setting("enabled", "1")
version = 1
if version < 2:
migrate_cache_file(self.storage_path("state.json"))
version = 2
self.set_setting("schema", str(version))
Migrations are performed sequentially and must be idempotent. Do not write a new schema version until the step has completed successfully.
Atomic write
First write new JSON to a temporary file, sync and replace the main file. If there is a read error, save the damaged file for diagnostics and run with safe defaults.
Secrets
The package and private storage are not protected vault. Do not put bot tokens and persistent keys in the plugin. If the service requires authorization, use short-lived user tokens and the ability to revoke them.
Developer guideTL 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 guideInterface 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 guideReliable 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 guideTesting the plugin
Matrix of checks before publication: clean installation, update, accounts, themes, lifecycle and error recovery.
Minimum matrix
| Scenario | What to check |
|---|
| Clean install | Manifest, defaults, first launch, permissions. |
| Update | Migrations, saving settings, replacing resources. |
| Disable/enable | Observers are not duplicated, the UI disappears and comes back. |
| Hot reload | Python modules have been cleaned, hooks and handles have been replaced. |
| Uninstall | No callbacks or registered pills. |
| Safe mode | The 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 guidePublication and updates
Versioning, changelog, compatibility, archive verification and secure release to the DevGram directory.
Version
Use a sequential scheme and increment the version for each package downloaded. Do not publish different archives under the same version: users and moderators will not be able to unambiguously check the content.
Changelog
Write only user changes: new actions, changed behavior, bug fixes and important restrictions. Separately specify breaking changes and the required minimum client version.
Examination
- Build a package from a clean checkout.
- View the list of archive files.
- Scan tokens, keys and local paths.
- Install the release package on a clean copy of DevGram.
- Check the signature or checksum of the published file.
Resubmission
After rejection, correct the reason given and increase the version. Don't change the plugin ID. If the plugin uses dangerous hooks, describe the purpose and fallback for the incompatible version.
Rollback
Keep the previous stable package and rollback-compatible migrations whenever possible. Never delete user data just because a new version couldn't parse it.
Developer guideSecurity 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 guideCompatibility 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 guideDiagnostics 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 guideGlossary
Key terms of DevGram Plugin SDK and differences between similar objects.
Account index
Local account number in the application. Not equal to Telegram user ID and is used to select controller.
Dialog ID
A conversation ID that can represent a user, group, or channel. Don't treat the number sign as your only type check.
TL object
Java type representation from Telegram schema. Fields and subclasses depend on the current version of core.
Observer
Subscribe to NotificationCenter. The returned handle belongs to the plugin and is closed upon unload.
Hook
Intercepting a Java method before or after the original call. It differs from the public plugin event in that it is more strictly dependent on the implementation.
Entity
Telegram text formatting range. Offset and length are measured in UTF-16 code units.
Safe mode
Recovery mode, in which problematic callbacks are not launched until manual action by the user.
Hot reload
Unloading instance and Python modules and then loading a new version without completely restarting the client.
Pill
A compact interactive Pill Stack element registered with a specific plugin ID.
Builder
Experimental DevGram-native specification of a high-level project structure. Not all of the described facades are included in the stable runtime.
Reference indexDeep reference
Navigation page for the complete DevGram SDK reference.
Stable Python API
Start with SDK contracts, then open the page for the desired verified module. It lists the actual classes, functions, signatures, and methods of the current assembly.
How-To Guides
For a large plugin, use the pages about architecture, flows, migrations, testing, and security.
Low-level runtime
Java hooks, reflection, class proxy and TL API depend on Telegram core. Check for API availability and provide fallback.
Experimental Builder
Builder describes the target structure of the project. For executable code, verified modules remain the source of truth.
BasePlugin: contract and state
BasePlugin created by the loader once per package. Fields id, name, version, author, description And icon used by the directory and plugin manager. Don't change id after publication: it is the key for settings, private storage and registered hooks.
| Method | Call | Contract |
|---|
on_load() | After import | Registration hooks, observers, pills. Don't block the UI. |
on_unload() | Before reload/remove | Remove everything created by the plugin. Recall is acceptable. |
on_send_message(text) | Before outgoing text | Return new text, None without change or False to cancel. |
on_receive_message(text) | After incoming text | Observation and logic; message change is not guaranteed. |
on_update(update) | TL update | Java object TLRPC.Update. Check the type via getClass().getSimpleName(). |
on_setting_changed(key,value) | From native settings | The 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.
| Function | Returns | When to use |
|---|
send_text(peer,text,account) | None | Simple text. |
send_formatted_text(peer,text,entities,account) | None | Text with native MessageEntity. |
get_messages_controller(account) | MessagesController | Dialogues, users and chats. |
get_connections_manager(account) | ConnectionsManager | Low-level TL queries. |
get_messages_storage(account) | MessagesStorage | Reading the local cache. Don't block main. |
get_media_data_controller(account) | MediaDataController | Media and stickers. |
get_contacts_controller(account) | ContactsController | Contacts and username. |
get_file_loader(account) | FileLoader | Loading local media files. |
get_download_controller(account) | DownloadController | Download 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
| Class | kind | Fields |
|---|
Header(text) | header | Section title. |
Switch(key,text,default,on_change) | switch | Boolean is stored as 1/0. |
Input(key,text,default,on_change) | text | String value. |
Selector(key,text,items,on_change) | selector | List of options via separator. |
Button(key,text) | button | Calling 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
- Manifest is valid, ID is stable, version has been increased.
- No bot tokens, Firebase keys, signing keys and debug logs.
- Verified Android 7+ and at least two accounts.
- Install, update, disable, enable, reload and uninstall have been checked.
- After a crash, safe mode restores the application.
- Observer, hook, intent and pill are cleared in unload.
Extended referenceAdvanced 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
| Region | Examples | Purpose |
|---|
| Screens | LaunchActivity, BaseFragment, ChatActivity | Navigation and current context. |
| Cells | Dialog, message and settings cells | Drawing list strings; Depends heavily on the version. |
| Data | MessagesController, MessagesStorage | Cache, users, chats and messages. |
| Net | ConnectionsManager | TL requests and connection state. |
How to examine a class and method
- Find a custom action that triggers the desired behavior.
- Find the corresponding Java class in the sources of the current version.
- Check overload, parameter types and return type.
- Install a temporary diagnostic hook without changing the behavior.
- 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
- Low-level functions have feature detection.
- Every hook and listener is removed.
- View works in both themes and does not overlap input.
- Videos and shaders have fallback.
- Two accounts do not mix controllers.
- Logs do not contain messages or tokens.
- The plugin survives reload and safe mode.