diff --git a/.github/workflows/play-metadata.yml b/.github/workflows/play-metadata.yml new file mode 100644 index 0000000..0904abf --- /dev/null +++ b/.github/workflows/play-metadata.yml @@ -0,0 +1,38 @@ +name: Upload Google Play descriptions + +on: + workflow_dispatch: + inputs: + metadata_commit: + description: "Full commit SHA containing the reviewed descriptions for the Play release" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: google-play-descriptions + cancel-in-progress: false + +jobs: + upload: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4.10" + bundler-cache: true + - name: Upload reviewed descriptions + env: + PLAY_METADATA_COMMIT: ${{ inputs.metadata_commit }} + SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }} + run: bundle exec fastlane android play_metadata + - name: Next step + run: echo 'Descriptions uploaded without submission. Check both languages and the matching release in Play Console before sending for review.' >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index e8e62bb..4447c83 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ google-services.json .DS_Store fastlane/report.xml DevServer/.env +ReviewServer/.env +ReviewServer/credentials.json .venv/ __pycache__/ diff --git a/ReviewServer/Caddyfile b/ReviewServer/Caddyfile new file mode 100644 index 0000000..8bda9ad --- /dev/null +++ b/ReviewServer/Caddyfile @@ -0,0 +1,7 @@ +message487.158.160.132.57.nip.io { + encode gzip + request_body { + max_size 1MB + } + reverse_proxy 172.30.48.2:5678 +} diff --git a/ReviewServer/README.md b/ReviewServer/README.md new file mode 100644 index 0000000..1ee534f --- /dev/null +++ b/ReviewServer/README.md @@ -0,0 +1,80 @@ +# Google Play SMS review server + +Dedicated synthetic-data n8n instance for reproducing Android → computer SMS transfer. +The deployed site is configured in [Caddyfile](Caddyfile); the VM directory is +`/opt/message487-review`. Keep it running throughout review. It is separate from the +MegaProxy SSH review account on the same VM. + +The n8n account owns only this disposable review instance. It has no personal workflows, +requires no invitation/OTP/payment, and has no scheduled expiry. Ordinary app users still +provide their own receiver. Never use this shared review account for private messages. + +## Configuration and startup + +Use Docker Compose and Caddy. On the small review VM, swap supplements RAM; this is a +low-volume test receiver. Resource limits and execution retention live in [compose.yaml](compose.yaml). +The Docker network has no external route. Caddy connects directly to the fixed container +address in Compose; n8n has no published host port. Outbound integrations are intentionally +unavailable on this instance. + +Create a private `.env` containing `REVIEW_HOST`, `REVIEW_EMAIL` and +`REVIEW_PASSWORD_HASH`. Use a unique reviewer password and a bcrypt hash; single-quote the +hash in `.env` so Compose does not interpolate its dollar signs. Create `credentials.json` +using the structure of [the local fixture](../DevServer/credentials/header-auth.json), +keeping credential ID `message487-header-auth` but replacing its value with `Bearer ` plus +a newly generated secret token. Do not deploy the public development credentials. +Keep the server directory accessible only to the administrator; the mounted JSON files must +be readable by the container's `node` user. Neither secret file belongs in Git. + +On a fresh instance, from its directory: + +```sh +sudo docker compose run --rm --no-deps n8n import:credentials --input=/bootstrap/credentials.json +sudo docker compose run --rm --no-deps n8n import:workflow --input=/bootstrap/workflow.json +sudo docker compose run --rm --no-deps n8n publish:workflow --id=message487-review +sudo docker compose up -d --wait +``` + +Imports replace the matching IDs. Stop this instance before intentionally reimporting; +ordinary restarts need only `docker compose up -d --wait`. Preserve its volume and generated +encryption key. Back up the existing Caddy configuration and add the site block without +removing other routes; validate before reloading Caddy. Ports 80/443 must be reachable for +certificate issuance and HTTPS. Do not change the host's existing SSH restrictions. + +## Reviewer procedure + +Private credentials and copyable instructions are stored locally in +`~/.my-tokens/message487-play-review-158.160.132.57.{json,txt}`. The two existing Play Console +App access instructions were updated to this instance on 2 October 2026. Recheck them before +resubmitting; the old n8n Cloud video is not a demonstration of this instance. + +1. Sign in on the computer and open **Message487 — Read SMS on computer**. + The workflow is already published; no setup or manual execution is required. +2. In Android **Connection**, save the HTTPS webhook URL and token without the `Bearer ` + prefix. Keep **n8n confirmation** on. +3. In **Sources**, leave Notifications off, enable Incoming SMS and grant SMS permission. +4. Receive a new synthetic SMS. For the dedicated emulator: + `adb -s emulator-5560 emu sms send +15551234567 'A fresh review message'`. +5. On the computer, open **Executions → newest run → Logs → Read SMS on computer → Output** + and choose **Table** or **Schema**. Expand the Logs panel if the output is hidden. +6. Match Sender and Message with the phone's SMS app, and Event ID with + **Message487 Journal → newest SMS**. A connection-test event alone does not test SMS access. + +Execution history contains webhook data, including authentication headers, and is accessible +to the dedicated reviewer account. Retention is bounded by the settings in Compose; +pruning is asynchronous. Remove test history through n8n when no longer needed. The receiver +acknowledges valid events but does not deduplicate retries or provide a general messaging inbox. + +## Verification + +```sh +python3 ReviewServer/check.py ~/.my-tokens/message487-play-review-158.160.132.57.json +``` + +This checks HTTPS, missing/wrong tokens, invalid payloads and a valid synthetic ACK. It does +not replace the Android SMS procedure. If a local HTTP proxy is incompatible with Python, +set `NO_PROXY` to the review hostname for that command. + +For a fresh video, keep the phone and computer output readable together and show the +permission, incoming SMS, matching message and event ID, and stopping capture. Use the +release being reviewed; do not expose passwords, tokens or webhook request headers. diff --git a/ReviewServer/check.py b/ReviewServer/check.py new file mode 100644 index 0000000..9d00dcd --- /dev/null +++ b/ReviewServer/check.py @@ -0,0 +1,44 @@ +import argparse +import datetime +import json +import uuid +import urllib.error +import urllib.request +from pathlib import Path + +parser = argparse.ArgumentParser(description='Check the live review webhook with synthetic data') +parser.add_argument('access_file', type=Path, help='Private JSON containing host and token') +args = parser.parse_args() +access = json.loads(args.access_file.read_text()) +url = f'https://{access["host"]}/webhook/message487/receive' +event = { + 'schema_version': 1, + 'event_id': str(uuid.uuid4()), + 'device_id': 'review-smoke-test', + 'device_code': 'review-smoke-test', + 'message_type': 'test', + 'occurred_at': datetime.datetime.now(datetime.timezone.utc).isoformat(), + 'source': 'life.andre.message487', + 'source_name': 'Message487', + 'text': 'Synthetic webhook check; this is not an Android SMS', +} + + +def post(body, token=None): + headers = {'Content-Type': 'application/json'} + if token is not None: + headers['Authorization'] = f'Bearer {token}' + request = urllib.request.Request(url, json.dumps(body).encode(), headers) + try: + with urllib.request.urlopen(request, timeout=30) as response: + return response.status, json.load(response) + except urllib.error.HTTPError as error: + return error.code, None + + +assert post(event)[0] in (401, 403), 'Unauthenticated requests must fail' +assert post(event, 'invalid-token')[0] in (401, 403), 'Wrong tokens must fail' +assert post({}, access['token'])[0] == 400, 'Invalid payloads must fail' +status, response = post(event, access['token']) +assert status == 200 and response == {'status': 'accepted', 'event_id': event['event_id']} +print(f'Webhook checks passed; synthetic event ID: {event["event_id"]}') diff --git a/ReviewServer/compose.yaml b/ReviewServer/compose.yaml new file mode 100644 index 0000000..0860b26 --- /dev/null +++ b/ReviewServer/compose.yaml @@ -0,0 +1,67 @@ +name: message487-review + +services: + n8n: + image: docker.n8n.io/n8nio/n8n:2.38.1 + restart: unless-stopped + networks: + default: + ipv4_address: 172.30.48.2 + mem_limit: 640m + memswap_limit: 1g + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + environment: + NODE_OPTIONS: --max-old-space-size=384 + N8N_HOST: ${REVIEW_HOST:?Set REVIEW_HOST} + N8N_PROTOCOL: https + N8N_EDITOR_BASE_URL: https://${REVIEW_HOST} + WEBHOOK_URL: https://${REVIEW_HOST}/ + N8N_PROXY_HOPS: "1" + N8N_SECURE_COOKIE: "true" + N8N_DIAGNOSTICS_ENABLED: "false" + N8N_VERSION_NOTIFICATIONS_ENABLED: "false" + N8N_TEMPLATES_ENABLED: "false" + N8N_PERSONALIZATION_ENABLED: "false" + N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true" + N8N_BLOCK_ENV_ACCESS_IN_NODE: "true" + N8N_COMMUNITY_PACKAGES_ENABLED: "false" + NODES_EXCLUDE: '["n8n-nodes-base.executeCommand","n8n-nodes-base.readWriteFile","n8n-nodes-base.ssh"]' + N8N_INSTANCE_OWNER_MANAGED_BY_ENV: "true" + N8N_INSTANCE_OWNER_EMAIL: ${REVIEW_EMAIL:?Set REVIEW_EMAIL} + N8N_INSTANCE_OWNER_FIRST_NAME: Google Play + N8N_INSTANCE_OWNER_LAST_NAME: Reviewer + N8N_INSTANCE_OWNER_PASSWORD_HASH: ${REVIEW_PASSWORD_HASH:?Set REVIEW_PASSWORD_HASH} + EXECUTIONS_DATA_SAVE_ON_ERROR: all + EXECUTIONS_DATA_SAVE_ON_SUCCESS: all + EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS: "true" + EXECUTIONS_DATA_PRUNE: "true" + EXECUTIONS_DATA_MAX_AGE: "168" + EXECUTIONS_DATA_PRUNE_MAX_COUNT: "1000" + GENERIC_TIMEZONE: UTC + TZ: UTC + volumes: + - n8n-data:/home/node/.n8n + - ./workflow.json:/bootstrap/workflow.json:ro + - ./credentials.json:/bootstrap/credentials.json:ro + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:5678/healthz/readiness').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 60s + logging: + driver: json-file + options: + max-size: 5m + max-file: "2" + +volumes: + n8n-data: + +networks: + default: + internal: true + ipam: + config: + - subnet: 172.30.48.0/24 diff --git a/ReviewServer/workflow.json b/ReviewServer/workflow.json new file mode 100644 index 0000000..b5caa59 --- /dev/null +++ b/ReviewServer/workflow.json @@ -0,0 +1,131 @@ +{ + "id": "message487-review", + "name": "Message487 — Read SMS on computer", + "active": false, + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "message487/receive", + "responseMode": "responseNode", + "options": {}, + "authentication": "headerAuth" + }, + "id": "webhook", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 0, + 0 + ], + "webhookId": "message487-review", + "credentials": { + "httpHeaderAuth": { + "id": "message487-header-auth", + "name": "Message487 review webhook" + } + } + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { status: (typeof $json.body.event_id === 'string' && $json.body.event_id.length > 0 && $json.body.schema_version === 1 && ['test', 'notification', 'sms'].includes($json.body.message_type) && typeof $json.body.text === 'string') ? 'accepted' : 'rejected', event_id: $json.body.event_id ?? null } }}", + "options": { + "responseCode": "={{ (typeof $json.body.event_id === 'string' && $json.body.event_id.length > 0 && $json.body.schema_version === 1 && ['test', 'notification', 'sms'].includes($json.body.message_type) && typeof $json.body.text === 'string') ? 200 : 400 }}" + } + }, + "id": "response", + "name": "Respond", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + 440, + 0 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "sender", + "name": "Sender", + "value": "={{ $json.body.sender }}", + "type": "string" + }, + { + "id": "text", + "name": "Message", + "value": "={{ $json.body.text }}", + "type": "string" + }, + { + "id": "occurred_at", + "name": "Received at", + "value": "={{ $json.body.occurred_at }}", + "type": "string" + }, + { + "id": "device_code", + "name": "Phone", + "value": "={{ $json.body.device_code }}", + "type": "string" + }, + { + "id": "event_id", + "name": "Event ID", + "value": "={{ $json.body.event_id }}", + "type": "string" + }, + { + "id": "message_type", + "name": "Type", + "value": "={{ $json.body.message_type }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "read-sms", + "name": "Read SMS on computer", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 660, + 0 + ] + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Respond", + "type": "main", + "index": 0 + } + ] + ] + }, + "Respond": { + "main": [ + [ + { + "node": "Read SMS on computer", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "all", + "saveManualExecutions": true + } +} diff --git a/docs/en/releases.md b/docs/en/releases.md index 311d899..976fa30 100644 --- a/docs/en/releases.md +++ b/docs/en/releases.md @@ -129,6 +129,27 @@ A failed Play upload fails the job but leaves the published GitHub Release avail The bundle signature verifier also rejects unsigned added entries, modified entries, missing required bundle entries and unexpected certificates; its regression fixtures run in Android CI. +### Store descriptions without a new binary + +Run the manual **Upload Google Play descriptions** workflow from `main` and supply the full +SHA of the reviewed commit containing the descriptions. Use a commit whose text matches the +version being reviewed in Play; it may include editorial fixes made after that version's tag. +The workflow runs current tooling and reads only EN/RU title, short description and full +description from that commit. It does not execute code from the selected commit. + +Locally, with the service-account environment loaded: + +```shell +bundle exec fastlane android play_metadata metadata_commit: +``` + +Omitting the SHA uses committed `HEAD`, never uncommitted text. Screenshots, artwork, binaries +and release notes stay untouched. Upload leaves changes unsubmitted and disables Fastlane's +automatic submission fallback. Check both languages and the matching release in Publishing +overview before sending for review. Upload, review approval and publication are separate steps. +The general store listing is shared across tracks. Neither an internal draft nor this workflow +binds the descriptions to a version. See [Google's publishing controls](https://support.google.com/googleplay/android-developer/answer/9859654?hl=en). + ## F-Droid Store descriptions, changelogs and artwork live in diff --git a/docs/ru/releases.md b/docs/ru/releases.md index d5b40e1..b71abc0 100644 --- a/docs/ru/releases.md +++ b/docs/ru/releases.md @@ -134,6 +134,27 @@ bundle exec fastlane android play_release изменённые файлы, отсутствие обязательных частей и чужой сертификат; регрессионные тесты этой проверки входят в Android CI. +### Описания магазина без новой сборки + +Запустите вручную workflow **Upload Google Play descriptions** из `main`, указав полный SHA +проверенного коммита с описаниями. Текст должен соответствовать версии на проверке в Play; +редакторские исправления могут быть сделаны после её релизного тега. Workflow использует +текущий код инструментов и читает из указанного коммита только название, краткое и полное +описание EN/RU. Код выбранного коммита не исполняется. + +Локально, после загрузки окружения с ключом сервисного аккаунта: + +```shell +bundle exec fastlane android play_metadata metadata_commit:<полный-sha-коммита> +``` + +Без SHA используется закоммиченный `HEAD`, а не рабочие правки. Скриншоты, изображения, +сборки и release notes не меняются. Загрузка оставляет изменения неотправленными; автоматическая +повторная попытка Fastlane с отправкой на проверку отключена. Проверьте оба языка и нужную +версию в «Обзоре публикации», затем отправляйте на проверку. Загрузка, одобрение и публикация — +разные этапы. Карточка общая для треков; internal draft и этот workflow не привязывают описание +к версии. См. [управление публикацией Google](https://support.google.com/googleplay/android-developer/answer/9859654?hl=ru). + ## F-Droid Описания, списки изменений и графика магазина находятся в diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0a105f1..5281e98 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,3 +1,25 @@ +require "json" +require "open3" +require "tmpdir" +require "fileutils" + +def play_credentials + json_key_data = ENV["SUPPLY_JSON_KEY_DATA"] + if json_key_data.to_s.strip.empty? + UI.user_error!("Set SUPPLY_JSON_KEY_DATA to the service-account JSON contents") + end + begin + credentials = JSON.parse(json_key_data) + rescue JSON::ParserError + UI.user_error!("SUPPLY_JSON_KEY_DATA must contain valid JSON") + end + unless credentials.is_a?(Hash) && credentials["type"] == "service_account" && + %w[client_email private_key token_uri].all? { |key| credentials[key].is_a?(String) && !credentials[key].strip.empty? } + UI.user_error!("SUPPLY_JSON_KEY_DATA must contain a service-account key with client_email, private_key and token_uri") + end + json_key_data +end + default_platform(:android) opt_out_usage ensure_bundle_exec @@ -67,6 +89,48 @@ platform :android do gradle(task: "installDebug", project_dir: project_root) end + desc "Upload store descriptions from a committed revision without submitting for review" + lane :play_metadata do |options| + unknown_options = options.keys - [:metadata_commit] + UI.user_error!("Unknown play_metadata options: #{unknown_options.join(', ')}") unless unknown_options.empty? + revision = options.fetch(:metadata_commit, ENV.fetch("PLAY_METADATA_COMMIT", "HEAD")).to_s + unless revision == "HEAD" || revision.match?(/\A[0-9a-f]{40}\z/) + UI.user_error!("metadata_commit must be HEAD or a full commit SHA") + end + commit, _, status = Open3.capture3("git", "-C", project_root, "rev-parse", "--verify", "#{revision}^{commit}") + UI.user_error!("Metadata commit is unavailable; fetch it first") unless status.success? + commit = commit.strip + Dir.mktmpdir("message487-play-metadata-") do |metadata_path| + %w[en-US ru-RU].each do |language| + FileUtils.mkdir_p(File.join(metadata_path, language)) + { "title" => 30, "short_description" => 80, "full_description" => 4000 }.each do |field, limit| + path = "fastlane/metadata/android/#{language}/#{field}.txt" + value, _, result = Open3.capture3("git", "-C", project_root, "show", "#{commit}:#{path}") + UI.user_error!("Missing #{path} at #{commit}") unless result.success? + value = value.force_encoding(Encoding::UTF_8).strip + unless value.valid_encoding? && !value.empty? && value.length <= limit + UI.user_error!("Invalid #{language}/#{field}: expected 1–#{limit} UTF-8 characters") + end + File.write(File.join(metadata_path, language, "#{field}.txt"), value) + end + end + UI.message("Store description source: #{commit}") + upload_to_play_store( + json_key_data: play_credentials, + package_name: "life.andre.message487", + metadata_path: metadata_path, + skip_upload_apk: true, + skip_upload_aab: true, + skip_upload_metadata: false, + skip_upload_changelogs: true, + skip_upload_images: true, + skip_upload_screenshots: true, + changes_not_sent_for_review: true, + rescue_changes_not_sent_for_review: false + ) + end + end + desc "Upload a signed release AAB and R8 mapping to Google Play (internal draft by default)" lane :play_release do |options| allowed_options = %i[aab mapping track release_status validate_only dry_run] @@ -108,19 +172,7 @@ platform :android do next end - json_key_data = ENV["SUPPLY_JSON_KEY_DATA"] - if json_key_data.to_s.strip.empty? - UI.user_error!("Set SUPPLY_JSON_KEY_DATA to the service-account JSON contents") - end - begin - credentials = JSON.parse(json_key_data) - rescue JSON::ParserError - UI.user_error!("SUPPLY_JSON_KEY_DATA must contain valid JSON") - end - unless credentials.is_a?(Hash) && credentials["type"] == "service_account" && - %w[client_email private_key token_uri].all? { |key| credentials[key].is_a?(String) && !credentials[key].strip.empty? } - UI.user_error!("SUPPLY_JSON_KEY_DATA must contain a service-account key with client_email, private_key and token_uri") - end + json_key_data = play_credentials upload_to_play_store( **files, diff --git a/fastlane/README.md b/fastlane/README.md index 398140e..2041ef6 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -63,6 +63,14 @@ Build a debug APK Install the debug APK on the connected emulator or device +### android play_metadata + +```sh +[bundle exec] fastlane android play_metadata metadata_commit: +``` + +Upload store descriptions from a committed revision without submitting for review. + ### android play_release ```sh diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 74fd492..590f305 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,7 +1,16 @@ -Message487 forwards selected Android notifications and new incoming SMS to an HTTPS webhook you configure. Connect your phone to n8n workflows or another webhook service; no n8n account is required when using a different server. +Read new SMS from your Android phone on your computer through your own n8n workflow. Message487 transfers incoming SMS text, sender and time to the HTTPS webhook you choose. This is one-way transfer of new messages: it does not import SMS history, send SMS or provide a two-way messaging inbox. + +HOW TO READ SMS ON YOUR COMPUTER +1. Set up and publish an n8n webhook workflow with Bearer-token authentication. +2. In Message487 Connection, save its HTTPS URL and token. In Sources, turn on Incoming SMS and grant the receive-SMS permission. +3. Receive a new SMS on the phone. On your computer, open the workflow's Executions in n8n and view the received message text and sender. Compare the event ID with Message487 Journal. +The Journal shows delivery status, not message contents. Notification access is not needed for SMS transfer. Without RECEIVE_SMS, Message487 cannot capture and transfer new incoming SMS; its separate notification feature can still be used. + +You supply the n8n service or another compatible HTTPS receiver and token. Message487 does not include a hosted inbox or an n8n subscription. Another receiver does not require an n8n account. + +OTHER FEATURES • Choose which applications can forward notifications, or select all visible launcher apps. -• Enable SMS forwarding separately. Existing SMS history is not read. • Authenticate requests with a Bearer token and optionally require an event acknowledgement. • Keep events in an encrypted local outbox and retry temporary delivery failures. • Pause forwarding, review delivery status, retry or delete queued events. diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index daf86da..964580a 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -Forward notifications and SMS to n8n or your own webhook +Read phone SMS on your computer via n8n; forward selected notifications diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt index ecec82b..5a4dc54 100644 --- a/fastlane/metadata/android/ru-RU/full_description.txt +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -1,7 +1,16 @@ -Message487 пересылает выбранные уведомления Android и новые входящие SMS на указанный вами HTTPS-веб-хук. Подключите телефон к сценариям n8n или другому серверу; при использовании другого сервера аккаунт n8n не нужен. +Читайте новые SMS своего Android-телефона на компьютере через собственный сценарий n8n. Message487 передаёт текст входящего SMS, отправителя и время на выбранный вами HTTPS-веб-хук. Это односторонняя передача новых сообщений: приложение не импортирует историю SMS, не отправляет SMS и не предоставляет двусторонний чат. + +КАК ПРОЧИТАТЬ SMS НА КОМПЬЮТЕРЕ +1. Настройте и опубликуйте в n8n сценарий с веб-хуком и авторизацией по Bearer-токену. +2. В разделе «Подключение» Message487 сохраните HTTPS-адрес и токен. В «Источниках» включите входящие SMS и предоставьте разрешение на их получение. +3. Получите новое SMS на телефоне. На компьютере откройте Executions вашего сценария n8n и просмотрите полученные текст и отправителя. Сопоставьте идентификатор события с журналом Message487. +Журнал показывает статус доставки, а не содержимое сообщения. Доступ к уведомлениям для передачи SMS не нужен. Без RECEIVE_SMS приложение не может получать и передавать новые входящие SMS; отдельная функция пересылки уведомлений остаётся доступна. + +Вы предоставляете сервис n8n либо другой совместимый HTTPS-получатель и токен. Message487 не включает готовый облачный почтовый ящик или подписку n8n. Для другого получателя аккаунт n8n не нужен. + +ДРУГИЕ ВОЗМОЖНОСТИ • Выберите приложения для пересылки уведомлений или все видимые приложения с иконкой запуска. -• Отдельно включите пересылку SMS. История SMS не читается. • Используйте Bearer-токен и при необходимости подтверждение приёма события. • Храните события в зашифрованной локальной очереди с повторными попытками при временных сбоях. • Приостанавливайте пересылку, проверяйте статусы, повторяйте или удаляйте события. diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt index 82b6de4..8dcd5b2 100644 --- a/fastlane/metadata/android/ru-RU/short_description.txt +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -1 +1 @@ -Пересылка уведомлений и SMS в n8n или ваш веб-хук +Читайте SMS с телефона на компьютере через n8n. Пересылайте уведомления diff --git a/scripts/test-play-release.rb b/scripts/test-play-release.rb index a79153e..f726455 100644 --- a/scripts/test-play-release.rb +++ b/scripts/test-play-release.rb @@ -10,12 +10,12 @@ def self.message(*) = nil end class LaneCheck - attr_reader :uploads - def initialize + attr_reader :uploads, :metadata + def initialize(root = File.expand_path("..", __dir__)) @lanes = {} @uploads = [] path = File.expand_path("../fastlane/Fastfile", __dir__) - instance_eval(File.read(path), path) + instance_eval(File.read(path), File.join(root, "fastlane/Fastfile")) end def default_platform(*) = nil def opt_out_usage = nil @@ -23,8 +23,13 @@ def ensure_bundle_exec = nil def desc(*) = nil def platform(*) = yield def lane(name, &block) = @lanes[name] = block - def upload_to_play_store(**options) = @uploads << options + def upload_to_play_store(**options) + @metadata = Dir.glob(File.join(options[:metadata_path], "**/*")).select { |path| File.file?(path) } + .to_h { |path| [path.delete_prefix("#{options[:metadata_path]}/"), File.read(path)] } + @uploads << options + end def run(**options) = @lanes.fetch(:play_release).call(options) + def run_metadata(**options) = @lanes.fetch(:play_metadata).call(options) end def assert(value) @@ -73,3 +78,57 @@ def assert(value) assert(check.uploads.last.values_at(:track, :release_status, :validate_only) == ["production", "completed", false]) end puts "play_release checks passed (no network calls)" + +Dir.mktmpdir("message487-metadata-test") do |root| + git = lambda do |*args| + output, error, status = Open3.capture3("git", "-C", root, *args) + raise error unless status.success? + output.strip + end + git.call("init", "-q") + git.call("config", "user.name", "Test") + git.call("config", "user.email", "test@example.invalid") + source = File.expand_path("../fastlane/metadata/android", __dir__) + destination = File.join(root, "fastlane/metadata/android") + FileUtils.mkdir_p(File.dirname(destination)) + FileUtils.cp_r(source, destination) + git.call("add", ".") + git.call("commit", "-qm", "Fixture") + commit = git.call("rev-parse", "HEAD") + check = LaneCheck.new(root) + ENV.delete("PLAY_METADATA_COMMIT") + check.run_metadata(metadata_commit: commit) + original = check.metadata + assert(original.size == 6) + upload = check.uploads.last + assert(upload[:skip_upload_aab] && upload[:skip_upload_apk] && upload[:skip_upload_changelogs]) + assert(upload[:skip_upload_images] && upload[:skip_upload_screenshots] && !upload[:skip_upload_metadata]) + assert(upload[:changes_not_sent_for_review] && !upload[:rescue_changes_not_sent_for_review]) + assert(!File.exist?(upload[:metadata_path])) + title = File.join(destination, "en-US/title.txt") + File.write(title, "Uncommitted text") + check.run_metadata + assert(check.metadata == original) + [{metadata_commit: "--help"}, {metadata_commit: "0" * 40}, {typo: true}].each do |options| + count = check.uploads.size + begin + check.run_metadata(**options) + raise "Expected invalid revision/options rejection" + rescue ArgumentError + assert(check.uploads.size == count) + end + end + ["", "x" * 31, "\xff".b, nil].each do |invalid| + invalid ? File.binwrite(title, invalid) : File.delete(title) + git.call("add", ".") + git.call("commit", "-qm", "Invalid metadata") + count = check.uploads.size + begin + check.run_metadata + raise "Expected invalid/missing text rejection" + rescue ArgumentError + assert(check.uploads.size == count) + end + end +end +puts "play_metadata checks passed (no network calls)"