instance silence
This commit is contained in:
parent
7373fc625a
commit
ba734a9f3c
12 changed files with 188 additions and 10 deletions
63
packages/backend/migration/1682844825247-InstanceSilence.js
Normal file
63
packages/backend/migration/1682844825247-InstanceSilence.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
export class InstanceSilence1682844825247 {
|
||||
name = 'InstanceSilence1682844825247'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`);
|
||||
await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`);
|
||||
await queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
}
|
|
@ -18,3 +18,20 @@ export async function shouldBlockInstance(
|
|||
(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a specific host (punycoded) should be limited.
|
||||
*
|
||||
* @param host punycoded instance host
|
||||
* @param meta a resolved Meta table
|
||||
* @returns whether the given host should be limited
|
||||
*/
|
||||
export async function shouldSilenceInstance(
|
||||
host: Instance["host"],
|
||||
meta?: Meta,
|
||||
): Promise<boolean> {
|
||||
const { silencedHosts } = meta ?? (await fetchMeta());
|
||||
return silencedHosts.some(
|
||||
(limitedHost) => host === limitedHost || host.endsWith(`.${limitedHost}`),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -97,6 +97,11 @@ export class Meta {
|
|||
})
|
||||
public blockedHosts: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, array: true, default: '{}',
|
||||
})
|
||||
public silencedHosts: string[];
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { db } from "@/db/postgre.js";
|
||||
import { Instance } from "@/models/entities/instance.js";
|
||||
import type { Packed } from "@/misc/schema.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import { shouldBlockInstance, shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||
|
||||
export const InstanceRepository = db.getRepository(Instance).extend({
|
||||
async pack(instance: Instance): Promise<Packed<"FederationInstance">> {
|
||||
const meta = await fetchMeta();
|
||||
return {
|
||||
id: instance.id,
|
||||
caughtAt: instance.caughtAt.toISOString(),
|
||||
|
@ -22,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
|
|||
isNotResponding: instance.isNotResponding,
|
||||
isSuspended: instance.isSuspended,
|
||||
isBlocked: await shouldBlockInstance(instance.host),
|
||||
isSilenced: await shouldSilenceInstance(instance.host),
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
openRegistrations: instance.openRegistrations,
|
||||
|
|
|
@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
isSilenced: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
softwareName: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
|
|
|
@ -259,6 +259,16 @@ export const meta = {
|
|||
nullable: false,
|
||||
},
|
||||
},
|
||||
silencedHosts: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
allowedHosts: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
|
@ -524,6 +534,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
customSplashIcons: instance.customSplashIcons,
|
||||
hiddenTags: instance.hiddenTags,
|
||||
blockedHosts: instance.blockedHosts,
|
||||
silencedHosts: instance.silencedHosts,
|
||||
allowedHosts: instance.allowedHosts,
|
||||
privateMode: instance.privateMode,
|
||||
secureMode: instance.secureMode,
|
||||
|
|
|
@ -61,6 +61,13 @@ export const paramDef = {
|
|||
type: "string",
|
||||
},
|
||||
},
|
||||
silencedHosts: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
allowedHosts: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
|
@ -219,6 +226,15 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.silencedHosts)) {
|
||||
let lastValue = "";
|
||||
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
|
||||
const lv = lastValue;
|
||||
lastValue = h;
|
||||
return h !== "" && h !== lv;
|
||||
});
|
||||
}
|
||||
|
||||
if (ps.themeColor !== undefined) {
|
||||
set.themeColor = ps.themeColor;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ export const paramDef = {
|
|||
notResponding: { type: "boolean", nullable: true },
|
||||
suspended: { type: "boolean", nullable: true },
|
||||
federating: { type: "boolean", nullable: true },
|
||||
silenced: { type: "boolean", nullable: true },
|
||||
subscribing: { type: "boolean", nullable: true },
|
||||
publishing: { type: "boolean", nullable: true },
|
||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 30 },
|
||||
|
@ -115,6 +116,22 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof ps.silenced === "boolean") {
|
||||
const meta = await fetchMeta(true);
|
||||
if (ps.silenced) {
|
||||
if (meta.silencedHosts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
query.andWhere("instance.host IN (:...silences)", {
|
||||
silences: meta.silencedHosts,
|
||||
});
|
||||
} else if (meta.silencedHosts.length > 0) {
|
||||
query.andWhere("instance.host NOT IN (:...silences)", {
|
||||
silences: meta.silencedHosts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ps.notResponding === "boolean") {
|
||||
if (ps.notResponding) {
|
||||
query.andWhere("instance.isNotResponding = TRUE");
|
||||
|
|
|
@ -27,6 +27,7 @@ import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js
|
|||
import type { Packed } from "@/misc/schema.js";
|
||||
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
||||
import { webhookDeliver } from "@/queue/index.js";
|
||||
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||
|
||||
const logger = new Logger("following/create");
|
||||
|
||||
|
@ -227,12 +228,14 @@ export default async function (
|
|||
|
||||
// フォロー対象が鍵アカウントである or
|
||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
||||
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
|
||||
// The follower is remote, the followee is local, and the follower is in a silenced instance.
|
||||
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
||||
if (
|
||||
followee.isLocked ||
|
||||
(followeeProfile.carefulBot && follower.isBot) ||
|
||||
(Users.isLocalUser(follower) && Users.isRemoteUser(followee))
|
||||
(Users.isLocalUser(follower) && Users.isRemoteUser(followee)) ||
|
||||
(Users.isRemoteUser(follower) && Users.isLocalUser(followee) && await shouldSilenceInstance(follower.host))
|
||||
) {
|
||||
let autoAccept = false;
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ import {
|
|||
} from "@/models/index.js";
|
||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import type { App } from "@/models/entities/app.js";
|
||||
import { Not, In } from "typeorm";
|
||||
import { Not, In, IsNull } from "typeorm";
|
||||
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
|
||||
import { genId } from "@/misc/gen-id.js";
|
||||
import {
|
||||
|
@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js";
|
|||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
||||
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||
|
||||
const mutedWordsCache = new Cache<
|
||||
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
|
||||
|
@ -166,7 +167,8 @@ export default async (
|
|||
data: Option,
|
||||
silent = false,
|
||||
) =>
|
||||
new Promise<Note>(async (res, rej) => {
|
||||
// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
|
||||
new Promise<Note>(async (res, rej) => {
|
||||
// If you reply outside the channel, match the scope of the target.
|
||||
// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
|
||||
if (
|
||||
|
@ -203,6 +205,13 @@ export default async (
|
|||
data.visibility = "home";
|
||||
}
|
||||
|
||||
const inSilencedInstance = Users.isRemoteUser(user) && await shouldSilenceInstance(user.host);
|
||||
|
||||
// If the
|
||||
if (data.visibility === "public" && inSilencedInstance) {
|
||||
data.visibility = "home";
|
||||
}
|
||||
|
||||
// Reject if the target of the renote is a public range other than "Home or Entire".
|
||||
if (
|
||||
data.renote &&
|
||||
|
@ -307,6 +316,14 @@ export default async (
|
|||
}
|
||||
}
|
||||
|
||||
// Remove from mention the local users who aren't following the remote user in the silenced instance.
|
||||
if (inSilencedInstance) {
|
||||
const relations = await Followings.findBy([
|
||||
{ followeeId: user.id, followerHost: IsNull() }, // a local user following the silenced user
|
||||
]).then(rels => rels.map(rel => rel.followerId));
|
||||
mentionedUsers = mentionedUsers.filter(mentioned => relations.includes(mentioned.id));
|
||||
}
|
||||
|
||||
const note = await insertNote(user, data, tags, emojis, mentionedUsers);
|
||||
|
||||
res(note);
|
||||
|
|
|
@ -55,6 +55,7 @@ export type Endpoints = {
|
|||
"admin/get-table-stats": { req: TODO; res: TODO };
|
||||
"admin/invite": { req: TODO; res: TODO };
|
||||
"admin/logs": { req: TODO; res: TODO };
|
||||
"admin/meta": { req: TODO; res: TODO };
|
||||
"admin/reset-password": { req: TODO; res: TODO };
|
||||
"admin/resolve-abuse-user-report": { req: TODO; res: TODO };
|
||||
"admin/resync-chart": { req: TODO; res: TODO };
|
||||
|
|
|
@ -2,18 +2,25 @@
|
|||
<MkStickyContainer>
|
||||
<template #header
|
||||
><MkPageHeader
|
||||
v-model:tab="tab"
|
||||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
:display-back-button="true"
|
||||
/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormTextarea v-model="blockedHosts" class="_formBlock">
|
||||
<FormTextarea v-if="tab === 'block'" v-model="blockedHosts" class="_formBlock">
|
||||
<span>{{ i18n.ts.blockedInstances }}</span>
|
||||
<template #caption>{{
|
||||
i18n.ts.blockedInstancesDescription
|
||||
}}</template>
|
||||
</FormTextarea>
|
||||
<FormTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock">
|
||||
<span>{{ i18n.ts.silencedInstances }}</span>
|
||||
<template #caption>{{
|
||||
i18n.ts.silencedInstancesDescription
|
||||
}}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"
|
||||
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
|
||||
|
@ -35,15 +42,21 @@ import { i18n } from "@/i18n";
|
|||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
||||
let blockedHosts: string = $ref("");
|
||||
let silencedHosts: string = $ref("");
|
||||
let tab = $ref("block");
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api("admin/meta");
|
||||
blockedHosts = meta.blockedHosts.join("\n");
|
||||
if (meta) {
|
||||
blockedHosts = meta.blockedHosts.join("\n");
|
||||
silencedHosts = meta.silencedHosts.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog("admin/update-meta", {
|
||||
blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
|
||||
silencedHosts: silencedHosts.split("\n").map((h) => h.trim()) || [],
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
@ -51,7 +64,18 @@ function save() {
|
|||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
const headerTabs = $computed(() => [
|
||||
{
|
||||
key: "block",
|
||||
title: i18n.ts.block,
|
||||
icon: "ph-prohibit ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "silence",
|
||||
title: i18n.ts.silence,
|
||||
icon: "ph-eye-slash ph-bold ph-lg",
|
||||
},
|
||||
]);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.instanceBlocking,
|
||||
|
|
Loading…
Reference in a new issue