Presets, requirements and a verdict per report
Everything that varies per club is a preset now: its filters (one of each
kind, pickable rather than pasted ids), the requirements it places on a
joiner, its own channels and its own formats, including a Discord embed the
user edits field by field. A join is reported once per matching preset.
Each report carries the worst verdict of the preset's checks: green when
everything was verified and holds, orange when something could not be checked,
red when a check failed. Templates read it as {result}, {resultEmoji} and
{resultColor}.
Facts come from ctx.vrchat, so the hand-written event narrowers are gone and
the manifest no longer needs host:actions or any event name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c226195255
commit
9ea036bc35
14 files changed
+763
-1002
No files matched your search
@@ -1,79 +1,87 @@
|
||||
# Club Security
|
||||
|
||||
Reports who joins your instance. For every join that passes the filters, one report:
|
||||
Reports who joins your instance, and whether they meet your club's rules.
|
||||
|
||||
Everything that varies per club is a **preset**: which instances it watches, what it expects of a
|
||||
joiner, and where its reports go. A join is reported once per enabled preset whose filters match
|
||||
the instance, so one account can run the door for several clubs at once.
|
||||
|
||||
Every report carries a verdict, and it is the worst of the preset's checks:
|
||||
|
||||
| Verdict | Means |
|
||||
| :--- | :--- |
|
||||
| ✅ green | Every requirement was checked and holds. |
|
||||
| ⚠️ orange | Something could not be checked: an unknown avatar rank, a hidden age status, a membership the player does not show publicly. |
|
||||
| ⛔ red | A requirement was checked and does not hold: a VeryPoor avatar under a Medium floor, a verified account that is not 18+, a non-friend where friendship is required. |
|
||||
|
||||
The verdict reaches your templates as `{result}`, `{resultText}`, `{resultEmoji}` and
|
||||
`{resultColor}` — the last one colours the Discord embed's bar.
|
||||
|
||||
## A preset
|
||||
|
||||
| Section | Setting | Meaning |
|
||||
| :--- | :--- | :--- |
|
||||
| Identity | Preset name, Enabled | The name appears in every report as `{preset}`. |
|
||||
| Filters | Instance types, Group, Worlds | One of each kind. Empty means "any". A pickable group and worlds, not pasted ids. |
|
||||
| Requirements | Require 18+, PC / Quest avatar rank at least, Must be a member of, Must be on my friend list | Each one becomes a check with its own verdict. |
|
||||
| Channels | In-app toast, Desktop, VR overlay, Discord | Per preset, so a strict club can post to Discord while a relaxed one only toasts. |
|
||||
| Formats | Report template, VR overlay template, Discord embed | The text report, the plain-text one for VR, and the embed. |
|
||||
|
||||
## The default report
|
||||
|
||||
```
|
||||
Player "Tupper" joined
|
||||
18+ Verified: Yes (18+)
|
||||
Avatar PC Performance Rank: Good
|
||||
Avatar Quest Performance Rank: Poor
|
||||
In Group: Yes
|
||||
Rejoin?: Yes (2 hours ago)
|
||||
✅ Tupper joined · Saturday Night
|
||||
✅ 18+ verified: 18+
|
||||
✅ PC avatar rank: Good
|
||||
✅ Group member: member
|
||||
Avatar: Ava (PC Good · Quest Poor)
|
||||
```
|
||||
|
||||
That is the default **Report template**, editable in the plugin's settings. The first line is
|
||||
the title on desktop, VR and Discord; the rest is the body. A line whose placeholders are all
|
||||
empty is left out, which is why `In Group` only appears when a group filter is set.
|
||||
First line = title, rest = body. A line whose placeholders all came out empty is left out, which
|
||||
is why the rejoin line only appears for someone who has been in that instance before.
|
||||
|
||||
`{name}` is short for `{{ name }}`; the full template language (conditions, filters, `{% if %}`
|
||||
blocks) is documented in the plugin system's API reference. For example:
|
||||
|
||||
```
|
||||
{rejoinEmoji} {name} {{ "is back" if rejoin else "is new" }} {ageVerifiedEmoji}
|
||||
{pcRankEmoji} PC {pcRankText} · {questRankEmoji} Quest {questRankText}
|
||||
{% if inGroup == false %}⚠️ Not a group member{% endif %}
|
||||
```
|
||||
blocks) is in the plugin system's API reference. The VR template is separate because WayVR draws
|
||||
with a single font and shows nothing for emoji.
|
||||
|
||||
| Kind | Variables |
|
||||
| :--- | :--- |
|
||||
| Text | `name` `playerId` `ageVerifiedText` `ageStatus` `pcRankText` `questRankText` `avatar` `platform` `inGroupText` `rejoinText` `rejoinAgo` `rejoinSince` `rejoinAt` `world` `worldId` `instanceType` `instanceId` `location` `groupId` `time` `date` `timestamp` |
|
||||
| Booleans (for conditions; empty when unknown) | `ageVerified` `inGroup` `rejoin`, plus `pcRank` and `questRank` (the raw rank, empty when unknown) |
|
||||
| Emoji | `ageVerifiedEmoji` ✅❌❔ · `pcRankEmoji` / `questRankEmoji` 🟢🔵🟡🟠🔴⚪ · `platformEmoji` 🖥️📱🍎 · `inGroupEmoji` · `rejoinEmoji` 🔁🆕❔ |
|
||||
| Verdict | `result` `resultText` `resultEmoji` `resultColor` `checksText` `checksPlainText` `failedText` `unverifiedText` |
|
||||
| Player | `name` `playerId` `platform` `platformEmoji` `isFriend` `friendText` `ageVerified` `ageVerifiedText` `ageVerifiedEmoji` `ageStatus` |
|
||||
| Avatar | `avatar` `avatarId` `avatarImageUrl` `pcRank` `pcRankText` `pcRankEmoji` `questRank` `questRankText` `questRankEmoji` |
|
||||
| Club | `preset` `inGroup` `inGroupText` `inGroupEmoji` |
|
||||
| History | `rejoin` `rejoinText` `rejoinEmoji` `rejoinAgo` `rejoinSince` `rejoinAt` |
|
||||
| Place and time | `world` `worldId` `instanceType` `instanceId` `location` `time` `date` `timestamp` |
|
||||
|
||||
A template that does not parse is reported in the log and the default is used instead.
|
||||
|
||||
## Filters
|
||||
|
||||
All optional, all in the plugin's settings card. Empty means "any".
|
||||
|
||||
| Setting | Meaning |
|
||||
| :--- | :--- |
|
||||
| Instance types | Comma-separated: `public`, `friends+`, `friends`, `hidden`, `private`, `invite_plus`, `group-public`, `group-plus`, `group-members`, or `group` for every group instance. |
|
||||
| Group | A `grp_…` id. Only instances of that group count, and the report says whether the joiner is a member. |
|
||||
| Worlds | Comma-separated `wrld_…` ids. |
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel | How |
|
||||
| :--- | :--- |
|
||||
| In-app toast | Always available. One line. |
|
||||
| Desktop | A VRCNext Bridge desktop target on any platform; VRCNext's tray toast on Windows when the bridge delivers nothing. |
|
||||
| VR | A VRCNext Bridge VR target (WayVR and similar) on any platform; SteamVR wrist overlay on Windows when the bridge delivers nothing. |
|
||||
| Discord | A `discord.com` webhook URL, posted as an embed through `ctx.http.fetch` (`discord.com` is the plugin's only declared host). |
|
||||
Booleans (`ageVerified`, `inGroup`, `rejoin`, `isFriend`) are empty when unknown, so
|
||||
`{{ "yes" if rejoin else "no" }}` and `{% if inGroup == false %}…{% endif %}` both behave. A
|
||||
template that does not parse is reported in the log and the default is used instead.
|
||||
|
||||
## Where each fact comes from
|
||||
|
||||
Everything goes through `ctx.vrchat`, which reads VRCNext's data **without opening its dialogs**.
|
||||
|
||||
| Fact | Source | Caveat |
|
||||
| :--- | :--- | :--- |
|
||||
| 18+ verified | VRCNext fetches the joiner's profile itself on join and re-pushes the instance; the plugin waits for that push. | Legacy accounts without a `usr_` id cannot be looked up. |
|
||||
| Avatar ranks | `vrcGetInstanceAvatars` resolves the avatar from the profile image through VRCNext's avatar databases, then `vrcGetAvatarDetail` gives the ranks. | Only works when one of the databases knows the avatar. The detail action also feeds VRCNext's avatar modal, so it is skipped while that modal is open. |
|
||||
| In group | `vrcGetGroupsForNetwork`, the joiner's publicly visible groups. | A member who hides the membership shows as "No". |
|
||||
| Rejoin | `getTimelineForUser`: the player's ten most recent timeline events, each with its location. Yes when one of them is this exact instance (same world and instance id) from before this join, with how long ago. | Survives VRCNext and VRChat restarts and reaches back to when VRCNext was installed, but only ten events deep per player. |
|
||||
| 18+ verification | The joiner's profile. `18+` passes, `verified` without `18+` fails, `hidden` or missing is unverified. | Legacy accounts without a `usr_` id cannot be looked up at all. |
|
||||
| Avatar and ranks | The avatar the player wears, resolved through VRCNext's avatar databases, then its performance ranks. | Only works when one of the databases knows the avatar; an unknown rank is unverified, never a failure. |
|
||||
| Group membership | The groups the user shows publicly. | A member who hides the membership is unverified, not a failure. |
|
||||
| Friendship | Your friend list. | — |
|
||||
| Rejoin | VRCNext's timeline: the player's ten most recent events, each with its location. Yes when one of them is this exact instance (same world **and** instance id) from before this join. | Survives restarts and reaches back to when VRCNext was installed, but only ten events deep per player. |
|
||||
|
||||
Everything waits at most the configured number of seconds, then reports what it has, with
|
||||
`Unknown` for the rest.
|
||||
Each lookup runs in parallel and degrades to "unknown" on its own timeout rather than holding up
|
||||
the report.
|
||||
|
||||
## Your own joins
|
||||
|
||||
The signed-in account comes from the `vrcUser` event VRCNext pushes after login; the plugin
|
||||
reads no page globals. VRChat logs an `OnPlayerJoined` line for the local player too, and right after it one line for
|
||||
every player already in the instance. The plugin ignores your own line and treats joins inside
|
||||
the settle window after it (or after a world change) as "already here" and does not report them.
|
||||
The signed-in account comes from `ctx.vrchat.self()`. VRChat logs an `OnPlayerJoined` line for the
|
||||
local player too, and right after it one line for every player already in the instance. The plugin
|
||||
ignores your own line and treats joins inside the settle window after it (or after a world change)
|
||||
as "already here".
|
||||
|
||||
## Layout and permissions
|
||||
|
||||
Flat, like every plugin: `plugin.json`, `main.ts`, `src/`. The manifest declares `host:events`
|
||||
(the five `vrc…` events above and `vrcUser`), `host:actions` (the four `vrcGet…` lookups),
|
||||
`gamelog` (joins and world changes), `notifications` (toast, confirm, Windows tray toast),
|
||||
`native` (bridge targets) and `network` with `discord.com` as its only host. Nothing is optional:
|
||||
every channel is a plain setting, and a category that is off in the settings is simply never called.
|
||||
Flat, like every plugin: `plugin.json`, `main.ts`, `src/`. The manifest declares `gamelog` (joins
|
||||
and world changes), `vrchat` (the read-only data above), `notifications` (toast, Windows tray
|
||||
toast), `native` (bridge targets) and `network` with `discord.com` as its only host. No VRCNext
|
||||
actions and no raw events: the `vrchat` capability covers everything this plugin reads.
|
||||
Reference in new issue
Block a user