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}; //# sourceMappingURL=club-security.js.map