generated from exteraGramPlugins/pluginBase
[no bump] Ghost Mode: AyuGram's ghost mode with per-account and per-chat settings
Publish plugin / publish (push) Successful in 5s
Publish plugin / publish (push) Successful in 5s
Read receipts, story views, typing and online status are held back per the options; read on interact, scheduled and silent sending as in AyuGram. Every option resolves chat -> account -> global; forum topics follow their group. Settings live in the plugin page, a chat/profile menu popup, and a drawer row for the current account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dfd1f659fe
commit
00553970b7
20 files changed
+3223
-178
No files matched your search
+9
-2
@@ -17,8 +17,15 @@
|
||||
plugin.json single source of truth for plugin metadata
|
||||
src/ the plugin, written as an ordinary Python package
|
||||
main.py entry module — exactly one BasePlugin subclass, absolute imports only
|
||||
greeting/ sample package (proves relative + cross-module imports survive bundling)
|
||||
textutil.py sample top-level sibling module
|
||||
features.py the ghost mode options and their defaults
|
||||
prefs.py storage and resolution: chat -> account -> global
|
||||
network.py request hooks: reads, stories, typing, online status, read on interact
|
||||
sending.py scheduled and silent sending, forward hook, scheduled-screen guard
|
||||
menus.py chat/profile/message/drawer menu items, the chat and account dialogs
|
||||
drawer.py the drawer row for drawer layouts without a Plugins entry
|
||||
settings_page.py the plugin settings screen and the per-account pages
|
||||
peers.py accounts, dialog ids and names
|
||||
hooking.py Xposed and UI-thread helpers
|
||||
tools/
|
||||
bundle.py compiles plugin.json + src/ into dist/<id>.plugin
|
||||
devclient.py speaks the on-device dev-server protocol (127.0.0.1:42690)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# How Ghost Mode works
|
||||
|
||||
## What is intercepted
|
||||
|
||||
Like AyuGram's hook in `ConnectionsManager.sendRequestInternal`, the plugin checks requests
|
||||
on their way out (`pre_request_hook`). Each request is checked against the options for the
|
||||
account it comes from and, where it has one, the chat it's about:
|
||||
|
||||
| Option | Requests | What happens |
|
||||
| --- | --- | --- |
|
||||
| Don't read messages | `messages.readHistory`, `messages.readDiscussion`, `messages.readEncryptedHistory`, `messages.readMessageContents`, `channels.readHistory`, `channels.readMessageContents` | dropped |
|
||||
| Don't read stories | `stories.readStories`, `stories.incrementStoryViews` | dropped |
|
||||
| Don't send typing | `messages.setTyping`, `messages.setEncryptedTyping` | dropped |
|
||||
| Don't send online | `account.updateStatus` | `offline` forced to true |
|
||||
| Go offline automatically | `account.updateStatus` | an offline status is sent after each online one |
|
||||
| Read on interact | `messages.sendMessage`, `sendMedia`, `sendMultiMedia`, `sendInlineBotResult`, `forwardMessages` | once the send succeeds, the chat is read up to its newest message |
|
||||
|
||||
Forum topics have no settings of their own. A topic's read receipt (`messages.readDiscussion`)
|
||||
and typing status carry the group as their peer, so the group's settings apply to every topic.
|
||||
The plugin's own read receipts follow the app's behaviour in forums: *Read until here* and
|
||||
*Read on interact* read the message's topic, and *Read all messages now* reads each loaded
|
||||
topic up to its newest message.
|
||||
|
||||
`messages.readMessageContents` carries only message ids. It is sent for the chat on screen,
|
||||
so that chat's settings apply. The app still marks messages read locally, so chats look read
|
||||
to you and unread to the sender. AyuGram behaves the same way.
|
||||
|
||||
The plugin's own read receipts (*Read on interact*, *Read all messages now*, *Read until
|
||||
here*) are let through by a one-shot allowance per chat that expires after 10 seconds.
|
||||
|
||||
Scheduled and silent sending change the message before the app builds it:
|
||||
`on_send_message_hook` handles text and media, and an Xposed hook on
|
||||
`SendMessagesHelper.sendMessage(ArrayList<MessageObject>, …)` handles forwards. Telegram sends
|
||||
anything scheduled less than 10 seconds ahead immediately, so the plugin schedules 12 seconds
|
||||
ahead, plus 10 seconds for photos and 15 for documents. All parts of an album share one date.
|
||||
When a scheduled message lands in the chat, the app would normally jump to the scheduled
|
||||
messages screen. For messages the plugin scheduled, it stays in the chat.
|
||||
|
||||
## How settings resolve
|
||||
|
||||
```
|
||||
chat override → account override → global switch
|
||||
```
|
||||
|
||||
- Global: `g_<option>` (bool), the switches on the main settings page.
|
||||
- Account: `a_<user id>_<option>` (0 Global, 1 On, 2 Off), the selectors on an account's
|
||||
page. Accounts are keyed by Telegram user id, not by app slot, so overrides follow the
|
||||
account when slots shift.
|
||||
- Chat: `c_<user id>`, a JSON object `{"<dialog id>": {"<option>": bool}}`. Only chat-level
|
||||
options can be stored here. *Don't send online* and *Go offline automatically* apply to
|
||||
the whole account.
|
||||
|
||||
"Ghost mode is on" means all five essentials are on, the same test as AyuGram's
|
||||
`isGhostModeActive`. At chat level only the three chat essentials count.
|
||||
|
||||
## The drawer row
|
||||
|
||||
exteraGram only shows plugin drawer items where the drawer layout has its *Plugins* entry, and
|
||||
that entry is optional. When the layout has no *Plugins* entry, an after-hook on
|
||||
`DrawerMenuView.rebuildMenu` appends a `DrawerMenuItemView`, built the way the app builds its
|
||||
own rows. With the entry, the app shows the plugin's `DRAWER_MENU` item there instead, so the
|
||||
row never shows twice. The row's icon is exteraGram's own `ghost` drawable.
|
||||
|
||||
## Runtime notes
|
||||
|
||||
Things the on-device runtime does differently from the SDK docs, all handled in `src/`:
|
||||
|
||||
- Bundled modules aren't in `sys.modules`, so `dataclasses` can't be used in them.
|
||||
- `param.setResult()` in a before-hook doesn't skip the original method. To stop the
|
||||
scheduled-screen jump, the chat is marked paused during that one
|
||||
`didReceivedNotification` call.
|
||||
- `SendMessageParams.notify` clashes with `Object.notify()`, so it is read and written with
|
||||
`hook_utils.get_private_field` / `set_private_field`.
|
||||
- A Python `int` put into a hooked method's `Object[]` arguments becomes a `Long`, so the
|
||||
forward hook writes a boxed `Integer` and `Boolean`.
|
||||
Reference in new issue
Block a user