Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
396a170
fix(backend): purge user profile cache when updating settings
PrivateGER Jun 12, 2026
df5b09c
fix(frontend): refresh stale instance meta within 10s instead of 101s
PrivateGER Jun 12, 2026
e582a9c
fix(backend): restore RSS feeds for legacy unapproved local users
PrivateGER Jun 12, 2026
cd1bcd4
fix(backend): bust user cache after admin avatar/banner unset
PrivateGER Jun 12, 2026
3d813c3
Add Lua MRF policy runtime
PrivateGER Jun 17, 2026
a1e9799
Add DB-backed MRF policy management
PrivateGER Jun 17, 2026
caa14a8
Run inbox MRF policies from Lua database rules
PrivateGER Jun 17, 2026
e92ca3f
fix(backend): harden Lua MRF policy basics
PrivateGER Jun 17, 2026
7fde99a
fix(backend): scope Lua MRF policy execution
PrivateGER Jun 18, 2026
d0a886e
fix(backend): tolerate stale Lua MRF params
PrivateGER Jun 18, 2026
7d28d56
fix(backend): make Lua MRF failures fail open
PrivateGER Jun 18, 2026
ddcff09
perf(backend): pool Lua MRF policy engines
PrivateGER Jun 18, 2026
1a77146
feat(backend): warn on Lua MRF persistent globals
PrivateGER Jun 18, 2026
d53fb46
fix(activitypub): correctly access JsonLd instance when signing relay…
claude Jun 21, 2026
bcde1f2
Merge pull request #35 from PrivateGER/claude/eloquent-franklin-jmw7ci
PrivateGER Jun 21, 2026
6949092
fix(backend): remove Lua MRF threads from the engine stack after use
PrivateGER Jul 2, 2026
fd443e9
fix(backend): do not discard Lua MRF decisions on snapshot failure
PrivateGER Jul 2, 2026
c00bcc1
fix(backend): reject non-JSON Lua MRF rewrite payloads
PrivateGER Jul 2, 2026
280da98
fix(backend): require a filter function in Lua MRF policy sources
PrivateGER Jul 2, 2026
826e3d0
refactor(backend): provide MrfLuaPolicyService as a DI singleton
PrivateGER Jul 2, 2026
db786d9
fix(backend): return structured errors for invalid Lua MRF sources
PrivateGER Jul 2, 2026
391f79e
feat(backend): log MRF policy mutations to the moderation log
PrivateGER Jul 2, 2026
bcd6764
perf(backend): cache enabled MRF policies for 10s
PrivateGER Jul 2, 2026
e2e73a3
fix(backend): correct Lua MRF unlist helper and local-host prefix check
PrivateGER Jul 2, 2026
81e8faf
refactor(backend): tighten MRF endpoint typing and document Lua limits
PrivateGER Jul 2, 2026
14bf0f9
feat(backend): add response schemas to MRF policy endpoints
PrivateGER Jul 3, 2026
d7bde53
fix(backend): satisfy typecheck and lint in MRF policy endpoints
PrivateGER Jul 3, 2026
dc7dbc5
merge: Lua MRF hardening (fix/lua-mrf-hardening)
PrivateGER Jul 3, 2026
8ed1d03
merge: origin/develop (relay signing fix)
PrivateGER Jul 3, 2026
d2f511a
Add emoji suggestion backend workflow
PrivateGER Aug 12, 2026
d514a6d
Add emoji suggestion review interface
PrivateGER Aug 12, 2026
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
3 changes: 3 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2734,6 +2734,9 @@ _moderationLogTypes:
createInvitation: "Invite generated"
createAd: "Ad created"
deleteAd: "Ad deleted"
createMrfPolicy: "MRF policy created"
updateMrfPolicy: "MRF policy updated"
deleteMrfPolicy: "MRF policy deleted"
updateAd: "Ad updated"
createAvatarDecoration: "Avatar decoration created"
updateAvatarDecoration: "Avatar decoration updated"
Expand Down
32 changes: 32 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12072,6 +12072,38 @@ export interface Locale extends ILocale {
* Approvals
*/
"approvals": string;
/**
* Emoji suggestions
*/
"emojiSuggestions": string;
/**
* Suggest an emoji
*/
"suggestEmoji": string;
/**
* Propose a custom emoji for this server. Your image remains in your Drive while moderators review it.
*/
"emojiSuggestionDescription": string;
/**
* There are no pending emoji suggestions.
*/
"emojiSuggestionNoPending": string;
/**
* Proposed by
*/
"emojiSuggestionProposedBy": string;
/**
* Accept :{name}: and add it to this server?
*/
"emojiSuggestionConfirmAccept": ParameterizedString<"name">;
/**
* Reject the suggestion for :{name}:? The image will remain in the proposer's Drive.
*/
"emojiSuggestionConfirmReject": ParameterizedString<"name">;
/**
* Cancel your suggestion for :{name}:? The image will remain in your Drive.
*/
"emojiSuggestionConfirmCancel": ParameterizedString<"name">;
/**
* Open remote profile
*/
Expand Down
198 changes: 198 additions & 0 deletions packages/backend/migration/1781706597000-MrfPolicy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class MrfPolicy1781706597000 {
name = 'MrfPolicy1781706597000'

async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "mrf_policy" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(256) NOT NULL, "enabled" boolean NOT NULL DEFAULT true, "priority" integer NOT NULL DEFAULT '1000', "source" text NOT NULL, "timeoutMs" integer NOT NULL DEFAULT '50', "scope" jsonb NOT NULL DEFAULT '{"activityTypes":["Create"],"objectTypes":["Note"]}', "isBuiltin" boolean NOT NULL DEFAULT false, "builtinPolicyId" character varying(128), "paramsSchema" jsonb NOT NULL DEFAULT '{}', "params" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_mrf_policy" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_mrf_policy_enabled_priority" ON "mrf_policy" ("enabled", "priority")`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_mrf_policy_builtinPolicyId" ON "mrf_policy" ("builtinPolicyId")`);
await queryRunner.query(`INSERT INTO "mrf_policy" ("id", "name", "enabled", "priority", "source", "timeoutMs", "isBuiltin", "builtinPolicyId", "paramsSchema", "params", "scope") VALUES ($1, $2, true, 10, $3, 50, true, $4, $5::jsonb, $6::jsonb, $7::jsonb)`, [
'mrfbuiltinkeyword001',
'Keyword filter',
`
policy = {
params = {
keywords = {
type = "string_array",
default = {
"https://discord.gg/ctkpaarr",
"@ap12@mastodon-japan.net",
"ctkpaarr",
},
label = "Blocked keywords",
},
},
}

function filter(ctx)
local note = mrf.activity.note(ctx.activity)
if note == nil then
return mrf.accept()
end

local content = mrf.note.content(note)
if type(content) ~= "string" then
return mrf.accept()
end

for _, keyword in ipairs(ctx.params.keywords) do
if string.find(content, keyword, 1, true) ~= nil then
return mrf.reject("keyword filter matched: " .. keyword)
end
end

return mrf.accept()
end
`,
'keyword-filter',
JSON.stringify({
keywords: {
type: 'string_array',
default: [
'https://discord.gg/ctkpaarr',
'@ap12@mastodon-japan.net',
'ctkpaarr',
],
label: 'Blocked keywords',
},
}),
'{}',
JSON.stringify({
activityTypes: ['Create'],
objectTypes: ['Note'],
}),
]);
await queryRunner.query(`INSERT INTO "mrf_policy" ("id", "name", "enabled", "priority", "source", "timeoutMs", "isBuiltin", "builtinPolicyId", "paramsSchema", "params", "scope") VALUES ($1, $2, true, 20, $3, 50, true, $4, $5::jsonb, $6::jsonb, $7::jsonb)`, [
'mrfbuiltinnewuserspam001',
'New user spam mention filter',
`
policy = {
params = {
maxFollowers = {
type = "integer",
default = 0,
label = "Maximum followers",
},
maxFollowing = {
type = "integer",
default = 0,
label = "Maximum following",
},
onlyTopLevelPosts = {
type = "boolean",
default = true,
label = "Only top-level posts",
},
},
}

function filter(ctx)
local note = mrf.activity.note(ctx.activity)
if note == nil then
return mrf.accept()
end

local mentions = mrf.note.mentions(note)
if #mentions == 0 then
return mrf.accept()
end

local local_prefix = "https://" .. ctx.localHost .. "/"
local has_local_mention = false
for _, mention in ipairs(mentions) do
if type(mention.href) == "string" and string.sub(mention.href, 1, #local_prefix) == local_prefix then
has_local_mention = true
break
end
end

if not has_local_mention then
return mrf.accept()
end

if (ctx.actor.followersCount or 0) <= ctx.params.maxFollowers and (ctx.actor.followingCount or 0) <= ctx.params.maxFollowing and (not ctx.params.onlyTopLevelPosts or mrf.is_nil(note.inReplyTo)) then
mrf.note.remove_mentions(note)
return mrf.rewrite(ctx.activity, "stripped unsolicited local mentions from new remote actor")
end

return mrf.accept()
end
`,
'new-user-spam',
JSON.stringify({
maxFollowers: {
type: 'integer',
default: 0,
label: 'Maximum followers',
},
maxFollowing: {
type: 'integer',
default: 0,
label: 'Maximum following',
},
onlyTopLevelPosts: {
type: 'boolean',
default: true,
label: 'Only top-level posts',
},
}),
'{}',
JSON.stringify({
activityTypes: ['Create'],
objectTypes: ['Note'],
}),
]);
await queryRunner.query(`INSERT INTO "mrf_policy" ("id", "name", "enabled", "priority", "source", "timeoutMs", "isBuiltin", "builtinPolicyId", "paramsSchema", "params", "scope") VALUES ($1, $2, true, 30, $3, 50, true, $4, $5::jsonb, $6::jsonb, $7::jsonb)`, [
'mrfbuiltinhellthread001',
'Hellthread mention filter',
`
policy = {
params = {
mentionThreshold = {
type = "integer",
default = 15,
label = "Mention threshold",
},
},
}

function filter(ctx)
local note = mrf.activity.note(ctx.activity)
if note == nil then
return mrf.accept()
end

if mrf.note.mention_count(note) >= ctx.params.mentionThreshold then
mrf.note.remove_mentions(note)
return mrf.rewrite(ctx.activity, "stripped hellthread mentions")
end

return mrf.accept()
end
`,
'hellthread',
JSON.stringify({
mentionThreshold: {
type: 'integer',
default: 15,
label: 'Mention threshold',
},
}),
'{}',
JSON.stringify({
activityTypes: ['Create'],
objectTypes: ['Note'],
}),
]);
}

async down(queryRunner) {
await queryRunner.query(`DROP INDEX "IDX_mrf_policy_builtinPolicyId"`);
await queryRunner.query(`DROP INDEX "IDX_mrf_policy_enabled_priority"`);
await queryRunner.query(`DROP TABLE "mrf_policy"`);
}
}
34 changes: 34 additions & 0 deletions packages/backend/migration/1786507200000-EmojiSuggestion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
export class EmojiSuggestion1786507200000 {
name = 'EmojiSuggestion1786507200000'

Comment on lines +15 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the missing semicolon.

ESLint reports @stylistic/semi at Line 16. Lint will fail in CI.

🔧 Proposed fix
-	name = 'EmojiSuggestion1786507200000'
+	name = 'EmojiSuggestion1786507200000';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export class EmojiSuggestion1786507200000 {
name = 'EmojiSuggestion1786507200000'
export class EmojiSuggestion1786507200000 {
name = 'EmojiSuggestion1786507200000';
🧰 Tools
🪛 ESLint

[error] 16-17: Missing semicolon.

(@stylistic/semi)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/migration/1786507200000-EmojiSuggestion.js` around lines 15
- 17, Add the required semicolon to the name property declaration in the
EmojiSuggestion1786507200000 class, satisfying the `@stylistic/semi` lint rule
without changing its value or surrounding migration logic.

Source: Linters/SAST tools

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "emoji_suggestion" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "fileId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "category" character varying(128), "aliases" character varying(128) array NOT NULL DEFAULT '{}', "license" character varying(1024), "localOnly" boolean NOT NULL DEFAULT false, "isSensitive" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_emoji_suggestion" PRIMARY KEY ("id"), CONSTRAINT "FK_emoji_suggestion_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_emoji_suggestion_file" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE INDEX "IDX_emoji_suggestion_user" ON "emoji_suggestion" ("userId")`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_emoji_suggestion_file" ON "emoji_suggestion" ("fileId")`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_emoji_suggestion_name" ON "emoji_suggestion" ("name")`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`DROP TABLE "emoji_suggestion"`);
}
}
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
"ulid": "3.0.1",
"uuid": "13.0.0",
"vary": "1.1.2",
"wasmoon": "1.16.0",
"web-push": "3.6.7",
"ws": "8.19.0"
},
Expand Down
7 changes: 5 additions & 2 deletions packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,8 +789,11 @@ export class CacheService implements OnApplicationShutdown {
this.userRelationsCache.dropMany(relationKeysToClear);
}

// Update the profile cache for local events only
// (isLocal may be undefined for local events, but will *always* be true for remote ones)
// Drop the cached profile. This runs for both local and remote events:
// isLocal is undefined for local events and false for remote ones, so the condition only skips
// callers that explicitly mark the event as local-only.
// For local events the quantum delete also syncs the purge to other processes; for remote events
// it is a redundant (but harmless) second purge.
if (ctx.isLocal !== true) {
await this.userProfileCache.delete(body.userId);
}
Expand Down
Loading
Loading