diff --git a/README.md b/README.md index 9932c2c..8b80802 100644 --- a/README.md +++ b/README.md @@ -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, @@ -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. @@ -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). diff --git a/docs/design.md b/docs/en/design.md similarity index 97% rename from docs/design.md rename to docs/en/design.md index 1d78815..caef775 100644 --- a/docs/design.md +++ b/docs/en/design.md @@ -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, diff --git a/docs/diagnostics.md b/docs/en/diagnostics.md similarity index 96% rename from docs/diagnostics.md rename to docs/en/diagnostics.md index 7c4ef66..90768dd 100644 --- a/docs/diagnostics.md +++ b/docs/en/diagnostics.md @@ -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. diff --git a/docs/en/n8n-telegram.md b/docs/en/n8n-telegram.md new file mode 100644 index 0000000..981a60a --- /dev/null +++ b/docs/en/n8n-telegram.md @@ -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 => ({ + '&': '&', '<': '<', '>': '>', +})[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. diff --git a/docs/en/n8n-webhook.md b/docs/en/n8n-webhook.md new file mode 100644 index 0000000..49c5142 --- /dev/null +++ b/docs/en/n8n-webhook.md @@ -0,0 +1,199 @@ +# Connect Message487 to n8n + +[English](../en/n8n-webhook.md) | [Русский](../ru/n8n-webhook.md) + +This guide targets Message487 0.0.1. You need the n8n editor and an HTTPS endpoint +reachable from your phone. For a local Android emulator, use the debug build and +[DevServer](../../DevServer/README.md). Release APKs reject HTTP endpoints. + +## Where to run n8n + +- [Official n8n documentation](https://docs.n8n.io/) covers nodes, expressions and workflows. +- [Get started with n8n Cloud](https://docs.n8n.io/deploy/use-n8n-cloud/start-your-free-trial) to use a managed instance. +- [Cloud or self-hosting](https://docs.n8n.io/choose-how-to-use-n8n) compares deployment options. +- [Self-hosted installation options](https://docs.n8n.io/deploy/host-n8n/install-options). +- Official [Docker Compose installation guide](https://docs.n8n.io/deploy/host-n8n/install-options/install-using-docker-compose). + +For Cloud, use your workspace's HTTPS Production URL. For self-hosting, configure +HTTPS, persistent storage and backups. DevServer is for local development; do not +expose it publicly with its bundled test credentials. + +## 1. Create a receiving workflow + +Import [receive.json](../../DevServer/workflows/receive.json) using **Import from File** +in the editor menu. It validates basic fields and acknowledges the incoming `event_id`. +Import a separate workflow rather than replacing an existing production workflow. + +```mermaid +flowchart LR + W[Webhook: POST] --> R[Respond to Webhook: JSON ACK] +``` + +To configure the same two nodes manually, set **Webhook** as follows: + +| Field | Value | +| --- | --- | +| HTTP Method | `POST` | +| Path | A unique path such as `message487/receive-` | +| Authentication | `None` for the current client version | +| Respond | `Using 'Respond to Webhook' Node` | + +Generate a random suffix with `openssl rand -hex 16`; replace the entire placeholder, +including angle brackets. Message487 currently sends no authentication headers, +Basic Auth or JWT. The n8n editor login does not automatically protect webhooks. +Keep the full URL private; a random path is not a substitute for authentication. +For personal messages, restrict endpoint access where possible, for example using +a private network reachable from the phone. Enabling Header/Basic/JWT authentication +without client support will reject delivery. + +Set **Respond to Webhook → Respond With → JSON**, **Response Code → 200**, and use +this **Expression** in **Response Body**: + +```javascript +{{ { status: 'accepted', event_id: $('Webhook').first().json.body.event_id } }} +``` + +`Webhook` is the trigger node's name; adjust the expression if you rename it. +This minimal manual example does not validate requests. The imported `receive.json` +also validates `schema_version`, `event_id`, `message_type` and `text`, returning +HTTP 400 for invalid input. + +The client expects an object, not an array, JSON-encoded string or HTML: + +```json +{"status":"accepted","event_id":"the-same-event_id-as-the-request"} +``` + +The status must be exactly `accepted` and the ID must match. The default n8n response +“Workflow got started” is not an ACK for this client. See the official +[Webhook](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/) and +[Respond to Webhook](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/) documentation. + +## 2. Publish and copy the URL + +Select **Publish** (or enable **Active** in older n8n versions), then copy the +Webhook node's **Production URL**, for example: + +```text +https://n8n.example.org/webhook/message487/receive- +``` + +The `/webhook-test/` **Test URL** is for temporary listening with **Listen for test event**. +Production requests use a published workflow and appear under **Executions**. +Publish again after editing the workflow. + +If a reverse proxy causes n8n to display an internal URL, configure `WEBHOOK_URL`, +`N8N_PROXY_HOPS` and forwarded headers using the official +[reverse-proxy guide](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/configuration-examples/configure-webhook-urls-with-reverse-proxy). +Use the final HTTPS URL with a trusted certificate: Message487 does not follow +HTTP redirects or disable TLS verification. + +## 3. Configure the app + +1. Open **Connection** and paste the complete Production URL. +2. Set a recognizable **Device code**, such as `personal-phone`. +3. Keep **n8n confirmation** enabled. +4. Save and send a test event. +5. Open the event in **Journal** and check its acknowledgement and HTTP 200. + +After the test succeeds, enable your desired **Sources**. Notifications require +Android notification access and selected apps; SMS requires receive-SMS permission. +Enabling SMS and selecting your SMS app can produce two events for one message. + +The bundled emulator endpoint is `http://10.0.2.2:5678/webhook/message487/receive`, +which requires a debug build. On a physical phone, `localhost` means the phone +itself, and `10.0.2.2` is not your computer's address. + +## 4. Inspect received data + +Open **Executions → execution → Webhook → Output → body** in n8n. Match its +`event_id` with the app journal. Enable saving successful execution data if needed; +DevServer already does this. Execution history contains complete messages, so +configure access and retention accordingly. + +Example notification body: + +```json +{ + "schema_version": 1, + "event_id": "5d8ee1a5-9360-4b0e-8412-942b2e1c99ab", + "device_id": "43b55766-95b6-40b8-8f4f-c0240f547914", + "device_code": "personal-phone", + "message_type": "notification", + "occurred_at": "2026-09-09T09:00:00Z", + "source": "org.example.chat", + "source_name": "Example Chat", + "title": "Test notification", + "text": "Connection check" +} +``` + +| Field | Meaning | +| --- | --- | +| `event_id` | Event ID, preserved across delivery retries | +| `device_id` | Installation ID | +| `device_code` | Editable device label | +| `message_type` | `test`, `notification` or `sms` | +| `occurred_at` | Event timestamp in ISO 8601 format | +| `source` / `source_name` | Source package / app name; SMS uses source `android` | +| `title` | Notification title; absent for SMS and tests | +| `sender` | SMS sender; absent for notifications and tests | +| `text` | Event text | + +Directly after Webhook use `{{ $json.body.text }}` or `{{ $json.body.device_code }}`. +If intermediate nodes replace the data, reference the original node explicitly: +`{{ $('Webhook').first().json.body.text }}`. + +## 5. Add message processing + +For a complete example, see [forwarding to Telegram](n8n-telegram.md). + +The bundled receiver only acknowledges requests; it does not send to Telegram, +store a separate durable queue or deduplicate. Put your actions before Respond to +Webhook, acknowledging only after the operation you consider acceptance succeeds. +For slow processing, persist the event to a durable queue/database first, ACK it, +and perform downstream work separately. + +The client uses 10-second connection and read timeouts. A lost response can cause +an already processed request to be retried. Use `event_id` as a unique storage key; +already accepted duplicates must receive the same successful ACK. A separate +“check then insert” without a unique constraint does not prevent concurrent duplicates. + +Acknowledging before processing removes the event body from the client's queue +once confirmed. A later n8n failure will not trigger a phone retry. Successful +n8n execution and successful client acknowledgement are different outcomes. + +## Test without a phone + +Use your actual Production URL and a new ID for each test. This request contains +synthetic data only: + +```sh +curl --fail-with-body --max-time 10 \ + -X POST 'https://n8n.example.org/webhook/message487/YOUR-PATH' \ + -H 'Content-Type: application/json' \ + --data '{"schema_version":1,"event_id":"manual-check-001","device_id":"manual-test","device_code":"test-phone","message_type":"test","occurred_at":"2026-09-09T09:00:00Z","source":"life.andre.message487","source_name":"Message487","text":"Synthetic connection test"}' +``` + +Expect HTTP 200 and `{"status":"accepted","event_id":"manual-check-001"}`. +This checks the server contract; also test the Android connection button and a new +notification or SMS to exercise actual app delivery. + +## Troubleshooting + +| Symptom | Check | +| --- | --- | +| HTTP 404 | Production URL, publication, POST method and path | +| HTTP 401/403 | Authentication and proxy/n8n access restrictions | +| HTTP 301/302 | Supply the final URL; redirects are not followed | +| `INVALID_ACK` with HTTP 200 | JSON object, `status: accepted`, matching ID and Webhook response mode | +| Timeout/network error | Phone connectivity, TLS, firewall and time until response | +| No execution in the editor | Executions tab and successful execution retention | +| Tests arrive, messages do not | Sources, Android permissions, selected apps and pause state | + +Network failures, timeouts, HTTP 408/425/429 and 5xx retry automatically. Invalid ACKs +and other HTTP errors require intervention and manual retry from the journal. +Changing the URL only affects new events; queued events retain their old destination. +Send a new test after fixing configuration and delete old records separately if needed. +The top-bar bug icon opens [diagnostics](diagnostics.md), which records delivery +outcomes without message text. diff --git a/docs/en/project-context.md b/docs/en/project-context.md new file mode 100644 index 0000000..f15c3dc --- /dev/null +++ b/docs/en/project-context.md @@ -0,0 +1,120 @@ +# Message487 context + +[English](../en/project-context.md) | [Русский](../ru/project-context.md) + +Updated September 8, 2026. + +## Confirmed by the user + +- A new Android application replaces sms487 in a separate repository. +- Package ID: `life.andre.message487`. +- Primary positioning is n8n integration; arbitrary webhooks must also be supported. +- Project presentation, code style, privacy policy, documentation and CI follow + [AndroidMegaProxy](https://github.com/andre487/AndroidMegaProxy). + +## Original system + +[sms487](https://github.com/andre487/sms487) forwards SMS and notifications through a Go API +and SQS to a separate Telegram bot. The bot's code was not reviewed. The running system +has not been migrated. + +## Initial implementation + +The user requested a Docker Compose DevServer with n8n test workflows and an Android +connection-testing client. The client saves the endpoint, sends a synthetic event, +checks acknowledgement with a matching `event_id` and displays the result. A generic +webhook mode accepts HTTP 2xx. The ACK format is our example contract, not the default +response of every n8n workflow. + +Fastlane handles builds/checks. The UI uses Kotlin/Compose with English and Russian +resources. Notification/SMS capture, a persistent queue and retries were added later. +Webhook authentication remains unimplemented. `DevServer/README.md` describes the local +server and its limitations. + +## Risks found in the old sms487 client + +The prior discussion and static Android-code review identified asynchronous delivery +outliving a Worker, an SMS receiver without `goAsync()`, a loss window before persistence, +and no stable event ID. An HTTP success callback marks a batch sent before validating +the response. Logs contain a message prefix. These were static findings, not reproduced +on a device. + +The old client appends `/add-sms` to its server address and uses a proprietary batch format. +Compatibility with that protocol was not agreed as a requirement for the new app. + +## Capture and delivery + +NotificationListenerService uses a package selection; SMS_RECEIVED uses RECEIVE_SMS and +goAsync. Both sources default to off. Existing SMS history is not read. Unchanged notification +updates are suppressed; changed content creates an event. Group summaries, ongoing notifications +and Message487's own notifications are excluded. + +Events are persisted in SQLite before delivery; request bodies use AES-GCM with Android +Keystore. WorkManager retries transient failures; a periodic recovery task restores scheduling. +Event ID, contents, URL and acknowledgement mode are captured together. Changing the connection +does not redirect existing queued events. Invalid ACKs and permanent HTTP errors require manual +retry. Unconfirmed events are not deleted by age; confirmed payloads are removed while a bounded +metadata history remains. The journal hides message contents. Pause stops capture and new +attempts; a running request may complete. Backup and device transfer exclude application data. + +Limitations: capture depends on Android, and the process may die before local persistence. +WorkManager does not promise immediate delivery. Sensitive-notification restrictions are not +bypassed. Physical erasure of SQLite pages is not guaranteed. SMS deduplication uses sender, +time, text and installation; it does not replace server-side deduplication. + +## Product direction + +These items describe the direction; showing the last successful delivery on the overview and +viewing journal message contents are not implemented yet. + +- n8n connection with a sample workflow and synthetic test; alternatively a full custom webhook + URL. Shared transport sends JSON over HTTPS. +- App selection and separate SMS enablement, with contextual permission requests. Filtering + happens on the phone before persistence and delivery. +- A local queue persisted before network calls, stable retry IDs and installation identity + rather than the phone model. +- Overview with connection/permission/queue status, global pause and last successful delivery; + journal with contents hidden by default and manual retry. +- Webhook acceptance is distinct from delivery to Telegram or another downstream service. + Unconfirmed events are not silently removed by age. +- Diagnostics exclude secrets and message contents. Encryption, backup and retention requirements + must be decided before implementing storage. + +n8n has test and production URLs; persistent integrations use a published workflow's URL. +`Immediately` acknowledges workflow startup, not completion of its actions. See the +[Webhook documentation](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/). + +## Open decisions + +1. Physical-device checks for power saving, reboot and permission restrictions. +2. ACK after workflow execution versus durable server-side queue persistence. HTTP success + alone does not establish durable storage or downstream delivery. +3. Webhook authentication and future contract changes. README describes the current contract + and queue; limits for the unconfirmed queue require a separate decision. +4. Distribution and required device checks. The initial minimum SDK is configured in Gradle; + its suitability for future capture behavior still needs verification. +5. Whether to migrate the existing Telegram scenario and retain SQS. n8n does not require + removing the existing queue; PostgreSQL is one replacement discussed. + +Lost responses can produce duplicates. Deduplication must use a stable event ID; exactly-once +was not agreed and must not be promised. + +## AndroidMegaProxy references + +Its main branch was reviewed at `c8190e97b705a2c4578d278c40a690e97c5d5f27`. + +- Kotlin, Compose Material 3, system light/dark themes, English and Russian resources. +- JDK 21, Gradle Kotlin DSL and Fastlane through Bundler for builds/checks. +- PR/main CI: JVM tests, Android lint and builds, without a mandatory hosted emulator. +- PR APK artifacts without release keys; signing and releases are separate. +- Public privacy documentation describes actual application behavior. + +VPN, Go/JNI, DNS diagnostics and MegaProxy's publication specifics are not Message487 +requirements. Its privacy policy cannot simply be copied: Message487 sends event contents +to the chosen recipient, and n8n/downstream services have their own retention policies. + +Local rotating diagnostics, next-launch crash prompts and manual ZIP reports are implemented; +see [diagnostics](diagnostics.md). Recipient: der-morgenstern@yandex.ru. + +[Test categories and CI commands](testing.md) include Compose tests on Robolectric without +an emulator. [Signed APK releases](releases.md) use environment-based signing as in MegaProxy. diff --git a/docs/releases.md b/docs/en/releases.md similarity index 97% rename from docs/releases.md rename to docs/en/releases.md index f835dee..54e5d05 100644 --- a/docs/releases.md +++ b/docs/en/releases.md @@ -1,5 +1,7 @@ # Signed APK releases +[English](../en/releases.md) | [Русский](../ru/releases.md) + Run `bundle exec fastlane android release_artifacts` with JDK 21 and Android SDK 36. The lane runs Android JVM/Compose tests, debug/release lint and a signed release build, then checks the certificate, package ID, version and non-debuggable flag. Outputs are `dist/release/message487-.apk`, diff --git a/docs/testing.md b/docs/en/testing.md similarity index 97% rename from docs/testing.md rename to docs/en/testing.md index 82f51e1..209093f 100644 --- a/docs/testing.md +++ b/docs/en/testing.md @@ -1,5 +1,7 @@ # Tests and CI +[English](../en/testing.md) | [Русский](../ru/testing.md) + Run the same suites locally and in GitHub Actions: | Suite | Command | Coverage | diff --git a/docs/ru/design.md b/docs/ru/design.md new file mode 100644 index 0000000..ceecae9 --- /dev/null +++ b/docs/ru/design.md @@ -0,0 +1,40 @@ +# Дизайн интерфейса + +[English](../en/design.md) | [Русский](../ru/design.md) + +Message487 использует Material 3 со стандартной базовой фиолетовой палитрой. +Светлая и тёмная схемы берутся из библиотеки без изменения цветов и динамических +цветов обоев. Ориентир — MegaProxy: заметный статус работы, сгруппированные настройки, +скруглённые поверхности, читаемая типографика и ограниченная ширина содержимого. + +Главный экран показывает готовность захвата и проблемы, требующие внимания. +Этот статус не доказывает доступность сервера: её проверяют тестовым событием и журналом. +На основных экранах вне главного есть кнопка возврата к главному. На узких окнах используется +нижняя навигация, на широких — боковая панель. Черновики подключения переживают переходы +между разделами. Длинные списки приложений и событий отображаются лениво. + +Журнал выделяет источник, время и состояние доставки. Нажатие открывает ID с возможностью +копирования, HTTP-ответ, попытки, повтор и удаление. Текст сообщения скрыт. Выбор источников +поддерживает поиск, фильтр выбранных и массовый выбор всего доступного списка. +Ручной ввод пакета вынесен в отдельный диалог. Справка объясняет очередь, повторы +и системные ограничения. + +Область нажатия должна быть не меньше 48 dp. Передавайте статус текстом и значками, +а не только цветом. Используйте смысловые роли цветов темы, масштабируемую типографику, +системные отступы и прокрутку; не фиксируйте высоту текста. Проверяйте обе локали, +тёмную тему, крупный текст и узкие/широкие окна на эмуляторе. + +## Источники + +- [Google: Themes](https://developer.android.com/design/ui/mobile/guides/styles/themes) — базовая палитра Material. +- [Google: Layout basics](https://developer.android.com/design/ui/mobile/guides/layout-and-content/layout-basics) — группировка, отступы и доступность действий. +- [Google: Accessibility](https://developer.android.com/design/ui/mobile/guides/foundations/accessibility) — контраст, масштабирование и области нажатия. +- [Material 3: Navigation bar](https://m3.material.io/components/navigation-bar/guidelines) — основные разделы. +- [Google Design: Expressive design research](https://design.google/library/expressive-material-design-google-research) — выделение значимой информации. +- [Nielsen Norman Group: Visual hierarchy](https://www.nngroup.com/articles/visual-hierarchy-ux-definition/) — масштаб, контраст и группировка. + +## Проверка + +Редизайн проверен на эмуляторе API 35 на английском и русском, в светлой и тёмной +темах, с крупным текстом в узком окне и с боковой навигацией в широком. Android Lint, +JVM-тесты и обе APK-сборки запускаются через существующий Fastlane lane `checks`. diff --git a/docs/ru/diagnostics.md b/docs/ru/diagnostics.md new file mode 100644 index 0000000..fe44052 --- /dev/null +++ b/docs/ru/diagnostics.md @@ -0,0 +1,35 @@ +# Диагностика + +[English](../en/diagnostics.md) | [Русский](../ru/diagnostics.md) + +Значок жука в верхней панели открывает диагностику. Обновление читает ограниченный +фрагмент лога; подготовка письма создаёт отдельный ZIP и передаёт его через FileProvider +с временным доступом только на чтение. Предпочтение отдаётся почтовым приложениям, +запасной вариант — системное меню отправки. Message487 самостоятельно не отправляет +письмо. Очистка удаляет логи, последний креш и кеш архивов, но не очередь сообщений. + +В приватном каталоге `files/logs` хранятся три файла по 256 КиБ и `crash-latest.log` +размером до 256 КиБ. Запись выполняется одним фоновым потоком через очередь на 256 +записей; ротация и экспорт используют общую блокировку. При переполнении новые записи +пропускаются; их количество фиксируется при следующей успешной записи. Ошибки ввода-вывода +не приводят к крешу и отображаются на экране диагностики. Предпросмотр содержит последние +48 КиБ и последний креш. В `cache/feedback` остаётся не более трёх ZIP с уникальными именами. + +Обработчик необработанных исключений синхронно записывает креш с `fsync`, сохраняет +признак для диалога следующего запуска и передаёт управление прежнему обработчику Android. +Отдельный файл креша сохраняется при обычной ротации. Обрабатываются необработанные +Java/Kotlin-исключения, но не ANR, нативные креши, принудительная остановка или завершение +без исключения. Нехватка места и тяжёлые сбои процесса могут помешать записи. +Тексты исключений и имена потоков исключены; глубина причин, подавленные исключения и +число кадров ограничены. Имена символов приложения и строки исходников сохраняются в release. + +Логи используют фиксированный набор событий и типизированные либо разрешённые значения +метаданных. Не добавляйте тела сообщений, отправителей, пакеты, URL, ID событий/установок, +коды устройств или произвольные тексты исключений. В архив входят версии приложения, +Android, сведения об устройстве, архитектура и флаги настроек без секретов. + +Команда `bundle exec fastlane android checks` проверяет ротацию и перезапуск, сохранение +креша, исключение секретов, ошибки хранилища, передачу управления прежнему обработчику, +хранение архивов и разрешения вложения. Для ручной проверки на debug-эмуляторе запустите +приложение, выполните `adb shell am crash life.andre.message487` и откройте его снова. +Проверьте диалог и ZIP. Остановитесь на редакторе письма, если не собираетесь отправлять отчёт. diff --git a/docs/ru/n8n-telegram.md b/docs/ru/n8n-telegram.md new file mode 100644 index 0000000..a172376 --- /dev/null +++ b/docs/ru/n8n-telegram.md @@ -0,0 +1,199 @@ +# Пересылка уведомлений из Message487 в Telegram + +[English](../en/n8n-telegram.md) | [Русский](../ru/n8n-telegram.md) + +Сначала настройте [приём webhook в n8n](n8n-webhook.md) и проверьте тестовое событие. +Там же есть ссылки на официальную документацию, n8n Cloud и самостоятельную установку. +Telegram-токен хранится в **Credentials n8n**; в Android-приложение его вводить не нужно. +Сообщения будут доступны выбранному Telegram-чату и могут сохраняться в истории n8n. + +## 1. Создайте бота и credential + +1. В Telegram откройте официального [@BotFather](https://t.me/BotFather). +2. Отправьте `/newbot` и задайте имя и username бота. +3. Сохраните выданный токен в новом credential типа **Telegram** в n8n, поле + **Access Token**. Не вставляйте токен в текст узла, webhook URL или экспорт workflow. +4. Откройте личный чат с новым ботом и нажмите **Start** либо отправьте `/start`. + Это нужно, чтобы бот мог писать вам. + +Справка: [создание бота](https://core.telegram.org/bots/features#botfather), +[Telegram credentials в n8n](https://docs.n8n.io/integrations/builtin/credentials/telegram/). + +## 2. Узнайте chat_id + +Для нового бота, к которому ещё не подключён Telegram Trigger, сохраните следующий +код в локальный файл `telegram-chat-id.py` и запустите `python3 telegram-chat-id.py`. +Он запросит токен скрытым вводом и прочитает входящие обновления, не отправляя сообщений. +Перед запуском отправьте боту `/start` со своего Telegram-аккаунта. + +```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.') +``` + +Скопируйте ID нужного чата в настройку узла Telegram. Для группы добавьте бота в +группу, отправьте `/start@имя_бота` и возьмите ID группы из обновлений; сохраняйте +знак минус, если он есть. Для публичного канала можно использовать `@username` +канала, добавив бота администратором с правом публикации сообщений. + +`getUpdates` несовместим с уже установленным webhook Telegram. Если этот бот +используется в Telegram Trigger, возьмите `message.chat.id` из входных данных +его execution. Не удаляйте webhook работающего бота ради получения ID. +Для отправки из Message487 **Telegram Trigger не нужен**: входной триггер — Webhook. +Официальная справка: [getUpdates](https://core.telegram.org/bots/api#getupdates). + +## 3. Соберите цепочку отправки + +```mermaid +flowchart LR + W[Webhook: POST] --> C[Code: Prepare Telegram text] + C --> T[Telegram: Send Message] + T --> R[Respond to Webhook: ACK] +``` + +В Webhook оставьте **Respond → Using 'Respond to Webhook' Node**. Если вы +импортировали `receive.json`, удалите прямое соединение Webhook → Respond и +вставьте Code и Telegram перед Respond. Не оставляйте параллельный путь, который +подтвердит событие раньше отправки. + +Добавьте узел **Code**, имя `Prepare Telegram text`, язык **JavaScript**, режим +**Run Once for All Items**, и вставьте: + +```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 = [ + `Устройство: ${event.device_code || event.device_id || '—'}`, + `Источник: ${event.source_name || event.source || '—'}`, + `Тип: ${event.message_type}`, + event.title ? `Заголовок: ${event.title}` : '', + event.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 => ({ + '&': '&', '<': '<', '>': '>', +})[character]); +return chunks.map((part, index) => ({ + json: { + telegram_text: escapeHtml(chunks.length > 1 + ? `[${index + 1}/${chunks.length}]\n${part}` : part), + }, +})); +``` + +Код сохраняет полный текст, разделяя длинное событие на несколько сообщений. +Эмодзи не разрезаются внутри UTF-16-пары; `<`, `>` и `&` экранируются для HTML. +Запас по длине оставлен под номер части. Telegram допускает до 4096 символов +после разбора entities; см. [sendMessage](https://core.telegram.org/bots/api#sendmessage). + +Настройте узел **Telegram**: + +| Поле | Значение | +| --- | --- | +| Credential | Созданный Telegram credential | +| Resource | `Message` | +| Operation | `Send Message` | +| Chat ID | Постоянный ID вашего чата, группы или канала | +| Text, режим Expression | `{{ $json.telegram_text }}` | +| Additional Fields → Parse Mode | `HTML` | +| Append n8n Attribution | Выключить | +| Disable WebPage Preview | Включить, если не нужны превью ссылок | + +Chat ID задайте сами, не берите его из входящего запроса. Узел обработает каждую +часть, которую вернул Code. Оставьте **On Error → Stop Workflow**, чтобы ошибка +Telegram не превратилась в успешное подтверждение. Настройки операции описаны +в [официальной документации узла Telegram](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/message-operations/#send-message). + +В **Respond to Webhook** выберите JSON и HTTP 200. В режиме Expression для +Response Body используйте исходный запрос, а не результат узла Telegram: + +```javascript +{{ { status: 'accepted', event_id: $('Webhook').first().json.body.event_id } }} +``` + +После узла Telegram `$json` содержит ответ Telegram, поэтому `$json.body.event_id` +здесь не подходит. Если импортировали `receive.json`, замените **и Response Body, +и Response Code**: старые выражения в обоих полях обращаются к `$json.body`. +Базовая валидация в этой цепочке уже выполнена в Code до отправки. + +Опубликуйте workflow повторно. + +## 4. Проверьте доставку + +1. В Message487 включите подтверждение n8n и отправьте тестовое событие. +2. Убедитесь, что в Telegram пришло сообщение с типом `test`. +3. В n8n проверьте execution и успешное выполнение Telegram, а в приложении — ACK. +4. Включите уведомления, выдайте доступ и выберите нужное приложение-источник. +5. Создайте новое уведомление и сопоставьте его с execution по `event_id`. + +Эта цепочка принимает также SMS и тесты. Если нужны только уведомления, добавьте +после Webhook узел **If** с условием `{{ $json.body.message_type }}` равным +`notification`. Ветку true направьте в Code → Telegram → Respond, а false — +в отдельный Respond с тем же ACK: отфильтрованное событие должно быть подтверждено, +иначе оно останется в очереди клиента. + +Не выбирайте Telegram источником уведомлений в Message487, когда этот же телефон +получает сообщения вашего бота: это может создать цикл «Telegram → Message487 → +n8n → Telegram». Проще исключить Telegram целиком из выбранных приложений. + +## Ошибки и повторы + +| Ошибка | Что проверить | +| --- | --- | +| `chat not found` | Chat ID; начат ли личный диалог; добавлен ли бот в группу/канал | +| `bot was blocked` / HTTP 403 | Разблокируйте бота или восстановите его права | +| `can't parse entities` | Parse Mode HTML и приведённое экранирование; не используйте Markdown для этого шаблона | +| HTTP 429 | Ограничение Telegram; уменьшите частоту, учитывайте `retry_after` | +| Сообщение пришло, но клиент повторяет запрос | Правильный `event_id` в ACK и время ответа webhook | + +Если отправка всех частей не успевает до таймаута клиента, сначала сохраняйте +событие в надёжную очередь, подтверждайте приём и выполняйте Telegram-отправку +отдельным workflow. Не добавляйте длинное ожидание перед ACK. + +В простом примере нет дедупликации. Если Telegram принял сообщение, а ACK потерялся, +повтор создаст дубль; при ошибке одной из частей могут повториться и предыдущие. +Для устойчивой обработки храните `event_id` и прогресс отправки частей в БД. +Даже это не гарантирует ровно одну отправку при сбое между ответом Telegram и +фиксацией результата — учитывайте этот случай в своей схеме повторов. diff --git a/docs/ru/n8n-webhook.md b/docs/ru/n8n-webhook.md new file mode 100644 index 0000000..01a25a6 --- /dev/null +++ b/docs/ru/n8n-webhook.md @@ -0,0 +1,215 @@ +# Как подключить Message487 к n8n + +[English](../en/n8n-webhook.md) | [Русский](../ru/n8n-webhook.md) + +Инструкция для Message487 0.0.1. Нужен доступ к редактору n8n и HTTPS-адрес, +доступный с телефона. Для локального Android-эмулятора используйте debug-сборку и +[готовый DevServer](../../DevServer/README.md). Релизный APK не принимает HTTP-адреса. + +## Где запустить n8n + +- [Официальная документация n8n](https://docs.n8n.io/) — узлы, выражения и работа с workflow. +- [n8n Cloud: начало работы](https://docs.n8n.io/deploy/use-n8n-cloud/start-your-free-trial) — облачная версия без установки сервера. +- [Облако или собственный сервер](https://docs.n8n.io/choose-how-to-use-n8n) — сравнение вариантов запуска. +- [Самостоятельная установка](https://docs.n8n.io/deploy/host-n8n/install-options) — варианты развёртывания. +- [Установка с Docker Compose](https://docs.n8n.io/deploy/host-n8n/install-options/install-using-docker-compose) — официальная инструкция. + +Для n8n Cloud используйте HTTPS Production URL своего workspace. Для собственного +сервера настройте доступ по HTTPS, хранение данных и резервные копии. +[DevServer](../../DevServer/README.md) предназначен для локальной разработки, +а не для публикации в интернете с его тестовыми учётными данными. + +## 1. Создайте принимающий workflow + +Быстрее всего импортировать [receive.json](../../DevServer/workflows/receive.json) +через **Import from File** в меню редактора n8n. Это готовый пример приёмника с +проверкой базовых полей и подтверждением `event_id`. Импортируйте его как отдельный +workflow; не заменяйте им рабочий сценарий с вашими действиями. + +В импортированном workflow уже соединены два узла: + +```mermaid +flowchart LR + W[Webhook: POST] --> R[Respond to Webhook: JSON ACK] +``` + +Для ручной настройки создайте те же узлы. В узле **Webhook** установите: + +| Поле | Значение | +| --- | --- | +| HTTP Method | `POST` | +| Path | Уникальный путь, например `message487/receive-<случайная-строка>` | +| Authentication | `None` для текущей версии клиента | +| Respond | `Using 'Respond to Webhook' Node` | + +Случайную часть пути можно сгенерировать командой `openssl rand -hex 16`. +Не вставляйте угловые скобки из примера в настоящий URL. + +Message487 пока не отправляет заголовки авторизации, Basic Auth или JWT. Пароль +входа в редактор n8n не защищает webhook автоматически. Не распространяйте полный +URL; случайный путь не заменяет полноценную авторизацию. Для передачи личных +сообщений ограничьте доступ к endpoint доступным вам способом, например частной +сетью с доступом телефона. Настройка узла с Header/Basic/JWT Auth без поддержки +со стороны клиента приведёт к отказу доставки. + +В узле **Respond to Webhook** выберите **Respond With → JSON**, добавьте +**Response Code → 200**, а **Response Body** переключите в режим **Expression**: + +```javascript +{{ { status: 'accepted', event_id: $('Webhook').first().json.body.event_id } }} +``` + +Здесь `Webhook` — имя первого узла; при переименовании исправьте ссылку в выражении. +Этот минимальный ручной пример подтверждает запрос без валидации. Импортированный +`receive.json` дополнительно проверяет `schema_version`, `event_id`, `message_type` +и `text` и возвращает HTTP 400 для некорректного тела. + +Клиент ждёт JSON-объект следующего вида, а не массив, строку с JSON или HTML: + +```json +{"status":"accepted","event_id":"тот-же-event_id-что-в-запросе"} +``` + +`status` должен быть ровно `accepted`, а `event_id` — совпадать с запросом. +Обычный ответ n8n «Workflow got started» не подходит для режима подтверждения. +Настройки узлов описаны в документации [Webhook](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/) +и [Respond to Webhook](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/). + +## 2. Опубликуйте workflow и скопируйте URL + +Нажмите **Publish** (в более старых версиях n8n — включите **Active**). В узле +Webhook скопируйте **Production URL**, например: + +```text +https://n8n.example.org/webhook/message487/receive-<случайная-строка> +``` + +Не используйте постоянным адресом **Test URL** с `/webhook-test/`: он предназначен +для временного прослушивания через **Listen for test event**. Production URL +работает для опубликованного workflow, а его вызовы смотрят во вкладке **Executions**. +После изменения workflow опубликуйте изменения повторно. + +Если n8n стоит за reverse proxy и показывает внутренний адрес, настройте внешний +`WEBHOOK_URL`, число доверенных proxy в `N8N_PROXY_HOPS` и forwarded-заголовки по +[инструкции n8n для reverse proxy](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/configuration-examples/configure-webhook-urls-with-reverse-proxy). +Телефон должен обращаться сразу к конечному HTTPS URL с доверенным сертификатом: +Message487 не следует HTTP-перенаправлениям и не отключает проверку TLS. + +## 3. Настройте приложение + +1. Откройте **Подключение / Connection**. +2. Вставьте полный Production URL в поле веб-хука. +3. Задайте понятный **Код устройства / Device code**, например `personal-phone`. +4. Оставьте включённым **Подтверждение n8n / n8n confirmation**. +5. Нажмите кнопку сохранения и отправки тестового события. +6. В **Журнале / Journal** откройте событие и проверьте подтверждение и HTTP 200. + +После успешного теста включите нужные источники в **Источниках / Sources**: +для уведомлений выдайте системный доступ и выберите приложения; для SMS выдайте +разрешение на получение SMS. Если выбраны и SMS, и уведомления SMS-приложения, +одно входящее SMS может породить два разных события. + +Для эмулятора готовый DevServer использует +`http://10.0.2.2:5678/webhook/message487/receive`. Такой URL допустим только в +debug-сборке. На физическом телефоне `localhost` обозначает сам телефон, а +`10.0.2.2` не является адресом вашего компьютера. + +## 4. Посмотрите полученные данные + +В n8n откройте **Executions → нужный запуск → Webhook → Output → body**. Найдите +`event_id` из журнала приложения. Для сохранения успешных запусков включите +соответствующую настройку сохранения execution data; в DevServer она уже включена. +История n8n содержит полные сообщения, поэтому настройте её срок хранения и доступ. + +Пример тела уведомления: + +```json +{ + "schema_version": 1, + "event_id": "5d8ee1a5-9360-4b0e-8412-942b2e1c99ab", + "device_id": "43b55766-95b6-40b8-8f4f-c0240f547914", + "device_code": "personal-phone", + "message_type": "notification", + "occurred_at": "2026-09-09T09:00:00Z", + "source": "org.example.chat", + "source_name": "Example Chat", + "title": "Тестовое уведомление", + "text": "Проверка подключения" +} +``` + +| Поле | Значение | +| --- | --- | +| `event_id` | ID события; сохраняется при повторах доставки | +| `device_id` | ID установки приложения | +| `device_code` | Редактируемая метка устройства | +| `message_type` | `test`, `notification` или `sms` | +| `occurred_at` | Время события в формате ISO 8601 | +| `source` / `source_name` | Пакет источника / имя приложения; для SMS источник `android` | +| `title` | Заголовок уведомления; отсутствует у SMS и теста | +| `sender` | Отправитель SMS; отсутствует у уведомления и теста | +| `text` | Текст события | + +В узле сразу после Webhook доступны выражения `{{ $json.body.text }}` и +`{{ $json.body.device_code }}`. Если промежуточный узел меняет данные, обращайтесь +к исходному событию явно: `{{ $('Webhook').first().json.body.text }}`. + +## 5. Добавьте обработку сообщения + +Готовый пошаговый сценарий: [пересылка уведомлений в Telegram](n8n-telegram.md). + + +Готовый приёмник только подтверждает входящий запрос: он не отправляет сообщение +в Telegram, не сохраняет его в отдельную очередь и не удаляет дубли. + +Добавьте нужные действия перед Respond to Webhook и отправляйте ACK после +успешного завершения того действия, которое хотите считать приёмом сообщения. +Для долгой обработки сначала надёжно сохраните событие в собственной очереди или +БД, затем верните ACK, а последующие действия выполняйте отдельно. + +Клиент использует 10-секундные таймауты подключения и чтения. Если ответ потерян, +он может повторить уже обработанный запрос. Используйте `event_id` как уникальный +ключ в хранилище; уже принятый дубль должен получить тот же успешный ACK. +Отдельные операции «проверить наличие → добавить» без уникального ограничения не +защищают от двух одновременных запросов. + +Если отправить ACK **до** обработки, приложение удалит тело события из своей +очереди после подтверждения. Последующая ошибка в n8n не вызовет повтор на телефоне. +Статус успешного execution в n8n и успешное подтверждение клиенту — разные вещи. + +## Проверка без телефона + +Следующий запрос содержит только синтетические данные. Подставьте свой Production +URL и при каждом новом тесте используйте новый `event_id`: + +```sh +curl --fail-with-body --max-time 10 \ + -X POST 'https://n8n.example.org/webhook/message487/ВАШ-ПУТЬ' \ + -H 'Content-Type: application/json' \ + --data '{"schema_version":1,"event_id":"manual-check-001","device_id":"manual-test","device_code":"test-phone","message_type":"test","occurred_at":"2026-09-09T09:00:00Z","source":"life.andre.message487","source_name":"Message487","text":"Synthetic connection test"}' +``` + +Ожидаемый ответ: `{"status":"accepted","event_id":"manual-check-001"}` с HTTP 200. +Это проверяет серверный контракт; реальную доставку из Android проверьте кнопкой +тестового события и затем новым уведомлением или SMS. + +## Если доставка не работает + +| Симптом | Что проверить | +| --- | --- | +| HTTP 404 | Production URL, публикацию workflow, метод POST и путь | +| HTTP 401/403 | Настройки авторизации и ограничения доступа на proxy/n8n | +| HTTP 301/302 | Укажите конечный URL: клиент не следует redirect | +| `INVALID_ACK` при HTTP 200 | JSON-объект, `status: accepted`, точное совпадение `event_id`, режим ответа Webhook | +| Таймаут / сетевая ошибка | Доступность адреса с телефона, TLS, firewall, время до ответа | +| Нет execution в редакторе | Откройте вкладку Executions, проверьте сохранение успешных запусков | +| Тест проходит, а сообщений нет | Источники, разрешения Android, выбор приложений и паузу пересылки | + +Сетевые ошибки, таймауты, HTTP 408/425/429 и 5xx приводят к автоматическим повторам. +Неверный ACK и прочие HTTP-ошибки требуют вмешательства и ручного повтора из журнала. +Изменение URL в настройках влияет только на **новые** события: уже поставленные +в очередь сохраняют прежний адрес. После исправления подключения отправьте новый +тест; старые записи при необходимости удалите отдельно. + +Для разбора ошибок откройте значок диагностики в верхней панели приложения. +[Диагностический лог](diagnostics.md) показывает исход доставки без текста сообщений. diff --git a/docs/project-context.md b/docs/ru/project-context.md similarity index 94% rename from docs/project-context.md rename to docs/ru/project-context.md index 8a99a00..349c285 100644 --- a/docs/project-context.md +++ b/docs/ru/project-context.md @@ -1,5 +1,7 @@ # Контекст Message487 +[English](../en/project-context.md) | [Русский](../ru/project-context.md) + Обновлено 8 сентября 2026 года. ## Подтверждено пользователем @@ -111,8 +113,8 @@ VPN, Go/JNI, DNS-диагностика и детали публикации Meg Политику конфиденциальности нельзя копировать дословно: Message487 передаёт содержимое событий выбранному получателю, а n8n и последующие сервисы имеют собственные правила хранения. -Local rotating diagnostics, a next-launch crash prompt and manual ZIP email reports are implemented; see [diagnostics](diagnostics.md). Recipient: der-morgenstern@yandex.ru. +Реализованы локальные логи с ротацией, диалог после креша и ручная отправка ZIP-отчёта; см. [диагностику](diagnostics.md). Адрес: der-morgenstern@yandex.ru. -CI test categories and commands are documented in [testing](testing.md); Compose UI tests run on Robolectric without an emulator. +Наборы тестов и команды CI описаны в [тестировании](testing.md); Compose UI-тесты выполняются на Robolectric без эмулятора. -Signed APK releases use environment-based signing as in MegaProxy; see [releases](releases.md). +Подписанные APK используют параметры окружения, как в MegaProxy; см. [релизы](releases.md). diff --git a/docs/ru/releases.md b/docs/ru/releases.md new file mode 100644 index 0000000..cb4b5e4 --- /dev/null +++ b/docs/ru/releases.md @@ -0,0 +1,46 @@ +# Выпуск подписанного APK + +[English](../en/releases.md) | [Русский](../ru/releases.md) + +Запустите `bundle exec fastlane android release_artifacts` с JDK 21 и Android SDK 36. +Lane выполняет JVM/Compose-тесты, debug/release lint и подписанную release-сборку, +затем проверяет сертификат, package ID, версию и отсутствие debug-флага. Результаты: +`dist/release/message487-.apk`, файл R8 `mapping.txt` и `SHA256SUMS`. +При разборе крешей используйте mapping от конкретного APK. + +Подпись следует контракту окружения MegaProxy с префиксом `MESSAGE487_`: + +| Переменная | Источник / значение по умолчанию | +| --- | --- | +| `MESSAGE487_KEYSTORE_PATH` | Локально `~/AndroidApkKey`; в CI — восстановленный временный файл | +| `MESSAGE487_KEY_PASSWORD_FILE` | Локально `~/.my-tokens/android-key-password`; в CI — временный файл | +| `MESSAGE487_KEYSTORE_PASSWORD` | Экспортируется release-скриптом из файла пароля | +| `MESSAGE487_KEY_ALIAS` | Локально `key0`; в CI — секрет `ANDROID_KEY_ALIAS` | +| `MESSAGE487_KEY_PASSWORD` | Содержимое файла пароля, если не задан отдельно | +| `MESSAGE487_EXPECTED_CERT_SHA256` | Ожидаемый публичный отпечаток сертификата, закреплённый в скрипте | +| `MESSAGE487_RELEASE_DIR` | `dist/release` | + +Gradle читает только четыре signing-переменные: путь, пароль хранилища, alias и пароль +ключа. Частичная конфигурация приводит к ошибке. PR-lane `checks` отвергает signing-параметры +и проверяет неподписанный release APK. Пароли не передаются аргументами командной строки; +их нельзя печатать, коммитить или передавать через Gradle `-P`. + +Имена GitHub Secrets совпадают с MegaProxy: `ANDROID_SIGNING_KEY_BASE64`, +`ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`. +Workflow восстанавливает ключ и пароль с приватными правами и удаляет временные файлы +даже при сбое. Job сборки имеет только чтение репозитория; GitHub Release может записывать +лишь отдельный job публикации. PR-workflow не использует signing-секреты. + +## Проверка и публикация + +- Тег `release-check/*` запускает сборку и проверку подписанного APK в GitHub Actions + без публикации Release. APK, mapping, контрольные суммы и отчёты доступны как артефакты. +- После слияния workflow в основную ветку ручной запуск также только собирает артефакты. +- Для публикации увеличьте `versionCode`, задайте нужный `versionName` в `app/build.gradle.kts` + и слейте проверенный PR после обязательных проверок. Отправьте тег `v`. + Workflow требует наличия коммита в `main` и совпадения версии с тегом. Проверенные APK, + mapping и контрольные суммы публикуются в GitHub Releases. Повторный запуск не перезаписывает + существующий Release. + +Первая настроенная версия — `0.0.1`, `versionCode = 1`. Для последующих версий увеличивайте +`versionCode`, чтобы Android мог обновить приложение. Сохраняйте тот же ключ подписи. diff --git a/docs/ru/testing.md b/docs/ru/testing.md new file mode 100644 index 0000000..08e9ead --- /dev/null +++ b/docs/ru/testing.md @@ -0,0 +1,43 @@ +# Тесты и CI + +[English](../en/testing.md) | [Русский](../ru/testing.md) + +Локально и в GitHub Actions используются одинаковые команды: + +| Набор | Команда | Что проверяется | +| --- | --- | --- | +| Android | `bundle exec fastlane android checks` | JVM-логика, HTTP через MockWebServer, БД/настройки/provider через Robolectric, Compose UI, локализация, манифест, контраст обеих тем, debug/release lint и сборки, отсутствие release-подписи | +| Python | `PYTHON=.venv/bin/python bundle exec fastlane android python_checks` | HTTP-клиент тестового сервера на локальном HTTP-стенде, форматирование закреплёнными Black/isort | +| n8n | `bundle exec fastlane android server_tests` | Приём test/notification/SMS, валидация, HTTP-ошибка, неверный ACK и таймаут на живом сервере | + +Для Python создайте `.venv` командой `python3 -m venv .venv` и установите +`requirements-dev.txt`. Для n8n сначала выполните +`docker compose -f DevServer/compose.yaml up -d --wait --wait-timeout 300`. +Серверные проверки используют синтетические данные и сохраняют их в истории execution. + +`.github/workflows/ci.yml` запускает все три job на PR, push в main и вручную. +XML/HTML-отчёты Android-тестов и lint загружаются и при ошибках. Успешный Android-job +также публикует debug APK и неподписанный release APK. Подпись release в этих проверках +не используется. Локальный успех не подтверждает результат GitHub для неотправленных изменений. + +## Сравнение с MegaProxy + +Совпадают применимые категории: JVM-логика, Android-интеграция, Compose через Robolectric, +контракты ресурсов/безопасности/UI, Python и форматирование, lint и сборки. Message487 +дополнительно проверяет живой Docker/n8n. Go race-тесты и необязательный fuzz-lane +MegaProxy относятся к его Go/JNI-коду; здесь такого кода нет. Его Python-тесты истории +CI относятся к скриптам, которых в Message487 нет. + +Compose-тесты используют настоящую навигацию, экраны, ViewModel и настройки с тестовым +Application без запуска Worker и установки глобального обработчика крешей. Явная +фабрика ViewModel привязывает каждый тест к своему Application; перед проверками +дожидаются фоновых операций. Покрыты переходы и возврат, валидация и сохранение подключения, +массовый выбор приложений, очистка диагностики. + +Эти тесты не подтверждают системную доставку SMS/уведомлений, работу разрешений, +планирование WorkManager, настоящий Android Keystore, смерть процесса или почтовый клиент. +Для этого нужны устройства/эмулятор; сценарий креша описан в [диагностике](diagnostics.md). +Как и MegaProxy, обязательный hosted CI не зависит от нестабильной доступности KVM. + +Подписанная release-сборка отдельно запускает Android-тесты и lint с signing-переменными. +PR-проверки отвергают эти переменные и остаются неподписанными. См. [релизы](releases.md).