Make the report a template
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
243b22e4f8
commit
ecb5a3bcbe
4 files changed
+65
-24
No files matched your search
@@ -11,7 +11,13 @@ In Group: Yes
|
|||||||
Rejoin?: Yes (2 hours ago)
|
Rejoin?: Yes (2 hours ago)
|
||||||
```
|
```
|
||||||
|
|
||||||
`In Group` only appears when a group filter is set.
|
That is the default **Report template**, editable in the plugin's settings. The first line is
|
||||||
|
the title on desktop, VR and Discord; the rest is the body. A line whose placeholders are all
|
||||||
|
empty is left out, which is why `In Group` only appears when a group filter is set.
|
||||||
|
|
||||||
|
Placeholders: `{name}` `{userId}` `{ageVerified}` `{ageStatus}` `{pcRank}` `{questRank}`
|
||||||
|
`{avatar}` `{platform}` `{inGroup}` `{rejoin}` `{rejoinAgo}` `{world}` `{worldId}`
|
||||||
|
`{instanceType}` `{location}` `{time}`.
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
|
|
||||||
|
|||||||
+36
-22
@@ -10,13 +10,11 @@
|
|||||||
* `hosts`, so that is the only webhook host accepted here.
|
* `hosts`, so that is the only webhook host accepted here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PluginContext, SettingsValues } from '@vrcnext/plugin-api';
|
import { fillTemplate, timeAgo, type PluginContext, type SettingsValues } from '@vrcnext/plugin-api';
|
||||||
|
|
||||||
import type { Facts, Joiner } from './collector.js';
|
import type { Facts, Joiner } from './collector.js';
|
||||||
import { timeAgo } from '@vrcnext/plugin-api';
|
|
||||||
|
|
||||||
import type { Rejoin } from './history.js';
|
import type { Rejoin } from './history.js';
|
||||||
import type { Settings } from './settings.js';
|
import { DEFAULT_TEMPLATE, type Settings } from './settings.js';
|
||||||
import type { CurrentInstance } from './vrcnext-data.js';
|
import type { CurrentInstance } from './vrcnext-data.js';
|
||||||
|
|
||||||
export interface Report {
|
export interface Report {
|
||||||
@@ -49,21 +47,36 @@ function rejoinText(rejoin: Rejoin): string {
|
|||||||
return rejoin.lastAt === undefined ? 'Yes' : `Yes (${timeAgo(rejoin.lastAt)})`;
|
return rejoin.lastAt === undefined ? 'Yes' : `Yes (${timeAgo(rejoin.lastAt)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The report as the spec lays it out, one fact per line. */
|
/** Everything a template may name. `undefined` means "not applicable", so its line is dropped. */
|
||||||
export function reportLines(report: Report): readonly string[] {
|
export function reportValues(report: Report): Readonly<Record<string, string | undefined>> {
|
||||||
const { facts } = report;
|
const { facts, joiner, instance } = report;
|
||||||
const age = facts.ageVerified === undefined
|
const age = facts.ageVerified === undefined
|
||||||
? 'Unknown'
|
? 'Unknown'
|
||||||
: `${yesNo(facts.ageVerified)}${facts.ageStatus === '' ? '' : ` (${facts.ageStatus})`}`;
|
: `${yesNo(facts.ageVerified)}${facts.ageStatus === '' ? '' : ` (${facts.ageStatus})`}`;
|
||||||
const lines = [
|
return {
|
||||||
`Player "${report.joiner.name}" joined`,
|
name: joiner.name,
|
||||||
`18+ Verified: ${age}`,
|
userId: joiner.userId,
|
||||||
`Avatar PC Performance Rank: ${rank(facts.avatar?.pc ?? '')}`,
|
ageVerified: age,
|
||||||
`Avatar Quest Performance Rank: ${rank(facts.avatar?.quest ?? '')}`,
|
ageStatus: facts.ageStatus,
|
||||||
];
|
pcRank: rank(facts.avatar?.pc ?? ''),
|
||||||
if (report.groupFilter !== '') lines.push(`In Group: ${yesNo(facts.inGroup, 'Unknown (only visible memberships count)')}`);
|
questRank: rank(facts.avatar?.quest ?? ''),
|
||||||
lines.push(`Rejoin?: ${rejoinText(report.facts.rejoin)}`);
|
avatar: facts.avatar?.name ?? '',
|
||||||
return lines;
|
platform: facts.platform,
|
||||||
|
inGroup: report.groupFilter === '' ? undefined : yesNo(facts.inGroup, 'Unknown (only visible memberships count)'),
|
||||||
|
rejoin: rejoinText(facts.rejoin),
|
||||||
|
rejoinAgo: facts.rejoin.lastAt === undefined ? '' : timeAgo(facts.rejoin.lastAt),
|
||||||
|
world: instance.worldName,
|
||||||
|
worldId: instance.worldId,
|
||||||
|
instanceType: instance.instanceType,
|
||||||
|
location: instance.location,
|
||||||
|
time: new Date(report.at).toLocaleTimeString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The report through the user's template, one line per entry. An empty template means the default. */
|
||||||
|
export function reportLines(report: Report, template: string = DEFAULT_TEMPLATE): readonly string[] {
|
||||||
|
const chosen = template.trim() === '' ? DEFAULT_TEMPLATE : template;
|
||||||
|
return fillTemplate(chosen, reportValues(report)).split('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Everything a single-line surface can hold. */
|
/** Everything a single-line surface can hold. */
|
||||||
@@ -114,10 +127,10 @@ async function sendNative(ctx: Ctx, wants: { readonly desktop: boolean; readonly
|
|||||||
ctx.logger.debug('The bridge has no target for the enabled channels.');
|
ctx.logger.debug('The bridge has no target for the enabled channels.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const lines = reportLines(report).slice(1);
|
const lines = reportLines(report, ctx.settings.values.template);
|
||||||
const result = await ctx.native.notify({
|
const result = await ctx.native.notify({
|
||||||
title: `${report.joiner.name} joined`,
|
title: lines[0] ?? `${report.joiner.name} joined`,
|
||||||
content: lines.join('\n'),
|
content: lines.slice(1).join('\n'),
|
||||||
timeoutSecs: 8,
|
timeoutSecs: 8,
|
||||||
icon: 'security-high',
|
icon: 'security-high',
|
||||||
sinks,
|
sinks,
|
||||||
@@ -137,9 +150,10 @@ async function sendDesktopAndVr(ctx: Ctx, values: SettingsValues<Settings>, repo
|
|||||||
|
|
||||||
// Windows without the bridge's targets: VRCNext's own tray toast and wrist overlay in one call.
|
// Windows without the bridge's targets: VRCNext's own tray toast and wrist overlay in one call.
|
||||||
if (ctx.notifications.desktopAvailable) {
|
if (ctx.notifications.desktopAvailable) {
|
||||||
|
const lines = reportLines(report, values.template);
|
||||||
ctx.notifications.desktop({
|
ctx.notifications.desktop({
|
||||||
title: `${report.joiner.name} joined`,
|
title: lines[0] ?? `${report.joiner.name} joined`,
|
||||||
subtitle: reportLines(report).slice(1).join(' · '),
|
subtitle: lines.slice(1).join(' · '),
|
||||||
accent: accentFor(report),
|
accent: accentFor(report),
|
||||||
...(report.joiner.userId === '' ? {} : { friendId: report.joiner.userId }),
|
...(report.joiner.userId === '' ? {} : { friendId: report.joiner.userId }),
|
||||||
});
|
});
|
||||||
@@ -155,7 +169,7 @@ async function sendDiscord(ctx: Ctx, values: SettingsValues<Settings>, report: R
|
|||||||
ctx.logger.warn('Discord webhook is enabled but the URL is not a discord.com webhook URL.');
|
ctx.logger.warn('Discord webhook is enabled but the URL is not a discord.com webhook URL.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lines = reportLines(report);
|
const lines = reportLines(report, values.template);
|
||||||
const colour = { ok: 0x3ba55d, warn: 0xed4245, info: 0x5865f2 }[accentFor(report)];
|
const colour = { ok: 0x3ba55d, warn: 0xed4245, info: 0x5865f2 }[accentFor(report)];
|
||||||
const body = {
|
const body = {
|
||||||
username: 'Club Security',
|
username: 'Club Security',
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ export class ReportPanel {
|
|||||||
const k = this.#ctx.ui.kit;
|
const k = this.#ctx.ui.kit;
|
||||||
if (this.#reports.length === 0) return [k.emptyState('No joins reported yet.')];
|
if (this.#reports.length === 0) return [k.emptyState('No joins reported yet.')];
|
||||||
return this.#reports.map((report) => {
|
return this.#reports.map((report) => {
|
||||||
const lines = reportLines(report);
|
const lines = reportLines(report, this.#ctx.settings.values.template);
|
||||||
const time = new Date(report.at).toLocaleTimeString();
|
const time = new Date(report.at).toLocaleTimeString();
|
||||||
return k.row({
|
return k.row({
|
||||||
label: `${time} · ${report.joiner.name}`,
|
label: `${time} · ${report.joiner.name}`,
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ export const INSTANCE_TYPES = [
|
|||||||
'group-members',
|
'group-members',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/** The report as first specified, one fact per line. */
|
||||||
|
export const DEFAULT_TEMPLATE = [
|
||||||
|
'Player "{name}" joined',
|
||||||
|
'18+ Verified: {ageVerified}',
|
||||||
|
'Avatar PC Performance Rank: {pcRank}',
|
||||||
|
'Avatar Quest Performance Rank: {questRank}',
|
||||||
|
'In Group: {inGroup}',
|
||||||
|
'Rejoin?: {rejoin}',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
export const settings = {
|
export const settings = {
|
||||||
enabled: {
|
enabled: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
@@ -68,6 +78,17 @@ export const settings = {
|
|||||||
max: 120,
|
max: 120,
|
||||||
step: 5,
|
step: 5,
|
||||||
},
|
},
|
||||||
|
template: {
|
||||||
|
kind: 'string',
|
||||||
|
multiline: true,
|
||||||
|
label: 'Report template',
|
||||||
|
description:
|
||||||
|
'The first line is the title on desktop, VR and Discord; the rest is the body. A line whose placeholders ' +
|
||||||
|
'are all empty is left out, which is how "In Group" only appears with a group filter. Placeholders: ' +
|
||||||
|
'{name} {userId} {ageVerified} {ageStatus} {pcRank} {questRank} {avatar} {platform} {inGroup} {rejoin} ' +
|
||||||
|
'{rejoinAgo} {world} {worldId} {instanceType} {location} {time}.',
|
||||||
|
default: DEFAULT_TEMPLATE,
|
||||||
|
},
|
||||||
notifyToast: {
|
notifyToast: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
label: 'In-app toast',
|
label: 'In-app toast',
|
||||||
|
|||||||
Reference in new issue
Block a user