Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ app does not depend on Telegram.

**Status:** development preview with notification/SMS capture, a persistent encrypted outbox,
background delivery, automatic retries and a delivery journal. Webhook authentication is not
implemented yet. Signed APK release automation is configured; see [Releases](docs/releases.md).
implemented yet. Signed APK release automation is configured; see [Releases](docs/en/releases.md).

## Getting started

Guides: [n8n webhook](docs/en/n8n-webhook.md) · [Telegram forwarding](docs/en/n8n-telegram.md).
На русском: [n8n webhook](docs/ru/n8n-webhook.md) · [Пересылка в Telegram](docs/ru/n8n-telegram.md).

1. Save the full published webhook URL and a device code in **Connection**. Send a test event.
2. Check **Journal** and find the same event ID in n8n **Executions**.
3. In **Sources**, enable notification forwarding, grant notification access in Android settings,
Expand Down Expand Up @@ -89,14 +92,14 @@ In VS Code, select **Run Message487 on Emulator** and **Run Without Debugging**,
The debug app starts with the local n8n receive endpoint configured. Follow the capture checks in
[DevServer/README.md](DevServer/README.md) using synthetic data only. Release builds require HTTPS.
UI strings are supplied in English and Russian. The interface supports light/dark themes,
bottom navigation on phones and rail navigation on wider windows. See the [design notes](docs/design.md)
bottom navigation on phones and rail navigation on wider windows. See the [design notes](docs/en/design.md)
for the visual conventions and references.

Fastlane's `debug_artifact` lane builds only the debug APK. `checks` runs JVM/Robolectric tests,
debug/release lint, and builds debug and unsigned release APKs under `app/build/outputs/apk/`.
PR CI has no release signing credentials and does not require an emulator.

See the [project context](docs/project-context.md) for remaining product decisions.
See the [project context](docs/en/project-context.md) for remaining product decisions.
This project succeeds [sms487](https://github.com/andre487/sms487).
[AndroidMegaProxy](https://github.com/andre487/AndroidMegaProxy) is the reference for project conventions.

Expand All @@ -108,6 +111,6 @@ Open the bug icon in the top bar to view or clear local diagnostic logs and prep
`der-morgenstern@yandex.ru`. A ZIP contains rotating logs, the last crash and device/app information;
message content and connection secrets are excluded. Sending requires action in your email app.
After an unhandled crash the next launch offers to review the report.
See [diagnostic behavior and development checks](docs/diagnostics.md) and [privacy details](PRIVACY.md).
See [diagnostic behavior and development checks](docs/en/diagnostics.md) and [privacy details](PRIVACY.md).

Test categories, local commands, CI jobs and device-only limitations: [Testing](docs/testing.md).
Test categories, local commands, CI jobs and device-only limitations: [Testing](docs/en/testing.md).
2 changes: 2 additions & 0 deletions docs/design.md → docs/en/design.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Interface design

[English](../en/design.md) | [Русский](../ru/design.md)

Message487 uses a restrained Material 3 interface with the standard purple baseline palette from
Material 3. Both light and dark schemes come directly from the library without color overrides
or wallpaper-derived dynamic colors. The visual reference is MegaProxy: prominent operational status,
Expand Down
2 changes: 2 additions & 0 deletions docs/diagnostics.md → docs/en/diagnostics.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Diagnostics

[English](../en/diagnostics.md) | [Русский](../ru/diagnostics.md)

The top-bar bug icon opens diagnostics. Refresh reads a bounded log preview; Prepare email creates
an immutable ZIP attachment using FileProvider and temporary read-only access. Email apps are
preferred, with the Android share sheet as fallback. No message is sent by Message487 itself.
Expand Down
195 changes: 195 additions & 0 deletions docs/en/n8n-telegram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Forward Message487 notifications to Telegram

[English](../en/n8n-telegram.md) | [Русский](../ru/n8n-telegram.md)

First configure the [n8n webhook](n8n-webhook.md) and confirm a test event. That guide
also links to official documentation, n8n Cloud and self-hosted installation options.
Keep the bot token in **n8n Credentials**; it is not needed in Android. Forwarded
messages are available to the selected Telegram chat and may remain in n8n history.

## 1. Create a bot and credential

1. Open the official [@BotFather](https://t.me/BotFather) in Telegram.
2. Send `/newbot`, then choose a name and username.
3. Save the issued token in a new **Telegram** credential in n8n, in **Access Token**.
Do not put it in node text, the webhook URL or exported workflow files.
4. Open a private chat with the new bot and press **Start** or send `/start` so it
can send you messages.

References: [creating bots](https://core.telegram.org/bots/features#botfather) and
[n8n Telegram credentials](https://docs.n8n.io/integrations/builtin/credentials/telegram/).

## 2. Find the chat ID

For a new bot that is not connected to a Telegram Trigger, save this code locally as
`telegram-chat-id.py` and run `python3 telegram-chat-id.py`. It prompts for the token
without echoing it and reads updates without sending messages. Send `/start` to the
bot from your Telegram account before running it.

```python
import getpass
import json
import urllib.error
import urllib.request

bot_token = getpass.getpass('Telegram bot token: ').strip()
request = urllib.request.Request(
f'https://api.telegram.org/bot{bot_token}/getUpdates',
data=b'{"timeout":0,"limit":100}',
headers={'Content-Type': 'application/json'},
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
result = json.load(response)
except (urllib.error.URLError, TimeoutError):
raise SystemExit('Could not read updates; check token, network and existing webhook') from None

chats = {}
for update in result.get('result', []):
message = update.get('message') or update.get('channel_post') or {}
chat = message.get('chat') or {}
if 'id' in chat:
chats[chat['id']] = chat.get('type', 'unknown')
for chat_id, chat_type in chats.items():
print(f'chat_id={chat_id} type={chat_type}')
if not chats:
print('No chats found. Send /start to the bot and run again.')
```

Copy the desired ID into the Telegram node. For a group, add the bot and send
`/start@your_bot_username`, then read the group ID from updates. Preserve any minus
sign. For a public channel, you can use `@channelusername`; add the bot as an
administrator with permission to post messages.

`getUpdates` cannot be used while a Telegram webhook is installed. If the bot is
already connected to Telegram Trigger, read `message.chat.id` from its execution
instead. Do not delete a working bot's webhook just to obtain an ID. This forwarding
workflow does not need Telegram Trigger: its incoming trigger is Webhook.
See [getUpdates](https://core.telegram.org/bots/api#getupdates).

## 3. Build the forwarding chain

```mermaid
flowchart LR
W[Webhook: POST] --> C[Code: Prepare Telegram text]
C --> T[Telegram: Send Message]
T --> R[Respond to Webhook: ACK]
```

Keep **Webhook → Respond → Using 'Respond to Webhook' Node**. If you imported
`receive.json`, remove the direct Webhook → Respond connection and insert Code
and Telegram before Respond. Do not leave another branch acknowledging early.

Add a **Code** node named `Prepare Telegram text`, select **JavaScript** and
**Run Once for All Items**, and paste:

```javascript
const event = $('Webhook').first().json.body;
if (!event || event.schema_version !== 1 ||
typeof event.event_id !== 'string' || !event.event_id ||
!['test', 'notification', 'sms'].includes(event.message_type) ||
typeof event.text !== 'string') {
throw new Error('Invalid Message487 event');
}

const text = [
`Device: ${event.device_code || event.device_id || '—'}`,
`Source: ${event.source_name || event.source || '—'}`,
`Type: ${event.message_type}`,
event.title ? `Title: ${event.title}` : '',
event.sender ? `Sender: ${event.sender}` : '',
event.text,
].filter(line => line !== '').join('\n');

const chunks = [];
let chunk = '';
for (const character of text) {
if (chunk.length + character.length > 3500) {
chunks.push(chunk);
chunk = '';
}
chunk += character;
}
if (chunk) chunks.push(chunk);
const escapeHtml = value => value.replace(/[&<>]/g, character => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;',
})[character]);
return chunks.map((part, index) => ({
json: {
telegram_text: escapeHtml(chunks.length > 1
? `[${index + 1}/${chunks.length}]\n${part}` : part),
},
}));
```

This preserves the complete text by splitting long events into several messages.
It does not split emoji UTF-16 pairs and escapes `<`, `>` and `&` for HTML.
The chunk size leaves room for part numbers. Telegram accepts up to 4096 characters
after entity parsing; see [sendMessage](https://core.telegram.org/bots/api#sendmessage).

Configure the **Telegram** node:

| Field | Value |
| --- | --- |
| Credential | Your Telegram credential |
| Resource | `Message` |
| Operation | `Send Message` |
| Chat ID | A fixed ID for your chat, group or channel |
| Text, Expression mode | `{{ $json.telegram_text }}` |
| Additional Fields → Parse Mode | `HTML` |
| Append n8n Attribution | Off |
| Disable WebPage Preview | On if previews are not wanted |

Set Chat ID yourself rather than taking it from the incoming payload. The node
processes each chunk returned by Code. Keep **On Error → Stop Workflow** so a
Telegram error does not turn into a successful ACK. See the official
[Telegram operation reference](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/message-operations/#send-message).

Set **Respond to Webhook** to JSON and HTTP 200. In Response Body's Expression mode,
reference the original request rather than the Telegram result:

```javascript
{{ { status: 'accepted', event_id: $('Webhook').first().json.body.event_id } }}
```

After Telegram, `$json` contains Telegram's response, so `$json.body.event_id` is
incorrect. If you imported `receive.json`, replace **both Response Body and Response
Code**: both original expressions read `$json.body`. Code already performs basic
validation before sending in this chain. Publish the workflow again.

## 4. Verify delivery

1. Enable n8n confirmation in Message487 and send a test event.
2. Confirm that Telegram receives a message with type `test`.
3. Check the n8n execution, successful Telegram operation and client ACK.
4. Enable notifications, grant access and select a source app.
5. Create a new notification and match it to the execution using `event_id`.

The chain also accepts SMS and tests. To forward only notifications, add an **If**
node after Webhook: `{{ $json.body.message_type }}` equals `notification`. Route true
to Code → Telegram → Respond and false to a separate Respond with the same ACK.
Filtered events must still be acknowledged or they remain in the client's queue.

Do not select Telegram as a notification source on the phone receiving your bot's
messages: that can create a Telegram → Message487 → n8n → Telegram loop. Excluding
Telegram from selected apps is the simplest way to prevent it.

## Errors and retries

| Error | Check |
| --- | --- |
| `chat not found` | Chat ID, private chat started, bot added to group/channel |
| `bot was blocked` / HTTP 403 | Unblock the bot or restore its permissions |
| `can't parse entities` | HTML mode and the supplied escaping; do not use Markdown for this template |
| HTTP 429 | Telegram limits; reduce frequency and honor `retry_after` |
| Message arrives but the client retries | ACK event ID and time until webhook response |

If sending all chunks exceeds the client's timeout, persist the event to a durable
queue first, acknowledge receipt and send to Telegram in a separate workflow.
Do not add a long wait before ACK.

This simple example does not deduplicate. If Telegram accepted a message but ACK
was lost, retrying creates a duplicate; a failed chunk may cause earlier chunks to
repeat too. For more resilient processing, persist `event_id` and per-part progress.
Even that cannot guarantee exactly-once delivery if a crash occurs between Telegram's
response and recording the result; account for this in your retry design.
Loading
Loading