Files
ghostMode/docs/how-it-works.md
T
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

4.3 KiB

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.