docs(rust): phase 11 as built and walked — §26.4-26.6, M16
The raid alert reached an emulator through ntfy.sh 19 seconds after a rig raid, titled from its inbox row; switched off in the app, the next raid enqueued in-app only. Closes M7's open QA item (on-device delivery against a live ntfy), and amends the M7 and engagement phase 8 passages D69 and D70 made stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -229,7 +229,8 @@ Verified green: `:app:testDebugUnitTest` (18 new JVM tests — notifications DTO
|
|||||||
parse incl. malformed, topic/URL building, stream→route map + personal gating) + `:app:lintDebug` +
|
parse incl. malformed, topic/URL building, stream→route map + personal gating) + `:app:lintDebug` +
|
||||||
`:app:assembleDebug`; backend 250 tests (+3 for `push.ntfyUrl`) and `npm run swagger` clean. The
|
`:app:assembleDebug`; backend 250 tests (+3 for `push.ntfyUrl`) and `npm run swagger` clean. The
|
||||||
foreground-service tradeoff (§11) and the POST_NOTIFICATIONS runtime permission are implemented as
|
foreground-service tradeoff (§11) and the POST_NOTIFICATIONS runtime permission are implemented as
|
||||||
planned; an on-device delivery pass against a live ntfy is the one open QA item.
|
planned; an on-device delivery pass against a live ntfy is the one open QA item. *(Closed by M16 on
|
||||||
|
2026-09-23: a raid alert delivered through `ntfy.sh` to an emulator, 19 seconds end to end.)*
|
||||||
|
|
||||||
**Part 2 (original plan) — the Android app.** UnifiedPush receiver + device registration
|
**Part 2 (original plan) — the Android app.** UnifiedPush receiver + device registration
|
||||||
against the merged Part-1 contract, a Notifications settings screen, and notification-tap deep-links.
|
against the merged Part-1 contract, a Notifications settings screen, and notification-tap deep-links.
|
||||||
@@ -313,7 +314,10 @@ Work items:
|
|||||||
`GET …/streams` with a per-stream toggle bound to `GET/PUT …/subscriptions`; a **personal** stream
|
`GET …/streams` with a per-stream toggle bound to `GET/PUT …/subscriptions`; a **personal** stream
|
||||||
(`requiresLinkedAccount`) is greyed with a "link a game account" hint until the user has a linked
|
(`requiresLinkedAccount`) is greyed with a "link a game account" hint until the user has a linked
|
||||||
account — reuse the linked-accounts signal already fetched for M4's player surface
|
account — reuse the linked-accounts signal already fetched for M4's player surface
|
||||||
(`PlayerShardRepository`), not a fresh source of truth. Toggling to a non-empty set triggers the
|
(`PlayerShardRepository`), not a fresh source of truth. *(M16 amends both halves: the signal is now
|
||||||
|
asked of the site's own module by capability (`LinkedAccountRepository`), because on a Rust site
|
||||||
|
`PlayerShardRepository` answered no for everyone. It holds back only switching push **on**, and no
|
||||||
|
longer greys the whole row.)* Toggling to a non-empty set triggers the
|
||||||
register flow (#5) and requests `POST_NOTIFICATIONS`; emptying the set unregisters. Each mutation
|
register flow (#5) and requests `POST_NOTIFICATIONS`; emptying the set unregisters. Each mutation
|
||||||
folds its `ApiResult` into a section-scoped, localized banner (§7 parity with M4).
|
folds its `ApiResult` into a section-scoped, localized banner (§7 parity with M4).
|
||||||
7. **Deep-links (resolves the §13 open item).** Tapping a notification opens the app to the stream's
|
7. **Deep-links (resolves the §13 open item).** Tapping a notification opens the app to the stream's
|
||||||
@@ -324,7 +328,8 @@ Work items:
|
|||||||
`label`) and deep-links — it does **not** pull `ref` content first; the content-free design means
|
`label`) and deep-links — it does **not** pull `ref` content first; the content-free design means
|
||||||
nothing needs decrypting to render the tap, and the target screen fetches fresh over the
|
nothing needs decrypting to render the tap, and the target screen fetches fresh over the
|
||||||
authenticated API on open. (Pulling `ref` for a richer inline notification is a possible later
|
authenticated API on open. (Pulling `ref` for a richer inline notification is a possible later
|
||||||
enhancement, not v1.)
|
enhancement, not v1.) *(M16 did it for `notification:<id>` refs: `PushContentResolver` pulls the
|
||||||
|
inbox row and titles the notification with it. Every other ref keeps the generic title.)*
|
||||||
8. **Menu.** Add a **Notifications** entry to the signed-in group in `ui/navigation/Menu.kt` (near My
|
8. **Menu.** Add a **Notifications** entry to the signed-in group in `ui/navigation/Menu.kt` (near My
|
||||||
Account), visible once signed in.
|
Account), visible once signed in.
|
||||||
9. **Permission UX.** Request `POST_NOTIFICATIONS` at the moment the user first enables a stream (API
|
9. **Permission UX.** Request `POST_NOTIFICATIONS` at the moment the user first enables a stream (API
|
||||||
@@ -1528,7 +1533,7 @@ push, and Play (M6–M8) follow the designed app.
|
|||||||
once the fleet was widened to signed-in — the app's bearer session reaching the module's viewer
|
once the fleet was widened to signed-in — the app's bearer session reaching the module's viewer
|
||||||
check. Branch `fix/rust-presence-visibility`.
|
check. Branch `fix/rust-presence-visibility`.
|
||||||
|
|
||||||
17. **M16 — a Rust notification on a phone** (post-v1; planned 2026-09-23). `module-rust` phase
|
17. **M16 — a Rust notification on a phone** (post-v1; built 2026-09-23). `module-rust` phase
|
||||||
11, R10's leg C: the app half of phase 10's notifications. **Design of record:
|
11, R10's leg C: the app half of phase 10's notifications. **Design of record:
|
||||||
[`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §26**, with its four decisions (D69–D72).
|
[`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §26**, with its four decisions (D69–D72).
|
||||||
|
|
||||||
@@ -1542,14 +1547,38 @@ push, and Play (M6–M8) follow the designed app.
|
|||||||
channel **off** is never gated, and only switching push **on** needs a link.
|
channel **off** is never gated, and only switching push **on** needs a link.
|
||||||
- **A push is titled from the item it points at.** A `notification:<id>` ref is pulled over the
|
- **A push is titled from the item it points at.** A `notification:<id>` ref is pulled over the
|
||||||
authenticated inbox API, and the relay still carries nothing. The per-stream title stays as the
|
authenticated inbox API, and the relay still carries nothing. The per-stream title stays as the
|
||||||
fallback and as the lock-screen version. §11's "nothing is fetched to show a notification"
|
fallback and as the lock-screen version. The M7 items that said nothing is
|
||||||
stops being true, and it gets rewritten when this lands.
|
fetched to show a notification are amended in place.
|
||||||
- **`/player/rust` and `/rust/servers/<id>?tab=<tab>` open natively.** M15 deferred the first
|
- **`/player/rust` and `/rust/servers/<id>?tab=<tab>` open natively.** M15 deferred the first
|
||||||
of these to exactly this phase.
|
of these to exactly this phase.
|
||||||
|
|
||||||
It also closes **M7's open QA item**. On-device delivery against a live ntfy has never been
|
It also closes **M7's open QA item**. On-device delivery against a live ntfy has never been
|
||||||
walked. It is walked here, on `ntfy.sh` (D72).
|
walked. It is walked here, on `ntfy.sh` (D72).
|
||||||
|
|
||||||
|
#### As built (2026-09-23)
|
||||||
|
|
||||||
|
Branch `feat/rust-phase-11-notifications`. The app suite is **672 tests, 0 failures**, up from
|
||||||
|
657, with `lintDebug` and `assembleDebug` green. It was walked on the `s22_ultra` emulator
|
||||||
|
against real core on `main`, with the module installed, the protocol-7 sidecar inside the Oxide
|
||||||
|
rig's container, and the relay on `ntfy.sh`:
|
||||||
|
|
||||||
|
- **Linked (raidowner1):** the raid alert's push switched on, and the device registered
|
||||||
|
`https://ntfy.sh/<topic>`. A door raided with phase 10's rig helper reached the phone
|
||||||
|
**19 seconds** later as *"Your base is being raided — A door was destroyed in P16 on Oxide
|
||||||
|
rig."* The notification was `vis=PRIVATE`, with a public version carrying only the generic
|
||||||
|
title. Tapping it opened the inbox, and the row opened the Oxide rig's screen natively.
|
||||||
|
- **Switched off in the app:** push off, and the next raid enqueued in-app only. The inbox row
|
||||||
|
arrived and no system notification was posted. With nothing else on push, the device was
|
||||||
|
unregistered and the push service stopped.
|
||||||
|
- **Not linked (walkadmin):** push held back with the hint, while email and in-app stayed live.
|
||||||
|
In-app was switched off, stored as `off`, and switched back on.
|
||||||
|
- **D71:** a row linking `?tab=leaderboard` opened on the Leaderboard tab, and `/player/rust`
|
||||||
|
opened *My Rust account*. `?tab=clans` went to a Custom Tab.
|
||||||
|
|
||||||
|
**Not walked:** a UO site (no UO core was running; the path is the call it always was, and a test
|
||||||
|
pins it), and the lock-screen rendering (the emulator has no screen lock; the public version was
|
||||||
|
read back from `dumpsys notification`).
|
||||||
|
|
||||||
### Deferred (not a milestone)
|
### Deferred (not a milestone)
|
||||||
|
|
||||||
- **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is
|
- **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is
|
||||||
@@ -1772,7 +1801,9 @@ of push.
|
|||||||
push streams, so `team.forum.post` would have landed on Home. `Routes.forTickle(stream, ref)` sends
|
push streams, so `team.forum.post` would have landed on Home. `Routes.forTickle(stream, ref)` sends
|
||||||
anything whose ref starts with `notification:` to the inbox and leaves every other tickle on the
|
anything whose ref starts with `notification:` to the inbox and leaves every other tickle on the
|
||||||
route it has always had. The ref is never decoded past that prefix and never rendered - it is a hint
|
route it has always had. The ref is never decoded past that prefix and never rendered - it is a hint
|
||||||
that a row exists, and the contract stays wake-and-pull.
|
that a row exists, and the contract stays wake-and-pull. *(M16 decodes it once more, to the id,
|
||||||
|
and only to pull that row over the authenticated inbox API and title the notification with it. It is
|
||||||
|
still never rendered.)*
|
||||||
|
|
||||||
**The settings screen** (`NotificationSettingsScreen` + `NotificationSettingsViewModel`) moved off
|
**The settings screen** (`NotificationSettingsScreen` + `NotificationSettingsViewModel`) moved off
|
||||||
`/notifications/subscriptions` onto `/notifications/channels`. Controls are rendered from the wire:
|
`/notifications/subscriptions` onto `/notifications/channels`. Controls are rendered from the wire:
|
||||||
|
|||||||
@@ -997,7 +997,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
|||||||
| 8 | **Android leg B** (R10), and the half of R2 a player may see. ✅ **Built 2026-09-22 — as built and findings in §22.** One drawer row under the player group shaped like `module-uo`'s own (the code card first, then what the code got them), gated on `rust` and `PLAYER`; the four refusals kept four pieces of advice; and **one new website route**, `GET /player/rust/permissions`, because phase 7 gave an operator every view of in-game privilege and a player none. Three org-lead decisions (§22.0), the scope arithmetic answered on the server so `inScope` exists once (§22.1), and **the rig moved the sidecar inside the game container** ([`INSTALL_RIG.md`](../../rust-link/INSTALL_RIG.md)), which retired the firewall wall phases 6, 7 and 7b each stopped at | Android-app + Module-Rust + docs | A player links from the app |
|
| 8 | **Android leg B** (R10), and the half of R2 a player may see. ✅ **Built 2026-09-22 — as built and findings in §22.** One drawer row under the player group shaped like `module-uo`'s own (the code card first, then what the code got them), gated on `rust` and `PLAYER`; the four refusals kept four pieces of advice; and **one new website route**, `GET /player/rust/permissions`, because phase 7 gave an operator every view of in-game privilege and a player none. Three org-lead decisions (§22.0), the scope arithmetic answered on the server so `inScope` exists once (§22.1), and **the rig moved the sidecar inside the game container** ([`INSTALL_RIG.md`](../../rust-link/INSTALL_RIG.md)), which retired the firewall wall phases 6, 7 and 7b each stopped at | Android-app + Module-Rust + docs | A player links from the app |
|
||||||
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* ✅ **Built 2026-09-23 — plan, as built and findings in §24** (D47–D58). Walked on the Oxide rig end to end and on Carbon hook by hook; the game's 100-clan ceiling accepted (D55); **one module per site** became a core rule on the way (§24.5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* ✅ **Built 2026-09-23 — plan, as built and findings in §24** (D47–D58). Walked on the Oxide rig end to end and on Carbon hook by hook; the game's 100-clan ceiling accepted (D55); **one module per site** became a core rule on the way (§24.5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
||||||
| 10 | **Notifications and engagement** (R7). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §25 (D59–D68).** The criterion walked on the Oxide rig (two authorised owners alerted once each; bystander, raider and an unlinked owner told nothing), and the same plugin loaded on Carbon; a live walk found that every generic notice never said which server (§25.6). Streams, triggers with `ceiling` and `subjectKey`, audiences and engagement seeds — **the catalogue is §10, as corrected by §25.2**. The announce leg and the post hook are **not** in it (D62 amends R7), and protocol 7 widens the raid frame, which brings the two bridge repositories in | all 3 + docs | The offline raid alert reaches the player whose base it was, and nobody else |
|
| 10 | **Notifications and engagement** (R7). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §25 (D59–D68).** The criterion walked on the Oxide rig (two authorised owners alerted once each; bystander, raider and an unlinked owner told nothing), and the same plugin loaded on Carbon; a live walk found that every generic notice never said which server (§25.6). Streams, triggers with `ceiling` and `subjectKey`, audiences and engagement seeds — **the catalogue is §10, as corrected by §25.2**. The announce leg and the post hook are **not** in it (D62 amends R7), and protocol 7 widens the raid frame, which brings the two bridge repositories in | all 3 + docs | The offline raid alert reaches the player whose base it was, and nobody else |
|
||||||
| 11 | **Android leg C** (R10). Inbox and notification preferences for Rust triggers. **Plan of record in §26 (D69–D72)**: the link check asked of the site's own module, a tickle titled from the item it points at, two links that stop opening the browser, and the relay hop walked on `ntfy.sh` for the first time on any site | Android-app | A Rust notification arrives on a phone and can be switched off there |
|
| 11 | **Android leg C** (R10). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §26 (D69–D72).** The link check behind personal streams asked `module-uo` and locked the raid alert on every Rust site; it now asks the site's own module and holds back only push-on. A tickle is titled from the inbox row it names, and two links stopped opening the browser. **The relay hop was walked on `ntfy.sh` for the first time on any site**: a rig raid reached the emulator in 19 seconds, and after push was switched off in the app the next raid enqueued in-app only | Android-app + docs | A Rust notification arrives on a phone and can be switched off there |
|
||||||
| 12 | **Events: budgets, option sources and the leases** (§9). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised | Module-Rust + 2 | A leased value is observed changing in the running game and restored, per key; `rust.group.membership` expires without core asking |
|
| 12 | **Events: budgets, option sources and the leases** (§9). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised | Module-Rust + 2 | A leased value is observed changing in the running game and restored, per key; `rust.group.membership` expires without core asking |
|
||||||
| 13 | **Events: the actions** (§9, R3, R16). `rust.kit.entitle` first, then `rust.prefab.place` and `rust.announce`; `reversible: 'ledger'`; the kit option source flags kits with no permission gate, plus **`reconcile()` and the boot-id watch calling `ctx.events.reconcile()`** (§11.1) | all 3 + docs | A reward granted at 03:00 is waiting in the kit menu when the player next logs in, and a revert withdraws it; a wipe reconciles the ledger instead of stranding it |
|
| 13 | **Events: the actions** (§9, R3, R16). `rust.kit.entitle` first, then `rust.prefab.place` and `rust.announce`; `reversible: 'ledger'`; the kit option source flags kits with no permission gate, plus **`reconcile()` and the boot-id watch calling `ctx.events.reconcile()`** (§11.1) | all 3 + docs | A reward granted at 03:00 is waiting in the kit menu when the player next logs in, and a revert withdraws it; a wipe reconciles the ledger instead of stranding it |
|
||||||
| 14 | **The live map** (R9). The map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch built on **our own** visibility layer (§11.2 — `shardVisibility` is `module-uo`'s, not core's) | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it |
|
| 14 | **The live map** (R9). The map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch built on **our own** visibility layer (§11.2 — `shardVisibility` is `module-uo`'s, not core's) | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it |
|
||||||
@@ -3992,6 +3992,77 @@ sidecar behind it:
|
|||||||
If the relay hop cannot be made on `ntfy.sh`, it is written down as a walk still to run, and the
|
If the relay hop cannot be made on `ntfy.sh`, it is written down as a walk still to run, and the
|
||||||
rest is proven with the tickle injected by `adb`, the extras `PushNotifier` builds.
|
rest is proven with the tickle injected by `adb`, the extras `PushNotifier` builds.
|
||||||
|
|
||||||
|
### 26.4 As built, 2026-09-23
|
||||||
|
|
||||||
|
**Android-app** (`feat/rust-phase-11-notifications`, into `edge`):
|
||||||
|
|
||||||
|
- **`LinkedAccountRepository`** asks `/player/rust/links` on a `rust` site and
|
||||||
|
`/player/shard/accounts` on a `shard` site. A host that has never answered gets both asked, and a
|
||||||
|
host that named neither module gets neither.
|
||||||
|
- **`pushNeedsLink` and `canSetMode`** replace `itemSelectable`. The row stays live, the hint names
|
||||||
|
the push control, and a held push switch that is already on can still be turned off.
|
||||||
|
- **`PushContentResolver`** pulls the first inbox page under a five-second timeout. It titles only
|
||||||
|
from a row whose id matches the ref **and** whose trigger matches the tickle's stream. The
|
||||||
|
notification is `VISIBILITY_PRIVATE`, with a public version carrying the generic title.
|
||||||
|
`PushService` launches each tickle in its own job.
|
||||||
|
- **`resolveWebPath`** learns `/player/rust`, and `tab` on a Rust server path. `RustTab` gained its
|
||||||
|
wire names, and `RUST_SERVER` an optional `tab` argument.
|
||||||
|
|
||||||
|
The suite is **672 tests, 0 failures**, up from 657, with `lintDebug` and `assembleDebug` green.
|
||||||
|
|
||||||
|
**Module-Rust and core: no change**, as planned. This is the fifth Rust phase in a row that core did
|
||||||
|
not need to change for.
|
||||||
|
|
||||||
|
**The walk.** Real core on `main` with this module installed, the protocol-7 sidecar inside the Oxide
|
||||||
|
rig's container, the `s22_ultra` emulator, and `NTFY_PUBLIC_URL`/`NTFY_BASE_URL`/`NTFY_ALLOWED_ORIGINS`
|
||||||
|
all set to `https://ntfy.sh` (D72).
|
||||||
|
|
||||||
|
1. **Linked (raidowner1):** the raid alert's push switch was live. Switching it on registered
|
||||||
|
`https://ntfy.sh/<topic>` as the device's endpoint and stored the subscription. With no link
|
||||||
|
(walkadmin), push was held with the hint, while email and in-app stayed live. In-app was switched
|
||||||
|
off, stored as `off`, and switched back on.
|
||||||
|
2. **Raid, then the phone:** `rgraid.door` on the live rig, attacker `…09`, authorising raidowner1's
|
||||||
|
Steam id. Core dispatched to **two** channels (in-app and push). **Nineteen seconds** after the
|
||||||
|
console command, the phone posted *"Your base is being raided — A door was destroyed in P16 on
|
||||||
|
Oxide rig."* It was `vis=PRIVATE`, with a public version. The tap opened the inbox, and the row
|
||||||
|
opened the Oxide rig's server screen natively.
|
||||||
|
3. **Switched off on the phone:** push off in the app. The server then held no subscription and no
|
||||||
|
device, since nothing else had push on. The next raid dispatched to **one** channel. The inbox
|
||||||
|
row arrived (*"A door was destroyed in R15"*), no system notification was posted, and the push
|
||||||
|
service had stopped.
|
||||||
|
4. **D71:** rows seeded with the module's own URLs. `?tab=leaderboard` opened on the Leaderboard tab,
|
||||||
|
`/player/rust` opened *My Rust account*, and `?tab=clans` went to a Custom Tab.
|
||||||
|
|
||||||
|
**The criterion is met: a Rust notification arrived on a phone and was switched off there.**
|
||||||
|
|
||||||
|
### 26.5 What the walk found
|
||||||
|
|
||||||
|
Nothing the suite had missed. That's the first leg of this workstream to say so, and the reason is
|
||||||
|
worth keeping: every defect this phase fixed was found **by reading** before any code was written
|
||||||
|
(§26.1). The UO-shaped link check was visible in one line of the view model. The generic title was
|
||||||
|
visible in `PushStreams`. The cancelled pull was a property of `collectLatest` that nobody had needed
|
||||||
|
to think about until a handler suspended.
|
||||||
|
|
||||||
|
Two things are worth writing down:
|
||||||
|
|
||||||
|
- **The first real relay hop on any site went cleanly.** M7 built the distributor in 2026-07 and
|
||||||
|
nobody had delivered a tickle through a live ntfy to a device since. It worked the first time,
|
||||||
|
including registration against core's endpoint validator and the unregister when the last push
|
||||||
|
preference went off.
|
||||||
|
- **Two channels, then one, is the proof that "off" was honoured by the server, not by the phone.**
|
||||||
|
The app never saw the second raid's push: core did not enqueue it. A phone that merely stopped
|
||||||
|
showing notifications would have looked the same on the screen and different in the log.
|
||||||
|
|
||||||
|
### 26.6 What is not proven here
|
||||||
|
|
||||||
|
- **A UO site, walked.** No UO core was running. The UO branch is the same call it always was, and
|
||||||
|
`aUoSiteStillAsksTheShardModuleAndNeverTheRustOne` pins it.
|
||||||
|
- **The lock screen, seen.** The emulator has no screen lock. The public version was read back from
|
||||||
|
`dumpsys notification`, not looked at.
|
||||||
|
- **A burst.** Each tickle now runs in its own job, which is what a burst needs, but the walk sent
|
||||||
|
one raid at a time.
|
||||||
|
- **A real raid by a real player**, for §25.7's reason: the rig has one Steam account.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
|
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
|
||||||
|
|||||||
Reference in New Issue
Block a user