From db2e9786f1a2c010eccd5e99fd4f153b6397f963 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 21:59:35 +0200 Subject: [PATCH] club-security: self detection via currentVrcUser, settle window after own join --- dist/club-security.js | 6 +++--- dist/club-security.js.map | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/club-security.js b/dist/club-security.js index 657353c..4c0dd32 100644 --- a/dist/club-security.js +++ b/dist/club-security.js @@ -1,4 +1,4 @@ -function f(n){return typeof n=="object"&&n!==null?n:void 0}function d(n){return typeof n=="string"?n:""}function G(n){return Array.isArray(n)?n.filter(e=>typeof e=="string"):[]}function J(n){return/~group\((grp_[0-9a-f-]+)\)/i.exec(n)?.[1]??""}function q(n){let e=f(n);if(e===void 0)return;let t=d(e.id),r=d(e.displayName);if(!(t===""&&r===""))return{id:t,displayName:r,ageVerified:e.ageVerified===!0,ageVerificationStatus:d(e.ageVerificationStatus),platform:d(e.platform),tags:G(e.tags),lastLogin:d(e.lastLogin),avatarId:d(e.avatarId),avatarName:d(e.avatarName)}}function N(n){let e=f(n);if(e===void 0)return;let t=d(e.location);if(t==="")return;let r=Array.isArray(e.users)?e.users:[];return{location:t,worldId:d(e.worldId),worldName:d(e.worldName),instanceType:d(e.instanceType),groupId:J(t),users:r.flatMap(i=>{let s=q(i);return s===void 0?[]:[s]})}}function C(n){return n.ageVerificationStatus!==""||n.tags.length>0||n.lastLogin!==""}function L(n){let e=f(n);if(e===void 0)return;let t=d(e.userId);if(t==="")return;let r=Array.isArray(e.groups)?e.groups:[];return{userId:t,groupIds:r.flatMap(i=>{let s=d(f(i)?.id);return s===""?[]:[s]})}}function R(n){let e=f(n);if(e===void 0)return;let t=d(e.userId);if(t!=="")return{userId:t,avatarId:d(e.avatarId),avatarName:d(e.avatarName)}}function A(n){let e=f(n);if(e===void 0)return;let t=d(e.id);if(t!=="")return{avatarId:t,name:d(e.name),pc:d(e.pcPerf),quest:d(e.questPerf)}}function $(n){return d(f(n)?.id)}function y(n,e,t,r){return new Promise(i=>{let s=!1,a=l=>{s||(s=!0,clearTimeout(u),c(),r.signal.removeEventListener("abort",o),i(l))},o=()=>{a(void 0)},u=setTimeout(()=>{a(void 0)},r.ms),c=n.on(e,l=>{let p=t(l);p!==void 0&&a(p)});r.signal.addEventListener("abort",o,{once:!0})})}function _(n,e){return n.users.find(t=>e.userId!==""?t.id===e.userId:t.displayName===e.name)}function W(){let n=document.getElementById("modalAvatarDetail");return n!==null&&n.offsetParent!==null}var h=class{#e;#n;#t=new Map;constructor(e){this.#e=e,e.events.on("vrcCurrentInstance",t=>{let r=N(t);r!==void 0&&(this.#n=r)}),e.events.on("vrcAvatarDetail",t=>{let r=A(t);r!==void 0&&this.#t.set(r.avatarId,{name:r.name,pc:r.pc,quest:r.quest})})}get instance(){return this.#n}async collect(e,t,r){let i=this.#e.signal,[s,a,o]=await Promise.all([this.#r(e,r,i),t===""?Promise.resolve(void 0):this.#i(e,t,r,i),this.#s(e,r,i)]);return{ageVerified:s===void 0?void 0:s.ageVerified,ageStatus:s?.ageVerificationStatus??"",platform:s?.platform??"",avatar:o,inGroup:a}}async#r(e,t,r){let i=this.#n===void 0?void 0:_(this.#n,e);return i!==void 0&&C(i)?i:await y(this.#e.events,"vrcCurrentInstance",a=>{let o=N(a),u=o===void 0?void 0:_(o,e);return u!==void 0&&C(u)?u:void 0},{ms:t,signal:r})??i}async#i(e,t,r,i){if(e.userId==="")return;let s=t.toLowerCase(),a=y(this.#e.events,"vrcGroupsForNetwork",u=>{let c=L(u);return c?.userId===e.userId?c.groupIds:void 0},{ms:r,signal:i});this.#e.bridge.send("vrcGetGroupsForNetwork",{userId:e.userId});let o=await a;return o===void 0?void 0:o.some(u=>u.toLowerCase()===s)}async#s(e,t,r){if(e.userId==="")return;let i=Date.now(),s=()=>Math.max(1e3,t-(Date.now()-i)),a=this.#n===void 0?"":_(this.#n,e)?.avatarId??"",o="";if(a===""){let p=y(this.#e.events,"vrcInstanceAvatarFound",O=>{let E=R(O);return E?.userId===e.userId?E:void 0},{ms:s(),signal:r});this.#e.bridge.send("vrcGetInstanceAvatars",{userIds:[e.userId]});let g=await p;if(g===void 0)return;a=g.avatarId,o=g.avatarName}if(a==="")return o===""?void 0:{name:o,pc:"",quest:""};let u=this.#t.get(a);if(u!==void 0)return u;if(W())return this.#e.logger.debug("Avatar modal is open; not fetching performance ranks behind it."),{name:o,pc:"",quest:""};let c=y(this.#e.events,"vrcAvatarDetail",p=>{let g=A(p);return g?.avatarId===a?g:void 0},{ms:s(),signal:r});this.#e.bridge.send("vrcGetAvatarDetail",{avatarId:a});let l=await c;return l===void 0?{name:o,pc:"",quest:""}:{name:l.name===""?o:l.name,pc:l.pc,quest:l.quest}}};function F(n){return n.split(/[,\s]+/).map(e=>e.trim().toLowerCase()).filter(e=>e.length>0)}function m(n){return{instanceTypes:F(n.instanceTypes),groupId:n.groupId.trim().toLowerCase(),worldIds:F(n.worldIds)}}function K(n,e){if(n.length===0)return!0;let t=e.toLowerCase();return n.some(r=>r===t||r==="group"&&t.startsWith("group"))}function I(n,e){if(!K(n.instanceTypes,e.instanceType))return!1;let t=n.groupId.toLowerCase();if(t!==""&&e.groupId.toLowerCase()!==t)return!1;let r=n.worldIds.map(i=>i.toLowerCase());return!(r.length>0&&!r.includes(e.worldId.toLowerCase()))}function V(n){let e=[];return n.instanceTypes.length>0&&e.push(`types: ${n.instanceTypes.join(", ")}`),n.groupId!==""&&e.push(`group: ${n.groupId}`),n.worldIds.length>0&&e.push(`worlds: ${String(n.worldIds.length)}`),e.length===0?"every instance":e.join(" \xB7 ")}function Y(n){try{let e=JSON.parse(n);if(typeof e!="object"||e===null||Array.isArray(e))return{};let t={};for(let[r,i]of Object.entries(e)){if(typeof i!="object"||i===null)continue;let s=i;t[r]={name:typeof s.name=="string"?s.name:"",joins:typeof s.joins=="number"?s.joins:1,firstSeen:typeof s.firstSeen=="number"?s.firstSeen:0,lastSeen:typeof s.lastSeen=="number"?s.lastSeen:0,lastLocation:typeof s.lastLocation=="string"?s.lastLocation:""}}return t}catch{return{}}}function H(n){let e=Object.entries(n);return e.length<=2e3?n:(e.sort((t,r)=>r[1].lastSeen-t[1].lastSeen),Object.fromEntries(e.slice(0,2e3)))}var b=class{#e;#n;constructor(e){this.#e=e,this.#n=Y(e.get("memory"))}get size(){return Object.keys(this.#n).length}previous(e){return this.#n[e]}async record(e,t,r){let i=this.#n[e],s=Date.now();return this.#n[e]={name:t,joins:(i?.joins??0)+1,firstSeen:i?.firstSeen??s,lastSeen:s,lastLocation:r},this.#n=H(this.#n),await this.#e.set("memory",JSON.stringify(this.#n)),i}async clear(){this.#n={},await this.#e.set("memory","{}")}};var X=/^https:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w-]+$/;function w(n,e="Unknown"){return n===void 0?e:n?"Yes":"No"}function S(n){return n===""?"Unknown":n}function z(n){if(n===void 0)return"No (first time)";let e=new Date(n.lastSeen).toLocaleString();return`Yes (${String(n.joins)}\xD7 before, last ${e})`}function k(n){let{facts:e}=n,t=e.ageVerified===void 0?"Unknown":`${w(e.ageVerified)}${e.ageStatus===""?"":` (${e.ageStatus})`}`,r=[`Player "${n.joiner.name}" joined`,`18+ Verified: ${t}`,`Avatar PC Performance Rank: ${S(e.avatar?.pc??"")}`,`Avatar Quest Performance Rank: ${S(e.avatar?.quest??"")}`];return n.groupFilter!==""&&r.push(`In Group: ${w(e.inGroup,"Unknown (only visible memberships count)")}`),r.push(`Rejoin?: ${z(n.previous)}`),r}function B(n){let{facts:e}=n,t=[`18+: ${w(e.ageVerified,"?")}`,`PC ${S(e.avatar?.pc??"").replace("Unknown","?")}`,`Quest ${S(e.avatar?.quest??"").replace("Unknown","?")}`];return n.groupFilter!==""&&t.push(`group: ${w(e.inGroup,"?")}`),t.push(n.previous===void 0?"new":"rejoin"),`${n.joiner.name} joined \xB7 ${t.join(" \xB7 ")}`}function v(n){let{facts:e}=n;return e.ageVerified===!1||e.avatar?.pc==="VeryPoor"?"warn":e.ageVerified===!0?"ok":"info"}async function Q(n,e,t){return(await n.native.targets()).filter(i=>/wayvr|openvr|steamvr|overlay|xso/i.test(i.name)?t:e).map(i=>i.name)}async function Z(n,e,t){let r=e.notifyDesktop,i=e.notifyVr;if(!r&&!i)return;let s=`${t.joiner.name} joined`,a=k(t).slice(1);if(await n.native.ready){let o=await Q(n,r,i);if(o.length===0)n.logger.debug("Native companion has no target for the enabled channels.");else{let u=await n.native.notify({title:s,content:a.join(` -`),timeoutSecs:8,icon:"security-high",sinks:o,urgency:v(t)==="warn"?"critical":"normal"});for(let c of u.failed)n.logger.warn(`Native target ${c.sink} failed: ${c.error}`);return}}if(n.notifications.desktopAvailable){n.notifications.desktop({title:s,subtitle:a.join(" \xB7 "),accent:v(t),...t.joiner.userId===""?{}:{friendId:t.joiner.userId}});return}n.logger.info("No desktop or VR channel is reachable: the native companion is not running and this platform has no tray toast.")}async function ee(n,e,t){if(!e.notifyDiscord)return;let r=e.discordWebhookUrl.trim();if(!X.test(r)){n.logger.warn("Discord webhook is enabled but the URL is not a discord.com webhook URL.");return}let i=k(t),s={ok:3908957,warn:15548997,info:5793266}[v(t)],a={username:"Club Security",embeds:[{title:i[0],description:i.slice(1).join(` -`),color:s,footer:{text:`${t.instance.worldName} \xB7 ${t.instance.instanceType}`},timestamp:new Date(t.at).toISOString()}],allowed_mentions:{parse:[]}},o=await fetch(r,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(a),signal:n.signal,credentials:"omit",referrerPolicy:"no-referrer"});o.ok||n.logger.warn(`Discord webhook answered ${String(o.status)}.`)}async function P(n,e){let t=n.settings.values;t.notifyToast&&n.notifications.toast({message:B(e),ok:v(e)!=="warn"});let r=await Promise.allSettled([Z(n,t,e),ee(n,t,e)]);for(let i of r)i.status==="rejected"&&n.logger.warn(`Notification channel failed: ${String(i.reason)}`)}var j=25,x=class{#e;#n;#t=[];#r;#i;constructor(e,t){this.#e=e,this.#n=t}install(){this.#e.ui.addNavTab({label:"Club Security",icon:"security",render:e=>{this.#s(e)}}),this.#e.ui.addSettingsCard({title:"Club Security",icon:"security",render:e=>{e.appendChild(this.#e.ui.kit.description("Reports go out for joins in instances that pass every filter above. Test them from the Club Security tab."))}}),this.#e.settings.onChange(()=>{this.refresh()})}push(e){this.#t.unshift(e),this.#t.length>j&&(this.#t.length=j),this.refresh()}refresh(){let e=this.#e.ui.kit;this.#r!==void 0&&e.setChildren(this.#r,this.#o()),this.#i!==void 0&&e.setChildren(this.#i,this.#a())}#s(e){let t=this.#e.ui.kit;this.#r=t.card({title:"Status",icon:"shield"}),this.#i=t.card({title:"Recent reports",icon:"history"});let r=t.card({title:"Actions",icon:"build",children:[t.description("A test report uses the current instance and your own name, and goes to every enabled channel."),t.buttonRow(t.button({label:"Send test notification",icon:"send",onClick:()=>{this.#n.sendTest()}}),t.button({label:"Forget who joined before",icon:"delete_sweep",onClick:()=>{this.#d()}}))]});e.append(t.layout(t.pair(this.#r,r),this.#i)),this.refresh()}#o(){let e=this.#e.ui.kit,t=this.#e.settings.values,r=m(t),i=this.#n.currentInstance(),s=i!==void 0&&I(r,i),a=[t.notifyToast&&"toast",t.notifyDesktop&&"desktop",t.notifyVr&&"VR",t.notifyDiscord&&"Discord"].filter(o=>typeof o=="string");return[e.row({label:"Reporting",value:t.enabled?e.badge("ok","On"):e.badge("neutral","Off")}),e.row({label:"Watching",detail:V(r)}),e.row({label:"Current instance",detail:i===void 0?"Not in an instance":`${i.worldName} \xB7 ${i.instanceType}`,value:i===void 0?void 0:e.badge(s?"ok":"neutral",s?"Matches":"Filtered out")}),e.row({label:"Channels",detail:a.length===0?"none enabled":a.join(", ")}),e.row({label:"Players remembered",value:e.valueText(String(this.#n.memory.size))})]}#a(){let e=this.#e.ui.kit;return this.#t.length===0?[e.emptyState("No joins reported yet.")]:this.#t.map(t=>{let r=k(t),i=new Date(t.at).toLocaleTimeString();return e.row({label:`${i} \xB7 ${t.joiner.name}`,detail:r.slice(1).join(" \xB7 "),value:t.previous===void 0?e.badge("accent","New"):e.badge("warn","Rejoin")})})}async#d(){await this.#e.notifications.confirm({title:"Forget join history",message:"Every player will count as a first-time joiner again. Continue?",confirmLabel:"Forget",icon:"delete_sweep"})&&(await this.#n.memory.clear(),this.#e.notifications.toast({message:"Join history cleared."}),this.refresh())}};var M={enabled:{kind:"boolean",label:"Report joins",description:"Master switch. Filters below decide which instances count.",default:!0},instanceTypes:{kind:"string",label:"Only in these instance types",description:'Comma-separated. Leave empty for every instance. Types: public, friends+, friends, hidden, private, invite_plus, group-public, group-plus, group-members, or just "group" for all group instances.',default:"",placeholder:"group-public, group-plus"},groupId:{kind:"string",label:"Only in instances of this group",description:"A grp_\u2026 id. Leave empty for any group. When set, the report also says whether the joiner is a member.",default:"",placeholder:"grp_00000000-0000-0000-0000-000000000000"},worldIds:{kind:"string",label:"Only in these worlds",description:"Comma-separated wrld_\u2026 ids. Leave empty for any world.",default:"",placeholder:"wrld_\u2026, wrld_\u2026"},collectTimeoutSecs:{kind:"number",label:"Seconds to wait for details",description:"How long to wait for VRCNext to load the profile and avatar before reporting what is known.",default:25,min:5,max:120,step:5},notifyToast:{kind:"boolean",label:"In-app toast",default:!0},notifyDesktop:{kind:"boolean",label:"Desktop notification",description:"Through the native companion on any platform, or VRCNext\u2019s tray toast on Windows.",default:!0},notifyVr:{kind:"boolean",label:"VR overlay notification",description:"Through the native companion\u2019s VR target on any platform, or the SteamVR wrist overlay on Windows.",default:!0},notifyDiscord:{kind:"boolean",label:"Discord webhook",default:!1},discordWebhookUrl:{kind:"string",label:"Discord webhook URL",description:"Server Settings \u2192 Integrations \u2192 Webhooks. Sent straight from this page; the URL is stored locally only.",default:"",placeholder:"https://discord.com/api/webhooks/\u2026"},memory:{kind:"string",label:"Join memory",description:"Internal: who has joined a matching instance before.",default:"{}",hidden:!0}};var ne="gl_player_join";function te(){let n=globalThis.currentVrcUser,e=typeof n=="object"&&n!==null?n.id:void 0;return typeof e=="string"?e:""}var T=class{#e;#n;#t;#r;#i=te();#s="";#o=new Set;constructor(e){this.#e=e,this.#n=new h(e),this.#t=new b(e.settings),this.#r=new x(e,{memory:this.#t,currentInstance:()=>this.#n.instance,sendTest:()=>this.#u()})}start(){this.#e.events.on("vrcUser",e=>{let t=$(e);t!==""&&(this.#i=t);let r=e.displayName;typeof r=="string"&&(this.#s=r)}),this.#e.events.on("vrcCurrentInstance",()=>{this.#r.refresh()}),this.#e.gameLog.onType(ne,e=>{this.#a({name:e.message,userId:e.detail})}),this.#r.install(),this.#e.bridge.send("vrcGetCurrentInstance"),this.#e.logger.info(`Club Security v${this.#e.version} watching for joins.`)}async#a(e){let t=this.#e.settings.values;if(!t.enabled||e.name===""||this.#d(e))return;let r=this.#n.instance;if(r===void 0){this.#e.logger.debug(`${e.name} joined but the current instance is not known yet.`);return}let i=m(t);if(!I(i,r))return;let s=e.userId===""?`name:${e.name}`:e.userId;if(!this.#o.has(s)){this.#o.add(s);try{let a=await this.#t.record(s,e.name,r.location),o=await this.#n.collect(e,i.groupId,t.collectTimeoutSecs*1e3),u={at:Date.now(),joiner:e,instance:r,facts:o,groupFilter:i.groupId,previous:a};this.#r.push(u),this.#e.logger.info(`Reported ${e.name}: 18+ ${String(o.ageVerified??"?")}, PC ${o.avatar?.pc??"?"}, Quest ${o.avatar?.quest??"?"}.`),await P(this.#e,u)}catch(a){this.#e.logger.error(`Report for ${e.name} failed: ${String(a)}`)}finally{this.#o.delete(s)}}}#d(e){return e.userId!==""&&this.#i!==""?e.userId===this.#i:this.#s!==""&&e.name===this.#s}async#u(){let e=this.#n.instance;if(e===void 0){this.#e.notifications.toast({message:"Join an instance first; the test uses it.",ok:!1});return}let t=e.users.find(u=>u.id===this.#i)??e.users[0],r=t?.displayName??this.#s,i={name:r===""?"Test player":r,userId:t?.id??""},s=m(this.#e.settings.values),a=await this.#n.collect(i,s.groupId,1e4),o={at:Date.now(),joiner:i,instance:e,facts:a,groupFilter:s.groupId,previous:this.#t.previous(i.userId===""?`name:${i.name}`:i.userId)};await P(this.#e,o),this.#e.notifications.toast({message:"Test report sent to every enabled channel."})}},un={id:"club-security",settings:M,activate(n){new T(n).start()},deactivate(){}};export{un as default}; +function f(t){return typeof t=="object"&&t!==null?t:void 0}function d(t){return typeof t=="string"?t:""}function G(t){return Array.isArray(t)?t.filter(e=>typeof e=="string"):[]}function J(t){return/~group\((grp_[0-9a-f-]+)\)/i.exec(t)?.[1]??""}function q(t){let e=f(t);if(e===void 0)return;let n=d(e.id),r=d(e.displayName);if(!(n===""&&r===""))return{id:n,displayName:r,ageVerified:e.ageVerified===!0,ageVerificationStatus:d(e.ageVerificationStatus),platform:d(e.platform),tags:G(e.tags),lastLogin:d(e.lastLogin),avatarId:d(e.avatarId),avatarName:d(e.avatarName)}}function N(t){let e=f(t);if(e===void 0)return;let n=d(e.location);if(n==="")return;let r=Array.isArray(e.users)?e.users:[];return{location:n,worldId:d(e.worldId),worldName:d(e.worldName),instanceType:d(e.instanceType),groupId:J(n),users:r.flatMap(i=>{let s=q(i);return s===void 0?[]:[s]})}}function C(t){return t.ageVerificationStatus!==""||t.tags.length>0||t.lastLogin!==""}function L(t){let e=f(t);if(e===void 0)return;let n=d(e.userId);if(n==="")return;let r=Array.isArray(e.groups)?e.groups:[];return{userId:n,groupIds:r.flatMap(i=>{let s=d(f(i)?.id);return s===""?[]:[s]})}}function R(t){let e=f(t);if(e===void 0)return;let n=d(e.userId);if(n!=="")return{userId:n,avatarId:d(e.avatarId),avatarName:d(e.avatarName)}}function _(t){let e=f(t);if(e===void 0)return;let n=d(e.id);if(n!=="")return{avatarId:n,name:d(e.name),pc:d(e.pcPerf),quest:d(e.questPerf)}}function $(t){return d(f(t)?.id)}function y(t,e,n,r){return new Promise(i=>{let s=!1,a=l=>{s||(s=!0,clearTimeout(u),c(),r.signal.removeEventListener("abort",o),i(l))},o=()=>{a(void 0)},u=setTimeout(()=>{a(void 0)},r.ms),c=t.on(e,l=>{let p=n(l);p!==void 0&&a(p)});r.signal.addEventListener("abort",o,{once:!0})})}function A(t,e){return t.users.find(n=>e.userId!==""?n.id===e.userId:n.displayName===e.name)}function W(){let t=document.getElementById("modalAvatarDetail");return t!==null&&t.offsetParent!==null}var h=class{#e;#n;#t=new Map;constructor(e){this.#e=e,e.events.on("vrcCurrentInstance",n=>{let r=N(n);r!==void 0&&(this.#n=r)}),e.events.on("vrcAvatarDetail",n=>{let r=_(n);r!==void 0&&this.#t.set(r.avatarId,{name:r.name,pc:r.pc,quest:r.quest})})}get instance(){return this.#n}async collect(e,n,r){let i=this.#e.signal,[s,a,o]=await Promise.all([this.#r(e,r,i),n===""?Promise.resolve(void 0):this.#i(e,n,r,i),this.#s(e,r,i)]);return{ageVerified:s===void 0?void 0:s.ageVerified,ageStatus:s?.ageVerificationStatus??"",platform:s?.platform??"",avatar:o,inGroup:a}}async#r(e,n,r){let i=this.#n===void 0?void 0:A(this.#n,e);return i!==void 0&&C(i)?i:await y(this.#e.events,"vrcCurrentInstance",a=>{let o=N(a),u=o===void 0?void 0:A(o,e);return u!==void 0&&C(u)?u:void 0},{ms:n,signal:r})??i}async#i(e,n,r,i){if(e.userId==="")return;let s=n.toLowerCase(),a=y(this.#e.events,"vrcGroupsForNetwork",u=>{let c=L(u);return c?.userId===e.userId?c.groupIds:void 0},{ms:r,signal:i});this.#e.bridge.send("vrcGetGroupsForNetwork",{userId:e.userId});let o=await a;return o===void 0?void 0:o.some(u=>u.toLowerCase()===s)}async#s(e,n,r){if(e.userId==="")return;let i=Date.now(),s=()=>Math.max(1e3,n-(Date.now()-i)),a=this.#n===void 0?"":A(this.#n,e)?.avatarId??"",o="";if(a===""){let p=y(this.#e.events,"vrcInstanceAvatarFound",O=>{let E=R(O);return E?.userId===e.userId?E:void 0},{ms:s(),signal:r});this.#e.bridge.send("vrcGetInstanceAvatars",{userIds:[e.userId]});let g=await p;if(g===void 0)return;a=g.avatarId,o=g.avatarName}if(a==="")return o===""?void 0:{name:o,pc:"",quest:""};let u=this.#t.get(a);if(u!==void 0)return u;if(W())return this.#e.logger.debug("Avatar modal is open; not fetching performance ranks behind it."),{name:o,pc:"",quest:""};let c=y(this.#e.events,"vrcAvatarDetail",p=>{let g=_(p);return g?.avatarId===a?g:void 0},{ms:s(),signal:r});this.#e.bridge.send("vrcGetAvatarDetail",{avatarId:a});let l=await c;return l===void 0?{name:o,pc:"",quest:""}:{name:l.name===""?o:l.name,pc:l.pc,quest:l.quest}}};function F(t){return t.split(/[,\s]+/).map(e=>e.trim().toLowerCase()).filter(e=>e.length>0)}function m(t){return{instanceTypes:F(t.instanceTypes),groupId:t.groupId.trim().toLowerCase(),worldIds:F(t.worldIds)}}function K(t,e){if(t.length===0)return!0;let n=e.toLowerCase();return t.some(r=>r===n||r==="group"&&n.startsWith("group"))}function I(t,e){if(!K(t.instanceTypes,e.instanceType))return!1;let n=t.groupId.toLowerCase();if(n!==""&&e.groupId.toLowerCase()!==n)return!1;let r=t.worldIds.map(i=>i.toLowerCase());return!(r.length>0&&!r.includes(e.worldId.toLowerCase()))}function V(t){let e=[];return t.instanceTypes.length>0&&e.push(`types: ${t.instanceTypes.join(", ")}`),t.groupId!==""&&e.push(`group: ${t.groupId}`),t.worldIds.length>0&&e.push(`worlds: ${String(t.worldIds.length)}`),e.length===0?"every instance":e.join(" \xB7 ")}function Y(t){try{let e=JSON.parse(t);if(typeof e!="object"||e===null||Array.isArray(e))return{};let n={};for(let[r,i]of Object.entries(e)){if(typeof i!="object"||i===null)continue;let s=i;n[r]={name:typeof s.name=="string"?s.name:"",joins:typeof s.joins=="number"?s.joins:1,firstSeen:typeof s.firstSeen=="number"?s.firstSeen:0,lastSeen:typeof s.lastSeen=="number"?s.lastSeen:0,lastLocation:typeof s.lastLocation=="string"?s.lastLocation:""}}return n}catch{return{}}}function H(t){let e=Object.entries(t);return e.length<=2e3?t:(e.sort((n,r)=>r[1].lastSeen-n[1].lastSeen),Object.fromEntries(e.slice(0,2e3)))}var b=class{#e;#n;constructor(e){this.#e=e,this.#n=Y(e.get("memory"))}get size(){return Object.keys(this.#n).length}previous(e){return this.#n[e]}async record(e,n,r){let i=this.#n[e],s=Date.now();return this.#n[e]={name:n,joins:(i?.joins??0)+1,firstSeen:i?.firstSeen??s,lastSeen:s,lastLocation:r},this.#n=H(this.#n),await this.#e.set("memory",JSON.stringify(this.#n)),i}async clear(){this.#n={},await this.#e.set("memory","{}")}};var X=/^https:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w-]+$/;function w(t,e="Unknown"){return t===void 0?e:t?"Yes":"No"}function S(t){return t===""?"Unknown":t}function z(t){if(t===void 0)return"No (first time)";let e=new Date(t.lastSeen).toLocaleString();return`Yes (${String(t.joins)}\xD7 before, last ${e})`}function x(t){let{facts:e}=t,n=e.ageVerified===void 0?"Unknown":`${w(e.ageVerified)}${e.ageStatus===""?"":` (${e.ageStatus})`}`,r=[`Player "${t.joiner.name}" joined`,`18+ Verified: ${n}`,`Avatar PC Performance Rank: ${S(e.avatar?.pc??"")}`,`Avatar Quest Performance Rank: ${S(e.avatar?.quest??"")}`];return t.groupFilter!==""&&r.push(`In Group: ${w(e.inGroup,"Unknown (only visible memberships count)")}`),r.push(`Rejoin?: ${z(t.previous)}`),r}function B(t){let{facts:e}=t,n=[`18+: ${w(e.ageVerified,"?")}`,`PC ${S(e.avatar?.pc??"").replace("Unknown","?")}`,`Quest ${S(e.avatar?.quest??"").replace("Unknown","?")}`];return t.groupFilter!==""&&n.push(`group: ${w(e.inGroup,"?")}`),n.push(t.previous===void 0?"new":"rejoin"),`${t.joiner.name} joined \xB7 ${n.join(" \xB7 ")}`}function v(t){let{facts:e}=t;return e.ageVerified===!1||e.avatar?.pc==="VeryPoor"?"warn":e.ageVerified===!0?"ok":"info"}async function Q(t,e,n){return(await t.native.targets()).filter(i=>/wayvr|openvr|steamvr|overlay|xso/i.test(i.name)?n:e).map(i=>i.name)}async function Z(t,e,n){let r=e.notifyDesktop,i=e.notifyVr;if(!r&&!i)return;let s=`${n.joiner.name} joined`,a=x(n).slice(1);if(await t.native.ready){let o=await Q(t,r,i);if(o.length===0)t.logger.debug("Native companion has no target for the enabled channels.");else{let u=await t.native.notify({title:s,content:a.join(` +`),timeoutSecs:8,icon:"security-high",sinks:o,urgency:v(n)==="warn"?"critical":"normal"});for(let c of u.failed)t.logger.warn(`Native target ${c.sink} failed: ${c.error}`);return}}if(t.notifications.desktopAvailable){t.notifications.desktop({title:s,subtitle:a.join(" \xB7 "),accent:v(n),...n.joiner.userId===""?{}:{friendId:n.joiner.userId}});return}t.logger.info("No desktop or VR channel is reachable: the native companion is not running and this platform has no tray toast.")}async function ee(t,e,n){if(!e.notifyDiscord)return;let r=e.discordWebhookUrl.trim();if(!X.test(r)){t.logger.warn("Discord webhook is enabled but the URL is not a discord.com webhook URL.");return}let i=x(n),s={ok:3908957,warn:15548997,info:5793266}[v(n)],a={username:"Club Security",embeds:[{title:i[0],description:i.slice(1).join(` +`),color:s,footer:{text:`${n.instance.worldName} \xB7 ${n.instance.instanceType}`},timestamp:new Date(n.at).toISOString()}],allowed_mentions:{parse:[]}},o=await fetch(r,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(a),signal:t.signal,credentials:"omit",referrerPolicy:"no-referrer"});o.ok||t.logger.warn(`Discord webhook answered ${String(o.status)}.`)}async function T(t,e){let n=t.settings.values;n.notifyToast&&t.notifications.toast({message:B(e),ok:v(e)!=="warn"});let r=await Promise.allSettled([Z(t,n,e),ee(t,n,e)]);for(let i of r)i.status==="rejected"&&t.logger.warn(`Notification channel failed: ${String(i.reason)}`)}var j=25,k=class{#e;#n;#t=[];#r;#i;constructor(e,n){this.#e=e,this.#n=n}install(){this.#e.ui.addNavTab({label:"Club Security",icon:"security",render:e=>{this.#s(e)}}),this.#e.ui.addSettingsCard({title:"Club Security",icon:"security",render:e=>{e.appendChild(this.#e.ui.kit.description("Reports go out for joins in instances that pass every filter above. Test them from the Club Security tab."))}}),this.#e.settings.onChange(()=>{this.refresh()})}push(e){this.#t.unshift(e),this.#t.length>j&&(this.#t.length=j),this.refresh()}refresh(){let e=this.#e.ui.kit;this.#r!==void 0&&e.setChildren(this.#r,this.#o()),this.#i!==void 0&&e.setChildren(this.#i,this.#a())}#s(e){let n=this.#e.ui.kit;this.#r=n.card({title:"Status",icon:"shield"}),this.#i=n.card({title:"Recent reports",icon:"history"});let r=n.card({title:"Actions",icon:"build",children:[n.description("A test report uses the current instance and your own name, and goes to every enabled channel."),n.buttonRow(n.button({label:"Send test notification",icon:"send",onClick:()=>{this.#n.sendTest()}}),n.button({label:"Forget who joined before",icon:"delete_sweep",onClick:()=>{this.#d()}}))]});e.append(n.layout(n.pair(this.#r,r),this.#i)),this.refresh()}#o(){let e=this.#e.ui.kit,n=this.#e.settings.values,r=m(n),i=this.#n.currentInstance(),s=i!==void 0&&I(r,i),a=[n.notifyToast&&"toast",n.notifyDesktop&&"desktop",n.notifyVr&&"VR",n.notifyDiscord&&"Discord"].filter(o=>typeof o=="string");return[e.row({label:"Reporting",value:n.enabled?e.badge("ok","On"):e.badge("neutral","Off")}),e.row({label:"Watching",detail:V(r)}),e.row({label:"Current instance",detail:i===void 0?"Not in an instance":`${i.worldName} \xB7 ${i.instanceType}`,value:i===void 0?void 0:e.badge(s?"ok":"neutral",s?"Matches":"Filtered out")}),e.row({label:"Channels",detail:a.length===0?"none enabled":a.join(", ")}),e.row({label:"Players remembered",value:e.valueText(String(this.#n.memory.size))})]}#a(){let e=this.#e.ui.kit;return this.#t.length===0?[e.emptyState("No joins reported yet.")]:this.#t.map(n=>{let r=x(n),i=new Date(n.at).toLocaleTimeString();return e.row({label:`${i} \xB7 ${n.joiner.name}`,detail:r.slice(1).join(" \xB7 "),value:n.previous===void 0?e.badge("accent","New"):e.badge("warn","Rejoin")})})}async#d(){await this.#e.notifications.confirm({title:"Forget join history",message:"Every player will count as a first-time joiner again. Continue?",confirmLabel:"Forget",icon:"delete_sweep"})&&(await this.#n.memory.clear(),this.#e.notifications.toast({message:"Join history cleared."}),this.refresh())}};var D={enabled:{kind:"boolean",label:"Report joins",description:"Master switch. Filters below decide which instances count.",default:!0},instanceTypes:{kind:"string",label:"Only in these instance types",description:'Comma-separated. Leave empty for every instance. Types: public, friends+, friends, hidden, private, invite_plus, group-public, group-plus, group-members, or just "group" for all group instances.',default:"",placeholder:"group-public, group-plus"},groupId:{kind:"string",label:"Only in instances of this group",description:"A grp_\u2026 id. Leave empty for any group. When set, the report also says whether the joiner is a member.",default:"",placeholder:"grp_00000000-0000-0000-0000-000000000000"},worldIds:{kind:"string",label:"Only in these worlds",description:"Comma-separated wrld_\u2026 ids. Leave empty for any world.",default:"",placeholder:"wrld_\u2026, wrld_\u2026"},settleSecs:{kind:"number",label:"Seconds to ignore after you join",description:"When you enter an instance, VRChat logs a join for everyone already there. Joins inside this window are remembered but not reported.",default:15,min:3,max:120,step:1},collectTimeoutSecs:{kind:"number",label:"Seconds to wait for details",description:"How long to wait for VRCNext to load the profile and avatar before reporting what is known.",default:25,min:5,max:120,step:5},notifyToast:{kind:"boolean",label:"In-app toast",default:!0},notifyDesktop:{kind:"boolean",label:"Desktop notification",description:"Through the native companion on any platform, or VRCNext\u2019s tray toast on Windows.",default:!0},notifyVr:{kind:"boolean",label:"VR overlay notification",description:"Through the native companion\u2019s VR target on any platform, or the SteamVR wrist overlay on Windows.",default:!0},notifyDiscord:{kind:"boolean",label:"Discord webhook",default:!1},discordWebhookUrl:{kind:"string",label:"Discord webhook URL",description:"Server Settings \u2192 Integrations \u2192 Webhooks. Sent straight from this page; the URL is stored locally only.",default:"",placeholder:"https://discord.com/api/webhooks/\u2026"},memory:{kind:"string",label:"Join memory",description:"Internal: who has joined a matching instance before.",default:"{}",hidden:!0}};var ne="gl_player_join",te="gl_world_join";function re(){return typeof currentVrcUser!="object"||currentVrcUser===null?{id:"",name:""}:{id:typeof currentVrcUser.id=="string"?currentVrcUser.id:"",name:typeof currentVrcUser.displayName=="string"?currentVrcUser.displayName:""}}var P=class{#e;#n;#t;#r;#i="";#s="";#o=new Set;#a=0;constructor(e){this.#e=e,this.#n=new h(e),this.#t=new b(e.settings),this.#r=new k(e,{memory:this.#t,currentInstance:()=>this.#n.instance,sendTest:()=>this.#f()})}start(){this.#e.events.on("vrcUser",e=>{let n=$(e);n!==""&&(this.#i=n);let r=e.displayName;typeof r=="string"&&(this.#s=r)}),this.#e.events.on("vrcCurrentInstance",()=>{this.#r.refresh()}),this.#e.gameLog.onType(te,()=>{this.#d()}),this.#e.gameLog.onType(ne,e=>{this.#c({name:e.message,userId:e.detail})}),this.#r.install(),this.#e.bridge.send("vrcGetCurrentInstance"),this.#e.logger.info(`Club Security v${this.#e.version} watching for joins.`)}#d(){this.#a=Date.now()+this.#e.settings.get("settleSecs")*1e3}async#c(e){let n=this.#e.settings.values;if(!n.enabled||e.name==="")return;if(this.#l(e)){this.#d();return}let r=this.#n.instance;if(r===void 0){this.#e.logger.debug(`${e.name} joined but the current instance is not known yet.`);return}let i=m(n);if(!I(i,r))return;let s=e.userId===""?`name:${e.name}`:e.userId;if(!this.#o.has(s)){this.#o.add(s);try{let a=await this.#t.record(s,e.name,r.location);if(Date.now()c.id===n.id)??e.users[0],i=r?.displayName??n.name,s={name:i===""?"Test player":i,userId:r?.id??""},a=m(this.#e.settings.values),o=await this.#n.collect(s,a.groupId,1e4),u={at:Date.now(),joiner:s,instance:e,facts:o,groupFilter:a.groupId,previous:this.#t.previous(s.userId===""?`name:${s.name}`:s.userId)};await T(this.#e,u),this.#e.notifications.toast({message:"Test report sent to every enabled channel."})}},cn={id:"club-security",settings:D,activate(t){new P(t).start()},deactivate(){}};export{cn as default}; //# sourceMappingURL=club-security.js.map diff --git a/dist/club-security.js.map b/dist/club-security.js.map index 392687f..5f0a34d 100644 --- a/dist/club-security.js.map +++ b/dist/club-security.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/vrcnext-data.ts", "../src/collector.ts", "../src/filters.ts", "../src/memory.ts", "../src/notify.ts", "../src/panel.ts", "../src/settings.ts", "../src/index.ts"], - "sourcesContent": ["/**\n * Narrowers for the VRCNext events this plugin consumes.\n *\n * None of these events are in the API's verified map, so every payload arrives as `unknown`.\n * Shapes were read out of VRCNext 2026.61.2: `InstanceController.PushCurrentInstanceFromCache`,\n * `GroupsController` (`vrcGetGroupsForNetwork`), `FriendsController` (`vrcGetInstanceAvatars`)\n * and `MessageRouter` (`vrcGetAvatarDetail`).\n */\n\nexport interface InstanceUser {\n readonly id: string;\n readonly displayName: string;\n readonly ageVerified: boolean;\n /** VRChat's own status string, e.g. `18+`, `verified`, `hidden`. Empty until the profile loaded. */\n readonly ageVerificationStatus: string;\n readonly platform: string;\n /** Present once VRCNext fetched the profile; an empty list means it has not yet. */\n readonly tags: readonly string[];\n readonly lastLogin: string;\n readonly avatarId: string;\n readonly avatarName: string;\n}\n\nexport interface CurrentInstance {\n readonly location: string;\n readonly worldId: string;\n readonly worldName: string;\n readonly instanceType: string;\n /** Parsed from `~group(grp_\u2026)` in the location; empty for non-group instances. */\n readonly groupId: string;\n readonly users: readonly InstanceUser[];\n}\n\nfunction rec(value: unknown): Record | undefined {\n return typeof value === 'object' && value !== null ? (value as Record) : undefined;\n}\n\nfunction str(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction strings(value: unknown): readonly string[] {\n return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : [];\n}\n\nexport function groupIdOf(location: string): string {\n const match = /~group\\((grp_[0-9a-f-]+)\\)/i.exec(location);\n return match?.[1] ?? '';\n}\n\nfunction toInstanceUser(value: unknown): InstanceUser | undefined {\n const r = rec(value);\n if (r === undefined) return undefined;\n const id = str(r['id']);\n const displayName = str(r['displayName']);\n if (id === '' && displayName === '') return undefined;\n return {\n id,\n displayName,\n ageVerified: r['ageVerified'] === true,\n ageVerificationStatus: str(r['ageVerificationStatus']),\n platform: str(r['platform']),\n tags: strings(r['tags']),\n lastLogin: str(r['lastLogin']),\n avatarId: str(r['avatarId']),\n avatarName: str(r['avatarName']),\n };\n}\n\n/** `undefined` for the `{ empty: true }` and `{ error }` variants. */\nexport function toCurrentInstance(payload: unknown): CurrentInstance | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const location = str(r['location']);\n if (location === '') return undefined;\n const rawUsers = Array.isArray(r['users']) ? r['users'] : [];\n return {\n location,\n worldId: str(r['worldId']),\n worldName: str(r['worldName']),\n instanceType: str(r['instanceType']),\n groupId: groupIdOf(location),\n users: rawUsers.flatMap((u) => {\n const user = toInstanceUser(u);\n return user === undefined ? [] : [user];\n }),\n };\n}\n\n/** VRCNext only fills these once it has fetched the profile from the API. */\nexport function profileLoaded(user: InstanceUser): boolean {\n return user.ageVerificationStatus !== '' || user.tags.length > 0 || user.lastLogin !== '';\n}\n\nexport interface UserGroups {\n readonly userId: string;\n readonly groupIds: readonly string[];\n}\n\nexport function toUserGroups(payload: unknown): UserGroups | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const userId = str(r['userId']);\n if (userId === '') return undefined;\n const groups = Array.isArray(r['groups']) ? r['groups'] : [];\n return {\n userId,\n groupIds: groups.flatMap((g) => {\n const id = str(rec(g)?.['id']);\n return id === '' ? [] : [id];\n }),\n };\n}\n\nexport interface InstanceAvatar {\n readonly userId: string;\n readonly avatarId: string;\n readonly avatarName: string;\n}\n\nexport function toInstanceAvatar(payload: unknown): InstanceAvatar | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const userId = str(r['userId']);\n if (userId === '') return undefined;\n return { userId, avatarId: str(r['avatarId']), avatarName: str(r['avatarName']) };\n}\n\nexport interface AvatarPerformance {\n readonly avatarId: string;\n readonly name: string;\n readonly pc: string;\n readonly quest: string;\n}\n\nexport function toAvatarPerformance(payload: unknown): AvatarPerformance | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const avatarId = str(r['id']);\n if (avatarId === '') return undefined;\n return {\n avatarId,\n name: str(r['name']),\n pc: str(r['pcPerf']),\n quest: str(r['questPerf']),\n };\n}\n\n/** The signed-in account, from the `vrcUser` event VRCNext sends after login. */\nexport function toSelfId(payload: unknown): string {\n return str(rec(payload)?.['id']);\n}\n", "/**\n * Gathers everything the report needs about one joiner, within a deadline.\n *\n * Each fact comes from a different VRCNext path, so they are collected in parallel and each one\n * degrades to \"unknown\" on its own timeout rather than holding up the report:\n *\n * - Age verification: VRCNext fetches the joiner's profile itself and re-pushes\n * `vrcCurrentInstance` once it has, so this waits for that push instead of making a request.\n * - Group membership: `vrcGetGroupsForNetwork` lists the groups the user shows publicly. A member\n * who hides that membership looks like a non-member; the report says \"visible\" for that reason.\n * - Avatar: `vrcGetInstanceAvatars` resolves the avatar id from the profile image through\n * VRCNext's avatar databases, then `vrcGetAvatarDetail` supplies the performance ranks. That\n * action also feeds VRCNext's avatar modal, so it is skipped while that modal is open.\n */\n\nimport type { EventBus, PluginContext } from '@vrcnext/plugin-api';\n\nimport type { Settings } from './settings.js';\nimport {\n profileLoaded,\n toAvatarPerformance,\n toCurrentInstance,\n toInstanceAvatar,\n toUserGroups,\n type CurrentInstance,\n type InstanceUser,\n} from './vrcnext-data.js';\n\nexport interface Joiner {\n readonly name: string;\n /** Empty for accounts VRChat still logs with a legacy id. */\n readonly userId: string;\n}\n\nexport interface AvatarFacts {\n readonly name: string;\n readonly pc: string;\n readonly quest: string;\n}\n\nexport interface Facts {\n readonly ageVerified: boolean | undefined;\n readonly ageStatus: string;\n readonly platform: string;\n readonly avatar: AvatarFacts | undefined;\n /** `undefined` when no group filter is configured. */\n readonly inGroup: boolean | undefined;\n}\n\ntype Ctx = PluginContext;\n\n/** Resolves with the first event `pick` accepts, or `undefined` on timeout or abort. */\nfunction waitFor(\n events: EventBus,\n type: string,\n pick: (payload: unknown) => T | undefined,\n options: { readonly ms: number; readonly signal: AbortSignal },\n): Promise {\n return new Promise((resolve) => {\n let done = false;\n const finish = (value: T | undefined): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n unsubscribe();\n options.signal.removeEventListener('abort', onAbort);\n resolve(value);\n };\n const onAbort = (): void => { finish(undefined); };\n const timer = setTimeout(() => { finish(undefined); }, options.ms);\n const unsubscribe = events.on(type, (payload) => {\n const value = pick(payload);\n if (value !== undefined) finish(value);\n });\n options.signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nfunction findUser(instance: CurrentInstance, joiner: Joiner): InstanceUser | undefined {\n return instance.users.find((u) =>\n joiner.userId !== '' ? u.id === joiner.userId : u.displayName === joiner.name,\n );\n}\n\nfunction avatarModalOpen(): boolean {\n const modal = document.getElementById('modalAvatarDetail');\n return modal !== null && modal.offsetParent !== null;\n}\n\nexport class FactCollector {\n readonly #ctx: Ctx;\n #latest: CurrentInstance | undefined;\n /** Performance ranks by avatar id, so a popular avatar is only fetched once. */\n readonly #perfCache = new Map();\n\n constructor(ctx: Ctx) {\n this.#ctx = ctx;\n ctx.events.on('vrcCurrentInstance', (payload) => {\n const instance = toCurrentInstance(payload);\n if (instance !== undefined) this.#latest = instance;\n });\n ctx.events.on('vrcAvatarDetail', (payload) => {\n const perf = toAvatarPerformance(payload);\n if (perf !== undefined) {\n this.#perfCache.set(perf.avatarId, { name: perf.name, pc: perf.pc, quest: perf.quest });\n }\n });\n }\n\n get instance(): CurrentInstance | undefined {\n return this.#latest;\n }\n\n /** Runs every lookup in parallel and returns whatever arrived before the deadline. */\n async collect(joiner: Joiner, groupId: string, deadlineMs: number): Promise {\n const signal = this.#ctx.signal;\n const [profile, inGroup, avatar] = await Promise.all([\n this.#profile(joiner, deadlineMs, signal),\n groupId === '' ? Promise.resolve(undefined) : this.#membership(joiner, groupId, deadlineMs, signal),\n this.#avatar(joiner, deadlineMs, signal),\n ]);\n return {\n ageVerified: profile === undefined ? undefined : profile.ageVerified,\n ageStatus: profile?.ageVerificationStatus ?? '',\n platform: profile?.platform ?? '',\n avatar,\n inGroup,\n };\n }\n\n async #profile(joiner: Joiner, ms: number, signal: AbortSignal): Promise {\n const current = this.#latest === undefined ? undefined : findUser(this.#latest, joiner);\n if (current !== undefined && profileLoaded(current)) return current;\n const loaded = await waitFor(\n this.#ctx.events,\n 'vrcCurrentInstance',\n (payload) => {\n const instance = toCurrentInstance(payload);\n const user = instance === undefined ? undefined : findUser(instance, joiner);\n return user !== undefined && profileLoaded(user) ? user : undefined;\n },\n { ms, signal },\n );\n // A profile that never loaded still tells us the platform VRChat logged.\n return loaded ?? current;\n }\n\n async #membership(joiner: Joiner, groupId: string, ms: number, signal: AbortSignal): Promise {\n if (joiner.userId === '') return undefined;\n const wanted = groupId.toLowerCase();\n const pending = waitFor(\n this.#ctx.events,\n 'vrcGroupsForNetwork',\n (payload) => {\n const groups = toUserGroups(payload);\n return groups?.userId === joiner.userId ? groups.groupIds : undefined;\n },\n { ms, signal },\n );\n this.#ctx.bridge.send('vrcGetGroupsForNetwork', { userId: joiner.userId });\n const groupIds = await pending;\n return groupIds === undefined ? undefined : groupIds.some((id) => id.toLowerCase() === wanted);\n }\n\n async #avatar(joiner: Joiner, ms: number, signal: AbortSignal): Promise {\n if (joiner.userId === '') return undefined;\n const started = Date.now();\n const remaining = (): number => Math.max(1_000, ms - (Date.now() - started));\n\n let avatarId = this.#latest === undefined ? '' : (findUser(this.#latest, joiner)?.avatarId ?? '');\n let avatarName = '';\n if (avatarId === '') {\n const pending = waitFor(\n this.#ctx.events,\n 'vrcInstanceAvatarFound',\n (payload) => {\n const found = toInstanceAvatar(payload);\n return found?.userId === joiner.userId ? found : undefined;\n },\n { ms: remaining(), signal },\n );\n this.#ctx.bridge.send('vrcGetInstanceAvatars', { userIds: [joiner.userId] });\n const found = await pending;\n if (found === undefined) return undefined;\n avatarId = found.avatarId;\n avatarName = found.avatarName;\n }\n if (avatarId === '') return avatarName === '' ? undefined : { name: avatarName, pc: '', quest: '' };\n\n const cached = this.#perfCache.get(avatarId);\n if (cached !== undefined) return cached;\n if (avatarModalOpen()) {\n this.#ctx.logger.debug('Avatar modal is open; not fetching performance ranks behind it.');\n return { name: avatarName, pc: '', quest: '' };\n }\n const pending = waitFor(\n this.#ctx.events,\n 'vrcAvatarDetail',\n (payload) => {\n const perf = toAvatarPerformance(payload);\n return perf?.avatarId === avatarId ? perf : undefined;\n },\n { ms: remaining(), signal },\n );\n this.#ctx.bridge.send('vrcGetAvatarDetail', { avatarId });\n const perf = await pending;\n return perf === undefined\n ? { name: avatarName, pc: '', quest: '' }\n : { name: perf.name === '' ? avatarName : perf.name, pc: perf.pc, quest: perf.quest };\n }\n}\n", "/**\n * Decides whether the current instance is one the user wants reports for.\n */\n\nimport type { SettingsValues } from '@vrcnext/plugin-api';\n\nimport type { CurrentInstance } from './vrcnext-data.js';\nimport type { Settings } from './settings.js';\n\nexport interface InstanceFilter {\n readonly instanceTypes: readonly string[];\n readonly groupId: string;\n readonly worldIds: readonly string[];\n}\n\nexport function splitList(value: string): readonly string[] {\n return value\n .split(/[,\\s]+/)\n .map((s) => s.trim().toLowerCase())\n .filter((s) => s.length > 0);\n}\n\nexport function filterFrom(values: SettingsValues): InstanceFilter {\n return {\n instanceTypes: splitList(values.instanceTypes),\n groupId: values.groupId.trim().toLowerCase(),\n worldIds: splitList(values.worldIds),\n };\n}\n\nfunction typeMatches(wanted: readonly string[], actual: string): boolean {\n if (wanted.length === 0) return true;\n const type = actual.toLowerCase();\n return wanted.some((w) => w === type || (w === 'group' && type.startsWith('group')));\n}\n\nexport function instanceMatches(filter: InstanceFilter, instance: CurrentInstance): boolean {\n if (!typeMatches(filter.instanceTypes, instance.instanceType)) return false;\n const groupId = filter.groupId.toLowerCase();\n if (groupId !== '' && instance.groupId.toLowerCase() !== groupId) return false;\n const worldIds = filter.worldIds.map((w) => w.toLowerCase());\n if (worldIds.length > 0 && !worldIds.includes(instance.worldId.toLowerCase())) return false;\n return true;\n}\n\n/** One line for the status card: what the filter currently restricts to. */\nexport function describeFilter(filter: InstanceFilter): string {\n const parts: string[] = [];\n if (filter.instanceTypes.length > 0) parts.push(`types: ${filter.instanceTypes.join(', ')}`);\n if (filter.groupId !== '') parts.push(`group: ${filter.groupId}`);\n if (filter.worldIds.length > 0) parts.push(`worlds: ${String(filter.worldIds.length)}`);\n return parts.length === 0 ? 'every instance' : parts.join(' \u00B7 ');\n}\n", "/**\n * Who has joined a matching instance before.\n *\n * Persisted in a hidden string setting as JSON, because the plugin API offers settings as its\n * only durable store. Capped so it cannot grow without bound; the oldest entries fall off.\n */\n\nimport type { SettingsStore } from '@vrcnext/plugin-api';\n\nimport type { Settings } from './settings.js';\n\nexport interface SeenEntry {\n readonly name: string;\n readonly joins: number;\n readonly firstSeen: number;\n readonly lastSeen: number;\n readonly lastLocation: string;\n}\n\nconst MAX_ENTRIES = 2000;\n\ntype SeenMap = Record;\n\nfunction parse(raw: string): SeenMap {\n try {\n const value: unknown = JSON.parse(raw);\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return {};\n const out: SeenMap = {};\n for (const [key, entry] of Object.entries(value as Record)) {\n if (typeof entry !== 'object' || entry === null) continue;\n const e = entry as Record;\n out[key] = {\n name: typeof e['name'] === 'string' ? e['name'] : '',\n joins: typeof e['joins'] === 'number' ? e['joins'] : 1,\n firstSeen: typeof e['firstSeen'] === 'number' ? e['firstSeen'] : 0,\n lastSeen: typeof e['lastSeen'] === 'number' ? e['lastSeen'] : 0,\n lastLocation: typeof e['lastLocation'] === 'string' ? e['lastLocation'] : '',\n };\n }\n return out;\n } catch {\n return {};\n }\n}\n\nfunction trimmed(map: SeenMap): SeenMap {\n const entries = Object.entries(map);\n if (entries.length <= MAX_ENTRIES) return map;\n entries.sort((a, b) => b[1].lastSeen - a[1].lastSeen);\n return Object.fromEntries(entries.slice(0, MAX_ENTRIES));\n}\n\nexport class JoinMemory {\n readonly #store: SettingsStore;\n #map: SeenMap;\n\n constructor(store: SettingsStore) {\n this.#store = store;\n this.#map = parse(store.get('memory'));\n }\n\n get size(): number {\n return Object.keys(this.#map).length;\n }\n\n /** The entry as it was before this join, or `undefined` for a first-timer. */\n previous(key: string): SeenEntry | undefined {\n return this.#map[key];\n }\n\n /** Records a join and persists. Returns the entry as it was before. */\n async record(key: string, name: string, location: string): Promise {\n const before = this.#map[key];\n const now = Date.now();\n this.#map[key] = {\n name,\n joins: (before?.joins ?? 0) + 1,\n firstSeen: before?.firstSeen ?? now,\n lastSeen: now,\n lastLocation: location,\n };\n this.#map = trimmed(this.#map);\n await this.#store.set('memory', JSON.stringify(this.#map));\n return before;\n }\n\n async clear(): Promise {\n this.#map = {};\n await this.#store.set('memory', '{}');\n }\n}\n", "/**\n * Formats a report and fans it out to every enabled channel.\n *\n * Channels:\n * - In-app toast: always available.\n * - Desktop and VR: on any platform through the native companion, whose targets are addressed by\n * name; on Windows without the companion, VRCNext's own tray toast plus SteamVR wrist overlay,\n * which is a single action that reaches both.\n * - Discord: a webhook POST straight from the page. Discord answers webhook requests with CORS\n * headers, so no proxy is needed.\n */\n\nimport type { PluginContext, SettingsValues } from '@vrcnext/plugin-api';\n\nimport type { Facts, Joiner } from './collector.js';\nimport type { SeenEntry } from './memory.js';\nimport type { Settings } from './settings.js';\nimport type { CurrentInstance } from './vrcnext-data.js';\n\nexport interface Report {\n readonly at: number;\n readonly joiner: Joiner;\n readonly instance: CurrentInstance;\n readonly facts: Facts;\n /** Whether a group filter was active, so the \"In Group\" line is only shown when it means something. */\n readonly groupFilter: string;\n readonly previous: SeenEntry | undefined;\n}\n\ntype Ctx = PluginContext;\n\nconst DISCORD_WEBHOOK = /^https:\\/\\/(?:ptb\\.|canary\\.)?discord(?:app)?\\.com\\/api\\/webhooks\\/\\d+\\/[\\w-]+$/;\n\nfunction yesNo(value: boolean | undefined, unknown = 'Unknown'): string {\n if (value === undefined) return unknown;\n return value ? 'Yes' : 'No';\n}\n\nfunction rank(value: string): string {\n return value === '' ? 'Unknown' : value;\n}\n\nfunction rejoinText(previous: SeenEntry | undefined): string {\n if (previous === undefined) return 'No (first time)';\n const when = new Date(previous.lastSeen).toLocaleString();\n return `Yes (${String(previous.joins)}\u00D7 before, last ${when})`;\n}\n\n/** The report as the spec lays it out, one fact per line. */\nexport function reportLines(report: Report): readonly string[] {\n const { facts } = report;\n const age = facts.ageVerified === undefined\n ? 'Unknown'\n : `${yesNo(facts.ageVerified)}${facts.ageStatus === '' ? '' : ` (${facts.ageStatus})`}`;\n const lines = [\n `Player \"${report.joiner.name}\" joined`,\n `18+ Verified: ${age}`,\n `Avatar PC Performance Rank: ${rank(facts.avatar?.pc ?? '')}`,\n `Avatar Quest Performance Rank: ${rank(facts.avatar?.quest ?? '')}`,\n ];\n if (report.groupFilter !== '') lines.push(`In Group: ${yesNo(facts.inGroup, 'Unknown (only visible memberships count)')}`);\n lines.push(`Rejoin?: ${rejoinText(report.previous)}`);\n return lines;\n}\n\n/** Everything a single-line surface can hold. */\nexport function reportSummary(report: Report): string {\n const { facts } = report;\n const bits = [\n `18+: ${yesNo(facts.ageVerified, '?')}`,\n `PC ${rank(facts.avatar?.pc ?? '').replace('Unknown', '?')}`,\n `Quest ${rank(facts.avatar?.quest ?? '').replace('Unknown', '?')}`,\n ];\n if (report.groupFilter !== '') bits.push(`group: ${yesNo(facts.inGroup, '?')}`);\n bits.push(report.previous === undefined ? 'new' : 'rejoin');\n return `${report.joiner.name} joined \u00B7 ${bits.join(' \u00B7 ')}`;\n}\n\nfunction accentFor(report: Report): 'ok' | 'warn' | 'info' {\n const { facts } = report;\n if (facts.ageVerified === false || facts.avatar?.pc === 'VeryPoor') return 'warn';\n if (facts.ageVerified === true) return 'ok';\n return 'info';\n}\n\n/** Native companion targets: the VR overlay is the one whose name says so; the rest are desktop. */\nasync function nativeSinks(ctx: Ctx, wantDesktop: boolean, wantVr: boolean): Promise {\n const targets = await ctx.native.targets();\n return targets\n .filter((t) => {\n const vr = /wayvr|openvr|steamvr|overlay|xso/i.test(t.name);\n return vr ? wantVr : wantDesktop;\n })\n .map((t) => t.name);\n}\n\nasync function sendDesktopAndVr(ctx: Ctx, values: SettingsValues, report: Report): Promise {\n const wantDesktop = values.notifyDesktop;\n const wantVr = values.notifyVr;\n if (!wantDesktop && !wantVr) return;\n const title = `${report.joiner.name} joined`;\n const lines = reportLines(report).slice(1);\n\n if (await ctx.native.ready) {\n const sinks = await nativeSinks(ctx, wantDesktop, wantVr);\n if (sinks.length === 0) {\n ctx.logger.debug('Native companion has no target for the enabled channels.');\n } else {\n const result = await ctx.native.notify({\n title,\n content: lines.join('\\n'),\n timeoutSecs: 8,\n icon: 'security-high',\n sinks,\n urgency: accentFor(report) === 'warn' ? 'critical' : 'normal',\n });\n for (const failure of result.failed) ctx.logger.warn(`Native target ${failure.sink} failed: ${failure.error}`);\n return;\n }\n }\n\n // Windows without the companion: VRCNext's own tray toast and wrist overlay in one call.\n if (ctx.notifications.desktopAvailable) {\n ctx.notifications.desktop({\n title,\n subtitle: lines.join(' \u00B7 '),\n accent: accentFor(report),\n ...(report.joiner.userId === '' ? {} : { friendId: report.joiner.userId }),\n });\n return;\n }\n ctx.logger.info('No desktop or VR channel is reachable: the native companion is not running and this platform has no tray toast.');\n}\n\nasync function sendDiscord(ctx: Ctx, values: SettingsValues, report: Report): Promise {\n if (!values.notifyDiscord) return;\n const url = values.discordWebhookUrl.trim();\n if (!DISCORD_WEBHOOK.test(url)) {\n ctx.logger.warn('Discord webhook is enabled but the URL is not a discord.com webhook URL.');\n return;\n }\n const lines = reportLines(report);\n const colour = { ok: 0x3ba55d, warn: 0xed4245, info: 0x5865f2 }[accentFor(report)];\n const body = {\n username: 'Club Security',\n embeds: [{\n title: lines[0],\n description: lines.slice(1).join('\\n'),\n color: colour,\n footer: { text: `${report.instance.worldName} \u00B7 ${report.instance.instanceType}` },\n timestamp: new Date(report.at).toISOString(),\n }],\n allowed_mentions: { parse: [] },\n };\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n signal: ctx.signal,\n credentials: 'omit',\n referrerPolicy: 'no-referrer',\n });\n if (!response.ok) ctx.logger.warn(`Discord webhook answered ${String(response.status)}.`);\n}\n\n/** Sends on every enabled channel. Never throws: a failed channel is logged, not fatal. */\nexport async function notifyAll(ctx: Ctx, report: Report): Promise {\n const values = ctx.settings.values;\n if (values.notifyToast) ctx.notifications.toast({ message: reportSummary(report), ok: accentFor(report) !== 'warn' });\n const results = await Promise.allSettled([\n sendDesktopAndVr(ctx, values, report),\n sendDiscord(ctx, values, report),\n ]);\n for (const result of results) {\n if (result.status === 'rejected') ctx.logger.warn(`Notification channel failed: ${String(result.reason)}`);\n }\n}\n", "/**\n * The plugin's sidebar tab: status, the last reports, and two maintenance buttons.\n * Built from `ctx.ui.kit` so it is VRCNext's own markup and follows its theme.\n */\n\nimport type { PluginContext } from '@vrcnext/plugin-api';\n\nimport { describeFilter, filterFrom, instanceMatches } from './filters.js';\nimport type { JoinMemory } from './memory.js';\nimport { reportLines, type Report } from './notify.js';\nimport type { Settings } from './settings.js';\nimport type { CurrentInstance } from './vrcnext-data.js';\n\ntype Ctx = PluginContext;\n\nconst MAX_SHOWN = 25;\n\nexport interface PanelDeps {\n readonly memory: JoinMemory;\n readonly currentInstance: () => CurrentInstance | undefined;\n readonly sendTest: () => Promise;\n}\n\nexport class ReportPanel {\n readonly #ctx: Ctx;\n readonly #deps: PanelDeps;\n readonly #reports: Report[] = [];\n #status: HTMLElement | undefined;\n #list: HTMLElement | undefined;\n\n constructor(ctx: Ctx, deps: PanelDeps) {\n this.#ctx = ctx;\n this.#deps = deps;\n }\n\n install(): void {\n this.#ctx.ui.addNavTab({\n label: 'Club Security',\n icon: 'security',\n render: (tab) => { this.#render(tab); },\n });\n // The host renders every schema setting as a row on this card before `render` runs.\n this.#ctx.ui.addSettingsCard({\n title: 'Club Security',\n icon: 'security',\n render: (card) => {\n card.appendChild(this.#ctx.ui.kit.description(\n 'Reports go out for joins in instances that pass every filter above. Test them from the Club Security tab.',\n ));\n },\n });\n this.#ctx.settings.onChange(() => { this.refresh(); });\n }\n\n push(report: Report): void {\n this.#reports.unshift(report);\n if (this.#reports.length > MAX_SHOWN) this.#reports.length = MAX_SHOWN;\n this.refresh();\n }\n\n refresh(): void {\n const k = this.#ctx.ui.kit;\n if (this.#status !== undefined) k.setChildren(this.#status, this.#statusRows());\n if (this.#list !== undefined) k.setChildren(this.#list, this.#reportRows());\n }\n\n #render(tab: HTMLElement): void {\n const k = this.#ctx.ui.kit;\n this.#status = k.card({ title: 'Status', icon: 'shield' });\n this.#list = k.card({ title: 'Recent reports', icon: 'history' });\n const actions = k.card({\n title: 'Actions',\n icon: 'build',\n children: [\n k.description('A test report uses the current instance and your own name, and goes to every enabled channel.'),\n k.buttonRow(\n k.button({ label: 'Send test notification', icon: 'send', onClick: () => { void this.#deps.sendTest(); } }),\n k.button({\n label: 'Forget who joined before',\n icon: 'delete_sweep',\n onClick: () => { void this.#clearMemory(); },\n }),\n ),\n ],\n });\n tab.append(k.layout(k.pair(this.#status, actions), this.#list));\n this.refresh();\n }\n\n #statusRows(): readonly (HTMLElement | DocumentFragment)[] {\n const k = this.#ctx.ui.kit;\n const values = this.#ctx.settings.values;\n const filter = filterFrom(values);\n const instance = this.#deps.currentInstance();\n const matches = instance !== undefined && instanceMatches(filter, instance);\n const channels = [\n values.notifyToast && 'toast',\n values.notifyDesktop && 'desktop',\n values.notifyVr && 'VR',\n values.notifyDiscord && 'Discord',\n ].filter((c): c is string => typeof c === 'string');\n return [\n k.row({ label: 'Reporting', value: values.enabled ? k.badge('ok', 'On') : k.badge('neutral', 'Off') }),\n k.row({ label: 'Watching', detail: describeFilter(filter) }),\n k.row({\n label: 'Current instance',\n detail: instance === undefined ? 'Not in an instance' : `${instance.worldName} \u00B7 ${instance.instanceType}`,\n value: instance === undefined ? undefined : k.badge(matches ? 'ok' : 'neutral', matches ? 'Matches' : 'Filtered out'),\n }),\n k.row({ label: 'Channels', detail: channels.length === 0 ? 'none enabled' : channels.join(', ') }),\n k.row({ label: 'Players remembered', value: k.valueText(String(this.#deps.memory.size)) }),\n ];\n }\n\n #reportRows(): readonly (HTMLElement | DocumentFragment)[] {\n const k = this.#ctx.ui.kit;\n if (this.#reports.length === 0) return [k.emptyState('No joins reported yet.')];\n return this.#reports.map((report) => {\n const lines = reportLines(report);\n const time = new Date(report.at).toLocaleTimeString();\n return k.row({\n label: `${time} \u00B7 ${report.joiner.name}`,\n detail: lines.slice(1).join(' \u00B7 '),\n value: report.previous === undefined ? k.badge('accent', 'New') : k.badge('warn', 'Rejoin'),\n });\n });\n }\n\n async #clearMemory(): Promise {\n const ok = await this.#ctx.notifications.confirm({\n title: 'Forget join history',\n message: 'Every player will count as a first-time joiner again. Continue?',\n confirmLabel: 'Forget',\n icon: 'delete_sweep',\n });\n if (!ok) return;\n await this.#deps.memory.clear();\n this.#ctx.notifications.toast({ message: 'Join history cleared.' });\n this.refresh();\n }\n}\n", "/**\n * Settings schema. The host renders these as rows on the plugin's settings card and persists\n * them; the plugin reads them through `ctx.settings`.\n */\n\nimport type { SettingsSchema } from '@vrcnext/plugin-api';\n\n/** Instance types as VRCNext's `ParseLocation` names them, plus a shorthand for every group type. */\nexport const INSTANCE_TYPES = [\n 'public',\n 'friends+',\n 'friends',\n 'hidden',\n 'private',\n 'invite_plus',\n 'group',\n 'group-public',\n 'group-plus',\n 'group-members',\n] as const;\n\nexport const settings = {\n enabled: {\n kind: 'boolean',\n label: 'Report joins',\n description: 'Master switch. Filters below decide which instances count.',\n default: true,\n },\n instanceTypes: {\n kind: 'string',\n label: 'Only in these instance types',\n description:\n 'Comma-separated. Leave empty for every instance. Types: public, friends+, friends, hidden, ' +\n 'private, invite_plus, group-public, group-plus, group-members, or just \"group\" for all group instances.',\n default: '',\n placeholder: 'group-public, group-plus',\n },\n groupId: {\n kind: 'string',\n label: 'Only in instances of this group',\n description: 'A grp_\u2026 id. Leave empty for any group. When set, the report also says whether the joiner is a member.',\n default: '',\n placeholder: 'grp_00000000-0000-0000-0000-000000000000',\n },\n worldIds: {\n kind: 'string',\n label: 'Only in these worlds',\n description: 'Comma-separated wrld_\u2026 ids. Leave empty for any world.',\n default: '',\n placeholder: 'wrld_\u2026, wrld_\u2026',\n },\n collectTimeoutSecs: {\n kind: 'number',\n label: 'Seconds to wait for details',\n description: 'How long to wait for VRCNext to load the profile and avatar before reporting what is known.',\n default: 25,\n min: 5,\n max: 120,\n step: 5,\n },\n notifyToast: {\n kind: 'boolean',\n label: 'In-app toast',\n default: true,\n },\n notifyDesktop: {\n kind: 'boolean',\n label: 'Desktop notification',\n description: 'Through the native companion on any platform, or VRCNext\u2019s tray toast on Windows.',\n default: true,\n },\n notifyVr: {\n kind: 'boolean',\n label: 'VR overlay notification',\n description: 'Through the native companion\u2019s VR target on any platform, or the SteamVR wrist overlay on Windows.',\n default: true,\n },\n notifyDiscord: {\n kind: 'boolean',\n label: 'Discord webhook',\n default: false,\n },\n discordWebhookUrl: {\n kind: 'string',\n label: 'Discord webhook URL',\n description: 'Server Settings \u2192 Integrations \u2192 Webhooks. Sent straight from this page; the URL is stored locally only.',\n default: '',\n placeholder: 'https://discord.com/api/webhooks/\u2026',\n },\n memory: {\n kind: 'string',\n label: 'Join memory',\n description: 'Internal: who has joined a matching instance before.',\n default: '{}',\n hidden: true,\n },\n} as const satisfies SettingsSchema;\n\nexport type Settings = typeof settings;\n", "/**\n * Club Security.\n *\n * Watches VRChat's game log for players joining your instance. When the instance passes the\n * configured filters (type, group, world), it gathers what VRCNext knows about the joiner and\n * sends one report per join to the enabled channels.\n */\n\nimport { definePlugin, type PluginContext, type PluginId } from '@vrcnext/plugin-api';\n\nimport { FactCollector, type Joiner } from './collector.js';\nimport { filterFrom, instanceMatches } from './filters.js';\nimport { JoinMemory } from './memory.js';\nimport { notifyAll, type Report } from './notify.js';\nimport { ReportPanel } from './panel.js';\nimport { settings } from './settings.js';\nimport { toSelfId } from './vrcnext-data.js';\n\ntype Ctx = PluginContext;\n\n/** VRCNext's parsed game-log kind for `[Behaviour] OnPlayerJoined`. */\nconst JOIN_EVENT = 'gl_player_join';\n\n/** Reads the signed-in account from VRCNext's page global, until the `vrcUser` event says otherwise. */\nfunction selfIdFromPage(): string {\n const user: unknown = (globalThis as { currentVrcUser?: unknown }).currentVrcUser;\n const id = typeof user === 'object' && user !== null ? (user as { id?: unknown }).id : undefined;\n return typeof id === 'string' ? id : '';\n}\n\nclass ClubSecurity {\n readonly #ctx: Ctx;\n readonly #collector: FactCollector;\n readonly #memory: JoinMemory;\n readonly #panel: ReportPanel;\n #selfId = selfIdFromPage();\n #selfName = '';\n /** Joiners currently being looked up, so a duplicate log line does not produce two reports. */\n readonly #inFlight = new Set();\n\n constructor(ctx: Ctx) {\n this.#ctx = ctx;\n this.#collector = new FactCollector(ctx);\n this.#memory = new JoinMemory(ctx.settings);\n this.#panel = new ReportPanel(ctx, {\n memory: this.#memory,\n currentInstance: () => this.#collector.instance,\n sendTest: () => this.#sendTest(),\n });\n }\n\n start(): void {\n this.#ctx.events.on('vrcUser', (payload) => {\n const id = toSelfId(payload);\n if (id !== '') this.#selfId = id;\n const name = (payload as { displayName?: unknown }).displayName;\n if (typeof name === 'string') this.#selfName = name;\n });\n this.#ctx.events.on('vrcCurrentInstance', () => { this.#panel.refresh(); });\n this.#ctx.gameLog.onType(JOIN_EVENT, (entry) => {\n void this.#onJoin({ name: entry.message, userId: entry.detail });\n });\n this.#panel.install();\n // Ask for the instance we are already in, so the first join after enabling is not missed.\n this.#ctx.bridge.send('vrcGetCurrentInstance');\n this.#ctx.logger.info(`Club Security v${this.#ctx.version} watching for joins.`);\n }\n\n async #onJoin(joiner: Joiner): Promise {\n const values = this.#ctx.settings.values;\n if (!values.enabled) return;\n if (joiner.name === '' || this.#isSelf(joiner)) return;\n const instance = this.#collector.instance;\n if (instance === undefined) {\n this.#ctx.logger.debug(`${joiner.name} joined but the current instance is not known yet.`);\n return;\n }\n const filter = filterFrom(values);\n if (!instanceMatches(filter, instance)) return;\n\n const key = joiner.userId === '' ? `name:${joiner.name}` : joiner.userId;\n if (this.#inFlight.has(key)) return;\n this.#inFlight.add(key);\n try {\n const previous = await this.#memory.record(key, joiner.name, instance.location);\n const facts = await this.#collector.collect(joiner, filter.groupId, values.collectTimeoutSecs * 1000);\n const report: Report = { at: Date.now(), joiner, instance, facts, groupFilter: filter.groupId, previous };\n this.#panel.push(report);\n this.#ctx.logger.info(`Reported ${joiner.name}: 18+ ${String(facts.ageVerified ?? '?')}, PC ${facts.avatar?.pc ?? '?'}, Quest ${facts.avatar?.quest ?? '?'}.`);\n await notifyAll(this.#ctx, report);\n } catch (error) {\n this.#ctx.logger.error(`Report for ${joiner.name} failed: ${String(error)}`);\n } finally {\n this.#inFlight.delete(key);\n }\n }\n\n #isSelf(joiner: Joiner): boolean {\n if (joiner.userId !== '' && this.#selfId !== '') return joiner.userId === this.#selfId;\n return this.#selfName !== '' && joiner.name === this.#selfName;\n }\n\n /** A report built from whatever the current instance knows about you, to check the channels. */\n async #sendTest(): Promise {\n const instance = this.#collector.instance;\n if (instance === undefined) {\n this.#ctx.notifications.toast({ message: 'Join an instance first; the test uses it.', ok: false });\n return;\n }\n const me = instance.users.find((u) => u.id === this.#selfId) ?? instance.users[0];\n const name = me?.displayName ?? this.#selfName;\n const joiner: Joiner = { name: name === '' ? 'Test player' : name, userId: me?.id ?? '' };\n const filter = filterFrom(this.#ctx.settings.values);\n const facts = await this.#collector.collect(joiner, filter.groupId, 10_000);\n const report: Report = {\n at: Date.now(),\n joiner,\n instance,\n facts,\n groupFilter: filter.groupId,\n previous: this.#memory.previous(joiner.userId === '' ? `name:${joiner.name}` : joiner.userId),\n };\n await notifyAll(this.#ctx, report);\n this.#ctx.notifications.toast({ message: 'Test report sent to every enabled channel.' });\n }\n}\n\nexport default definePlugin({\n id: 'club-security' as PluginId,\n settings,\n\n activate(ctx) {\n new ClubSecurity(ctx).start();\n },\n\n deactivate() {\n // Every listener and panel went through `ctx`, so the host tears them down.\n },\n});\n"], - "mappings": "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", - "names": ["rec", "value", "str", "strings", "v", "groupIdOf", "location", "toInstanceUser", "r", "id", "displayName", "toCurrentInstance", "payload", "rawUsers", "u", "user", "profileLoaded", "toUserGroups", "userId", "groups", "g", "toInstanceAvatar", "toAvatarPerformance", "avatarId", "toSelfId", "waitFor", "events", "type", "pick", "options", "resolve", "done", "finish", "value", "timer", "unsubscribe", "onAbort", "payload", "findUser", "instance", "joiner", "u", "avatarModalOpen", "modal", "FactCollector", "#ctx", "#latest", "#perfCache", "ctx", "toCurrentInstance", "perf", "toAvatarPerformance", "groupId", "deadlineMs", "signal", "profile", "inGroup", "avatar", "#profile", "#membership", "#avatar", "ms", "current", "profileLoaded", "user", "wanted", "pending", "groups", "toUserGroups", "groupIds", "id", "started", "remaining", "avatarId", "avatarName", "found", "toInstanceAvatar", "cached", "splitList", "value", "s", "filterFrom", "values", "typeMatches", "wanted", "actual", "type", "w", "instanceMatches", "filter", "instance", "groupId", "worldIds", "describeFilter", "parts", "parse", "raw", "value", "out", "key", "entry", "e", "trimmed", "map", "entries", "a", "b", "JoinMemory", "#store", "#map", "store", "name", "location", "before", "now", "DISCORD_WEBHOOK", "yesNo", "value", "unknown", "rank", "rejoinText", "previous", "when", "reportLines", "report", "facts", "age", "lines", "reportSummary", "bits", "accentFor", "nativeSinks", "ctx", "wantDesktop", "wantVr", "t", "sendDesktopAndVr", "values", "title", "sinks", "result", "failure", "sendDiscord", "url", "colour", "body", "response", "notifyAll", "results", "MAX_SHOWN", "ReportPanel", "#ctx", "#deps", "#reports", "#status", "#list", "ctx", "deps", "tab", "#render", "card", "report", "k", "#statusRows", "#reportRows", "actions", "#clearMemory", "values", "filter", "filterFrom", "instance", "matches", "instanceMatches", "channels", "c", "describeFilter", "lines", "reportLines", "time", "settings", "JOIN_EVENT", "selfIdFromPage", "user", "id", "ClubSecurity", "#ctx", "#collector", "#memory", "#panel", "#selfId", "#selfName", "#inFlight", "ctx", "FactCollector", "JoinMemory", "ReportPanel", "#sendTest", "payload", "toSelfId", "name", "entry", "#onJoin", "joiner", "values", "#isSelf", "instance", "filter", "filterFrom", "instanceMatches", "key", "previous", "facts", "report", "notifyAll", "error", "me", "index_default", "settings"] + "sourcesContent": ["/**\n * Narrowers for the VRCNext events this plugin consumes.\n *\n * None of these events are in the API's verified map, so every payload arrives as `unknown`.\n * Shapes were read out of VRCNext 2026.61.2: `InstanceController.PushCurrentInstanceFromCache`,\n * `GroupsController` (`vrcGetGroupsForNetwork`), `FriendsController` (`vrcGetInstanceAvatars`)\n * and `MessageRouter` (`vrcGetAvatarDetail`).\n */\n\nexport interface InstanceUser {\n readonly id: string;\n readonly displayName: string;\n readonly ageVerified: boolean;\n /** VRChat's own status string, e.g. `18+`, `verified`, `hidden`. Empty until the profile loaded. */\n readonly ageVerificationStatus: string;\n readonly platform: string;\n /** Present once VRCNext fetched the profile; an empty list means it has not yet. */\n readonly tags: readonly string[];\n readonly lastLogin: string;\n readonly avatarId: string;\n readonly avatarName: string;\n}\n\nexport interface CurrentInstance {\n readonly location: string;\n readonly worldId: string;\n readonly worldName: string;\n readonly instanceType: string;\n /** Parsed from `~group(grp_\u2026)` in the location; empty for non-group instances. */\n readonly groupId: string;\n readonly users: readonly InstanceUser[];\n}\n\nfunction rec(value: unknown): Record | undefined {\n return typeof value === 'object' && value !== null ? (value as Record) : undefined;\n}\n\nfunction str(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction strings(value: unknown): readonly string[] {\n return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : [];\n}\n\nexport function groupIdOf(location: string): string {\n const match = /~group\\((grp_[0-9a-f-]+)\\)/i.exec(location);\n return match?.[1] ?? '';\n}\n\nfunction toInstanceUser(value: unknown): InstanceUser | undefined {\n const r = rec(value);\n if (r === undefined) return undefined;\n const id = str(r['id']);\n const displayName = str(r['displayName']);\n if (id === '' && displayName === '') return undefined;\n return {\n id,\n displayName,\n ageVerified: r['ageVerified'] === true,\n ageVerificationStatus: str(r['ageVerificationStatus']),\n platform: str(r['platform']),\n tags: strings(r['tags']),\n lastLogin: str(r['lastLogin']),\n avatarId: str(r['avatarId']),\n avatarName: str(r['avatarName']),\n };\n}\n\n/** `undefined` for the `{ empty: true }` and `{ error }` variants. */\nexport function toCurrentInstance(payload: unknown): CurrentInstance | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const location = str(r['location']);\n if (location === '') return undefined;\n const rawUsers = Array.isArray(r['users']) ? r['users'] : [];\n return {\n location,\n worldId: str(r['worldId']),\n worldName: str(r['worldName']),\n instanceType: str(r['instanceType']),\n groupId: groupIdOf(location),\n users: rawUsers.flatMap((u) => {\n const user = toInstanceUser(u);\n return user === undefined ? [] : [user];\n }),\n };\n}\n\n/** VRCNext only fills these once it has fetched the profile from the API. */\nexport function profileLoaded(user: InstanceUser): boolean {\n return user.ageVerificationStatus !== '' || user.tags.length > 0 || user.lastLogin !== '';\n}\n\nexport interface UserGroups {\n readonly userId: string;\n readonly groupIds: readonly string[];\n}\n\nexport function toUserGroups(payload: unknown): UserGroups | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const userId = str(r['userId']);\n if (userId === '') return undefined;\n const groups = Array.isArray(r['groups']) ? r['groups'] : [];\n return {\n userId,\n groupIds: groups.flatMap((g) => {\n const id = str(rec(g)?.['id']);\n return id === '' ? [] : [id];\n }),\n };\n}\n\nexport interface InstanceAvatar {\n readonly userId: string;\n readonly avatarId: string;\n readonly avatarName: string;\n}\n\nexport function toInstanceAvatar(payload: unknown): InstanceAvatar | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const userId = str(r['userId']);\n if (userId === '') return undefined;\n return { userId, avatarId: str(r['avatarId']), avatarName: str(r['avatarName']) };\n}\n\nexport interface AvatarPerformance {\n readonly avatarId: string;\n readonly name: string;\n readonly pc: string;\n readonly quest: string;\n}\n\nexport function toAvatarPerformance(payload: unknown): AvatarPerformance | undefined {\n const r = rec(payload);\n if (r === undefined) return undefined;\n const avatarId = str(r['id']);\n if (avatarId === '') return undefined;\n return {\n avatarId,\n name: str(r['name']),\n pc: str(r['pcPerf']),\n quest: str(r['questPerf']),\n };\n}\n\n/** The signed-in account, from the `vrcUser` event VRCNext sends after login. */\nexport function toSelfId(payload: unknown): string {\n return str(rec(payload)?.['id']);\n}\n", "/**\n * Gathers everything the report needs about one joiner, within a deadline.\n *\n * Each fact comes from a different VRCNext path, so they are collected in parallel and each one\n * degrades to \"unknown\" on its own timeout rather than holding up the report:\n *\n * - Age verification: VRCNext fetches the joiner's profile itself and re-pushes\n * `vrcCurrentInstance` once it has, so this waits for that push instead of making a request.\n * - Group membership: `vrcGetGroupsForNetwork` lists the groups the user shows publicly. A member\n * who hides that membership looks like a non-member; the report says \"visible\" for that reason.\n * - Avatar: `vrcGetInstanceAvatars` resolves the avatar id from the profile image through\n * VRCNext's avatar databases, then `vrcGetAvatarDetail` supplies the performance ranks. That\n * action also feeds VRCNext's avatar modal, so it is skipped while that modal is open.\n */\n\nimport type { EventBus, PluginContext } from '@vrcnext/plugin-api';\n\nimport type { Settings } from './settings.js';\nimport {\n profileLoaded,\n toAvatarPerformance,\n toCurrentInstance,\n toInstanceAvatar,\n toUserGroups,\n type CurrentInstance,\n type InstanceUser,\n} from './vrcnext-data.js';\n\nexport interface Joiner {\n readonly name: string;\n /** Empty for accounts VRChat still logs with a legacy id. */\n readonly userId: string;\n}\n\nexport interface AvatarFacts {\n readonly name: string;\n readonly pc: string;\n readonly quest: string;\n}\n\nexport interface Facts {\n readonly ageVerified: boolean | undefined;\n readonly ageStatus: string;\n readonly platform: string;\n readonly avatar: AvatarFacts | undefined;\n /** `undefined` when no group filter is configured. */\n readonly inGroup: boolean | undefined;\n}\n\ntype Ctx = PluginContext;\n\n/** Resolves with the first event `pick` accepts, or `undefined` on timeout or abort. */\nfunction waitFor(\n events: EventBus,\n type: string,\n pick: (payload: unknown) => T | undefined,\n options: { readonly ms: number; readonly signal: AbortSignal },\n): Promise {\n return new Promise((resolve) => {\n let done = false;\n const finish = (value: T | undefined): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n unsubscribe();\n options.signal.removeEventListener('abort', onAbort);\n resolve(value);\n };\n const onAbort = (): void => { finish(undefined); };\n const timer = setTimeout(() => { finish(undefined); }, options.ms);\n const unsubscribe = events.on(type, (payload) => {\n const value = pick(payload);\n if (value !== undefined) finish(value);\n });\n options.signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nfunction findUser(instance: CurrentInstance, joiner: Joiner): InstanceUser | undefined {\n return instance.users.find((u) =>\n joiner.userId !== '' ? u.id === joiner.userId : u.displayName === joiner.name,\n );\n}\n\nfunction avatarModalOpen(): boolean {\n const modal = document.getElementById('modalAvatarDetail');\n return modal !== null && modal.offsetParent !== null;\n}\n\nexport class FactCollector {\n readonly #ctx: Ctx;\n #latest: CurrentInstance | undefined;\n /** Performance ranks by avatar id, so a popular avatar is only fetched once. */\n readonly #perfCache = new Map();\n\n constructor(ctx: Ctx) {\n this.#ctx = ctx;\n ctx.events.on('vrcCurrentInstance', (payload) => {\n const instance = toCurrentInstance(payload);\n if (instance !== undefined) this.#latest = instance;\n });\n ctx.events.on('vrcAvatarDetail', (payload) => {\n const perf = toAvatarPerformance(payload);\n if (perf !== undefined) {\n this.#perfCache.set(perf.avatarId, { name: perf.name, pc: perf.pc, quest: perf.quest });\n }\n });\n }\n\n get instance(): CurrentInstance | undefined {\n return this.#latest;\n }\n\n /** Runs every lookup in parallel and returns whatever arrived before the deadline. */\n async collect(joiner: Joiner, groupId: string, deadlineMs: number): Promise {\n const signal = this.#ctx.signal;\n const [profile, inGroup, avatar] = await Promise.all([\n this.#profile(joiner, deadlineMs, signal),\n groupId === '' ? Promise.resolve(undefined) : this.#membership(joiner, groupId, deadlineMs, signal),\n this.#avatar(joiner, deadlineMs, signal),\n ]);\n return {\n ageVerified: profile === undefined ? undefined : profile.ageVerified,\n ageStatus: profile?.ageVerificationStatus ?? '',\n platform: profile?.platform ?? '',\n avatar,\n inGroup,\n };\n }\n\n async #profile(joiner: Joiner, ms: number, signal: AbortSignal): Promise {\n const current = this.#latest === undefined ? undefined : findUser(this.#latest, joiner);\n if (current !== undefined && profileLoaded(current)) return current;\n const loaded = await waitFor(\n this.#ctx.events,\n 'vrcCurrentInstance',\n (payload) => {\n const instance = toCurrentInstance(payload);\n const user = instance === undefined ? undefined : findUser(instance, joiner);\n return user !== undefined && profileLoaded(user) ? user : undefined;\n },\n { ms, signal },\n );\n // A profile that never loaded still tells us the platform VRChat logged.\n return loaded ?? current;\n }\n\n async #membership(joiner: Joiner, groupId: string, ms: number, signal: AbortSignal): Promise {\n if (joiner.userId === '') return undefined;\n const wanted = groupId.toLowerCase();\n const pending = waitFor(\n this.#ctx.events,\n 'vrcGroupsForNetwork',\n (payload) => {\n const groups = toUserGroups(payload);\n return groups?.userId === joiner.userId ? groups.groupIds : undefined;\n },\n { ms, signal },\n );\n this.#ctx.bridge.send('vrcGetGroupsForNetwork', { userId: joiner.userId });\n const groupIds = await pending;\n return groupIds === undefined ? undefined : groupIds.some((id) => id.toLowerCase() === wanted);\n }\n\n async #avatar(joiner: Joiner, ms: number, signal: AbortSignal): Promise {\n if (joiner.userId === '') return undefined;\n const started = Date.now();\n const remaining = (): number => Math.max(1_000, ms - (Date.now() - started));\n\n let avatarId = this.#latest === undefined ? '' : (findUser(this.#latest, joiner)?.avatarId ?? '');\n let avatarName = '';\n if (avatarId === '') {\n const pending = waitFor(\n this.#ctx.events,\n 'vrcInstanceAvatarFound',\n (payload) => {\n const found = toInstanceAvatar(payload);\n return found?.userId === joiner.userId ? found : undefined;\n },\n { ms: remaining(), signal },\n );\n this.#ctx.bridge.send('vrcGetInstanceAvatars', { userIds: [joiner.userId] });\n const found = await pending;\n if (found === undefined) return undefined;\n avatarId = found.avatarId;\n avatarName = found.avatarName;\n }\n if (avatarId === '') return avatarName === '' ? undefined : { name: avatarName, pc: '', quest: '' };\n\n const cached = this.#perfCache.get(avatarId);\n if (cached !== undefined) return cached;\n if (avatarModalOpen()) {\n this.#ctx.logger.debug('Avatar modal is open; not fetching performance ranks behind it.');\n return { name: avatarName, pc: '', quest: '' };\n }\n const pending = waitFor(\n this.#ctx.events,\n 'vrcAvatarDetail',\n (payload) => {\n const perf = toAvatarPerformance(payload);\n return perf?.avatarId === avatarId ? perf : undefined;\n },\n { ms: remaining(), signal },\n );\n this.#ctx.bridge.send('vrcGetAvatarDetail', { avatarId });\n const perf = await pending;\n return perf === undefined\n ? { name: avatarName, pc: '', quest: '' }\n : { name: perf.name === '' ? avatarName : perf.name, pc: perf.pc, quest: perf.quest };\n }\n}\n", "/**\n * Decides whether the current instance is one the user wants reports for.\n */\n\nimport type { SettingsValues } from '@vrcnext/plugin-api';\n\nimport type { CurrentInstance } from './vrcnext-data.js';\nimport type { Settings } from './settings.js';\n\nexport interface InstanceFilter {\n readonly instanceTypes: readonly string[];\n readonly groupId: string;\n readonly worldIds: readonly string[];\n}\n\nexport function splitList(value: string): readonly string[] {\n return value\n .split(/[,\\s]+/)\n .map((s) => s.trim().toLowerCase())\n .filter((s) => s.length > 0);\n}\n\nexport function filterFrom(values: SettingsValues): InstanceFilter {\n return {\n instanceTypes: splitList(values.instanceTypes),\n groupId: values.groupId.trim().toLowerCase(),\n worldIds: splitList(values.worldIds),\n };\n}\n\nfunction typeMatches(wanted: readonly string[], actual: string): boolean {\n if (wanted.length === 0) return true;\n const type = actual.toLowerCase();\n return wanted.some((w) => w === type || (w === 'group' && type.startsWith('group')));\n}\n\nexport function instanceMatches(filter: InstanceFilter, instance: CurrentInstance): boolean {\n if (!typeMatches(filter.instanceTypes, instance.instanceType)) return false;\n const groupId = filter.groupId.toLowerCase();\n if (groupId !== '' && instance.groupId.toLowerCase() !== groupId) return false;\n const worldIds = filter.worldIds.map((w) => w.toLowerCase());\n if (worldIds.length > 0 && !worldIds.includes(instance.worldId.toLowerCase())) return false;\n return true;\n}\n\n/** One line for the status card: what the filter currently restricts to. */\nexport function describeFilter(filter: InstanceFilter): string {\n const parts: string[] = [];\n if (filter.instanceTypes.length > 0) parts.push(`types: ${filter.instanceTypes.join(', ')}`);\n if (filter.groupId !== '') parts.push(`group: ${filter.groupId}`);\n if (filter.worldIds.length > 0) parts.push(`worlds: ${String(filter.worldIds.length)}`);\n return parts.length === 0 ? 'every instance' : parts.join(' \u00B7 ');\n}\n", "/**\n * Who has joined a matching instance before.\n *\n * Persisted in a hidden string setting as JSON, because the plugin API offers settings as its\n * only durable store. Capped so it cannot grow without bound; the oldest entries fall off.\n */\n\nimport type { SettingsStore } from '@vrcnext/plugin-api';\n\nimport type { Settings } from './settings.js';\n\nexport interface SeenEntry {\n readonly name: string;\n readonly joins: number;\n readonly firstSeen: number;\n readonly lastSeen: number;\n readonly lastLocation: string;\n}\n\nconst MAX_ENTRIES = 2000;\n\ntype SeenMap = Record;\n\nfunction parse(raw: string): SeenMap {\n try {\n const value: unknown = JSON.parse(raw);\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return {};\n const out: SeenMap = {};\n for (const [key, entry] of Object.entries(value as Record)) {\n if (typeof entry !== 'object' || entry === null) continue;\n const e = entry as Record;\n out[key] = {\n name: typeof e['name'] === 'string' ? e['name'] : '',\n joins: typeof e['joins'] === 'number' ? e['joins'] : 1,\n firstSeen: typeof e['firstSeen'] === 'number' ? e['firstSeen'] : 0,\n lastSeen: typeof e['lastSeen'] === 'number' ? e['lastSeen'] : 0,\n lastLocation: typeof e['lastLocation'] === 'string' ? e['lastLocation'] : '',\n };\n }\n return out;\n } catch {\n return {};\n }\n}\n\nfunction trimmed(map: SeenMap): SeenMap {\n const entries = Object.entries(map);\n if (entries.length <= MAX_ENTRIES) return map;\n entries.sort((a, b) => b[1].lastSeen - a[1].lastSeen);\n return Object.fromEntries(entries.slice(0, MAX_ENTRIES));\n}\n\nexport class JoinMemory {\n readonly #store: SettingsStore;\n #map: SeenMap;\n\n constructor(store: SettingsStore) {\n this.#store = store;\n this.#map = parse(store.get('memory'));\n }\n\n get size(): number {\n return Object.keys(this.#map).length;\n }\n\n /** The entry as it was before this join, or `undefined` for a first-timer. */\n previous(key: string): SeenEntry | undefined {\n return this.#map[key];\n }\n\n /** Records a join and persists. Returns the entry as it was before. */\n async record(key: string, name: string, location: string): Promise {\n const before = this.#map[key];\n const now = Date.now();\n this.#map[key] = {\n name,\n joins: (before?.joins ?? 0) + 1,\n firstSeen: before?.firstSeen ?? now,\n lastSeen: now,\n lastLocation: location,\n };\n this.#map = trimmed(this.#map);\n await this.#store.set('memory', JSON.stringify(this.#map));\n return before;\n }\n\n async clear(): Promise {\n this.#map = {};\n await this.#store.set('memory', '{}');\n }\n}\n", "/**\n * Formats a report and fans it out to every enabled channel.\n *\n * Channels:\n * - In-app toast: always available.\n * - Desktop and VR: on any platform through the native companion, whose targets are addressed by\n * name; on Windows without the companion, VRCNext's own tray toast plus SteamVR wrist overlay,\n * which is a single action that reaches both.\n * - Discord: a webhook POST straight from the page. Discord answers webhook requests with CORS\n * headers, so no proxy is needed.\n */\n\nimport type { PluginContext, SettingsValues } from '@vrcnext/plugin-api';\n\nimport type { Facts, Joiner } from './collector.js';\nimport type { SeenEntry } from './memory.js';\nimport type { Settings } from './settings.js';\nimport type { CurrentInstance } from './vrcnext-data.js';\n\nexport interface Report {\n readonly at: number;\n readonly joiner: Joiner;\n readonly instance: CurrentInstance;\n readonly facts: Facts;\n /** Whether a group filter was active, so the \"In Group\" line is only shown when it means something. */\n readonly groupFilter: string;\n readonly previous: SeenEntry | undefined;\n}\n\ntype Ctx = PluginContext;\n\nconst DISCORD_WEBHOOK = /^https:\\/\\/(?:ptb\\.|canary\\.)?discord(?:app)?\\.com\\/api\\/webhooks\\/\\d+\\/[\\w-]+$/;\n\nfunction yesNo(value: boolean | undefined, unknown = 'Unknown'): string {\n if (value === undefined) return unknown;\n return value ? 'Yes' : 'No';\n}\n\nfunction rank(value: string): string {\n return value === '' ? 'Unknown' : value;\n}\n\nfunction rejoinText(previous: SeenEntry | undefined): string {\n if (previous === undefined) return 'No (first time)';\n const when = new Date(previous.lastSeen).toLocaleString();\n return `Yes (${String(previous.joins)}\u00D7 before, last ${when})`;\n}\n\n/** The report as the spec lays it out, one fact per line. */\nexport function reportLines(report: Report): readonly string[] {\n const { facts } = report;\n const age = facts.ageVerified === undefined\n ? 'Unknown'\n : `${yesNo(facts.ageVerified)}${facts.ageStatus === '' ? '' : ` (${facts.ageStatus})`}`;\n const lines = [\n `Player \"${report.joiner.name}\" joined`,\n `18+ Verified: ${age}`,\n `Avatar PC Performance Rank: ${rank(facts.avatar?.pc ?? '')}`,\n `Avatar Quest Performance Rank: ${rank(facts.avatar?.quest ?? '')}`,\n ];\n if (report.groupFilter !== '') lines.push(`In Group: ${yesNo(facts.inGroup, 'Unknown (only visible memberships count)')}`);\n lines.push(`Rejoin?: ${rejoinText(report.previous)}`);\n return lines;\n}\n\n/** Everything a single-line surface can hold. */\nexport function reportSummary(report: Report): string {\n const { facts } = report;\n const bits = [\n `18+: ${yesNo(facts.ageVerified, '?')}`,\n `PC ${rank(facts.avatar?.pc ?? '').replace('Unknown', '?')}`,\n `Quest ${rank(facts.avatar?.quest ?? '').replace('Unknown', '?')}`,\n ];\n if (report.groupFilter !== '') bits.push(`group: ${yesNo(facts.inGroup, '?')}`);\n bits.push(report.previous === undefined ? 'new' : 'rejoin');\n return `${report.joiner.name} joined \u00B7 ${bits.join(' \u00B7 ')}`;\n}\n\nfunction accentFor(report: Report): 'ok' | 'warn' | 'info' {\n const { facts } = report;\n if (facts.ageVerified === false || facts.avatar?.pc === 'VeryPoor') return 'warn';\n if (facts.ageVerified === true) return 'ok';\n return 'info';\n}\n\n/** Native companion targets: the VR overlay is the one whose name says so; the rest are desktop. */\nasync function nativeSinks(ctx: Ctx, wantDesktop: boolean, wantVr: boolean): Promise {\n const targets = await ctx.native.targets();\n return targets\n .filter((t) => {\n const vr = /wayvr|openvr|steamvr|overlay|xso/i.test(t.name);\n return vr ? wantVr : wantDesktop;\n })\n .map((t) => t.name);\n}\n\nasync function sendDesktopAndVr(ctx: Ctx, values: SettingsValues, report: Report): Promise {\n const wantDesktop = values.notifyDesktop;\n const wantVr = values.notifyVr;\n if (!wantDesktop && !wantVr) return;\n const title = `${report.joiner.name} joined`;\n const lines = reportLines(report).slice(1);\n\n if (await ctx.native.ready) {\n const sinks = await nativeSinks(ctx, wantDesktop, wantVr);\n if (sinks.length === 0) {\n ctx.logger.debug('Native companion has no target for the enabled channels.');\n } else {\n const result = await ctx.native.notify({\n title,\n content: lines.join('\\n'),\n timeoutSecs: 8,\n icon: 'security-high',\n sinks,\n urgency: accentFor(report) === 'warn' ? 'critical' : 'normal',\n });\n for (const failure of result.failed) ctx.logger.warn(`Native target ${failure.sink} failed: ${failure.error}`);\n return;\n }\n }\n\n // Windows without the companion: VRCNext's own tray toast and wrist overlay in one call.\n if (ctx.notifications.desktopAvailable) {\n ctx.notifications.desktop({\n title,\n subtitle: lines.join(' \u00B7 '),\n accent: accentFor(report),\n ...(report.joiner.userId === '' ? {} : { friendId: report.joiner.userId }),\n });\n return;\n }\n ctx.logger.info('No desktop or VR channel is reachable: the native companion is not running and this platform has no tray toast.');\n}\n\nasync function sendDiscord(ctx: Ctx, values: SettingsValues, report: Report): Promise {\n if (!values.notifyDiscord) return;\n const url = values.discordWebhookUrl.trim();\n if (!DISCORD_WEBHOOK.test(url)) {\n ctx.logger.warn('Discord webhook is enabled but the URL is not a discord.com webhook URL.');\n return;\n }\n const lines = reportLines(report);\n const colour = { ok: 0x3ba55d, warn: 0xed4245, info: 0x5865f2 }[accentFor(report)];\n const body = {\n username: 'Club Security',\n embeds: [{\n title: lines[0],\n description: lines.slice(1).join('\\n'),\n color: colour,\n footer: { text: `${report.instance.worldName} \u00B7 ${report.instance.instanceType}` },\n timestamp: new Date(report.at).toISOString(),\n }],\n allowed_mentions: { parse: [] },\n };\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n signal: ctx.signal,\n credentials: 'omit',\n referrerPolicy: 'no-referrer',\n });\n if (!response.ok) ctx.logger.warn(`Discord webhook answered ${String(response.status)}.`);\n}\n\n/** Sends on every enabled channel. Never throws: a failed channel is logged, not fatal. */\nexport async function notifyAll(ctx: Ctx, report: Report): Promise {\n const values = ctx.settings.values;\n if (values.notifyToast) ctx.notifications.toast({ message: reportSummary(report), ok: accentFor(report) !== 'warn' });\n const results = await Promise.allSettled([\n sendDesktopAndVr(ctx, values, report),\n sendDiscord(ctx, values, report),\n ]);\n for (const result of results) {\n if (result.status === 'rejected') ctx.logger.warn(`Notification channel failed: ${String(result.reason)}`);\n }\n}\n", "/**\n * The plugin's sidebar tab: status, the last reports, and two maintenance buttons.\n * Built from `ctx.ui.kit` so it is VRCNext's own markup and follows its theme.\n */\n\nimport type { PluginContext } from '@vrcnext/plugin-api';\n\nimport { describeFilter, filterFrom, instanceMatches } from './filters.js';\nimport type { JoinMemory } from './memory.js';\nimport { reportLines, type Report } from './notify.js';\nimport type { Settings } from './settings.js';\nimport type { CurrentInstance } from './vrcnext-data.js';\n\ntype Ctx = PluginContext;\n\nconst MAX_SHOWN = 25;\n\nexport interface PanelDeps {\n readonly memory: JoinMemory;\n readonly currentInstance: () => CurrentInstance | undefined;\n readonly sendTest: () => Promise;\n}\n\nexport class ReportPanel {\n readonly #ctx: Ctx;\n readonly #deps: PanelDeps;\n readonly #reports: Report[] = [];\n #status: HTMLElement | undefined;\n #list: HTMLElement | undefined;\n\n constructor(ctx: Ctx, deps: PanelDeps) {\n this.#ctx = ctx;\n this.#deps = deps;\n }\n\n install(): void {\n this.#ctx.ui.addNavTab({\n label: 'Club Security',\n icon: 'security',\n render: (tab) => { this.#render(tab); },\n });\n // The host renders every schema setting as a row on this card before `render` runs.\n this.#ctx.ui.addSettingsCard({\n title: 'Club Security',\n icon: 'security',\n render: (card) => {\n card.appendChild(this.#ctx.ui.kit.description(\n 'Reports go out for joins in instances that pass every filter above. Test them from the Club Security tab.',\n ));\n },\n });\n this.#ctx.settings.onChange(() => { this.refresh(); });\n }\n\n push(report: Report): void {\n this.#reports.unshift(report);\n if (this.#reports.length > MAX_SHOWN) this.#reports.length = MAX_SHOWN;\n this.refresh();\n }\n\n refresh(): void {\n const k = this.#ctx.ui.kit;\n if (this.#status !== undefined) k.setChildren(this.#status, this.#statusRows());\n if (this.#list !== undefined) k.setChildren(this.#list, this.#reportRows());\n }\n\n #render(tab: HTMLElement): void {\n const k = this.#ctx.ui.kit;\n this.#status = k.card({ title: 'Status', icon: 'shield' });\n this.#list = k.card({ title: 'Recent reports', icon: 'history' });\n const actions = k.card({\n title: 'Actions',\n icon: 'build',\n children: [\n k.description('A test report uses the current instance and your own name, and goes to every enabled channel.'),\n k.buttonRow(\n k.button({ label: 'Send test notification', icon: 'send', onClick: () => { void this.#deps.sendTest(); } }),\n k.button({\n label: 'Forget who joined before',\n icon: 'delete_sweep',\n onClick: () => { void this.#clearMemory(); },\n }),\n ),\n ],\n });\n tab.append(k.layout(k.pair(this.#status, actions), this.#list));\n this.refresh();\n }\n\n #statusRows(): readonly (HTMLElement | DocumentFragment)[] {\n const k = this.#ctx.ui.kit;\n const values = this.#ctx.settings.values;\n const filter = filterFrom(values);\n const instance = this.#deps.currentInstance();\n const matches = instance !== undefined && instanceMatches(filter, instance);\n const channels = [\n values.notifyToast && 'toast',\n values.notifyDesktop && 'desktop',\n values.notifyVr && 'VR',\n values.notifyDiscord && 'Discord',\n ].filter((c): c is string => typeof c === 'string');\n return [\n k.row({ label: 'Reporting', value: values.enabled ? k.badge('ok', 'On') : k.badge('neutral', 'Off') }),\n k.row({ label: 'Watching', detail: describeFilter(filter) }),\n k.row({\n label: 'Current instance',\n detail: instance === undefined ? 'Not in an instance' : `${instance.worldName} \u00B7 ${instance.instanceType}`,\n value: instance === undefined ? undefined : k.badge(matches ? 'ok' : 'neutral', matches ? 'Matches' : 'Filtered out'),\n }),\n k.row({ label: 'Channels', detail: channels.length === 0 ? 'none enabled' : channels.join(', ') }),\n k.row({ label: 'Players remembered', value: k.valueText(String(this.#deps.memory.size)) }),\n ];\n }\n\n #reportRows(): readonly (HTMLElement | DocumentFragment)[] {\n const k = this.#ctx.ui.kit;\n if (this.#reports.length === 0) return [k.emptyState('No joins reported yet.')];\n return this.#reports.map((report) => {\n const lines = reportLines(report);\n const time = new Date(report.at).toLocaleTimeString();\n return k.row({\n label: `${time} \u00B7 ${report.joiner.name}`,\n detail: lines.slice(1).join(' \u00B7 '),\n value: report.previous === undefined ? k.badge('accent', 'New') : k.badge('warn', 'Rejoin'),\n });\n });\n }\n\n async #clearMemory(): Promise {\n const ok = await this.#ctx.notifications.confirm({\n title: 'Forget join history',\n message: 'Every player will count as a first-time joiner again. Continue?',\n confirmLabel: 'Forget',\n icon: 'delete_sweep',\n });\n if (!ok) return;\n await this.#deps.memory.clear();\n this.#ctx.notifications.toast({ message: 'Join history cleared.' });\n this.refresh();\n }\n}\n", "/**\n * Settings schema. The host renders these as rows on the plugin's settings card and persists\n * them; the plugin reads them through `ctx.settings`.\n */\n\nimport type { SettingsSchema } from '@vrcnext/plugin-api';\n\n/** Instance types as VRCNext's `ParseLocation` names them, plus a shorthand for every group type. */\nexport const INSTANCE_TYPES = [\n 'public',\n 'friends+',\n 'friends',\n 'hidden',\n 'private',\n 'invite_plus',\n 'group',\n 'group-public',\n 'group-plus',\n 'group-members',\n] as const;\n\nexport const settings = {\n enabled: {\n kind: 'boolean',\n label: 'Report joins',\n description: 'Master switch. Filters below decide which instances count.',\n default: true,\n },\n instanceTypes: {\n kind: 'string',\n label: 'Only in these instance types',\n description:\n 'Comma-separated. Leave empty for every instance. Types: public, friends+, friends, hidden, ' +\n 'private, invite_plus, group-public, group-plus, group-members, or just \"group\" for all group instances.',\n default: '',\n placeholder: 'group-public, group-plus',\n },\n groupId: {\n kind: 'string',\n label: 'Only in instances of this group',\n description: 'A grp_\u2026 id. Leave empty for any group. When set, the report also says whether the joiner is a member.',\n default: '',\n placeholder: 'grp_00000000-0000-0000-0000-000000000000',\n },\n worldIds: {\n kind: 'string',\n label: 'Only in these worlds',\n description: 'Comma-separated wrld_\u2026 ids. Leave empty for any world.',\n default: '',\n placeholder: 'wrld_\u2026, wrld_\u2026',\n },\n settleSecs: {\n kind: 'number',\n label: 'Seconds to ignore after you join',\n description:\n 'When you enter an instance, VRChat logs a join for everyone already there. Joins inside this window are remembered but not reported.',\n default: 15,\n min: 3,\n max: 120,\n step: 1,\n },\n collectTimeoutSecs: {\n kind: 'number',\n label: 'Seconds to wait for details',\n description: 'How long to wait for VRCNext to load the profile and avatar before reporting what is known.',\n default: 25,\n min: 5,\n max: 120,\n step: 5,\n },\n notifyToast: {\n kind: 'boolean',\n label: 'In-app toast',\n default: true,\n },\n notifyDesktop: {\n kind: 'boolean',\n label: 'Desktop notification',\n description: 'Through the native companion on any platform, or VRCNext\u2019s tray toast on Windows.',\n default: true,\n },\n notifyVr: {\n kind: 'boolean',\n label: 'VR overlay notification',\n description: 'Through the native companion\u2019s VR target on any platform, or the SteamVR wrist overlay on Windows.',\n default: true,\n },\n notifyDiscord: {\n kind: 'boolean',\n label: 'Discord webhook',\n default: false,\n },\n discordWebhookUrl: {\n kind: 'string',\n label: 'Discord webhook URL',\n description: 'Server Settings \u2192 Integrations \u2192 Webhooks. Sent straight from this page; the URL is stored locally only.',\n default: '',\n placeholder: 'https://discord.com/api/webhooks/\u2026',\n },\n memory: {\n kind: 'string',\n label: 'Join memory',\n description: 'Internal: who has joined a matching instance before.',\n default: '{}',\n hidden: true,\n },\n} as const satisfies SettingsSchema;\n\nexport type Settings = typeof settings;\n", "/**\n * Club Security.\n *\n * Watches VRChat's game log for players joining your instance. When the instance passes the\n * configured filters (type, group, world), it gathers what VRCNext knows about the joiner and\n * sends one report per join to the enabled channels.\n */\n\nimport { definePlugin, type PluginContext, type PluginId } from '@vrcnext/plugin-api';\n\nimport { FactCollector, type Joiner } from './collector.js';\nimport { filterFrom, instanceMatches } from './filters.js';\nimport { JoinMemory } from './memory.js';\nimport { notifyAll, type Report } from './notify.js';\nimport { ReportPanel } from './panel.js';\nimport { settings } from './settings.js';\nimport { toSelfId } from './vrcnext-data.js';\n\ntype Ctx = PluginContext;\n\n/** VRCNext's parsed game-log kinds for `[Behaviour] OnPlayerJoined` and `Joining wrld_\u2026`. */\nconst JOIN_EVENT = 'gl_player_join';\nconst WORLD_JOIN_EVENT = 'gl_world_join';\n\n/**\n * The signed-in account, read live from VRCNext's own `currentVrcUser` binding.\n *\n * Read on every join rather than cached: it is `null` until login completes, and it changes when\n * the user switches accounts. VRChat logs the local player's own `OnPlayerJoined` too, so without\n * this every instance change would report the user to themselves.\n */\nfunction selfFromPage(): { readonly id: string; readonly name: string } {\n if (typeof currentVrcUser !== 'object' || currentVrcUser === null) return { id: '', name: '' };\n return {\n id: typeof currentVrcUser.id === 'string' ? currentVrcUser.id : '',\n name: typeof currentVrcUser.displayName === 'string' ? currentVrcUser.displayName : '',\n };\n}\n\nclass ClubSecurity {\n readonly #ctx: Ctx;\n readonly #collector: FactCollector;\n readonly #memory: JoinMemory;\n readonly #panel: ReportPanel;\n /** From the `vrcUser` login event; the page binding is consulted first. */\n #selfId = '';\n #selfName = '';\n /** Joiners currently being looked up, so a duplicate log line does not produce two reports. */\n readonly #inFlight = new Set();\n /**\n * Until when joins count as \"already here\". VRChat logs an `OnPlayerJoined` for every player\n * present when the local player arrives, in one burst right after the local player's own line.\n */\n #settledAt = 0;\n\n constructor(ctx: Ctx) {\n this.#ctx = ctx;\n this.#collector = new FactCollector(ctx);\n this.#memory = new JoinMemory(ctx.settings);\n this.#panel = new ReportPanel(ctx, {\n memory: this.#memory,\n currentInstance: () => this.#collector.instance,\n sendTest: () => this.#sendTest(),\n });\n }\n\n start(): void {\n this.#ctx.events.on('vrcUser', (payload) => {\n const id = toSelfId(payload);\n if (id !== '') this.#selfId = id;\n const name = (payload as { displayName?: unknown }).displayName;\n if (typeof name === 'string') this.#selfName = name;\n });\n this.#ctx.events.on('vrcCurrentInstance', () => { this.#panel.refresh(); });\n this.#ctx.gameLog.onType(WORLD_JOIN_EVENT, () => { this.#startSettling(); });\n this.#ctx.gameLog.onType(JOIN_EVENT, (entry) => {\n void this.#onJoin({ name: entry.message, userId: entry.detail });\n });\n this.#panel.install();\n // Ask for the instance we are already in, so the first join after enabling is not missed.\n this.#ctx.bridge.send('vrcGetCurrentInstance');\n this.#ctx.logger.info(`Club Security v${this.#ctx.version} watching for joins.`);\n }\n\n #startSettling(): void {\n this.#settledAt = Date.now() + this.#ctx.settings.get('settleSecs') * 1000;\n }\n\n async #onJoin(joiner: Joiner): Promise {\n const values = this.#ctx.settings.values;\n if (!values.enabled || joiner.name === '') return;\n if (this.#isSelf(joiner)) {\n this.#startSettling();\n return;\n }\n const instance = this.#collector.instance;\n if (instance === undefined) {\n this.#ctx.logger.debug(`${joiner.name} joined but the current instance is not known yet.`);\n return;\n }\n const filter = filterFrom(values);\n if (!instanceMatches(filter, instance)) return;\n\n const key = joiner.userId === '' ? `name:${joiner.name}` : joiner.userId;\n if (this.#inFlight.has(key)) return;\n this.#inFlight.add(key);\n try {\n const previous = await this.#memory.record(key, joiner.name, instance.location);\n if (Date.now() < this.#settledAt) {\n this.#ctx.logger.debug(`${joiner.name} was already here when you joined; remembered, not reported.`);\n this.#panel.refresh();\n return;\n }\n const facts = await this.#collector.collect(joiner, filter.groupId, values.collectTimeoutSecs * 1000);\n const report: Report = { at: Date.now(), joiner, instance, facts, groupFilter: filter.groupId, previous };\n this.#panel.push(report);\n this.#ctx.logger.info(`Reported ${joiner.name}: 18+ ${String(facts.ageVerified ?? '?')}, PC ${facts.avatar?.pc ?? '?'}, Quest ${facts.avatar?.quest ?? '?'}.`);\n await notifyAll(this.#ctx, report);\n } catch (error) {\n this.#ctx.logger.error(`Report for ${joiner.name} failed: ${String(error)}`);\n } finally {\n this.#inFlight.delete(key);\n }\n }\n\n #self(): { readonly id: string; readonly name: string } {\n const page = selfFromPage();\n return { id: page.id === '' ? this.#selfId : page.id, name: page.name === '' ? this.#selfName : page.name };\n }\n\n #isSelf(joiner: Joiner): boolean {\n const self = this.#self();\n if (joiner.userId !== '' && self.id !== '') return joiner.userId === self.id;\n return self.name !== '' && joiner.name === self.name;\n }\n\n /** A report built from whatever the current instance knows about you, to check the channels. */\n async #sendTest(): Promise {\n const instance = this.#collector.instance;\n if (instance === undefined) {\n this.#ctx.notifications.toast({ message: 'Join an instance first; the test uses it.', ok: false });\n return;\n }\n const self = this.#self();\n const me = instance.users.find((u) => u.id === self.id) ?? instance.users[0];\n const name = me?.displayName ?? self.name;\n const joiner: Joiner = { name: name === '' ? 'Test player' : name, userId: me?.id ?? '' };\n const filter = filterFrom(this.#ctx.settings.values);\n const facts = await this.#collector.collect(joiner, filter.groupId, 10_000);\n const report: Report = {\n at: Date.now(),\n joiner,\n instance,\n facts,\n groupFilter: filter.groupId,\n previous: this.#memory.previous(joiner.userId === '' ? `name:${joiner.name}` : joiner.userId),\n };\n await notifyAll(this.#ctx, report);\n this.#ctx.notifications.toast({ message: 'Test report sent to every enabled channel.' });\n }\n}\n\nexport default definePlugin({\n id: 'club-security' as PluginId,\n settings,\n\n activate(ctx) {\n new ClubSecurity(ctx).start();\n },\n\n deactivate() {\n // Every listener and panel went through `ctx`, so the host tears them down.\n },\n});\n"], + "mappings": "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", + "names": ["rec", "value", "str", "strings", "v", "groupIdOf", "location", "toInstanceUser", "r", "id", "displayName", "toCurrentInstance", "payload", "rawUsers", "u", "user", "profileLoaded", "toUserGroups", "userId", "groups", "g", "toInstanceAvatar", "toAvatarPerformance", "avatarId", "toSelfId", "waitFor", "events", "type", "pick", "options", "resolve", "done", "finish", "value", "timer", "unsubscribe", "onAbort", "payload", "findUser", "instance", "joiner", "u", "avatarModalOpen", "modal", "FactCollector", "#ctx", "#latest", "#perfCache", "ctx", "toCurrentInstance", "perf", "toAvatarPerformance", "groupId", "deadlineMs", "signal", "profile", "inGroup", "avatar", "#profile", "#membership", "#avatar", "ms", "current", "profileLoaded", "user", "wanted", "pending", "groups", "toUserGroups", "groupIds", "id", "started", "remaining", "avatarId", "avatarName", "found", "toInstanceAvatar", "cached", "splitList", "value", "s", "filterFrom", "values", "typeMatches", "wanted", "actual", "type", "w", "instanceMatches", "filter", "instance", "groupId", "worldIds", "describeFilter", "parts", "parse", "raw", "value", "out", "key", "entry", "e", "trimmed", "map", "entries", "a", "b", "JoinMemory", "#store", "#map", "store", "name", "location", "before", "now", "DISCORD_WEBHOOK", "yesNo", "value", "unknown", "rank", "rejoinText", "previous", "when", "reportLines", "report", "facts", "age", "lines", "reportSummary", "bits", "accentFor", "nativeSinks", "ctx", "wantDesktop", "wantVr", "t", "sendDesktopAndVr", "values", "title", "sinks", "result", "failure", "sendDiscord", "url", "colour", "body", "response", "notifyAll", "results", "MAX_SHOWN", "ReportPanel", "#ctx", "#deps", "#reports", "#status", "#list", "ctx", "deps", "tab", "#render", "card", "report", "k", "#statusRows", "#reportRows", "actions", "#clearMemory", "values", "filter", "filterFrom", "instance", "matches", "instanceMatches", "channels", "c", "describeFilter", "lines", "reportLines", "time", "settings", "JOIN_EVENT", "WORLD_JOIN_EVENT", "selfFromPage", "ClubSecurity", "#ctx", "#collector", "#memory", "#panel", "#selfId", "#selfName", "#inFlight", "#settledAt", "ctx", "FactCollector", "JoinMemory", "ReportPanel", "#sendTest", "payload", "id", "toSelfId", "name", "#startSettling", "entry", "#onJoin", "joiner", "values", "#isSelf", "instance", "filter", "filterFrom", "instanceMatches", "key", "previous", "facts", "report", "notifyAll", "error", "#self", "page", "self", "me", "u", "index_default", "settings"] }