Здравствуйте, дорогие читатели!
В данной статье мы разберём, как работают плагины, а так-же напишем пару собственных.
В данной статье мы разберём, как работают плагины, а так-же напишем пару собственных.
ExteraGram - неофициальный Telegram клиент, который добавляет многие возможности, среди которых имеется собственный движок для плагинов.
AyuGram построен на базе ExteraGram, и сохраняет функциональность данного клиента.
Каждый плагин пишется на языке Python, а так-же придерживается определённого синтаксиса. Подробнее об этом расписано ниже.
Метаданные - это глобальные переменные, которые принято писать в начале файла (после импортов).
Пример заполненных метаданных:
__id__ = 'my_plugin'
__name__ = 'Plugin Name'
__description__ = 'Amazing plugin!'
__version__ = '1.2.3'
__icon__ = 'Foxfox8/3'
__min_version__ = '11.12.0'
python
Сами по себе эти данные ничего не делают, но даже такой плагин уже можно установить!
__id__
- Уникальный идентификатор плагина. Используется, чтобы клиент мог различать плагины между собой. Должен быть от 2 до 32 символов в длину, и содержать только символы a-z, 0-9, а так-же - и _.
__id__
- Уникальный идентификатор плагина. Используется, чтобы клиент мог различать плагины между собой. Должен быть от 2 до 32 символов в длину, и содержать только символы a-z, 0-9, а так-же - и _.
__name__
- Имя плагина, отображается при установке.
__name__
- Имя плагина, отображается при установке.
__description__
- Описание плагина, отображается при установке.
__description__
- Описание плагина, отображается при установке.
__author__
- Имя автора, отображается при установке.
__author__
- Имя автора, отображается при установке.
__version__
- Версия плагина. Для корректного обновления плагина необходимо увеличивать её, чтобы клиент знал, что это новая версия.
Рекомендуется указывать версию в формате SemVer, то есть: 1.0.0, 1.0.1 и тп.
- Версия плагина. Для корректного обновления плагина необходимо увеличивать её, чтобы клиент знал, что это новая версия.
Рекомендуется указывать версию в формате SemVer, то есть: 1.0.0, 1.0.1 и тп.
__icon__
- Иконка плагина. ExteraGram позволяет использовать стикеры в качестве кастомных иконок.
__icon__
- Иконка плагина. ExteraGram позволяет использовать стикеры в качестве кастомных иконок.
Для примера я взял стикерпак с лисичками. Ссылка на пак:
t.me/addstickers/Foxfox8
Нас отсюда интересует только строка Foxfox8.
Выбираем понравившийся стикер, в моём случае четвёртый по счёту:
Если отсчитывать с нуля, он будет иметь номер 3.
Подставляем эти данные и получаем такое значение:
__icon__ = 'Foxfox8/3'
t.me/addstickers/Foxfox8
Нас отсюда интересует только строка Foxfox8.
Выбираем понравившийся стикер, в моём случае четвёртый по счёту:
Если отсчитывать с нуля, он будет иметь номер 3.
Подставляем эти данные и получаем такое значение:
__icon__ = 'Foxfox8/3'
__min_version__
- Минимальная версия клиента, так-же придерживается формата SemVer. Необязательное поле!
Пример:
11.12.0 - Плагин установится только на версию клиента выше или равную 11.12.0.
Зачем это нужно?
Новые версии Telegram могут отличаться, где-то меняется интерфейс, где-то функционал и прочее, разумеется это может ломать логику новых плагинов, и они банально могут не запускаться на более старых версиях клиента.
- Минимальная версия клиента, так-же придерживается формата SemVer. Необязательное поле!
Пример:
11.12.0 - Плагин установится только на версию клиента выше или равную 11.12.0.
Зачем это нужно?
Новые версии Telegram могут отличаться, где-то меняется интерфейс, где-то функционал и прочее, разумеется это может ломать логику новых плагинов, и они банально могут не запускаться на более старых версиях клиента.
Плагин не будет установлен, а вы увидите ошибку:
Plugin requires app version 12.12.12 or higher. Current is <текущая_версия_клиента>
Plugin requires app version 12.12.12 or higher. Current is <текущая_версия_клиента>
Пример заполненных метаданных:
__id__ = 'my_plugin'
__name__ = 'Plugin Name'
__description__ = 'Amazing plugin!'
__version__ = '1.2.3'
__icon__ = 'Foxfox8/3'
__min_version__ = '11.12.0'
python
Код:
__id__ = 'my_plugin'
__name__ = 'Plugin Name'
__description__ = 'Amazing plugin!'
__version__ = '1.2.3'
__icon__ = 'Foxfox8/3'
__min_version__ = '11.12.0'
Сами по себе эти данные ничего не делают, но даже такой плагин уже можно установить!
Для того, чтобы придать нашему плагину функционал, нужно установить библиотеку exteragram-utils:
python -m pip install exteragram-utils
Данная библиотека добавляет модули base_plugin, hook_utils и android_utils, о них поговорим позже.
Из модуля base_plugin необходимо импортировать класс BasePlugin.
Это основа, от которой наследуется класс нашего плагина:
from base_plugin import BasePlugin
__name__ = 'custom icon!'
__description__ = 'Plugin with custom icon'
__id__ = 'test_custom_icon'
__author__ = 'reassurance'
__version__ = '1.0.0'
__icon__ = 'Foxfox8/3'
__min_version__ = '11.12.0'
class MyPlugin(BasePlugin):
def __init__(self):
super().__init__()
def on_plugin_load(self):
pass
def on_plugin_unload(self):
pass
python
Метод __init__ это конструктор, в котором вы добавляете хуки, кнопки в интерфейс, вешаете на них обработчики и так далее.
Методы on_plugin_load и on_plugin_unload будут вызваны каждый раз, когда плагин будет загружен/выгружен.
python -m pip install exteragram-utils
Данная библиотека добавляет модули base_plugin, hook_utils и android_utils, о них поговорим позже.
Из модуля base_plugin необходимо импортировать класс BasePlugin.
Это основа, от которой наследуется класс нашего плагина:
from base_plugin import BasePlugin
__name__ = 'custom icon!'
__description__ = 'Plugin with custom icon'
__id__ = 'test_custom_icon'
__author__ = 'reassurance'
__version__ = '1.0.0'
__icon__ = 'Foxfox8/3'
__min_version__ = '11.12.0'
class MyPlugin(BasePlugin):
def __init__(self):
super().__init__()
def on_plugin_load(self):
pass
def on_plugin_unload(self):
pass
python
Код:
from base_plugin import BasePlugin
__name__ = 'custom icon!'
__description__ = 'Plugin with custom icon'
__id__ = 'test_custom_icon'
__author__ = 'reassurance'
__version__ = '1.0.0'
__icon__ = 'Foxfox8/3'
__min_version__ = '11.12.0'
class MyPlugin(BasePlugin):
def __init__(self):
super().__init__()
def on_plugin_load(self):
pass
def on_plugin_unload(self):
pass
Метод __init__ это конструктор, в котором вы добавляете хуки, кнопки в интерфейс, вешаете на них обработчики и так далее.
Методы on_plugin_load и on_plugin_unload будут вызваны каждый раз, когда плагин будет загружен/выгружен.
УСТАНОВКА ПЛАГИНА
После того, как вы собрали каркас и указали все необходимые метаданные, плагин готов к установке.
Для этого переименуйте расширение файла в .plugin, затем загрузите файл в избранное (или любой другой чат), после чего кликните по нему, клиент сам предложит установить плагин.
Если вы хотите обновить существующий плагин, просто установите его поверх старого (клиент будет ориентироваться на его версию).
УДАЛЕНИЕ ПЛАГИНА
Для того, чтобы удалить плагин, откройте Settings > ExteraGram Preferences > Plugins, выберите ваш плагин и нажмите кнопку Удалить.
После того, как вы собрали каркас и указали все необходимые метаданные, плагин готов к установке.
Для этого переименуйте расширение файла в .plugin, затем загрузите файл в избранное (или любой другой чат), после чего кликните по нему, клиент сам предложит установить плагин.
Если вы хотите обновить существующий плагин, просто установите его поверх старого (клиент будет ориентироваться на его версию).
УДАЛЕНИЕ ПЛАГИНА
Для того, чтобы удалить плагин, откройте Settings > ExteraGram Preferences > Plugins, выберите ваш плагин и нажмите кнопку Удалить.
Для отладки плагина можно воспользоваться методом BasePlugin.log.
Данный метод принимает единственный аргумент - строку, в последствии можно прочитать историю логов при помощи adb.
Данный метод принимает единственный аргумент - строку, в последствии можно прочитать историю логов при помощи adb.
После того, как мы разобрались с основами, предлагаю ознакомиться с более подробной информацией.
Кастомные кнопки регистрируются через метод BasePlugin.add_menu_item.
Он принимает единственный аргумент MenuItemData (импортируется из base_plugin), который содержит поля:
- menu_type - Тип меню (см. ниже), относится к MenuItemType (импортируется из base_plugin)
- text - Текст, отображаемый на кнопке
- on_click - Функция-обработчик, вызывается при клике на кнопку
- icon - Иконка кнопки (см. спойлер "Иконки")
Он принимает единственный аргумент MenuItemData (импортируется из base_plugin), который содержит поля:
- menu_type - Тип меню (см. ниже), относится к MenuItemType (импортируется из base_plugin)
- text - Текст, отображаемый на кнопке
- on_click - Функция-обработчик, вызывается при клике на кнопку
- icon - Иконка кнопки (см. спойлер "Иконки")
Кнопка добавляется в нижнюю часть бокового меню.
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.DRAWER_MENU,
text="Drawer",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.DRAWER_MENU,
text="Drawer",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
Код:
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.DRAWER_MENU,
text="Drawer",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
Кнопка добавляется в попап-меню, которое можно увидеть при нажатии на сообщение.
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.MESSAGE_CONTEXT_MENU,
text="Message Context",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.MESSAGE_CONTEXT_MENU,
text="Message Context",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
Код:
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.MESSAGE_CONTEXT_MENU,
text="Message Context",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
Кнопка добавляется в меню открытого профиля.
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="Profile Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="Profile Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
Код:
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="Profile Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
Кнопка добавляется в меню открытого чата.
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.CHAT_ACTION_MENU,
text="Chat Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.CHAT_ACTION_MENU,
text="Chat Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
python
Код:
from typing import Any, Dict
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'my_menu'
__name__ = 'test menu'
class MyMenuPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.CHAT_ACTION_MENU,
text="Chat Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def on_plugin_unload(self):
pass
def handler(self, context: Dict[str, Any]):
pass
Как вы могли заметить по коду, у каждого обработчика есть аргумент context.
Это переменная, которая хранит информацию о контексте, например для MenuItemType.MESSAGE_CONTEXT_MENU там будет объект message с информацией о сообщении, с которым мы работаем:
message = context.get("message")
# message.getId() - айди сообщения
# message.getSenderId() - айди отправителя
# message.messageText - текст сообщения
python
Более подробная информация по контексту:
Это переменная, которая хранит информацию о контексте, например для MenuItemType.MESSAGE_CONTEXT_MENU там будет объект message с информацией о сообщении, с которым мы работаем:
message = context.get("message")
# message.getId() - айди сообщения
# message.getSenderId() - айди отправителя
# message.messageText - текст сообщения
python
Код:
message = context.get("message")
# message.getId() - айди сообщения
# message.getSenderId() - айди отправителя
# message.messageText - текст сообщения
Более подробная информация по контексту:
fragment
- <org.telegram.ui.ChatActivity>
context
- <org.telegram.ui.LaunchActivity>
dialog_id
- Идентификатор текущего диалога
message
- <org.telegram.messenger.MessageObject>
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
account
- Внутренний идентификатор аккаунта в клиенте
- <org.telegram.ui.ChatActivity>
context
- <org.telegram.ui.LaunchActivity>
dialog_id
- Идентификатор текущего диалога
message
- <org.telegram.messenger.MessageObject>
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
account
- Внутренний идентификатор аккаунта в клиенте
fragment
- <org.telegram.ui.ChatActivity>
context
- <org.telegram.ui.LaunchActivity>
dialog_id
- Идентификатор текущего диалога
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
account
- Внутренний идентификатор аккаунта в клиенте
- <org.telegram.ui.ChatActivity>
context
- <org.telegram.ui.LaunchActivity>
dialog_id
- Идентификатор текущего диалога
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
account
- Внутренний идентификатор аккаунта в клиенте
fragment
- <org.telegram.ui.ProfileActivity>
context
- <org.telegram.ui.LaunchActivity>
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
userFull
- <org.telegram.tgnet.TLRPC$TL_userFull>
account
- Внутренний идентификатор аккаунта в клиенте
- <org.telegram.ui.ProfileActivity>
context
- <org.telegram.ui.LaunchActivity>
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
userFull
- <org.telegram.tgnet.TLRPC$TL_userFull>
account
- Внутренний идентификатор аккаунта в клиенте
context
- <org.telegram.ui.LaunchActivity>
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
account
- Внутренний идентификатор аккаунта в клиенте
- <org.telegram.ui.LaunchActivity>
user
- <org.telegram.tgnet.TLRPC$TL_user>
userId
- Идентификатор собеседника (в текущем диалоге)
account
- Внутренний идентификатор аккаунта в клиенте
Данные классы полностью повторяют оригинальные на Java из официального репозитория Telegram.
Пример, Python класс <org.telegram.messenger.MessageObject> имеет все поля и методы оригинального класса.
Пример, Python класс <org.telegram.messenger.MessageObject> имеет все поля и методы оригинального класса.
Это хук на стороне клиента, ДО отправки сообщения.
Для его использования в классе BasePlugin реализованы методы on_send_message_hook и add_on_send_message_hook, а так-же вспомогательные HookResult и HookStrategy.
Для того, чтобы активировать такой хук - необходимо вызвать add_on_send_message_hook при инициализации плагина:
def on_plugin_load(self):
self.add_on_send_message_hook()
python
Так-же необходимо переопределить метод on_send_message_hook (он будет вызываться при каждой отправке сообщения), данный метод должен возвращать HookResult:
def on_send_message_hook(self, account, params):
return HookResult()
python
Данный метод принимает аргументы account и params.
Изменяя params.message, мы можем влиять на содержимое сообщения, которое в конечном счёте дойдёт до серверов Telegram:
def on_send_message_hook(self, account, params):
if not isinstance(params.message, str):
return HookResult()
params.message = 'hooked message'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
python
В целом, здесь может быть любая логика - парсинг каких-либо данных, ping / pong и так далее, всё ограничивается лишь вашей фантазией.
Для его использования в классе BasePlugin реализованы методы on_send_message_hook и add_on_send_message_hook, а так-же вспомогательные HookResult и HookStrategy.
Для того, чтобы активировать такой хук - необходимо вызвать add_on_send_message_hook при инициализации плагина:
def on_plugin_load(self):
self.add_on_send_message_hook()
python
Код:
def on_plugin_load(self):
self.add_on_send_message_hook()
Так-же необходимо переопределить метод on_send_message_hook (он будет вызываться при каждой отправке сообщения), данный метод должен возвращать HookResult:
def on_send_message_hook(self, account, params):
return HookResult()
python
Код:
def on_send_message_hook(self, account, params):
return HookResult()
Данный метод принимает аргументы account и params.
Изменяя params.message, мы можем влиять на содержимое сообщения, которое в конечном счёте дойдёт до серверов Telegram:
def on_send_message_hook(self, account, params):
if not isinstance(params.message, str):
return HookResult()
params.message = 'hooked message'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
python
Код:
def on_send_message_hook(self, account, params):
if not isinstance(params.message, str):
return HookResult()
params.message = 'hooked message'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
HookStrategy.DEFAULT - Никаких изменений не вносится.
HookStrategy.MODIFY - Сообщение модифицируется тем, что мы передаём в HookResult (params).
HookStrategy.MODIFY_FINAL - Работает аналогично MODIFY, только имеет наивысший приоритет (по сравнению с другими плагинами), т.е другой плагин, использующий MODIFY для этого сообщения, не сможет как-либо изменить данные.
HookStrategy.CANCEL - Сообщение не отправляется. Полезно для юзер ботов, когда мы хотим выполнить какое-то действие, но не хотим, чтобы собеседник увидел об этом.
HookStrategy.MODIFY - Сообщение модифицируется тем, что мы передаём в HookResult (params).
HookStrategy.MODIFY_FINAL - Работает аналогично MODIFY, только имеет наивысший приоритет (по сравнению с другими плагинами), т.е другой плагин, использующий MODIFY для этого сообщения, не сможет как-либо изменить данные.
HookStrategy.CANCEL - Сообщение не отправляется. Полезно для юзер ботов, когда мы хотим выполнить какое-то действие, но не хотим, чтобы собеседник увидел об этом.
В целом, здесь может быть любая логика - парсинг каких-либо данных, ping / pong и так далее, всё ограничивается лишь вашей фантазией.
Речь идёт о методах send_text, send_audio, send_media и тп.
Они импортируется из модуля client_utils и позволяет отправлять сообщения прямо из вашего плагина.
Метод send_text имеет всего два аргумента:
peer - Идентификатор получателя
text - Текст сообщения
У методов send_audio, send_document и send_video их немного больше:
peer - Идентификатор получателя
file_path - Путь к медиа файлу
caption - Подпись под медиа (необязательно)
Метод send_photo похож на предыдущие, но имеет ещё один необязательный аргумент:
high_quality - Отправлять медиа в высоком качестве, по умолчанию false
Для примера я написал небольшой плагин, который отправляет получателю сообщение о том, что плагин был запущен:
from base_plugin import BasePlugin
from client_utils import send_text
__id__ = 'tg_hook'
__name__ = 'hook'
class HookPlugin(BasePlugin):
def on_plugin_load(self):
send_text(<ТУТ_БЫЛ_АЙДИ>, "plugin loaded")
python
Они импортируется из модуля client_utils и позволяет отправлять сообщения прямо из вашего плагина.
Метод send_text имеет всего два аргумента:
peer - Идентификатор получателя
text - Текст сообщения
У методов send_audio, send_document и send_video их немного больше:
peer - Идентификатор получателя
file_path - Путь к медиа файлу
caption - Подпись под медиа (необязательно)
Метод send_photo похож на предыдущие, но имеет ещё один необязательный аргумент:
high_quality - Отправлять медиа в высоком качестве, по умолчанию false
Для примера я написал небольшой плагин, который отправляет получателю сообщение о том, что плагин был запущен:
from base_plugin import BasePlugin
from client_utils import send_text
__id__ = 'tg_hook'
__name__ = 'hook'
class HookPlugin(BasePlugin):
def on_plugin_load(self):
send_text(<ТУТ_БЫЛ_АЙДИ>, "plugin loaded")
python
Код:
from base_plugin import BasePlugin
from client_utils import send_text
__id__ = 'tg_hook'
__name__ = 'hook'
class HookPlugin(BasePlugin):
def on_plugin_load(self):
send_text(<ТУТ_БЫЛ_АЙДИ>, "plugin loaded")
Это наиболее интересный и гибкий способ хука, но к сожалению он так-же является самым сложным, потому что требует анализа и глубокого понимания Java кода.
Я решил хукнуть метод
private void onGiftPermiumClicked()
у класса <org.telegram.ui.ProfileActivity> и отключить его, чтобы по нажатию на "Отправить подарок" ничего не происходило.
Начнём с того, что нам нужно найти нужный нам класс при помощи метода find_class:
profileActivity = find_class("org.telegram.ui.ProfileActivity")
python
Затем необходимо хукнуть сам метод при помощи BasePlugin.hook_method:
method = profileActivity.getClass().getDeclaredMethod("onGiftPermiumClicked")
self.hook_method(method, GiftHook(self))
python
Напишем вспомогательный класс GiftHook, который будет реализовывать функцию before_hooked_method:
class GiftHook(MethodHook):
def __init__(self, plg):
self.plg = plg
def before_hooked_method(self, param):
param.setResult(None)
python
param.setResult(None) моментально завершает выполнение функции, возвращая результат - None (по сути то же самое, что и void).
Помимо этого, можно изменять аргументы метода (рекомендую убедиться, что у метода они вообще имеются):
param.args[1] = 123
param.args[2] = "custom text"
python
А так-же подменять значение после выполнения метода (допустим, оригинальная функция возвращает число 150, а мы хотим чтобы было больше):
def after_hooked_method(self, param):
orig = param.getResult()
param.setResult(orig + 10000)
python
Бинго! Теперь после активации хука, каждый раз при нажатии на кнопку "Отправить подарок" ничего не происходит, потому что мы перехватили этот метод и отключили его)
Я решил хукнуть метод
private void onGiftPermiumClicked()
у класса <org.telegram.ui.ProfileActivity> и отключить его, чтобы по нажатию на "Отправить подарок" ничего не происходило.
Начнём с того, что нам нужно найти нужный нам класс при помощи метода find_class:
profileActivity = find_class("org.telegram.ui.ProfileActivity")
python
Код:
profileActivity = find_class("org.telegram.ui.ProfileActivity")
Затем необходимо хукнуть сам метод при помощи BasePlugin.hook_method:
method = profileActivity.getClass().getDeclaredMethod("onGiftPermiumClicked")
self.hook_method(method, GiftHook(self))
python
Код:
method = profileActivity.getClass().getDeclaredMethod("onGiftPermiumClicked")
self.hook_method(method, GiftHook(self))
Напишем вспомогательный класс GiftHook, который будет реализовывать функцию before_hooked_method:
class GiftHook(MethodHook):
def __init__(self, plg):
self.plg = plg
def before_hooked_method(self, param):
param.setResult(None)
python
Код:
class GiftHook(MethodHook):
def __init__(self, plg):
self.plg = plg
def before_hooked_method(self, param):
param.setResult(None)
param.setResult(None) моментально завершает выполнение функции, возвращая результат - None (по сути то же самое, что и void).
Помимо этого, можно изменять аргументы метода (рекомендую убедиться, что у метода они вообще имеются):
param.args[1] = 123
param.args[2] = "custom text"
python
Код:
param.args[1] = 123
param.args[2] = "custom text"
А так-же подменять значение после выполнения метода (допустим, оригинальная функция возвращает число 150, а мы хотим чтобы было больше):
def after_hooked_method(self, param):
orig = param.getResult()
param.setResult(orig + 10000)
python
Код:
def after_hooked_method(self, param):
orig = param.getResult()
param.setResult(orig + 10000)
from base_plugin import BasePlugin, MenuItemData, MenuItemType, MethodHook
from typing import Dict, Any
from hook_utils import find_class
__id__ = 'hook'
__name__ = 'hook ProfileActivity'
class GiftHook(MethodHook):
def __init__(self, plg):
self.plg = plg
def before_hooked_method(self, param):
param.setResult(None)
class HookPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="Profile Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def handler(self, context: Dict[str, Any]):
try:
profileActivity = find_class("org.telegram.ui.ProfileActivity")
if profileActivity:
method = profileActivity.getClass().getDeclaredMethod("onGiftPermiumClicked")
self.hook_method(method, GiftHook(self))
except Exception as e:
pass
python
from typing import Dict, Any
from hook_utils import find_class
__id__ = 'hook'
__name__ = 'hook ProfileActivity'
class GiftHook(MethodHook):
def __init__(self, plg):
self.plg = plg
def before_hooked_method(self, param):
param.setResult(None)
class HookPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="Profile Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def handler(self, context: Dict[str, Any]):
try:
profileActivity = find_class("org.telegram.ui.ProfileActivity")
if profileActivity:
method = profileActivity.getClass().getDeclaredMethod("onGiftPermiumClicked")
self.hook_method(method, GiftHook(self))
except Exception as e:
pass
python
Код:
from base_plugin import BasePlugin, MenuItemData, MenuItemType, MethodHook
from typing import Dict, Any
from hook_utils import find_class
__id__ = 'hook'
__name__ = 'hook ProfileActivity'
class GiftHook(MethodHook):
def __init__(self, plg):
self.plg = plg
def before_hooked_method(self, param):
param.setResult(None)
class HookPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="Profile Action",
on_click=self.handler,
icon="msg_addcontacts"
)
)
def handler(self, context: Dict[str, Any]):
try:
profileActivity = find_class("org.telegram.ui.ProfileActivity")
if profileActivity:
method = profileActivity.getClass().getDeclaredMethod("onGiftPermiumClicked")
self.hook_method(method, GiftHook(self))
except Exception as e:
pass
Бинго! Теперь после активации хука, каждый раз при нажатии на кнопку "Отправить подарок" ничего не происходит, потому что мы перехватили этот метод и отключили его)
Таким образом можно перехватить практически любые методы у любых Java классов и поменять их логику / возвращаемое значение.
При помощи BulletinHelper можно выводить сообщения, своего рода
alert
из JavaScript.
BulletinHelper.show_info принимает два аргумента:
message
- Отображаемый текст
fragment
- Объект Activity (например ProfileActivity или ChatActivity), указывать необязательно!
from ui.bulletin import BulletinHelper
# ...
BulletinHelper.show_info('some important info!', fragment)
python
В целом, fragment указывать необязательно, но если вы очень хотите получить текущий Activity, это можно сделать так:
from client_utils import get_last_fragment
# ...
fragment = get_last_fragment()
python
Помимо метода show_info так-же существуют show_success и show_error, они различаются только визуально.
alert
из JavaScript.
BulletinHelper.show_info принимает два аргумента:
message
- Отображаемый текст
fragment
- Объект Activity (например ProfileActivity или ChatActivity), указывать необязательно!
from ui.bulletin import BulletinHelper
# ...
BulletinHelper.show_info('some important info!', fragment)
python
Код:
from ui.bulletin import BulletinHelper
# ...
BulletinHelper.show_info('some important info!', fragment)
В целом, fragment указывать необязательно, но если вы очень хотите получить текущий Activity, это можно сделать так:
from client_utils import get_last_fragment
# ...
fragment = get_last_fragment()
python
Код:
from client_utils import get_last_fragment
# ...
fragment = get_last_fragment()
Помимо метода show_info так-же существуют show_success и show_error, они различаются только визуально.
Вы можете создавать, читать и удалять файлы в директории клиента:
/data/data/<PACKAGE>/files/plugins/
Получить имя пакета можно при помощи метода ApplicationLoader.applicationContext.getPackageName.
from org.telegram.messenger import ApplicationLoader
package = ApplicationLoader.applicationContext.getPackageName()
python
/data/data/<PACKAGE>/files/plugins/
Получить имя пакета можно при помощи метода ApplicationLoader.applicationContext.getPackageName.
from org.telegram.messenger import ApplicationLoader
package = ApplicationLoader.applicationContext.getPackageName()
python
Код:
from org.telegram.messenger import ApplicationLoader
package = ApplicationLoader.applicationContext.getPackageName()
Это можно сделать, переопределив метод create_settings в основном классе:
def create_settings(self):
return [
Header(text="Заголовок"),
Input(key="param", text="Поле ввода", default="", subtext="Описание", icon="msg_link"),
Divider()
]
python
Данный метод возвращает массив из частей интерфейса:
Получить значение того или иного параметра можно через метод get_setting:
self.get_setting('param', 'default_value')
Так-же принудительно устанавливать свои параметры можно через метод set_setting:
self.set_setting('param', 'value')
def create_settings(self):
return [
Header(text="Заголовок"),
Input(key="param", text="Поле ввода", default="", subtext="Описание", icon="msg_link"),
Divider()
]
python
Код:
def create_settings(self):
return [
Header(text="Заголовок"),
Input(key="param", text="Поле ввода", default="", subtext="Описание", icon="msg_link"),
Divider()
]
Данный метод возвращает массив из частей интерфейса:
Заголовок, необходимо указать аргумент
text
.
text
.
Поле ввода, необходимо указать:
key
- Название переменной (для get_setting)
text
- Отображаемый текст (основной)
default
- Значение по умолчанию
subtext
- Описание
icon
- Название иконки
on_change
- Функция-обработчик, вызывается каждый раз при изменении значения
key
- Название переменной (для get_setting)
text
- Отображаемый текст (основной)
default
- Значение по умолчанию
subtext
- Описание
icon
- Название иконки
on_change
- Функция-обработчик, вызывается каждый раз при изменении значения
Поле с текстом, используется для вывода какой-либо дополнительной информации о вашем плагине:
text
- Отображаемый текст
icon
- Название иконки
on_click
- Функция-обработчик, она будет вызвана при нажатии на текст
text
- Отображаемый текст
icon
- Название иконки
on_click
- Функция-обработчик, она будет вызвана при нажатии на текст
Разделитель:
text
- Отображаемый текст (необязательно)
text
- Отображаемый текст (необязательно)
Элемент, который позволяет выбрать из нескольких вариантов. Аргументы:
key
- Название переменной (для get_setting)
text
- Отображаемый текст
default
- Индекс варианта по умолчанию
items
- Массив из вариантов. Пример: ["one", "two", "three"]
icon
- Название иконки
on_change
- Функция-обработчик, вызывается каждый раз при изменении значения
key
- Название переменной (для get_setting)
text
- Отображаемый текст
default
- Индекс варианта по умолчанию
items
- Массив из вариантов. Пример: ["one", "two", "three"]
icon
- Название иконки
on_change
- Функция-обработчик, вызывается каждый раз при изменении значения
Элемент переключатель (вкл/выкл), создан для типа данных Boolean. Аргументы:
key
- Название переменной (для get_setting)
text
- Отображаемый текст
default
- Значение по умолчанию. False либо True
subtext
- Описание
icon
- Название иконки
on_change
- Функция-обработчик, вызывается каждый раз при изменении значения
key
- Название переменной (для get_setting)
text
- Отображаемый текст
default
- Значение по умолчанию. False либо True
subtext
- Описание
icon
- Название иконки
on_change
- Функция-обработчик, вызывается каждый раз при изменении значения
self.get_setting('param', 'default_value')
Так-же принудительно устанавливать свои параметры можно через метод set_setting:
self.set_setting('param', 'value')
А теперь напишем собственные плагины!
Данный юзер-бот при отправке
.id
выдаст информацию о текущем диалоге (его идентификатор).
Напишем заглушку с on_send_message_hook для того, чтобы посмотреть, что это вообще за объект params:
import json
from base_plugin import BasePlugin, HookResult
__id__ = 'chat_id'
__name__ = 'chat id viewer'
class ChatIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_on_send_message_hook()
def on_send_message_hook(self, account, params):
self.log(json.dumps(params, indent=2, default=str))
return HookResult()
python
Оказалось, что это <org.telegram.messenger.SendMessagesHelper$SendMessageParams>.
Давайте откроем код Telegram и посмотрим, какие поля у него имеются:
Скорее всего,
public long peer;
и будет содержать идентификатор текущего диалога.
Добавим пару строк, чтобы наше сообщение заменялось на peer:
params.message = f'peer: {params.peer}'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
python
Устанавливаем новый плагин, отправляем
.id
и видим, что содержимое сообщения действительно поменялось:
А вот, что у нас получилось:
.id
выдаст информацию о текущем диалоге (его идентификатор).
Напишем заглушку с on_send_message_hook для того, чтобы посмотреть, что это вообще за объект params:
import json
from base_plugin import BasePlugin, HookResult
__id__ = 'chat_id'
__name__ = 'chat id viewer'
class ChatIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_on_send_message_hook()
def on_send_message_hook(self, account, params):
self.log(json.dumps(params, indent=2, default=str))
return HookResult()
python
Код:
import json
from base_plugin import BasePlugin, HookResult
__id__ = 'chat_id'
__name__ = 'chat id viewer'
class ChatIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_on_send_message_hook()
def on_send_message_hook(self, account, params):
self.log(json.dumps(params, indent=2, default=str))
return HookResult()
Оказалось, что это <org.telegram.messenger.SendMessagesHelper$SendMessageParams>.
Давайте откроем код Telegram и посмотрим, какие поля у него имеются:
Скорее всего,
public long peer;
и будет содержать идентификатор текущего диалога.
Добавим пару строк, чтобы наше сообщение заменялось на peer:
params.message = f'peer: {params.peer}'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
python
Код:
params.message = f'peer: {params.peer}'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
Устанавливаем новый плагин, отправляем
.id
и видим, что содержимое сообщения действительно поменялось:
А вот, что у нас получилось:
from base_plugin import BasePlugin, HookResult, HookStrategy
__id__ = 'chat_id'
__name__ = 'chat id viewer'
class ChatIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_on_send_message_hook()
def on_send_message_hook(self, account, params):
if not isinstance(params.message, str) or not params.message.startswith(".id"):
return HookResult()
params.message = f'peer: {params.peer}'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
python
__id__ = 'chat_id'
__name__ = 'chat id viewer'
class ChatIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_on_send_message_hook()
def on_send_message_hook(self, account, params):
if not isinstance(params.message, str) or not params.message.startswith(".id"):
return HookResult()
params.message = f'peer: {params.peer}'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
python
Код:
from base_plugin import BasePlugin, HookResult, HookStrategy
__id__ = 'chat_id'
__name__ = 'chat id viewer'
class ChatIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_on_send_message_hook()
def on_send_message_hook(self, account, params):
if not isinstance(params.message, str) or not params.message.startswith(".id"):
return HookResult()
params.message = f'peer: {params.peer}'
return HookResult(strategy=HookStrategy.MODIFY, params=params)
Мы напишем плагин, который добавит кнопку в профиль, по которой мы будем искать аккаунт пользователя на форуме.
Для начала рекомендуется ознакомиться с официальной документацией и получить токен тут.
Напишем простой код для поиска пользователя по привязанному Telegram:
import requests
lolz_token = 'ВАШ_ТОКЕН_ТУТ'
api_url = 'https://prod-api.lolz.live'
def get_profile_id(username: str):
try:
resp = requests.get(f'{api_url}/users/find?custom_fields[telegram]={username}', headers={
'accept': 'application/json',
'authorization': f'Bearer {lolz_token}'
})
users = resp.json().get('users')
if len(users) > 0:
return users[0].get('user_id')
return 0
except Exception as e:
return 0
python
Затем добавим кнопку в профиль:
from typing import Dict, Any
from base_plugin import BasePlugin, MenuItemData, MenuItemType
class LolzIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="View Lolzteam",
on_click=self.on_click,
icon="msg_addcontacts"
)
)
def on_click(self, context: Dict[str, Any]):
pass
python
При клике на кнопку "View Lolzteam" будет вызываться метод on_click. Давайте сделаем так, чтобы при каждом его вызове происходил запрос на API форума:
def on_click(self, context: Dict[str, Any]):
user = context.get("user")
if user and user.username:
lolz_id = get_profile(user.username)
if lolz_id == 0: return
python
Теперь давайте воспользуемся полученными выше знаниями и используем fragment для того, чтобы вывести информацию на экран:
fragment = context.get("fragment")
BulletinHelper.show_info(f'https://lolz.live/members/{lolz_id}', fragment)
python
Что получилось?
Зайдя в профиль к другу и нажав на кнопку, я увидел ссылку на его профиль внизу экрана:
Для начала рекомендуется ознакомиться с официальной документацией и получить токен тут.
Напишем простой код для поиска пользователя по привязанному Telegram:
import requests
lolz_token = 'ВАШ_ТОКЕН_ТУТ'
api_url = 'https://prod-api.lolz.live'
def get_profile_id(username: str):
try:
resp = requests.get(f'{api_url}/users/find?custom_fields[telegram]={username}', headers={
'accept': 'application/json',
'authorization': f'Bearer {lolz_token}'
})
users = resp.json().get('users')
if len(users) > 0:
return users[0].get('user_id')
return 0
except Exception as e:
return 0
python
Код:
import requests
lolz_token = 'ВАШ_ТОКЕН_ТУТ'
api_url = 'https://prod-api.lolz.live'
def get_profile_id(username: str):
try:
resp = requests.get(f'{api_url}/users/find?custom_fields[telegram]={username}', headers={
'accept': 'application/json',
'authorization': f'Bearer {lolz_token}'
})
users = resp.json().get('users')
if len(users) > 0:
return users[0].get('user_id')
return 0
except Exception as e:
return 0
Затем добавим кнопку в профиль:
from typing import Dict, Any
from base_plugin import BasePlugin, MenuItemData, MenuItemType
class LolzIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="View Lolzteam",
on_click=self.on_click,
icon="msg_addcontacts"
)
)
def on_click(self, context: Dict[str, Any]):
pass
python
Код:
from typing import Dict, Any
from base_plugin import BasePlugin, MenuItemData, MenuItemType
class LolzIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="View Lolzteam",
on_click=self.on_click,
icon="msg_addcontacts"
)
)
def on_click(self, context: Dict[str, Any]):
pass
При клике на кнопку "View Lolzteam" будет вызываться метод on_click. Давайте сделаем так, чтобы при каждом его вызове происходил запрос на API форума:
def on_click(self, context: Dict[str, Any]):
user = context.get("user")
if user and user.username:
lolz_id = get_profile(user.username)
if lolz_id == 0: return
python
Код:
def on_click(self, context: Dict[str, Any]):
user = context.get("user")
if user and user.username:
lolz_id = get_profile(user.username)
if lolz_id == 0: return
Теперь давайте воспользуемся полученными выше знаниями и используем fragment для того, чтобы вывести информацию на экран:
fragment = context.get("fragment")
BulletinHelper.show_info(f'https://lolz.live/members/{lolz_id}', fragment)
python
Код:
fragment = context.get("fragment")
BulletinHelper.show_info(f'https://lolz.live/members/{lolz_id}', fragment)
Что получилось?
Зайдя в профиль к другу и нажав на кнопку, я увидел ссылку на его профиль внизу экрана:
import requests
from ui.bulletin import BulletinHelper
from typing import Dict, Any
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'lolz_id'
__name__ = 'Lolz By Telegram'
__author__ = 'reassurance'
lolz_token = 'ВАШ_ТОКЕН_ТУТ'
api_url = 'https://prod-api.lolz.live'
def get_profile(username):
try:
resp = requests.get(f'{api_url}/users/find?custom_fields[telegram]={username}', headers={
'accept': 'application/json',
'authorization': f'Bearer {lolz_token}'
})
users = resp.json().get('users')
if len(users) > 0:
return users[0].get('user_id')
return 0
except Exception as e:
return e
class LolzIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="View Lolzteam",
on_click=self.get_lolz,
icon="msg_addcontacts"
)
)
def on_click(self, context: Dict[str, Any]):
user = context.get("user")
if user and user.username:
lolz_id = get_profile(user.username)
if lolz_id == 0: return
fragment = context.get("fragment")
BulletinHelper.show_info(f'https://lolz.live/members/{lolz_id}', fragment)
python
from ui.bulletin import BulletinHelper
from typing import Dict, Any
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'lolz_id'
__name__ = 'Lolz By Telegram'
__author__ = 'reassurance'
lolz_token = 'ВАШ_ТОКЕН_ТУТ'
api_url = 'https://prod-api.lolz.live'
def get_profile(username):
try:
resp = requests.get(f'{api_url}/users/find?custom_fields[telegram]={username}', headers={
'accept': 'application/json',
'authorization': f'Bearer {lolz_token}'
})
users = resp.json().get('users')
if len(users) > 0:
return users[0].get('user_id')
return 0
except Exception as e:
return e
class LolzIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="View Lolzteam",
on_click=self.get_lolz,
icon="msg_addcontacts"
)
)
def on_click(self, context: Dict[str, Any]):
user = context.get("user")
if user and user.username:
lolz_id = get_profile(user.username)
if lolz_id == 0: return
fragment = context.get("fragment")
BulletinHelper.show_info(f'https://lolz.live/members/{lolz_id}', fragment)
python
Код:
import requests
from ui.bulletin import BulletinHelper
from typing import Dict, Any
from base_plugin import BasePlugin, MenuItemData, MenuItemType
__id__ = 'lolz_id'
__name__ = 'Lolz By Telegram'
__author__ = 'reassurance'
lolz_token = 'ВАШ_ТОКЕН_ТУТ'
api_url = 'https://prod-api.lolz.live'
def get_profile(username):
try:
resp = requests.get(f'{api_url}/users/find?custom_fields[telegram]={username}', headers={
'accept': 'application/json',
'authorization': f'Bearer {lolz_token}'
})
users = resp.json().get('users')
if len(users) > 0:
return users[0].get('user_id')
return 0
except Exception as e:
return e
class LolzIdPlugin(BasePlugin):
def on_plugin_load(self):
self.add_menu_item(
MenuItemData(
menu_type=MenuItemType.PROFILE_ACTION_MENU,
text="View Lolzteam",
on_click=self.get_lolz,
icon="msg_addcontacts"
)
)
def on_click(self, context: Dict[str, Any]):
user = context.get("user")
if user and user.username:
lolz_id = get_profile(user.username)
if lolz_id == 0: return
fragment = context.get("fragment")
BulletinHelper.show_info(f'https://lolz.live/members/{lolz_id}', fragment)
Благодарю за прочтение моей статьи













Комментарии 0