5 lines
16 KiB
JavaScript
5 lines
16 KiB
JavaScript
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()<this.#a){this.#e.logger.debug(`${e.name} was already here when you joined; remembered, not reported.`),this.#r.refresh();return}let o=await this.#n.collect(e,i.groupId,n.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 T(this.#e,u)}catch(a){this.#e.logger.error(`Report for ${e.name} failed: ${String(a)}`)}finally{this.#o.delete(s)}}}#u(){let e=re();return{id:e.id===""?this.#i:e.id,name:e.name===""?this.#s:e.name}}#l(e){let n=this.#u();return e.userId!==""&&n.id!==""?e.userId===n.id:n.name!==""&&e.name===n.name}async#f(){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 n=this.#u(),r=e.users.find(c=>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
|