Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12772,6 +12772,172 @@ export interface Locale extends ILocale {
* New policy
*/
"newPolicyName": string;
/**
* Comma-separated.
*/
"commaSeparated": string;
/**
* Syntax reference
*/
"reference": string;
"_reference": {
/**
* Every policy must define a global filter(ctx) function that returns a decision. An optional policy table declares configurable parameters.
*/
"contract": string;
/**
* Example
*/
"example": string;
/**
* Runs in a sandboxed Lua 5.4 VM. os, io, package, require, load and debug are unavailable. Define helpers as local; extra globals trigger persistence warnings. Execution is bounded by the policy timeout.
*/
"sandboxNote": string;
/**
* Context (ctx)
*/
"contextTitle": string;
/**
* The context passed to filter(ctx).
*/
"contextNote": string;
/**
* The raw ActivityPub activity as a table (mutate this for rewrites).
*/
"ctxActivity": string;
/**
* URI of the activity's actor.
*/
"ctxActorUri": string;
/**
* Host of the actor, or nil for local.
*/
"ctxActorHost": string;
/**
* Actor follower count, if known.
*/
"ctxFollowers": string;
/**
* Actor following count, if known.
*/
"ctxFollowing": string;
/**
* This instance's host.
*/
"ctxLocalHost": string;
/**
* Host that signed the request.
*/
"ctxSignerHost": string;
/**
* ISO timestamp of when the activity was received.
*/
"ctxReceivedAt": string;
/**
* Configured parameter values, with schema defaults applied.
*/
"ctxParams": string;
/**
* Decisions
*/
"decisionsTitle": string;
/**
* Accept the activity unchanged. Optional reason.
*/
"decAccept": string;
/**
* Reject the activity. Requires a non-empty reason (logged).
*/
"decReject": string;
/**
* Accept a modified activity. Must be JSON-serializable.
*/
"decRewrite": string;
/**
* Activity helpers
*/
"activityTitle": string;
/**
* Activity type string (e.g. Create, Announce), or nil.
*/
"actType": string;
/**
* The activity's object, or nil.
*/
"actObject": string;
/**
* Actor URI whether actor is a string or object, or nil.
*/
"actActorUri": string;
/**
* The Note object if this is a Create of a Note, else nil.
*/
"actNote": string;
/**
* Note helpers
*/
"noteTitle": string;
/**
* The note's content string, or nil.
*/
"noteContent": string;
/**
* Array of Mention tags on the note.
*/
"noteMentions": string;
/**
* Number of mentions on the note.
*/
"noteMentionCount": string;
/**
* Remove all Mention tags; returns the note.
*/
"noteRemoveMentions": string;
/**
* Mark the note sensitive; sets summary to reason if unset.
*/
"noteMarkSensitive": string;
/**
* Move Public from 'to' into 'cc' (unlists the note).
*/
"noteUnlist": string;
/**
* True if the note has at least one attachment.
*/
"noteHasMedia": string;
/**
* Lookups
*/
"lookupTitle": string;
/**
* Query the local database. These are async and their latency counts against the policy timeout.
*/
"lookupSectionNote": string;
/**
* Fetch a known user by URI, or nil.
*/
"lookupUserUri": string;
/**
* Fetch a known user by mention tag or acct string, or nil.
*/
"lookupUserMention": string;
/**
* Fetch instance metadata by host, or nil.
*/
"lookupInstance": string;
/**
* Fetch a known note by URI, or nil.
*/
"lookupNoteUri": string;
/**
* Utilities
*/
"miscTitle": string;
/**
* True if the value is Lua nil or a JSON null.
*/
"miscIsNil": string;
};
};
"_mfm": {
/**
Expand Down
12 changes: 5 additions & 7 deletions packages/frontend/src/pages/admin/mrf-policies.policy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ entry.def.label ?? entry.key }}</template>
<template v-if="entry.def.description" #caption>{{ entry.def.description }}</template>
</MkInput>
<MkInput v-else-if="entry.def.type === 'string_array'" v-model="draft.params[entry.key]">
<template #label>{{ entry.def.label ?? entry.key }}</template>
<template #caption>{{ entry.def.description ? entry.def.description + ' ' : '' }}{{ i18n.ts._mrfPolicies.commaSeparated }}</template>
</MkInput>
<MkInput v-else v-model="draft.params[entry.key]">
<template #label>{{ entry.def.label ?? entry.key }}</template>
<template v-if="entry.def.description" #caption>{{ entry.def.description }}</template>
Expand Down Expand Up @@ -210,12 +214,6 @@ const currentSchema = ref<Record<string, ParamDef>>(initialSchema);

const paramEntries = computed(() => Object.entries(currentSchema.value).map(([key, def]) => ({ key, def })));

function pruneParams(schema: Record<string, ParamDef>) {
for (const key of Object.keys(draft.params)) {
if (!Object.hasOwn(schema, key)) delete draft.params[key];
}
}

const scopeActivityTypes = ref((props.policy.scope?.activityTypes ?? []).join(', '));
const scopeObjectTypes = ref((props.policy.scope?.objectTypes ?? []).join(', '));

Expand Down Expand Up @@ -324,7 +322,7 @@ async function runTest() {
// Sync the param form to the schema the tested source actually produced, so editing
// the source live and re-testing converges instead of leaving stale param fields.
currentSchema.value = { ...(result.paramsSchema ?? {}) as Record<string, ParamDef> };
pruneParams(currentSchema.value);
draft.params = toEditableParams(currentSchema.value, { ...(result.params ?? {}) });
} catch (err: any) {
os.alert({ type: 'error', text: err.message ?? String(err) });
} finally {
Expand Down
186 changes: 186 additions & 0 deletions packages/frontend/src/pages/admin/mrf-policies.reference.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<div class="_gaps_m">
<div>
<p :class="$style.lead">{{ i18n.ts._mrfPolicies._reference.contract }}</p>
<MkCode :code="contractExample" lang="lua"/>
</div>

<section v-for="group in groups" :key="group.title" class="_gaps_s">
<div :class="$style.groupTitle">{{ group.title }}</div>
<p v-if="group.note" :class="$style.note">{{ group.note }}</p>
<table :class="$style.table">
<tbody>
<tr v-for="row in group.rows" :key="row.sig">
<td :class="$style.sig"><code>{{ row.sig }}</code></td>
<td :class="$style.desc">{{ row.desc }}</td>
</tr>
</tbody>
</table>
</section>

<div>
<div :class="$style.groupTitle">{{ i18n.ts._mrfPolicies._reference.example }}</div>
<MkCode :code="workedExample" lang="lua"/>
</div>

<MkInfo warn>{{ i18n.ts._mrfPolicies._reference.sandboxNote }}</MkInfo>
</div>
</template>

<script lang="ts" setup>
import MkCode from '@/components/MkCode.vue';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';

const t = i18n.ts._mrfPolicies._reference;

const contractExample = `-- Optional: declare configurable parameters (shown as form fields).
policy = {
\tparams = {
\t\tmax_mentions = { type = "integer", default = 5, label = "Max mentions" },
\t\tblocked_words = { type = "string_array", default = {} },
\t},
}

-- Required: filter(ctx) runs for every matching inbound activity.
function filter(ctx)
\treturn mrf.accept()
end`;

const workedExample = `-- Declare the parameter this policy reads.
policy = {
\tparams = {
\t\tmax_mentions = { type = "integer", default = 5, label = "Max mentions" },
\t},
}

function filter(ctx)
\tlocal note = mrf.activity.note(ctx.activity)
\tif note == nil then
\t\treturn mrf.accept()
\tend

\t-- Reject hellthreads.
\tif mrf.note.mention_count(note) > ctx.params.max_mentions then
\t\treturn mrf.reject("too many mentions")
\tend

\t-- Force-CW posts from brand-new remote accounts.
\tif (ctx.actor.followersCount or 0) < 1 then
\t\tmrf.note.mark_sensitive(note, "new account")
\t\treturn mrf.rewrite(ctx.activity, "cw new account")
\tend

\treturn mrf.accept()
end`;

const groups = [
{
title: t.contextTitle,
note: t.contextNote,
rows: [
{ sig: 'ctx.activity', desc: t.ctxActivity },
{ sig: 'ctx.actor.uri', desc: t.ctxActorUri },
{ sig: 'ctx.actor.host', desc: t.ctxActorHost },
{ sig: 'ctx.actor.followersCount', desc: t.ctxFollowers },
{ sig: 'ctx.actor.followingCount', desc: t.ctxFollowing },
{ sig: 'ctx.localHost', desc: t.ctxLocalHost },
{ sig: 'ctx.signerHost', desc: t.ctxSignerHost },
{ sig: 'ctx.receivedAt', desc: t.ctxReceivedAt },
{ sig: 'ctx.params.<key>', desc: t.ctxParams },
],
},
{
title: t.decisionsTitle,
rows: [
{ sig: 'mrf.accept(reason?)', desc: t.decAccept },
{ sig: 'mrf.reject(reason)', desc: t.decReject },
{ sig: 'mrf.rewrite(activity, reason?)', desc: t.decRewrite },
],
},
{
title: t.activityTitle,
rows: [
{ sig: 'mrf.activity.type(activity)', desc: t.actType },
{ sig: 'mrf.activity.object(activity)', desc: t.actObject },
{ sig: 'mrf.activity.actor_uri(activity)', desc: t.actActorUri },
{ sig: 'mrf.activity.note(activity)', desc: t.actNote },
],
},
{
title: t.noteTitle,
rows: [
{ sig: 'mrf.note.content(note)', desc: t.noteContent },
{ sig: 'mrf.note.mentions(note)', desc: t.noteMentions },
{ sig: 'mrf.note.mention_count(note)', desc: t.noteMentionCount },
{ sig: 'mrf.note.remove_mentions(note)', desc: t.noteRemoveMentions },
{ sig: 'mrf.note.mark_sensitive(note, reason?)', desc: t.noteMarkSensitive },
{ sig: 'mrf.note.unlist(note)', desc: t.noteUnlist },
{ sig: 'mrf.note.has_media(note)', desc: t.noteHasMedia },
],
},
{
title: t.lookupTitle,
note: t.lookupSectionNote,
rows: [
{ sig: 'mrf.lookup.user_by_uri(uri)', desc: t.lookupUserUri },
{ sig: 'mrf.lookup.user_by_mention(mention)', desc: t.lookupUserMention },
{ sig: 'mrf.lookup.instance_by_host(host)', desc: t.lookupInstance },
{ sig: 'mrf.lookup.note_by_uri(uri)', desc: t.lookupNoteUri },
],
},
{
title: t.miscTitle,
rows: [
{ sig: 'mrf.is_nil(value)', desc: t.miscIsNil },
],
},
];
</script>

<style lang="scss" module>
.lead {
margin: 0 0 8px;
}

.groupTitle {
font-weight: bold;
margin-bottom: 4px;
}

.note {
margin: 0 0 6px;
opacity: 0.8;
font-size: 0.9em;
}

.table {
width: 100%;
border-collapse: collapse;
}

.table td {
vertical-align: top;
padding: 4px 8px 4px 0;
border-bottom: 1px solid var(--MI_THEME-divider);
}

.sig {
white-space: nowrap;

> code {
font-family: Consolas, Monaco, monospace;
font-size: 0.9em;
}
}

.desc {
width: 100%;
opacity: 0.9;
}
</style>
Loading
Loading