From 23038977873715aaf63bc11f583f6a2e75a46375 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 21:53:08 +0200 Subject: [PATCH] Add club-security; rebuild kitchen-sink with the correct join event type --- dist/club-security.js | 4 ++++ dist/club-security.js.map | 7 +++++++ dist/kitchen-sink.js | 2 +- dist/kitchen-sink.js.map | 4 ++-- vrcnext-plugins.json | 30 +++++++++++++++++++++++++++++- 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 dist/club-security.js create mode 100644 dist/club-security.js.map diff --git a/dist/club-security.js b/dist/club-security.js new file mode 100644 index 0000000..70ee59e --- /dev/null +++ b/dist/club-security.js @@ -0,0 +1,4 @@ +function l(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=l(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=l(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 o=q(i);return o===void 0?[]:[o]})}}function A(n){return n.ageVerificationStatus!==""||n.tags.length>0||n.lastLogin!==""}function L(n){let e=l(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 o=d(l(i)?.id);return o===""?[]:[o]})}}function $(n){let e=l(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 _(n){let e=l(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 F(n){return d(l(n)?.id)}function y(n,e,t,r){return new Promise(i=>{let o=!1,a=f=>{o||(o=!0,clearTimeout(u),c(),r.signal.removeEventListener("abort",s),i(f))},s=()=>{a(void 0)},u=setTimeout(()=>{a(void 0)},r.ms),c=n.on(e,f=>{let p=t(f);p!==void 0&&a(p)});r.signal.addEventListener("abort",s,{once:!0})})}function C(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=_(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,[o,a,s]=await Promise.all([this.#r(e,r,i),t===""?Promise.resolve(void 0):this.#i(e,t,r,i),this.#o(e,r,i)]);return{ageVerified:o===void 0?void 0:o.ageVerified,ageStatus:o?.ageVerificationStatus??"",platform:o?.platform??"",avatar:s,inGroup:a}}async#r(e,t,r){let i=this.#n===void 0?void 0:C(this.#n,e);return i!==void 0&&A(i)?i:await y(this.#e.events,"vrcCurrentInstance",a=>{let s=N(a),u=s===void 0?void 0:C(s,e);return u!==void 0&&A(u)?u:void 0},{ms:t,signal:r})??i}async#i(e,t,r,i){if(e.userId==="")return;let o=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 s=await a;return s===void 0?void 0:s.some(u=>u.toLowerCase()===o)}async#o(e,t,r){if(e.userId==="")return;let i=Date.now(),o=()=>Math.max(1e3,t-(Date.now()-i)),a=this.#n===void 0?"":C(this.#n,e)?.avatarId??"",s="";if(a===""){let p=y(this.#e.events,"vrcInstanceAvatarFound",O=>{let E=$(O);return E?.userId===e.userId?E:void 0},{ms:o(),signal:r});this.#e.bridge.send("vrcGetInstanceAvatars",{userIds:[e.userId]});let g=await p;if(g===void 0)return;a=g.avatarId,s=g.avatarName}if(a==="")return s===""?void 0:{name:s,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:s,pc:"",quest:""};let c=y(this.#e.events,"vrcAvatarDetail",p=>{let g=_(p);return g?.avatarId===a?g:void 0},{ms:o(),signal:r});this.#e.bridge.send("vrcGetAvatarDetail",{avatarId:a});let f=await c;return f===void 0?{name:s,pc:"",quest:""}:{name:f.name===""?s:f.name,pc:f.pc,quest:f.quest}}};function R(n){return n.split(/[,\s]+/).map(e=>e.trim().toLowerCase()).filter(e=>e.length>0)}function m(n){return{instanceTypes:R(n.instanceTypes),groupId:n.groupId.trim().toLowerCase(),worldIds:R(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 o=i;t[r]={name:typeof o.name=="string"?o.name:"",joins:typeof o.joins=="number"?o.joins:1,firstSeen:typeof o.firstSeen=="number"?o.firstSeen:0,lastSeen:typeof o.lastSeen=="number"?o.lastSeen:0,lastLocation:typeof o.lastLocation=="string"?o.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 w=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],o=Date.now();return this.#n[e]={name:t,joins:(i?.joins??0)+1,firstSeen:i?.firstSeen??o,lastSeen:o,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 b(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":`${b(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: ${b(e.inGroup,"Unknown (only visible memberships count)")}`),r.push(`Rejoin?: ${z(n.previous)}`),r}function B(n){let{facts:e}=n,t=[`18+: ${b(e.ageVerified,"?")}`,`PC ${S(e.avatar?.pc??"").replace("Unknown","?")}`,`Quest ${S(e.avatar?.quest??"").replace("Unknown","?")}`];return n.groupFilter!==""&&t.push(`group: ${b(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 o=`${t.joiner.name} joined`,a=k(t).slice(1);if(await n.native.ready){let s=await Q(n,r,i);if(s.length===0)n.logger.debug("Native companion has no target for the enabled channels.");else{let u=await n.native.notify({title:o,content:a.join(` +`),timeoutSecs:8,icon:"security-high",sinks:s,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:o,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),o={ok:3908957,warn:15548997,info:5793266}[v(t)],a={username:"Club Security",embeds:[{title:i[0],description:i.slice(1).join(` +`),color:o,footer:{text:`${t.instance.worldName} \xB7 ${t.instance.instanceType}`},timestamp:new Date(t.at).toISOString()}],allowed_mentions:{parse:[]}},s=await fetch(r,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(a),signal:n.signal,credentials:"omit",referrerPolicy:"no-referrer"});s.ok||n.logger.warn(`Discord webhook answered ${String(s.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.#o(e)}}),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.#s()),this.#i!==void 0&&e.setChildren(this.#i,this.#a())}#o(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()}#s(){let e=this.#e.ui.kit,t=this.#e.settings.values,r=m(t),i=this.#n.currentInstance(),o=i!==void 0&&I(r,i),a=[t.notifyToast&&"toast",t.notifyDesktop&&"desktop",t.notifyVr&&"VR",t.notifyDiscord&&"Discord"].filter(s=>typeof s=="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(o?"ok":"neutral",o?"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();#o="";#s=new Set;constructor(e){this.#e=e,this.#n=new h(e),this.#t=new w(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=F(e);t!==""&&(this.#i=t);let r=e.displayName;typeof r=="string"&&(this.#o=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 o=e.userId===""?`name:${e.name}`:e.userId;if(!this.#s.has(o)){this.#s.add(o);try{let a=await this.#t.record(o,e.name,r.location),s=await this.#n.collect(e,i.groupId,t.collectTimeoutSecs*1e3),u={at:Date.now(),joiner:e,instance:r,facts:s,groupFilter:i.groupId,previous:a};this.#r.push(u),this.#e.logger.info(`Reported ${e.name}: 18+ ${String(s.ageVerified??"?")}, PC ${s.avatar?.pc??"?"}, Quest ${s.avatar?.quest??"?"}.`),await P(this.#e,u)}catch(a){this.#e.logger.error(`Report for ${e.name} failed: ${String(a)}`)}finally{this.#s.delete(o)}}}#d(e){return e.userId!==""&&this.#i!==""?e.userId===this.#i:this.#o!==""&&e.name===this.#o}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.#o,i={name:r===""?"Test player":r,userId:t?.id??""},o=m(this.#e.settings.values),a=await this.#n.collect(i,o.groupId,1e4),s={at:Date.now(),joiner:i,instance:e,facts:a,groupFilter:o.groupId,previous:this.#t.previous(i.userId===""?`name:${i.name}`:i.userId)};await P(this.#e,s),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 diff --git a/dist/club-security.js.map b/dist/club-security.js.map new file mode 100644 index 0000000..2ab1e0c --- /dev/null +++ b/dist/club-security.js.map @@ -0,0 +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 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", "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"] +} diff --git a/dist/kitchen-sink.js b/dist/kitchen-sink.js index 69f1fde..8005a23 100644 --- a/dist/kitchen-sink.js +++ b/dist/kitchen-sink.js @@ -37,5 +37,5 @@ var a=` .ks-log-line { white-space: pre-wrap; word-break: break-word; } .ks-log-line + .ks-log-line { margin-top: 3px; } -`;function d(e){let t=document.createElement("div");return t.className="ks-log",e.appendChild(t),{append(n){let i=document.createElement("div");i.className="ks-log-line";let o=new Date().toLocaleTimeString();for(i.textContent=`${o} ${n}`,t.appendChild(i);t.childElementCount>200;)t.firstElementChild?.remove();t.scrollHeight-t.scrollTop-t.clientHeight<40&&(t.scrollTop=t.scrollHeight)},clear(){t.replaceChildren()}}}var u={watchGameLog:{kind:"boolean",label:"Follow the VRChat game log",description:"Streams player joins and world changes into the panel below.",default:!0},oscParameter:{kind:"string",label:"OSC parameter to pulse",description:"Sent to /avatar/parameters/ by the dashboard button.",default:"VRCEmote",placeholder:"VRCEmote"},oscValue:{kind:"number",label:"OSC value",default:1,min:0,max:255,step:1},verbosity:{kind:"select",label:"Log verbosity",default:"normal",options:[{value:"quiet",label:"Quiet"},{value:"normal",label:"Normal"},{value:"loud",label:"Everything"}]}},pe={id:"kitchen-sink",settings:u,activate(e){let t={gameLogCount:0,oscParamCount:0,panel:void 0,log:n=>{e.settings.get("verbosity")!=="quiet"&&(e.logger.info(n),t.panel?.append(n))}};e.ui.injectCss(a),p(e),h(e,t),v(e,t),b(e,t),y(e,t),S(e,t),C(e,t),w(e,t),k(e,t),N(e,t),e.logger.info(`Kitchen Sink v${e.version} ready.`)},deactivate(){}};function p(e){e.disposables.add(e.settings.onChange(t=>{e.logger.debug(`Settings changed; verbosity is now ${t.verbosity}.`)}))}function h(e,t){e.events.on("friendTimelineEvent",n=>{t.log(`[friend] ${n.friendName} \u2192 ${n.type}`)}),e.events.on("vrcCurrentInstance",n=>{if(typeof n!="object"||n===null)return;let i=n.worldName;typeof i=="string"&&t.log(`[instance] now in ${i}`)}),e.events.onAny(n=>{e.settings.get("verbosity")==="loud"&&e.logger.debug(`event ${n.type}`)})}function v(e,t){e.gameLog.on(n=>{e.settings.get("watchGameLog")&&(t.gameLogCount+=1,t.log(`[gamelog] ${n.type}: ${n.message}`))}),e.gameLog.onType("OnPlayerJoined",n=>{t.log(`[join] ${n.detail||n.message}`)}),e.gameLog.history(e.signal).then(n=>{e.logger.info(`Game log backlog: ${String(n.length)} entries.`)}).catch(n=>{e.signal.aborted||e.logger.warn(`Could not read the game log: ${String(n)}`)})}function b(e,t){if(!e.osc.available){t.log("[osc] OSC is Windows-only in VRCNext; skipping.");return}e.osc.connect(),e.osc.onParam(n=>{t.oscParamCount+=1,e.settings.get("verbosity")==="loud"&&t.log(`[osc] ${n.name} = ${String(n.value)}`)}),e.osc.onAvatarChange(n=>{t.log(`[osc] avatar ${n.avatarId} with ${String(n.parameters.length)} parameters`)})}function c(e){if(!e.osc.available)return"OSC unavailable (Windows only)";let t=e.settings.get("oscParameter"),n=e.settings.get("oscValue");return e.osc.send(t,"int",n),`${t} = ${String(n)}`}function y(e,t){e.deepLinks.onPrefix("wrld",n=>{t.log(`[link] world ${n.id}`)}),e.deepLinks.on(n=>{t.log(`[link] ${n.prefix}:${n.id}${n.action?` (${n.action})`:""}`)})}function S(e,t){e.router.get("stats",()=>Response.json({gameLogEvents:t.gameLogCount,oscParams:t.oscParamCount,verbosity:e.settings.get("verbosity")})),e.router.get("greet/:name",n=>Response.json({hello:n.params.name??"world"})),e.router.post("osc/pulse",()=>Response.json({sent:c(e)})),e.logger.info(`Routes mounted at ${e.router.base.pathname}`)}function C(e,t){e.contextMenu.contribute(n=>{let i=[{kind:"divider"},{kind:"item",icon:"auto_awesome",label:"Kitchen Sink: log this element",onSelect:()=>{t.log(`[ctx] ${n.element.tagName.toLowerCase()}`)}},{kind:"submenu",icon:"tune",label:"Kitchen Sink: verbosity",items:()=>u.verbosity.options.map(o=>({kind:"item",icon:"adjust",label:o.label,checked:e.settings.get("verbosity")===o.value,onSelect:()=>{e.settings.set("verbosity",o.value)}}))}];if(n.entity!==void 0){let o=n.entity;i.push({kind:"item",icon:"content_copy",label:`Copy ${o.type} id`,onSelect:()=>{navigator.clipboard.writeText(o.id)}})}return i})}function w(e,t){e.gameLog.onType("OnPlayerJoined",n=>{let i=n.detail||n.message;e.notifications.toast({message:`${i} joined.`}),e.notifications.desktopAvailable?e.notifications.desktop({title:"Player joined",subtitle:i,accent:"info"}):t.log("[notify] desktop/VR notifications are Windows-only.")}),e.events.on("friendTimelineEvent",n=>{n.type==="online"&&e.notifications.notifToast({kind:"notification",sender:n.friendName,message:"came online"})})}async function k(e,t){if(!await e.native.ready){t.log(`[native] no companion at ${e.native.endpoint}; VR targets unavailable.`);return}let n=await e.native.targets();t.log(`[native] targets: ${n.map(o=>o.name).join(", ")||"none"}`),e.gameLog.onType("OnPlayerJoined",o=>{let s=o.detail||o.message;e.native.notify({title:"Player joined",content:s,timeoutSecs:4,icon:"user-available",overrides:{wayvr:{content:`${s} joined the instance`,height:200,opacity:.85,alwaysShow:!0}}})}),e.events.on("friendTimelineEvent",o=>{o.type==="online"&&e.native.notify({title:o.friendName,content:"came online",sinks:["wayvr"],timeoutSecs:3,opacity:.7})});let i=await e.native.describe();i!==void 0&&t.log(`[native] companion ${i.version}, services: ${Object.keys(i.services).join(", ")}`)}function N(e,t){e.ui.addDashboardCard({title:"Kitchen Sink",icon:"auto_awesome",order:10,render:n=>{let i=document.createElement("div");i.className="ks-grid",i.append(l("Game log events",()=>t.gameLogCount),l("OSC parameters",()=>t.oscParamCount)),n.appendChild(i);let o=document.createElement("button");o.textContent="Send OSC pulse",o.addEventListener("click",()=>{e.ui.toast({message:`Sent ${c(e)}.`})}),n.appendChild(o)}}),L(e,t),e.ui.addSettingsCard({title:"Kitchen Sink",icon:"auto_awesome",render:n=>{n.appendChild(e.ui.createToggleRow("Follow the VRChat game log",e.settings.get("watchGameLog"),o=>{e.settings.set("watchGameLog",o)}));let i=document.createElement("button");i.textContent="Reset settings",i.addEventListener("click",()=>{e.notifications.confirm({title:"Reset Kitchen Sink",message:"Restore every Kitchen Sink setting to its default?",confirmLabel:"Reset",icon:"restart_alt"}).then(async o=>{o&&(await e.settings.reset(),e.notifications.toast({message:"Kitchen Sink settings reset."}))})}),n.appendChild(i)}})}function L(e,t){e.ui.addNavTab({label:"Kitchen Sink",icon:"auto_awesome",render:n=>{let i=e.ui.kit,o=i.card({title:"Live activity",icon:"radar"});t.panel=d(o);let s=i.emptyState("No request yet.");n.append(i.layout(i.grid([i.card({title:"Counters",icon:"bolt",children:[i.stat({label:"Game log events",value:String(t.gameLogCount)}),i.stat({label:"OSC parameters",value:String(t.oscParamCount)})]}),i.card({title:"Capabilities",icon:"tune",children:[i.row({label:"OSC",value:e.osc.available?i.badge("ok","Ready"):i.badge("warn","Windows only"),detail:"Sent through VRCNext\u2019s own sockets."}),i.row({label:"Native companion",value:e.native.available?i.badge("ok","Connected"):i.badge("neutral","Not running"),detail:e.native.endpoint}),i.toggleRow({label:"Follow the game log",detail:"Mirrors VRChat log lines into the panel below.",value:e.settings.get("watchGameLog"),onChange:r=>{e.settings.set("watchGameLog",r)}})]}),i.card({title:"In-page routes",icon:"account_tree",children:[i.description("Plugin routes are reachable from this page only, never from curl."),i.buttonRow(i.button({label:"GET stats",icon:"download",onClick:()=>{e.router.fetch("stats").then(async r=>{s.textContent=await r.text()}).catch(r=>{s.textContent=String(r)})}})),s]})]),o))}})}function l(e,t){let n=document.createElement("div");n.className="ks-stat";let i=document.createElement("div");i.className="ks-stat-label",i.textContent=e;let o=document.createElement("div");return o.className="ks-stat-value",o.textContent=String(t()),new IntersectionObserver(r=>{for(let g of r)g.isIntersecting&&(o.textContent=String(t()))}).observe(n),n.append(i,o),n}export{pe as default}; +`;function l(e){let n=document.createElement("div");return n.className="ks-log",e.appendChild(n),{append(t){let i=document.createElement("div");i.className="ks-log-line";let o=new Date().toLocaleTimeString();for(i.textContent=`${o} ${t}`,n.appendChild(i);n.childElementCount>200;)n.firstElementChild?.remove();n.scrollHeight-n.scrollTop-n.clientHeight<40&&(n.scrollTop=n.scrollHeight)},clear(){n.replaceChildren()}}}var u={watchGameLog:{kind:"boolean",label:"Follow the VRChat game log",description:"Streams player joins and world changes into the panel below.",default:!0},oscParameter:{kind:"string",label:"OSC parameter to pulse",description:"Sent to /avatar/parameters/ by the dashboard button.",default:"VRCEmote",placeholder:"VRCEmote"},oscValue:{kind:"number",label:"OSC value",default:1,min:0,max:255,step:1},verbosity:{kind:"select",label:"Log verbosity",default:"normal",options:[{value:"quiet",label:"Quiet"},{value:"normal",label:"Normal"},{value:"loud",label:"Everything"}]}},be={id:"kitchen-sink",settings:u,activate(e){let n={gameLogCount:0,oscParamCount:0,panel:void 0,log:t=>{e.settings.get("verbosity")!=="quiet"&&(e.logger.info(t),n.panel?.append(t))}};e.ui.injectCss(a),m(e),h(e,n),v(e,n),b(e,n),y(e,n),S(e,n),C(e,n),_(e,n),w(e,n),k(e,n),e.logger.info(`Kitchen Sink v${e.version} ready.`)},deactivate(){}};function m(e){e.disposables.add(e.settings.onChange(n=>{e.logger.debug(`Settings changed; verbosity is now ${n.verbosity}.`)}))}function h(e,n){e.events.on("friendTimelineEvent",t=>{n.log(`[friend] ${t.friendName} \u2192 ${t.type}`)}),e.events.on("vrcCurrentInstance",t=>{if(typeof t!="object"||t===null)return;let i=t.worldName;typeof i=="string"&&n.log(`[instance] now in ${i}`)}),e.events.onAny(t=>{e.settings.get("verbosity")==="loud"&&e.logger.debug(`event ${t.type}`)})}function v(e,n){e.gameLog.on(t=>{e.settings.get("watchGameLog")&&(n.gameLogCount+=1,n.log(`[gamelog] ${t.type}: ${t.message}`))}),e.gameLog.onType("gl_player_join",t=>{n.log(`[join] ${t.message}`)}),e.gameLog.history(e.signal).then(t=>{e.logger.info(`Game log backlog: ${String(t.length)} entries.`)}).catch(t=>{e.signal.aborted||e.logger.warn(`Could not read the game log: ${String(t)}`)})}function b(e,n){if(!e.osc.available){n.log("[osc] OSC is Windows-only in VRCNext; skipping.");return}e.osc.connect(),e.osc.onParam(t=>{n.oscParamCount+=1,e.settings.get("verbosity")==="loud"&&n.log(`[osc] ${t.name} = ${String(t.value)}`)}),e.osc.onAvatarChange(t=>{n.log(`[osc] avatar ${t.avatarId} with ${String(t.parameters.length)} parameters`)})}function c(e){if(!e.osc.available)return"OSC unavailable (Windows only)";let n=e.settings.get("oscParameter"),t=e.settings.get("oscValue");return e.osc.send(n,"int",t),`${n} = ${String(t)}`}function y(e,n){e.deepLinks.onPrefix("wrld",t=>{n.log(`[link] world ${t.id}`)}),e.deepLinks.on(t=>{n.log(`[link] ${t.prefix}:${t.id}${t.action?` (${t.action})`:""}`)})}function S(e,n){e.router.get("stats",()=>Response.json({gameLogEvents:n.gameLogCount,oscParams:n.oscParamCount,verbosity:e.settings.get("verbosity")})),e.router.get("greet/:name",t=>Response.json({hello:t.params.name??"world"})),e.router.post("osc/pulse",()=>Response.json({sent:c(e)})),e.logger.info(`Routes mounted at ${e.router.base.pathname}`)}function C(e,n){e.contextMenu.contribute(t=>{let i=[{kind:"divider"},{kind:"item",icon:"auto_awesome",label:"Kitchen Sink: log this element",onSelect:()=>{n.log(`[ctx] ${t.element.tagName.toLowerCase()}`)}},{kind:"submenu",icon:"tune",label:"Kitchen Sink: verbosity",items:()=>u.verbosity.options.map(o=>({kind:"item",icon:"adjust",label:o.label,checked:e.settings.get("verbosity")===o.value,onSelect:()=>{e.settings.set("verbosity",o.value)}}))}];if(t.entity!==void 0){let o=t.entity;i.push({kind:"item",icon:"content_copy",label:`Copy ${o.type} id`,onSelect:()=>{navigator.clipboard.writeText(o.id)}})}return i})}function _(e,n){e.gameLog.onType("gl_player_join",t=>{let i=t.message;e.notifications.toast({message:`${i} joined.`}),e.notifications.desktopAvailable?e.notifications.desktop({title:"Player joined",subtitle:i,accent:"info"}):n.log("[notify] desktop/VR notifications are Windows-only.")}),e.events.on("friendTimelineEvent",t=>{t.type==="online"&&e.notifications.notifToast({kind:"notification",sender:t.friendName,message:"came online"})})}async function w(e,n){if(!await e.native.ready){n.log(`[native] no companion at ${e.native.endpoint}; VR targets unavailable.`);return}let t=await e.native.targets();n.log(`[native] targets: ${t.map(o=>o.name).join(", ")||"none"}`),e.gameLog.onType("gl_player_join",o=>{let s=o.message;e.native.notify({title:"Player joined",content:s,timeoutSecs:4,icon:"user-available",overrides:{wayvr:{content:`${s} joined the instance`,height:200,opacity:.85,alwaysShow:!0}}})}),e.events.on("friendTimelineEvent",o=>{o.type==="online"&&e.native.notify({title:o.friendName,content:"came online",sinks:["wayvr"],timeoutSecs:3,opacity:.7})});let i=await e.native.describe();i!==void 0&&n.log(`[native] companion ${i.version}, services: ${Object.keys(i.services).join(", ")}`)}function k(e,n){e.ui.addDashboardCard({title:"Kitchen Sink",icon:"auto_awesome",order:10,render:t=>{let i=document.createElement("div");i.className="ks-grid",i.append(d("Game log events",()=>n.gameLogCount),d("OSC parameters",()=>n.oscParamCount)),t.appendChild(i);let o=document.createElement("button");o.textContent="Send OSC pulse",o.addEventListener("click",()=>{e.ui.toast({message:`Sent ${c(e)}.`})}),t.appendChild(o)}}),N(e,n),e.ui.addSettingsCard({title:"Kitchen Sink",icon:"auto_awesome",render:t=>{t.appendChild(e.ui.createToggleRow("Follow the VRChat game log",e.settings.get("watchGameLog"),o=>{e.settings.set("watchGameLog",o)}));let i=document.createElement("button");i.textContent="Reset settings",i.addEventListener("click",()=>{e.notifications.confirm({title:"Reset Kitchen Sink",message:"Restore every Kitchen Sink setting to its default?",confirmLabel:"Reset",icon:"restart_alt"}).then(async o=>{o&&(await e.settings.reset(),e.notifications.toast({message:"Kitchen Sink settings reset."}))})}),t.appendChild(i)}})}function N(e,n){e.ui.addNavTab({label:"Kitchen Sink",icon:"auto_awesome",render:t=>{let i=e.ui.kit,o=i.card({title:"Live activity",icon:"radar"});n.panel=l(o);let s=i.emptyState("No request yet.");t.append(i.layout(i.grid([i.card({title:"Counters",icon:"bolt",children:[i.stat({label:"Game log events",value:String(n.gameLogCount)}),i.stat({label:"OSC parameters",value:String(n.oscParamCount)})]}),i.card({title:"Capabilities",icon:"tune",children:[i.row({label:"OSC",value:e.osc.available?i.badge("ok","Ready"):i.badge("warn","Windows only"),detail:"Sent through VRCNext\u2019s own sockets."}),i.row({label:"Native companion",value:e.native.available?i.badge("ok","Connected"):i.badge("neutral","Not running"),detail:e.native.endpoint}),i.toggleRow({label:"Follow the game log",detail:"Mirrors VRChat log lines into the panel below.",value:e.settings.get("watchGameLog"),onChange:r=>{e.settings.set("watchGameLog",r)}})]}),i.card({title:"In-page routes",icon:"account_tree",children:[i.description("Plugin routes are reachable from this page only, never from curl."),i.buttonRow(i.button({label:"GET stats",icon:"download",onClick:()=>{e.router.fetch("stats").then(async r=>{s.textContent=await r.text()}).catch(r=>{s.textContent=String(r)})}})),s]})]),o))}})}function d(e,n){let t=document.createElement("div");t.className="ks-stat";let i=document.createElement("div");i.className="ks-stat-label",i.textContent=e;let o=document.createElement("div");return o.className="ks-stat-value",o.textContent=String(n()),new IntersectionObserver(r=>{for(let g of r)g.isIntersecting&&(o.textContent=String(n()))}).observe(t),t.append(i,o),t}export{be as default}; //# sourceMappingURL=kitchen-sink.js.map diff --git a/dist/kitchen-sink.js.map b/dist/kitchen-sink.js.map index 2039aa1..e16daa5 100644 --- a/dist/kitchen-sink.js.map +++ b/dist/kitchen-sink.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/styles.ts", "../src/log-panel.ts", "../src/index.ts"], - "sourcesContent": ["/**\n * Stylesheet injected by the plugin.\n *\n * Colours come from VRCNext's CSS custom properties rather than literals, so the plugin follows\n * the user's theme \u2014 including custom themes \u2014 instead of fighting it.\n */\n\nexport const PLUGIN_CSS = `\n.ks-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 10px;\n padding: 10px 0;\n}\n\n.ks-stat {\n background: var(--bg-input);\n border-radius: 10px;\n padding: 12px 14px;\n}\n\n.ks-stat-label {\n font-size: calc(11px + var(--fs-off, 0px));\n color: var(--tx3);\n}\n\n.ks-stat-value {\n font-size: calc(20px + var(--fs-off, 0px));\n font-weight: 600;\n color: var(--tx0);\n font-variant-numeric: tabular-nums;\n}\n\n.ks-log {\n max-height: 220px;\n overflow-y: auto;\n font-family: ui-monospace, monospace;\n font-size: calc(11px + var(--fs-off, 0px));\n color: var(--tx2);\n background: var(--bg-input);\n border-radius: 8px;\n padding: 8px 10px;\n}\n\n.ks-log-line { white-space: pre-wrap; word-break: break-word; }\n.ks-log-line + .ks-log-line { margin-top: 3px; }\n`;\n", "/**\n * Small scrolling log view used by the Kitchen Sink tab.\n *\n * Kept in its own module to show the normal pattern: UI helpers separate from the plugin's\n * lifecycle wiring, so `index.ts` stays readable.\n */\n\nconst MAX_LINES = 200;\n\nexport interface LogPanel {\n append(message: string): void;\n clear(): void;\n}\n\nexport function createLogPanel(parent: HTMLElement): LogPanel {\n const list = document.createElement('div');\n list.className = 'ks-log';\n parent.appendChild(list);\n\n return {\n append(message: string): void {\n const line = document.createElement('div');\n line.className = 'ks-log-line';\n const time = new Date().toLocaleTimeString();\n line.textContent = `${time} ${message}`;\n list.appendChild(line);\n\n while (list.childElementCount > MAX_LINES) list.firstElementChild?.remove();\n\n // Only chase the tail when the user has not scrolled up to read history.\n const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 40;\n if (atBottom) list.scrollTop = list.scrollHeight;\n },\n\n clear(): void {\n list.replaceChildren();\n },\n };\n}\n", "/**\n * Kitchen Sink \u2014 the reference VRCNext plugin.\n *\n * Exercises every capability the host provides, in the order a plugin author is likely to need\n * them. Each section is small and commented so it can be copied out on its own.\n */\n\nimport {\n definePlugin,\n type ContextMenuEntry,\n type PluginContext,\n type PluginId,\n type SettingsSchema,\n} from '@vrcnext/plugin-api';\n\nimport { PLUGIN_CSS } from './styles.js';\nimport { createLogPanel, type LogPanel } from './log-panel.js';\n\nconst settings = {\n watchGameLog: {\n kind: 'boolean',\n label: 'Follow the VRChat game log',\n description: 'Streams player joins and world changes into the panel below.',\n default: true,\n },\n oscParameter: {\n kind: 'string',\n label: 'OSC parameter to pulse',\n description: 'Sent to /avatar/parameters/ by the dashboard button.',\n default: 'VRCEmote',\n placeholder: 'VRCEmote',\n },\n oscValue: {\n kind: 'number',\n label: 'OSC value',\n default: 1,\n min: 0,\n max: 255,\n step: 1,\n },\n verbosity: {\n kind: 'select',\n label: 'Log verbosity',\n default: 'normal',\n options: [\n { value: 'quiet', label: 'Quiet' },\n { value: 'normal', label: 'Normal' },\n { value: 'loud', label: 'Everything' },\n ],\n },\n} as const satisfies SettingsSchema;\n\ntype Ctx = PluginContext;\n\n/** Shared mutable state for the session. Reset each activation. */\ninterface State {\n gameLogCount: number;\n oscParamCount: number;\n panel: LogPanel | undefined;\n /** Writes to both the plugin logger and the on-screen panel, honouring `verbosity`. */\n log(message: string): void;\n}\n\nexport default definePlugin({\n id: 'kitchen-sink' as PluginId,\n settings,\n\n activate(ctx) {\n const state: State = {\n gameLogCount: 0,\n oscParamCount: 0,\n panel: undefined,\n log: (message) => {\n if (ctx.settings.get('verbosity') === 'quiet') return;\n ctx.logger.info(message);\n state.panel?.append(message);\n },\n };\n\n // 1. Custom CSS, removed automatically on deactivate.\n ctx.ui.injectCss(PLUGIN_CSS);\n\n installSettingsWatch(ctx);\n installEvents(ctx, state);\n installGameLog(ctx, state);\n installOsc(ctx, state);\n installDeepLinks(ctx, state);\n installRoutes(ctx, state);\n installContextMenu(ctx, state);\n installNotifications(ctx, state);\n // Fire-and-forget: the companion probe must not hold up the rest of activation.\n void installNative(ctx, state);\n installUi(ctx, state);\n\n ctx.logger.info(`Kitchen Sink v${ctx.version} ready.`);\n },\n\n deactivate() {\n // Everything above registered through `ctx`, so the host tears it all down. Only resources\n // created outside the context would need explicit cleanup here.\n },\n});\n\n/** 2. React to settings changes. `values` keeps its literal types. */\nfunction installSettingsWatch(ctx: Ctx): void {\n ctx.disposables.add(\n ctx.settings.onChange((values) => {\n ctx.logger.debug(`Settings changed; verbosity is now ${values.verbosity}.`);\n }),\n );\n}\n\n/** 3. Typed host events, plus the untyped escape hatch. */\nfunction installEvents(ctx: Ctx, state: State): void {\n // Fully typed: `payload.friendName` is a string.\n ctx.events.on('friendTimelineEvent', (payload) => {\n state.log(`[friend] ${payload.friendName} \u2192 ${payload.type}`);\n });\n\n // Not in the verified map, so the payload is `unknown` and must be narrowed.\n ctx.events.on('vrcCurrentInstance', (payload) => {\n if (typeof payload !== 'object' || payload === null) return;\n const worldName = (payload as { worldName?: unknown }).worldName;\n if (typeof worldName === 'string') state.log(`[instance] now in ${worldName}`);\n });\n\n // Diagnostics: every event flowing from VRCNext.\n ctx.events.onAny((envelope) => {\n if (ctx.settings.get('verbosity') === 'loud') {\n ctx.logger.debug(`event ${envelope.type}`);\n }\n });\n}\n\n/** 4. VRChat game log \u2014 the lowest-latency source of in-game activity. */\nfunction installGameLog(ctx: Ctx, state: State): void {\n ctx.gameLog.on((entry) => {\n if (!ctx.settings.get('watchGameLog')) return;\n state.gameLogCount += 1;\n state.log(`[gamelog] ${entry.type}: ${entry.message}`);\n });\n\n ctx.gameLog.onType('OnPlayerJoined', (entry) => {\n state.log(`[join] ${entry.detail || entry.message}`);\n });\n\n // Backlog is fetched once, aborted if the plugin is disabled mid-flight.\n void ctx.gameLog\n .history(ctx.signal)\n .then((entries) => { ctx.logger.info(`Game log backlog: ${String(entries.length)} entries.`); })\n .catch((error: unknown) => {\n if (ctx.signal.aborted) return;\n ctx.logger.warn(`Could not read the game log: ${String(error)}`);\n });\n}\n\n/** 5. OSC in and out. Windows-only in VRCNext \u2014 always guard on `available`. */\nfunction installOsc(ctx: Ctx, state: State): void {\n if (!ctx.osc.available) {\n state.log('[osc] OSC is Windows-only in VRCNext; skipping.');\n return;\n }\n ctx.osc.connect();\n\n ctx.osc.onParam((event) => {\n state.oscParamCount += 1;\n if (ctx.settings.get('verbosity') === 'loud') {\n state.log(`[osc] ${event.name} = ${String(event.value)}`);\n }\n });\n\n ctx.osc.onAvatarChange((event) => {\n state.log(`[osc] avatar ${event.avatarId} with ${String(event.parameters.length)} parameters`);\n });\n}\n\n/** Sends the configured parameter. Shared by the dashboard button and the HTTP route. */\nfunction pulseOsc(ctx: Ctx): string {\n if (!ctx.osc.available) return 'OSC unavailable (Windows only)';\n const name = ctx.settings.get('oscParameter');\n const value = ctx.settings.get('oscValue');\n ctx.osc.send(name, 'int', value);\n return `${name} = ${String(value)}`;\n}\n\n/** 6. Deep links VRCNext delivers. Custom prefixes are not possible \u2014 see the docs. */\nfunction installDeepLinks(ctx: Ctx, state: State): void {\n ctx.deepLinks.onPrefix('wrld', (event) => {\n state.log(`[link] world ${event.id}`);\n return undefined; // Not handled \u2014 let other plugins see it too.\n });\n\n ctx.deepLinks.on((event) => {\n state.log(`[link] ${event.prefix}:${event.id}${event.action ? ` (${event.action})` : ''}`);\n return undefined;\n });\n}\n\n/** 7. In-page HTTP routes. Reachable from the page, not from outside it. */\nfunction installRoutes(ctx: Ctx, state: State): void {\n ctx.router.get('stats', () =>\n Response.json({\n gameLogEvents: state.gameLogCount,\n oscParams: state.oscParamCount,\n verbosity: ctx.settings.get('verbosity'),\n }),\n );\n\n ctx.router.get('greet/:name', (request) =>\n Response.json({ hello: request.params['name'] ?? 'world' }),\n );\n\n ctx.router.post('osc/pulse', () => Response.json({ sent: pulseOsc(ctx) }));\n\n ctx.logger.info(`Routes mounted at ${ctx.router.base.pathname}`);\n}\n\n/** 8. Context-menu items, dividers and submenus. */\nfunction installContextMenu(ctx: Ctx, state: State): void {\n ctx.contextMenu.contribute((target) => {\n const entries: ContextMenuEntry[] = [\n { kind: 'divider' },\n {\n kind: 'item',\n icon: 'auto_awesome',\n label: 'Kitchen Sink: log this element',\n onSelect: () => { state.log(`[ctx] ${target.element.tagName.toLowerCase()}`); },\n },\n {\n kind: 'submenu',\n icon: 'tune',\n label: 'Kitchen Sink: verbosity',\n items: () =>\n settings.verbosity.options.map(\n (option): ContextMenuEntry => ({\n kind: 'item',\n icon: 'adjust',\n label: option.label,\n checked: ctx.settings.get('verbosity') === option.value,\n onSelect: () => { void ctx.settings.set('verbosity', option.value); },\n }),\n ),\n },\n ];\n\n if (target.entity !== undefined) {\n const entity = target.entity;\n entries.push({\n kind: 'item',\n icon: 'content_copy',\n label: `Copy ${entity.type} id`,\n onSelect: () => { void navigator.clipboard.writeText(entity.id); },\n });\n }\n return entries;\n });\n}\n\n/** 9. Every notification surface VRCNext exposes. */\nfunction installNotifications(ctx: Ctx, state: State): void {\n ctx.gameLog.onType('OnPlayerJoined', (entry) => {\n const who = entry.detail || entry.message;\n\n // In-app toast \u2014 always available.\n ctx.notifications.toast({ message: `${who} joined.` });\n\n // OS tray toast + SteamVR wrist overlay, in one call. Windows only.\n if (ctx.notifications.desktopAvailable) {\n ctx.notifications.desktop({\n title: 'Player joined',\n subtitle: who,\n accent: 'info',\n });\n } else {\n state.log('[notify] desktop/VR notifications are Windows-only.');\n }\n });\n\n // Styled like one of VRCNext's own notification kinds.\n ctx.events.on('friendTimelineEvent', (payload) => {\n if (payload.type !== 'online') return;\n ctx.notifications.notifToast({\n kind: 'notification',\n sender: payload.friendName,\n message: 'came online',\n });\n });\n}\n\n/**\n * 10. The optional native companion \u2014 VR overlay and desktop notification targets.\n *\n * Everything here is written for the companion being absent, because for most users it is. Note\n * the two-step: ask what targets exist, then address them by name. Hard-coding `'wayvr'` would\n * work today and break the moment someone runs a different overlay.\n */\nasync function installNative(ctx: Ctx, state: State): Promise {\n // `await ctx.native.ready`, not `if (ctx.native.available)`. The host's probe is still in flight\n // when plugins activate, so reading the snapshot here is a race: on a fast answer it is true, on\n // a slow one false, and the plugin silently loses its VR notifications.\n if (!(await ctx.native.ready)) {\n state.log(`[native] no companion at ${ctx.native.endpoint}; VR targets unavailable.`);\n return;\n }\n\n const targets = await ctx.native.targets();\n state.log(`[native] targets: ${targets.map((t) => t.name).join(', ') || 'none'}`);\n\n ctx.gameLog.onType('OnPlayerJoined', (entry) => {\n const who = entry.detail || entry.message;\n\n // One call, presented differently in each place: a tall translucent panel in VR, an ordinary\n // toast on the monitor. Omitting `sinks` means \"every target the companion has\".\n void ctx.native.notify({\n title: 'Player joined',\n content: who,\n timeoutSecs: 4,\n icon: 'user-available',\n overrides: {\n wayvr: {\n content: `${who} joined the instance`,\n height: 200,\n opacity: 0.85,\n alwaysShow: true,\n },\n },\n });\n });\n\n // VR only \u2014 deliberately nothing on the monitor, because this fires often.\n ctx.events.on('friendTimelineEvent', (payload) => {\n if (payload.type !== 'online') return;\n void ctx.native.notify({\n title: payload.friendName,\n content: 'came online',\n sinks: ['wayvr'],\n timeoutSecs: 3,\n opacity: 0.7,\n });\n });\n\n // The forward-compatible path: call a service this example predates.\n const description = await ctx.native.describe();\n if (description !== undefined) {\n state.log(\n `[native] companion ${description.version}, services: ${Object.keys(description.services).join(', ')}`,\n );\n }\n}\n\n/** 11. UI: a dashboard card, a sidebar tab and a settings card. */\nfunction installUi(ctx: Ctx, state: State): void {\n ctx.ui.addDashboardCard({\n title: 'Kitchen Sink',\n icon: 'auto_awesome',\n order: 10,\n render: (card) => {\n const grid = document.createElement('div');\n grid.className = 'ks-grid';\n grid.append(\n statTile('Game log events', () => state.gameLogCount),\n statTile('OSC parameters', () => state.oscParamCount),\n );\n card.appendChild(grid);\n\n const pulse = document.createElement('button');\n pulse.textContent = 'Send OSC pulse';\n pulse.addEventListener('click', () => {\n ctx.ui.toast({ message: `Sent ${pulseOsc(ctx)}.` });\n });\n card.appendChild(pulse);\n },\n });\n\n installNavTab(ctx, state);\n\n ctx.ui.addSettingsCard({\n title: 'Kitchen Sink',\n icon: 'auto_awesome',\n render: (card) => {\n card.appendChild(\n ctx.ui.createToggleRow(\n 'Follow the VRChat game log',\n ctx.settings.get('watchGameLog'),\n (checked) => { void ctx.settings.set('watchGameLog', checked); },\n ),\n );\n\n // Blocking confirmation modal; resolves false on cancel, backdrop click or dismissal.\n const reset = document.createElement('button');\n reset.textContent = 'Reset settings';\n reset.addEventListener('click', () => {\n void ctx.notifications\n .confirm({\n title: 'Reset Kitchen Sink',\n message: 'Restore every Kitchen Sink setting to its default?',\n confirmLabel: 'Reset',\n icon: 'restart_alt',\n })\n .then(async (confirmed) => {\n if (!confirmed) return;\n await ctx.settings.reset();\n ctx.notifications.toast({ message: 'Kitchen Sink settings reset.' });\n });\n });\n card.appendChild(reset);\n },\n });\n}\n\n/**\n * The plugin's tab, declaratively, from `ctx.ui.kit`.\n *\n * No class names, no `createElement`, no stylesheet \u2014 and the result is VRCNext's own markup, so\n * it inherits the theme, the font-size offset and every future restyle of the app.\n */\nfunction installNavTab(ctx: Ctx, state: State): void {\n ctx.ui.addNavTab({\n label: 'Kitchen Sink',\n icon: 'auto_awesome',\n render: (tab) => {\n const k = ctx.ui.kit;\n const activity = k.card({ title: 'Live activity', icon: 'radar' });\n state.panel = createLogPanel(activity);\n\n const output = k.emptyState('No request yet.');\n\n tab.append(\n k.layout(\n // A grid rather than full-width cards: two short cards side by side beats two wide ones.\n k.grid([\n k.card({\n title: 'Counters',\n icon: 'bolt',\n children: [\n k.stat({ label: 'Game log events', value: String(state.gameLogCount) }),\n k.stat({ label: 'OSC parameters', value: String(state.oscParamCount) }),\n ],\n }),\n k.card({\n title: 'Capabilities',\n icon: 'tune',\n children: [\n k.row({\n label: 'OSC',\n value: ctx.osc.available ? k.badge('ok', 'Ready') : k.badge('warn', 'Windows only'),\n detail: 'Sent through VRCNext\u2019s own sockets.',\n }),\n k.row({\n label: 'Native companion',\n value: ctx.native.available\n ? k.badge('ok', 'Connected')\n : k.badge('neutral', 'Not running'),\n detail: ctx.native.endpoint,\n }),\n k.toggleRow({\n label: 'Follow the game log',\n detail: 'Mirrors VRChat log lines into the panel below.',\n value: ctx.settings.get('watchGameLog'),\n onChange: (next) => { void ctx.settings.set('watchGameLog', next); },\n }),\n ],\n }),\n k.card({\n title: 'In-page routes',\n icon: 'account_tree',\n children: [\n k.description('Plugin routes are reachable from this page only, never from curl.'),\n k.buttonRow(\n k.button({\n label: 'GET stats',\n icon: 'download',\n onClick: () => {\n void ctx.router\n .fetch('stats')\n .then(async (response) => {\n output.textContent = await response.text();\n })\n .catch((error: unknown) => { output.textContent = String(error); });\n },\n }),\n ),\n output,\n ],\n }),\n ]),\n activity,\n ),\n );\n },\n });\n}\n\n/** Stat tile that refreshes itself from a getter while the dashboard is visible. */\nfunction statTile(label: string, read: () => number): HTMLElement {\n const tile = document.createElement('div');\n tile.className = 'ks-stat';\n\n const labelEl = document.createElement('div');\n labelEl.className = 'ks-stat-label';\n labelEl.textContent = label;\n\n const valueEl = document.createElement('div');\n valueEl.className = 'ks-stat-value';\n valueEl.textContent = String(read());\n\n // Repaint only while the tile is actually on screen.\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) valueEl.textContent = String(read());\n }\n });\n observer.observe(tile);\n\n tile.append(labelEl, valueEl);\n return tile;\n}\n"], - "mappings": "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", + "sourcesContent": ["/**\n * Stylesheet injected by the plugin.\n *\n * Colours come from VRCNext's CSS custom properties rather than literals, so the plugin follows\n * the user's theme \u2014 including custom themes \u2014 instead of fighting it.\n */\n\nexport const PLUGIN_CSS = `\n.ks-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 10px;\n padding: 10px 0;\n}\n\n.ks-stat {\n background: var(--bg-input);\n border-radius: 10px;\n padding: 12px 14px;\n}\n\n.ks-stat-label {\n font-size: calc(11px + var(--fs-off, 0px));\n color: var(--tx3);\n}\n\n.ks-stat-value {\n font-size: calc(20px + var(--fs-off, 0px));\n font-weight: 600;\n color: var(--tx0);\n font-variant-numeric: tabular-nums;\n}\n\n.ks-log {\n max-height: 220px;\n overflow-y: auto;\n font-family: ui-monospace, monospace;\n font-size: calc(11px + var(--fs-off, 0px));\n color: var(--tx2);\n background: var(--bg-input);\n border-radius: 8px;\n padding: 8px 10px;\n}\n\n.ks-log-line { white-space: pre-wrap; word-break: break-word; }\n.ks-log-line + .ks-log-line { margin-top: 3px; }\n`;\n", "/**\n * Small scrolling log view used by the Kitchen Sink tab.\n *\n * Kept in its own module to show the normal pattern: UI helpers separate from the plugin's\n * lifecycle wiring, so `index.ts` stays readable.\n */\n\nconst MAX_LINES = 200;\n\nexport interface LogPanel {\n append(message: string): void;\n clear(): void;\n}\n\nexport function createLogPanel(parent: HTMLElement): LogPanel {\n const list = document.createElement('div');\n list.className = 'ks-log';\n parent.appendChild(list);\n\n return {\n append(message: string): void {\n const line = document.createElement('div');\n line.className = 'ks-log-line';\n const time = new Date().toLocaleTimeString();\n line.textContent = `${time} ${message}`;\n list.appendChild(line);\n\n while (list.childElementCount > MAX_LINES) list.firstElementChild?.remove();\n\n // Only chase the tail when the user has not scrolled up to read history.\n const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 40;\n if (atBottom) list.scrollTop = list.scrollHeight;\n },\n\n clear(): void {\n list.replaceChildren();\n },\n };\n}\n", "/**\n * Kitchen Sink \u2014 the reference VRCNext plugin.\n *\n * Exercises every capability the host provides, in the order a plugin author is likely to need\n * them. Each section is small and commented so it can be copied out on its own.\n */\n\nimport {\n definePlugin,\n type ContextMenuEntry,\n type PluginContext,\n type PluginId,\n type SettingsSchema,\n} from '@vrcnext/plugin-api';\n\nimport { PLUGIN_CSS } from './styles.js';\nimport { createLogPanel, type LogPanel } from './log-panel.js';\n\nconst settings = {\n watchGameLog: {\n kind: 'boolean',\n label: 'Follow the VRChat game log',\n description: 'Streams player joins and world changes into the panel below.',\n default: true,\n },\n oscParameter: {\n kind: 'string',\n label: 'OSC parameter to pulse',\n description: 'Sent to /avatar/parameters/ by the dashboard button.',\n default: 'VRCEmote',\n placeholder: 'VRCEmote',\n },\n oscValue: {\n kind: 'number',\n label: 'OSC value',\n default: 1,\n min: 0,\n max: 255,\n step: 1,\n },\n verbosity: {\n kind: 'select',\n label: 'Log verbosity',\n default: 'normal',\n options: [\n { value: 'quiet', label: 'Quiet' },\n { value: 'normal', label: 'Normal' },\n { value: 'loud', label: 'Everything' },\n ],\n },\n} as const satisfies SettingsSchema;\n\ntype Ctx = PluginContext;\n\n/** Shared mutable state for the session. Reset each activation. */\ninterface State {\n gameLogCount: number;\n oscParamCount: number;\n panel: LogPanel | undefined;\n /** Writes to both the plugin logger and the on-screen panel, honouring `verbosity`. */\n log(message: string): void;\n}\n\nexport default definePlugin({\n id: 'kitchen-sink' as PluginId,\n settings,\n\n activate(ctx) {\n const state: State = {\n gameLogCount: 0,\n oscParamCount: 0,\n panel: undefined,\n log: (message) => {\n if (ctx.settings.get('verbosity') === 'quiet') return;\n ctx.logger.info(message);\n state.panel?.append(message);\n },\n };\n\n // 1. Custom CSS, removed automatically on deactivate.\n ctx.ui.injectCss(PLUGIN_CSS);\n\n installSettingsWatch(ctx);\n installEvents(ctx, state);\n installGameLog(ctx, state);\n installOsc(ctx, state);\n installDeepLinks(ctx, state);\n installRoutes(ctx, state);\n installContextMenu(ctx, state);\n installNotifications(ctx, state);\n // Fire-and-forget: the companion probe must not hold up the rest of activation.\n void installNative(ctx, state);\n installUi(ctx, state);\n\n ctx.logger.info(`Kitchen Sink v${ctx.version} ready.`);\n },\n\n deactivate() {\n // Everything above registered through `ctx`, so the host tears it all down. Only resources\n // created outside the context would need explicit cleanup here.\n },\n});\n\n/** 2. React to settings changes. `values` keeps its literal types. */\nfunction installSettingsWatch(ctx: Ctx): void {\n ctx.disposables.add(\n ctx.settings.onChange((values) => {\n ctx.logger.debug(`Settings changed; verbosity is now ${values.verbosity}.`);\n }),\n );\n}\n\n/** 3. Typed host events, plus the untyped escape hatch. */\nfunction installEvents(ctx: Ctx, state: State): void {\n // Fully typed: `payload.friendName` is a string.\n ctx.events.on('friendTimelineEvent', (payload) => {\n state.log(`[friend] ${payload.friendName} \u2192 ${payload.type}`);\n });\n\n // Not in the verified map, so the payload is `unknown` and must be narrowed.\n ctx.events.on('vrcCurrentInstance', (payload) => {\n if (typeof payload !== 'object' || payload === null) return;\n const worldName = (payload as { worldName?: unknown }).worldName;\n if (typeof worldName === 'string') state.log(`[instance] now in ${worldName}`);\n });\n\n // Diagnostics: every event flowing from VRCNext.\n ctx.events.onAny((envelope) => {\n if (ctx.settings.get('verbosity') === 'loud') {\n ctx.logger.debug(`event ${envelope.type}`);\n }\n });\n}\n\n/** 4. VRChat game log \u2014 the lowest-latency source of in-game activity. */\nfunction installGameLog(ctx: Ctx, state: State): void {\n ctx.gameLog.on((entry) => {\n if (!ctx.settings.get('watchGameLog')) return;\n state.gameLogCount += 1;\n state.log(`[gamelog] ${entry.type}: ${entry.message}`);\n });\n\n ctx.gameLog.onType('gl_player_join', (entry) => {\n state.log(`[join] ${entry.message}`);\n });\n\n // Backlog is fetched once, aborted if the plugin is disabled mid-flight.\n void ctx.gameLog\n .history(ctx.signal)\n .then((entries) => { ctx.logger.info(`Game log backlog: ${String(entries.length)} entries.`); })\n .catch((error: unknown) => {\n if (ctx.signal.aborted) return;\n ctx.logger.warn(`Could not read the game log: ${String(error)}`);\n });\n}\n\n/** 5. OSC in and out. Windows-only in VRCNext \u2014 always guard on `available`. */\nfunction installOsc(ctx: Ctx, state: State): void {\n if (!ctx.osc.available) {\n state.log('[osc] OSC is Windows-only in VRCNext; skipping.');\n return;\n }\n ctx.osc.connect();\n\n ctx.osc.onParam((event) => {\n state.oscParamCount += 1;\n if (ctx.settings.get('verbosity') === 'loud') {\n state.log(`[osc] ${event.name} = ${String(event.value)}`);\n }\n });\n\n ctx.osc.onAvatarChange((event) => {\n state.log(`[osc] avatar ${event.avatarId} with ${String(event.parameters.length)} parameters`);\n });\n}\n\n/** Sends the configured parameter. Shared by the dashboard button and the HTTP route. */\nfunction pulseOsc(ctx: Ctx): string {\n if (!ctx.osc.available) return 'OSC unavailable (Windows only)';\n const name = ctx.settings.get('oscParameter');\n const value = ctx.settings.get('oscValue');\n ctx.osc.send(name, 'int', value);\n return `${name} = ${String(value)}`;\n}\n\n/** 6. Deep links VRCNext delivers. Custom prefixes are not possible \u2014 see the docs. */\nfunction installDeepLinks(ctx: Ctx, state: State): void {\n ctx.deepLinks.onPrefix('wrld', (event) => {\n state.log(`[link] world ${event.id}`);\n return undefined; // Not handled \u2014 let other plugins see it too.\n });\n\n ctx.deepLinks.on((event) => {\n state.log(`[link] ${event.prefix}:${event.id}${event.action ? ` (${event.action})` : ''}`);\n return undefined;\n });\n}\n\n/** 7. In-page HTTP routes. Reachable from the page, not from outside it. */\nfunction installRoutes(ctx: Ctx, state: State): void {\n ctx.router.get('stats', () =>\n Response.json({\n gameLogEvents: state.gameLogCount,\n oscParams: state.oscParamCount,\n verbosity: ctx.settings.get('verbosity'),\n }),\n );\n\n ctx.router.get('greet/:name', (request) =>\n Response.json({ hello: request.params['name'] ?? 'world' }),\n );\n\n ctx.router.post('osc/pulse', () => Response.json({ sent: pulseOsc(ctx) }));\n\n ctx.logger.info(`Routes mounted at ${ctx.router.base.pathname}`);\n}\n\n/** 8. Context-menu items, dividers and submenus. */\nfunction installContextMenu(ctx: Ctx, state: State): void {\n ctx.contextMenu.contribute((target) => {\n const entries: ContextMenuEntry[] = [\n { kind: 'divider' },\n {\n kind: 'item',\n icon: 'auto_awesome',\n label: 'Kitchen Sink: log this element',\n onSelect: () => { state.log(`[ctx] ${target.element.tagName.toLowerCase()}`); },\n },\n {\n kind: 'submenu',\n icon: 'tune',\n label: 'Kitchen Sink: verbosity',\n items: () =>\n settings.verbosity.options.map(\n (option): ContextMenuEntry => ({\n kind: 'item',\n icon: 'adjust',\n label: option.label,\n checked: ctx.settings.get('verbosity') === option.value,\n onSelect: () => { void ctx.settings.set('verbosity', option.value); },\n }),\n ),\n },\n ];\n\n if (target.entity !== undefined) {\n const entity = target.entity;\n entries.push({\n kind: 'item',\n icon: 'content_copy',\n label: `Copy ${entity.type} id`,\n onSelect: () => { void navigator.clipboard.writeText(entity.id); },\n });\n }\n return entries;\n });\n}\n\n/** 9. Every notification surface VRCNext exposes. */\nfunction installNotifications(ctx: Ctx, state: State): void {\n ctx.gameLog.onType('gl_player_join', (entry) => {\n const who = entry.message;\n\n // In-app toast \u2014 always available.\n ctx.notifications.toast({ message: `${who} joined.` });\n\n // OS tray toast + SteamVR wrist overlay, in one call. Windows only.\n if (ctx.notifications.desktopAvailable) {\n ctx.notifications.desktop({\n title: 'Player joined',\n subtitle: who,\n accent: 'info',\n });\n } else {\n state.log('[notify] desktop/VR notifications are Windows-only.');\n }\n });\n\n // Styled like one of VRCNext's own notification kinds.\n ctx.events.on('friendTimelineEvent', (payload) => {\n if (payload.type !== 'online') return;\n ctx.notifications.notifToast({\n kind: 'notification',\n sender: payload.friendName,\n message: 'came online',\n });\n });\n}\n\n/**\n * 10. The optional native companion \u2014 VR overlay and desktop notification targets.\n *\n * Everything here is written for the companion being absent, because for most users it is. Note\n * the two-step: ask what targets exist, then address them by name. Hard-coding `'wayvr'` would\n * work today and break the moment someone runs a different overlay.\n */\nasync function installNative(ctx: Ctx, state: State): Promise {\n // `await ctx.native.ready`, not `if (ctx.native.available)`. The host's probe is still in flight\n // when plugins activate, so reading the snapshot here is a race: on a fast answer it is true, on\n // a slow one false, and the plugin silently loses its VR notifications.\n if (!(await ctx.native.ready)) {\n state.log(`[native] no companion at ${ctx.native.endpoint}; VR targets unavailable.`);\n return;\n }\n\n const targets = await ctx.native.targets();\n state.log(`[native] targets: ${targets.map((t) => t.name).join(', ') || 'none'}`);\n\n ctx.gameLog.onType('gl_player_join', (entry) => {\n const who = entry.message;\n\n // One call, presented differently in each place: a tall translucent panel in VR, an ordinary\n // toast on the monitor. Omitting `sinks` means \"every target the companion has\".\n void ctx.native.notify({\n title: 'Player joined',\n content: who,\n timeoutSecs: 4,\n icon: 'user-available',\n overrides: {\n wayvr: {\n content: `${who} joined the instance`,\n height: 200,\n opacity: 0.85,\n alwaysShow: true,\n },\n },\n });\n });\n\n // VR only \u2014 deliberately nothing on the monitor, because this fires often.\n ctx.events.on('friendTimelineEvent', (payload) => {\n if (payload.type !== 'online') return;\n void ctx.native.notify({\n title: payload.friendName,\n content: 'came online',\n sinks: ['wayvr'],\n timeoutSecs: 3,\n opacity: 0.7,\n });\n });\n\n // The forward-compatible path: call a service this example predates.\n const description = await ctx.native.describe();\n if (description !== undefined) {\n state.log(\n `[native] companion ${description.version}, services: ${Object.keys(description.services).join(', ')}`,\n );\n }\n}\n\n/** 11. UI: a dashboard card, a sidebar tab and a settings card. */\nfunction installUi(ctx: Ctx, state: State): void {\n ctx.ui.addDashboardCard({\n title: 'Kitchen Sink',\n icon: 'auto_awesome',\n order: 10,\n render: (card) => {\n const grid = document.createElement('div');\n grid.className = 'ks-grid';\n grid.append(\n statTile('Game log events', () => state.gameLogCount),\n statTile('OSC parameters', () => state.oscParamCount),\n );\n card.appendChild(grid);\n\n const pulse = document.createElement('button');\n pulse.textContent = 'Send OSC pulse';\n pulse.addEventListener('click', () => {\n ctx.ui.toast({ message: `Sent ${pulseOsc(ctx)}.` });\n });\n card.appendChild(pulse);\n },\n });\n\n installNavTab(ctx, state);\n\n ctx.ui.addSettingsCard({\n title: 'Kitchen Sink',\n icon: 'auto_awesome',\n render: (card) => {\n card.appendChild(\n ctx.ui.createToggleRow(\n 'Follow the VRChat game log',\n ctx.settings.get('watchGameLog'),\n (checked) => { void ctx.settings.set('watchGameLog', checked); },\n ),\n );\n\n // Blocking confirmation modal; resolves false on cancel, backdrop click or dismissal.\n const reset = document.createElement('button');\n reset.textContent = 'Reset settings';\n reset.addEventListener('click', () => {\n void ctx.notifications\n .confirm({\n title: 'Reset Kitchen Sink',\n message: 'Restore every Kitchen Sink setting to its default?',\n confirmLabel: 'Reset',\n icon: 'restart_alt',\n })\n .then(async (confirmed) => {\n if (!confirmed) return;\n await ctx.settings.reset();\n ctx.notifications.toast({ message: 'Kitchen Sink settings reset.' });\n });\n });\n card.appendChild(reset);\n },\n });\n}\n\n/**\n * The plugin's tab, declaratively, from `ctx.ui.kit`.\n *\n * No class names, no `createElement`, no stylesheet \u2014 and the result is VRCNext's own markup, so\n * it inherits the theme, the font-size offset and every future restyle of the app.\n */\nfunction installNavTab(ctx: Ctx, state: State): void {\n ctx.ui.addNavTab({\n label: 'Kitchen Sink',\n icon: 'auto_awesome',\n render: (tab) => {\n const k = ctx.ui.kit;\n const activity = k.card({ title: 'Live activity', icon: 'radar' });\n state.panel = createLogPanel(activity);\n\n const output = k.emptyState('No request yet.');\n\n tab.append(\n k.layout(\n // A grid rather than full-width cards: two short cards side by side beats two wide ones.\n k.grid([\n k.card({\n title: 'Counters',\n icon: 'bolt',\n children: [\n k.stat({ label: 'Game log events', value: String(state.gameLogCount) }),\n k.stat({ label: 'OSC parameters', value: String(state.oscParamCount) }),\n ],\n }),\n k.card({\n title: 'Capabilities',\n icon: 'tune',\n children: [\n k.row({\n label: 'OSC',\n value: ctx.osc.available ? k.badge('ok', 'Ready') : k.badge('warn', 'Windows only'),\n detail: 'Sent through VRCNext\u2019s own sockets.',\n }),\n k.row({\n label: 'Native companion',\n value: ctx.native.available\n ? k.badge('ok', 'Connected')\n : k.badge('neutral', 'Not running'),\n detail: ctx.native.endpoint,\n }),\n k.toggleRow({\n label: 'Follow the game log',\n detail: 'Mirrors VRChat log lines into the panel below.',\n value: ctx.settings.get('watchGameLog'),\n onChange: (next) => { void ctx.settings.set('watchGameLog', next); },\n }),\n ],\n }),\n k.card({\n title: 'In-page routes',\n icon: 'account_tree',\n children: [\n k.description('Plugin routes are reachable from this page only, never from curl.'),\n k.buttonRow(\n k.button({\n label: 'GET stats',\n icon: 'download',\n onClick: () => {\n void ctx.router\n .fetch('stats')\n .then(async (response) => {\n output.textContent = await response.text();\n })\n .catch((error: unknown) => { output.textContent = String(error); });\n },\n }),\n ),\n output,\n ],\n }),\n ]),\n activity,\n ),\n );\n },\n });\n}\n\n/** Stat tile that refreshes itself from a getter while the dashboard is visible. */\nfunction statTile(label: string, read: () => number): HTMLElement {\n const tile = document.createElement('div');\n tile.className = 'ks-stat';\n\n const labelEl = document.createElement('div');\n labelEl.className = 'ks-stat-label';\n labelEl.textContent = label;\n\n const valueEl = document.createElement('div');\n valueEl.className = 'ks-stat-value';\n valueEl.textContent = String(read());\n\n // Repaint only while the tile is actually on screen.\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) valueEl.textContent = String(read());\n }\n });\n observer.observe(tile);\n\n tile.append(labelEl, valueEl);\n return tile;\n}\n"], + "mappings": "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", "names": ["PLUGIN_CSS", "createLogPanel", "parent", "list", "message", "line", "time", "settings", "index_default", "ctx", "state", "message", "PLUGIN_CSS", "installSettingsWatch", "installEvents", "installGameLog", "installOsc", "installDeepLinks", "installRoutes", "installContextMenu", "installNotifications", "installNative", "installUi", "values", "payload", "worldName", "envelope", "entry", "entries", "error", "event", "pulseOsc", "name", "value", "request", "target", "option", "entity", "who", "targets", "t", "description", "card", "grid", "statTile", "pulse", "installNavTab", "checked", "reset", "confirmed", "tab", "k", "activity", "createLogPanel", "output", "next", "response", "label", "read", "tile", "labelEl", "valueEl"] } diff --git a/vrcnext-plugins.json b/vrcnext-plugins.json index b8bc997..2613e7f 100644 --- a/vrcnext-plugins.json +++ b/vrcnext-plugins.json @@ -21,6 +21,34 @@ "apiVersion": "^0.1.0", "author": "vrcnext-plugins", "homepage": "https://github.com/vrcnext-plugins/vrcnext-plugin-system" + }, + { + "id": "club-security", + "name": "Club Security", + "version": "1.0.0", + "description": "When someone joins your instance, gathers age verification, avatar performance, group membership and rejoin history, then notifies you in-app, on the desktop, in VR or on Discord.", + "entry": "dist/club-security.js", + "apiVersion": "^0.1.0", + "author": "vrcnext-plugins", + "homepage": "https://github.com/vrcnext-plugins/vrcnext-plugin-system", + "icon": "security", + "tags": [ + "Notifications", + "Privacy", + "Activity" + ], + "searchTerms": [ + "security", + "door", + "bouncer", + "join", + "age verified", + "performance rank", + "group", + "rejoin", + "discord", + "webhook" + ] } ] -} +} \ No newline at end of file