Files
lyuksovannyyandClaude Opus 5.5 00553970b7
Publish plugin / publish (push) Successful in 5s
[no bump] Ghost Mode: AyuGram's ghost mode with per-account and per-chat settings
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>
2026-10-09 15:54:41 +02:00

76 lines
4.3 KiB
Markdown

# 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`.