inugram-plugin-port
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePorting Python plugins to Inugram
将Python插件移植到Inugram
A port is a rewrite against a different, higher-level API, not a transliteration. The source
plugins reach into app internals because their host offers little else. Inugram usually offers a
typed, granted, thread-safe API for the same thing. The usual outcome is a fraction of the
original's code with fewer and narrower grants.
Contract: the typings, plus and
( in a plugin project, in the
Inugram repo). Concepts and quirks: the plugin docs ( in the Inugram repo, or
https://github.com/teidesu/inugram/tree/main/sdk/docs). Read the docs for every API area the port
touches before writing code. The typings tag each API with and .
@inugram/plugin-typescommon.d.tsandroid.*.d.tscanvas.d.tsnode_modules/@inugram/plugin-types/sdk/types/sdk/docs/*.md@needs-grant**Limits:**移植是基于不同的、更上层的API进行重写,而非逐字转译。源插件需要深入应用内部实现,因为其宿主提供的API非常有限。而Inugram通常为相同功能提供了带类型、需授权、线程安全的API。移植后的代码量通常只有原版的一小部分,所需的授权更少、范围也更窄。
开发约定:参考类型定义,包括、和(插件项目中位于,Inugram仓库中位于)。概念与注意事项:参考插件文档(Inugram仓库中位于,或访问https://github.com/teidesu/inugram/tree/main/sdk/docs)。在编写代码前,请先阅读移植涉及的所有API领域的文档。类型定义中每个API都标注了`@needs-grant`(所需授权)和`**Limits:**`(使用限制)。
@inugram/plugin-typescommon.d.tsandroid.*.d.tscanvas.d.tsnode_modules/@inugram/plugin-types/sdk/types/sdk/docs/*.md1. Understand before writing
1. 编写代码前先理解需求
- Read the whole original. List each user-visible behavior and each setting. Ignore its implementation for now.
- Audit it. Do not port, and report to the user, anything not needed for the advertised feature:
joining/subscribing to channels, sending data to hosts unrelated to the feature, self-update or
"check min version" logic, telemetry, /
exec/marshal/base64 blobs of code, embedded DEX payloads. An embedded DEX without source is a blocker: ask the user, do not ship it.zlib - Check Inugram already does it: in the Inugram repo, stock settings (including Lite Mode), and built-in plugin APIs (e.g. translation providers). If it does, say so instead of porting.
FEATURES.md - Split. A source plugin bundling several unrelated tweaks behind toggles becomes several small plugins, one feature each, no enable/disable toggles (the plugin list is the toggle). Propose the split before writing. Each gets only the grants it needs.
- The API is a work in progress - if while porting you find a missing feature, instead of trying to hack around it, tell the user about that so that they can reach out to Inugram devs.
- 通读原插件的全部代码。列出所有用户可见的行为和所有设置项。暂时忽略其具体实现方式。
- 对原插件进行审计。对于实现宣传功能不需要的内容,不要移植,并告知用户:包括加入/订阅频道、向与功能无关的主机发送数据、自更新或“检查最低版本”逻辑、遥测数据、/
exec/marshal/base64编码的代码块、嵌入式DEX payload等。没有源码的嵌入式DEX是阻塞性问题:请询问用户,不要直接打包移植。zlib - 检查Inugram是否已经内置了该功能:参考Inugram仓库中的、原生设置(包括Lite Mode)以及内置插件API(例如翻译提供程序)。如果已内置,请告知用户,无需进行移植。
FEATURES.md - 拆分功能。如果原插件通过开关捆绑了多个互不相关的调整功能,应拆分为多个小插件,每个插件只实现一个功能,不需要启用/禁用开关(插件列表本身就是开关)。在编写代码前先向用户提出拆分方案。每个插件仅申请其所需的授权。
- API仍在开发中——如果在移植过程中发现缺少某个功能,不要尝试用hack方式绕过,请告知用户,以便他们联系Inugram开发人员。
2. Pick the highest API that works
2. 优先选用最上层的可用API
Go down this list and stop at the first rung that covers the behavior:
- Data and events: /
onNewMessage/onMessageEdited,onMessageDeleted,onUpdate,interceptSendMessage,interceptRpc. Most "hide X", "block ads", "change what is sent", "auto-do Y on message" plugins are one interceptor. Rewriting the server's answer beats hooking every UI place that shows it.interceptUpdate - Account API: /
account.sendMessage/sendMedia/editMessage/setReaction,readHistory/getUser/getChat/getMessagesCachedetc. TheniterHistoryscoped per method for anything not covered.invokeRpc - Declarative UI: ,
inu.ui.settingsPage,register{Global,Chat,Message,Profile}Action,bulletin,dialog,chooser,prompt,selectPeers,openPage,openChatHistory/getCurrentScreen,onScreenChanged.inu.icons.* - Web-like I/O: (scoped to hosts),
fetch,localStorage,inu.fs,inu.canvas,inu.clipboard.inu.openUrl - : reflection,
unsafe.jvmfor interfaces/subclasses/listeners,inu.jvm.defineClassfor custom views.inu.android.nativeView - : method hooks, only when the behavior lives purely in app code with no data-layer seam (layout, drawing, animations, stock UI logic).
unsafe.xposed - : effectively never. Use
inu.jvm.loadDexwith routine bodies instead.defineClass
Every grant is shown to the user as dangerous. Mixing rungs is fine: a UI tweak can be
an xposed hook while its settings are a plain .
unsafe.*settingsPage按以下列表从上到下查找,停在第一个能覆盖所需行为的层级:
- 数据与事件:/
onNewMessage/onMessageEdited、onMessageDeleted、onUpdate、interceptSendMessage、interceptRpc。大多数“隐藏X”、“屏蔽广告”、“修改发送内容”、“收到消息自动执行Y”类的插件只需要一个拦截器即可实现。改写服务器响应比给所有显示该内容的UI位置加钩子更高效。interceptUpdate - 账户API:/
account.sendMessage/sendMedia/editMessage/setReaction、readHistory/getUser/getChat/getMessagesCached等。对于未覆盖的功能,再使用按方法限定范围的iterHistory。invokeRpc - 声明式UI:、
inu.ui.settingsPage、register{Global,Chat,Message,Profile}Action、bulletin、dialog、chooser、prompt、selectPeers、openPage、openChatHistory/getCurrentScreen、onScreenChanged。inu.icons.* - 类Web I/O:(按主机限定范围)、
fetch、localStorage、inu.fs、inu.canvas、inu.clipboard。inu.openUrl - :反射、用于接口/子类/监听器的
unsafe.jvm、用于自定义视图的inu.jvm.defineClass。inu.android.nativeView - :方法钩子,仅当行为完全存在于应用代码中、没有数据层入口时使用(例如布局、绘制、动画、原生UI逻辑)。
unsafe.xposed - :几乎永远不要使用。请改用带routine体的
inu.jvm.loadDex。defineClass
所有授权都会向用户标记为危险。可以混合使用不同层级:一个UI调整可以用xposed钩子实现,而其设置页面用普通的实现。
unsafe.*settingsPage3. API map
3. API映射表
Source API on the left, the recommended Inugram equivalent on the right. "Avoid" rows name the
mechanical equivalent only so it is recognized; use the recommended one instead. Exact signatures
are in the typings.
左侧为源API,右侧为推荐的Inugram等效API。标有“避免使用”的行仅列出了机械对应的API以便识别,请优先使用推荐的API。具体签名请参考类型定义。
Plugin metadata and lifecycle (base_plugin.BasePlugin
)
base_plugin.BasePlugin插件元数据与生命周期(base_plugin.BasePlugin
)
base_plugin.BasePlugin| Source | Inugram |
|---|---|
| manifest |
| manifest |
| manifest |
| manifest |
| drop |
| top-level code. Register interceptors first (startup waits up to 500 ms for them). No top-level |
| usually nothing: every registration, timer, page and hook is released on unload. |
| call the |
| |
| |
| drop. Uncaught errors are logged; |
| 源API | Inugram等效 |
|---|---|
| 清单中的 |
| 清单中的 |
| 清单中的 |
| 清单中的 |
| 直接移除 |
| 顶层代码。请先注册拦截器(启动时最多等待500毫秒用于注册拦截器)。不要使用顶层 |
| 通常不需要处理:所有注册、定时器、页面和钩子都会在卸载时自动释放。仅当应用的Java对象中残留了状态时才使用 |
| 调用注册时返回的 |
| |
| |
包裹所有代码的 | 直接移除。未捕获的错误会自动记录;请为游离的promise添加 |
Settings (get_setting
, set_setting
, create_settings
, ui.settings
)
get_settingset_settingcreate_settingsui.settings设置(get_setting
、set_setting
、create_settings
、ui.settings
)
get_settingset_settingcreate_settingsui.settings| Source | Inugram |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
numeric | |
| rethink with stock rows first; else |
conditional rows ( | same |
| |
| 源API | Inugram等效 |
|---|---|
| 搭配 |
| |
| |
| |
| |
| |
| 点击 |
| |
用于范围内数值的数字 | |
| 优先用原生行组件重新设计;否则使用 |
条件行( | 在 |
| |
Messages and requests (client_utils
, send/request/update hooks)
client_utils消息与请求(client_utils
、发送/请求/更新钩子)
client_utils| Source | Inugram |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| mutate |
| return |
| async middleware: |
| return |
| |
| same middleware: |
| return a response without calling |
| |
| |
| list exact constructors / methods; there is no substring match |
| |
| |
| typed account method if one exists, else |
| |
| |
| |
| |
| |
| |
| |
| the |
| |
| |
| |
| custom translation via hooks on the translate controller | |
| the event APIs above first; |
| hooks suppressing system notifications | |
| |
| 源API | Inugram等效 |
|---|---|
| |
| 使用 |
| |
| |
| |
| |
| |
| 修改 |
| 不做修改,返回 |
| 异步中间件: |
无内容可发送的 | 返回 |
| |
| 同一个中间件: |
| 不调用 |
| |
仅用于响应的 | |
| 列出确切的构造函数/方法;不支持子字符串匹配 |
| |
| |
| 如果有对应的类型化账户方法则使用,否则使用 |
| |
| |
| |
| |
| |
| |
| |
| 处理函数中传入的 |
| |
| |
通过 | |
| 通过翻译控制器钩子实现的自定义翻译 | |
| 优先使用上述事件API;最后再考虑 |
| 屏蔽系统通知的钩子 | |
| |
UI (add_menu_item
, ui.bulletin
, ui.alert
, fragments)
add_menu_itemui.bulletinui.alertUI(add_menu_item
、ui.bulletin
、ui.alert
、fragment)
add_menu_itemui.bulletinui.alert| Source | Inugram |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| bulletin with |
custom | |
| a fragment showing a list of messages | |
contact/chat picker activities ( | |
| |
| |
| |
| |
Intent-based file pickers, | |
| |
| the row's |
| 源API | Inugram等效 |
|---|---|
| |
| |
| |
| |
| |
| |
用于打开页面的 | |
| |
| |
| |
| |
| |
带标题、消息、确定/取消/中性按钮的 | |
| |
通过 | |
| chooser项设置 |
| 搭配 |
自定义 | 用 |
| 显示消息列表的fragment | |
联系人/聊天选择器Activity(带委托的 | |
用于检查用户位置的 | |
作为Java对象的 | |
| |
| |
基于Intent的文件选择器、 | |
| |
设置或菜单行的 | 行的 |
Threads, I/O, Python libraries
线程、I/O与Python库
| Source | Inugram |
|---|---|
| not needed: high-level APIs hop threads themselves. Use |
| |
| |
| |
| JS builtins; |
| |
| |
| writing to public Downloads / gallery | |
| JSON settings files on disk | |
| |
| |
| |
| bundled pure-Python libs | an npm package bundled by the CLI (no DOM/Node APIs), or a few lines of TS. Ask before adding a dependency |
| 源API | Inugram等效 |
|---|---|
| 不需要:上层API会自动切换线程。使用 |
| |
| |
| 搭配 |
| JS内置对象; |
| |
| 私有目录下的 |
| 写入公共下载目录/相册 | |
| 磁盘上的JSON设置文件 | |
| |
用于图像处理的反射调用 | |
用于获取帧/尺寸的 | |
| 捆绑的纯Python库 | 由CLI打包的npm包(不能使用DOM/Node API),或者几行TS代码。添加依赖前请先询问用户 |
JVM and hooking (hook_utils
, java
, xposed)
hook_utilsjavaJVM与钩子(hook_utils
、java
、xposed)
hook_utilsjavaAlmost all of the below need (and some additionally need ).
unsafe.jvmunsafe.xposed| Source | Inugram |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| a plain TL object |
| |
| |
| Python subclass of a Java class | |
| |
| |
| |
| |
| |
| |
| |
| |
| none; hooks chain in registration / plugin order |
| |
| hook on a hot path (bind, measure, layout, draw, per-message) in Python | |
| plain |
| do not exist. Find the stock Telegram equivalent or drop the feature |
以下几乎所有API都需要授权(部分还需要授权)。
unsafe.jvmunsafe.xposed| 源API | Inugram等效 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
手动构建的 | 普通TL对象 |
| |
| |
| Java类的Python子类 | |
带捆绑DEX的 | |
带 | |
| 在 |
| |
| |
| |
| |
| |
| 不支持;钩子按注册/插件顺序链式执行 |
| 用 |
| Python中热路径上的钩子(绑定、测量、布局、绘制、每条消息、每个列表项) | |
| 搭配routine阶段的普通 |
| 不存在。请找到原生Telegram的等效实现,或者移除该功能 |
4. When JVM or xposed is unavoidable
4. 不可避免需要使用JVM或xposed的情况
- Verify every class, method, field and descriptor against the app's sources: in
rginside the Inugram repo, otherwise the stock Telegram Android sources or a jadx decompile of the Inugram APK. The source client is a different fork; names drift. Pin overloads with Dalvik descriptors (worktree/TMessagesProj/src/main/java).'areTabsEnabled(Lorg/telegram/tgnet/TLRPC$Chat;)Z' - Hook as close to the data as possible: one controller/model method beats many view methods.
- Hot path (binding, layout, drawing, per-message, per-list-item) or must-always-apply logic:
/
inu.xposed.routine. Readinu.jvm.routinefor the subset (no closures,routines.mdonly, Java members, captures are snapshots, 250 ms).=== - Rare hook that needs plugin APIs: routine + JS
filter/before.after - Plain JS hooks only for rare calls off hot paths.
- bodies called often or off the plugin thread: routine bodies.
defineClass - Read constants once outside routines () and capture them.
const X = Cls.getStaticField('X') - Do not port defensive around every reflection call. Hook phase errors are already logged and skipped; keep a
try/exceptonly where a failure is expected and must not stop the rest.try - Reach out! In many cases an awkward xposed hook is best battled with a proper high-level API
- 对照应用源码验证每个类、方法、字段和描述符:在Inugram仓库的目录下用
worktree/TMessagesProj/src/main/java搜索,否则使用原生Telegram Android源码或Inugram APK的jadx反编译结果。源客户端是另一个分支,类名可能有差异。请使用Dalvik描述符(例如rg)来确定重载方法。'areTabsEnabled(Lorg/telegram/tgnet/TLRPC$Chat;)Z' - 钩子位置尽量靠近数据层:一个控制器/模型方法的钩子胜过多个视图方法的钩子。
- 热路径(绑定、布局、绘制、每条消息、每个列表项)或必须始终生效的逻辑:使用/
inu.xposed.routine。请阅读inu.jvm.routine了解可用的语法子集(不支持闭包、仅支持routines.md、只能访问Java成员、捕获的变量是快照、执行时间限制250毫秒)。=== - 需要插件API的低频钩子:使用routine + JS的
filter/before。after - 普通JS钩子仅用于非热路径的低频调用。
- 的方法体如果调用频繁或不在插件线程执行:使用routine体。
defineClass - 在routine外部一次性读取常量()并捕获它们。
const X = Cls.getStaticField('X') - 不要移植每个反射调用周围的防御性。钩子阶段的错误会自动记录并跳过;仅在预期会失败且不能中断其余逻辑的地方保留
try/except。try - 请及时反馈!在很多情况下,一个别扭的xposed钩子最好通过添加合适的上层API来解决。
5. Writing the code
5. 编写代码
- Less code. Drop the source's boilerplate: logging wrappers, dumps, UI-thread juggling, manual listener bookkeeping, version gates, dead settings.
traceback - Keep behavior and defaults the user sees. Where the port must differ (an API cannot read something, a setting makes no sense here), note it in a short comment and in the report.
- No enable/disable switch for the whole plugin.
- Localization: only if the original had it. Pick by ; keep the original strings.
inu.info().language - Narrowest grants: scoped /
account.read(...),account.write(...),fetch(host),invokeRpc(method),interceptRpc(method). Use theonUpdate(type)filter so non-matching sends never enter JS.interceptSendMessage - Attach to every promise not awaited; an unhandled rejection disables the plugin.
.catch - Do not keep TL views or interceptor contexts past the call; copy fields out.
- No .
minify
- 代码越少越好。移除源插件中的样板代码:日志包装器、转储、UI线程切换、手动监听器管理、版本判断、无用设置。
traceback - 保持用户可见的行为和默认值不变。如果移植版必须有所不同(例如某个API无法读取某些内容、某个设置在这里没有意义),请在简短注释和报告中说明。
- 不要为整个插件添加启用/禁用开关。
- 本地化:仅当原插件有本地化时才添加。通过判断语言;保留原有的字符串。
inu.info().language - 最小化授权范围:使用限定范围的/
account.read(...)、account.write(...)、fetch(host)、invokeRpc(method)、interceptRpc(method)。使用onUpdate(type)的过滤器,让不匹配的发送请求不会进入JS层。interceptSendMessage - 为每个未被await的promise添加;未处理的拒绝会导致插件被禁用。
.catch - 不要在调用结束后保留TL视图或拦截器上下文;请将需要的字段复制出来。
- 不要使用。
minify
7. Report
7. 报告
Short: what the port does, the grants and why each is needed, what was split out, what was
dropped or changed versus the original and why, anything from the audit, and anything that needs
on-device checking (hooks especially).
报告应简洁明了:说明移植版的功能、所需的每项授权及其原因、拆分出了哪些功能、相比原版移除或修改了哪些内容及原因、审计中发现的问题,以及任何需要在真机上验证的内容(尤其是钩子)。