Merge pull request 'feat: Suppress notifications by silenced accounts and instances' (#9965) from nmkj/calckey:instance-silence into develop

Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9965
This commit is contained in:
Kainoa Kanter 2023-05-01 02:18:37 +00:00
commit 34a6e1caa4
21 changed files with 388 additions and 26 deletions

View File

@ -197,6 +197,7 @@ perHour: "Per Hour"
perDay: "Per Day"
stopActivityDelivery: "Stop sending activities"
blockThisInstance: "Block this instance"
silenceThisInstance: "Silence this instance"
operations: "Operations"
software: "Software"
version: "Version"
@ -218,10 +219,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to\
\ block. Listed instances will no longer be able to communicate with this instance."
silencedInstances: "Silenced Instances"
silencedInstancesDescription: "List the hostnames of the instances that you want to\
\ silence. Accounts in the listed instances are treated as \"Silenced\", can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
hiddenTags: "Hidden Hashtags"
hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
\ to hide from trending and explore. Hidden hashtags are still discoverable via\
\ other means."
\ other means. Blocked instances are not affected even if listed here."
muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users"
blockedUsers: "Blocked users"
@ -240,6 +244,7 @@ noCustomEmojis: "There are no emoji"
noJobs: "There are no jobs"
federating: "Federating"
blocked: "Blocked"
silenced: "Silenced"
suspended: "Suspended"
all: "All"
subscribing: "Subscribing"
@ -829,7 +834,7 @@ active: "Active"
offline: "Offline"
notRecommended: "Not recommended"
botProtection: "Bot Protection"
instanceBlocking: "Blocked Instances"
instanceBlocking: "Federation Block/Silence"
selectAccount: "Select account"
switchAccount: "Switch account"
enabled: "Enabled"

View File

@ -183,6 +183,7 @@ perHour: "1時間ごと"
perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このインスタンスをブロック"
silenceThisInstance: "このインスタンスをサイレンス"
operations: "操作"
software: "ソフトウェア"
version: "バージョン"
@ -202,6 +203,8 @@ clearCachedFiles: "キャッシュをクリア"
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
blockedInstances: "ブロックしたインスタンス"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたインスタンス"
silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
@ -220,6 +223,7 @@ noCustomEmojis: "絵文字はありません"
noJobs: "ジョブはありません"
federating: "連合中"
blocked: "ブロック中"
silenced: "サイレンス中"
suspended: "配信停止"
all: "全て"
subscribing: "購読中"
@ -768,7 +772,7 @@ active: "アクティブ"
offline: "オフライン"
notRecommended: "非推奨"
botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック"
instanceBlocking: "連合ブロック・サイレンス"
selectAccount: "アカウントを選択"
switchAccount: "アカウントを切り替え"
enabled: "有効"

View File

@ -0,0 +1,165 @@
export class InstanceSilence1682891890317 {
name = "InstanceSilence1682891890317";
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`,
);
}
}

View File

@ -18,3 +18,21 @@ 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(
(silencedHost) =>
host === silencedHost || host.endsWith(`.${silencedHost}`),
);
}

View File

@ -97,6 +97,11 @@ export class Meta {
})
public blockedHosts: string[];
@Column('varchar', {
length: 256, array: true, default: '{}',
})
public silencedHosts: string[];
@Column('boolean', {
default: false,
})

View File

@ -1,12 +1,13 @@
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 +23,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,

View File

@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = {
optional: false,
nullable: false,
},
isSilenced: {
type: "boolean",
optional: false,
nullable: false,
},
softwareName: {
type: "string",
optional: false,

View File

@ -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,

View File

@ -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;
}

View File

@ -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");

View File

@ -6,11 +6,13 @@ import {
NoteThreadMutings,
UserProfiles,
Users,
Followings,
} from "@/models/index.js";
import { genId } from "@/misc/gen-id.js";
import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js";
import { sendEmailNotification } from "./send-email-notification.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
export async function createNotification(
notifieeId: User["id"],
@ -21,6 +23,26 @@ export async function createNotification(
return null;
}
if (
data.notifierId &&
["mention", "reply", "renote", "quote", "reaction"].includes(type)
) {
const notifier = await Users.findOneBy({ id: data.notifierId });
// suppress if the notifier does not exist or is silenced.
if (!notifier) return null;
// suppress if the notifier is silenced or in a silenced instance, and not followed by the notifiee.
if (
(notifier.isSilenced ||
(Users.isRemoteUser(notifier) &&
(await shouldSilenceInstance(notifier.host)))) &&
!(await Followings.exist({
where: { followerId: notifieeId, followeeId: data.notifierId },
}))
)
return null;
}
const profile = await UserProfiles.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type);

View File

@ -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");
@ -226,13 +227,19 @@ export default async function (
});
// フォロー対象が鍵アカウントである or
// The follower is silenced, or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
// The follower is remote, the followee is local, and the follower is in a silenced instance.
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if (
followee.isLocked ||
follower.isSilenced ||
(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;

View File

@ -80,7 +80,13 @@ export default async function (
}
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderFollow(follower, followee, requestId ?? `${config.url}/follows/${followRequest.id}`));
const content = renderActivity(
renderFollow(
follower,
followee,
requestId ?? `${config.url}/follows/${followRequest.id}`,
),
);
deliver(follower, content, followee.inbox);
}
}

View File

@ -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,6 +167,7 @@ export default async (
data: Option,
silent = false,
) =>
// 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.)
@ -203,6 +205,15 @@ export default async (
data.visibility = "home";
}
// Enforce home visibility if the user is in a silenced instance.
if (
data.visibility === "public" &&
Users.isRemoteUser(user) &&
(await shouldSilenceInstance(user.host))
) {
data.visibility = "home";
}
// Reject if the target of the renote is a public range other than "Home or Entire".
if (
data.renote &&

View File

@ -118,7 +118,7 @@ export default async (
userId: user.id,
});
// リアクションされたユーザーがローカルユーザーなら通知を作成
// Create notification if the reaction target is a local user.
if (note.userHost === null) {
createNotification(note.userId, "reaction", {
notifierId: user.id,
@ -143,7 +143,7 @@ export default async (
}
});
//#region 配信
//#region deliver
if (Users.isLocalUser(user) && !note.localOnly) {
const content = renderActivity(await renderLike(record, note));
const dm = new DeliverManager(user, content);

View File

@ -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 };

View File

@ -5,6 +5,7 @@
{
yellow: instance.isNotResponding,
red: instance.isBlocked,
purple: instance.isSilenced,
gray: instance.isSuspended,
},
]"
@ -23,13 +24,13 @@
</template>
<script lang="ts" setup>
import * as misskey from "calckey-js";
import * as calckey from "calckey-js";
import MkMiniChart from "@/components/MkMiniChart.vue";
import * as os from "@/os";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
const props = defineProps<{
instance: misskey.entities.Instance;
instance: calckey.entities.Instance;
}>();
let chartValues = $ref<number[] | null>(null);
@ -135,6 +136,21 @@ function getInstanceIcon(instance): string {
background-size: 16px 16px;
}
&:global(.purple) {
--c: rgba(196, 0, 255, 0.15);
background-image: linear-gradient(
45deg,
var(--c) 16.67%,
transparent 16.67%,
transparent 50%,
var(--c) 50%,
var(--c) 66.67%,
transparent 66.67%,
transparent 100%
);
background-size: 16px 16px;
}
&:global(.gray) {
--c: var(--bg);
background-image: linear-gradient(

View File

@ -18,6 +18,7 @@
<option value="publishing">{{ i18n.ts.publishing }}</option>
<option value="suspended">{{ i18n.ts.suspended }}</option>
<option value="blocked">{{ i18n.ts.blocked }}</option>
<option value="silenced">{{ i18n.ts.silenced }}</option>
<option value="notResponding">
{{ i18n.ts.notResponding }}
</option>
@ -105,13 +106,11 @@
<script lang="ts" setup>
import { computed } from "vue";
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue";
import MkSelect from "@/components/form/select.vue";
import MkPagination from "@/components/MkPagination.vue";
import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue";
import FormSplit from "@/components/form/split.vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
let host = $ref("");
@ -134,6 +133,8 @@ const pagination = {
? { suspended: true }
: state === "blocked"
? { blocked: true }
: state === "silenced"
? { silenced: true }
: state === "notResponding"
? { notResponding: true }
: {}),
@ -143,6 +144,7 @@ const pagination = {
function getStatus(instance) {
if (instance.isSuspended) return "Suspended";
if (instance.isBlocked) return "Blocked";
if (instance.isSilenced) return "Silenced";
if (instance.isNotResponding) return "Error";
return "Alive";
}

View File

@ -3,7 +3,6 @@
<MkStickyContainer>
<template #header
><MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
:display-back-button="true"

View File

@ -7,13 +7,31 @@
:display-back-button="true"
/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<MkTab v-model="tab" class="_formBlock">
<option value="block">{{ i18n.ts.blockedInstances }}</option>
<option value="silence">{{ i18n.ts.silencedInstances }}</option>
</MkTab>
<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>
@ -29,21 +47,28 @@ import {} from "vue";
import FormButton from "@/components/MkButton.vue";
import FormTextarea from "@/components/form/textarea.vue";
import FormSuspense from "@/components/form/suspense.vue";
import MkTab from "@/components/MkTab.vue";
import * as os from "@/os";
import { fetchInstance } from "@/instance";
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();
});

View File

@ -98,6 +98,14 @@
@update:modelValue="toggleBlock"
>{{ i18n.ts.blockThisInstance }}</FormSwitch
>
<FormSwitch
v-model="isSilenced"
class="_formBlock"
@update:modelValue="toggleSilence"
>{{
i18n.ts.silenceThisInstance
}}</FormSwitch
>
</FormSuspense>
<MkButton @click="refreshMetadata"
><i
@ -329,7 +337,7 @@
import { watch } from "vue";
import { Virtual } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue";
import type * as misskey from "calckey-js";
import type * as calckey from "calckey-js";
import MkChart from "@/components/MkChart.vue";
import MkObjectView from "@/components/MkObjectView.vue";
import FormLink from "@/components/form/link.vue";
@ -352,11 +360,13 @@ import "swiper/scss";
import "swiper/scss/virtual";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
type AugmentedInstanceMetadata = calckey.entities.DetailedInstanceMetadata & {
blockedHosts: string[];
silencedHosts: string[];
};
type AugmentedInstance = misskey.entities.Instance & {
type AugmentedInstance = calckey.entities.Instance & {
isBlocked: boolean;
isSilenced: boolean;
};
const props = defineProps<{
@ -373,6 +383,7 @@ let meta = $ref<AugmentedInstanceMetadata | null>(null);
let instance = $ref<AugmentedInstance | null>(null);
let suspended = $ref(false);
let isBlocked = $ref(false);
let isSilenced = $ref(false);
let faviconUrl = $ref(null);
const usersPagination = {
@ -386,16 +397,14 @@ const usersPagination = {
offsetMode: true,
};
async function init() {
meta = await os.api("admin/meta");
}
async function fetch() {
meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
instance = (await os.api("federation/show-instance", {
host: props.host,
})) as AugmentedInstance;
suspended = instance.isSuspended;
isBlocked = instance.isBlocked;
isSilenced = instance.isSilenced;
faviconUrl =
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
getProxiedImageUrlNullable(instance.iconUrl, "preview");
@ -417,6 +426,22 @@ async function toggleBlock() {
});
}
async function toggleSilence() {
if (meta == null) return;
if (!instance) {
throw new Error(`Instance info not loaded`);
}
let silencedHosts: string[];
if (isSilenced) {
silencedHosts = meta.silencedHosts.concat([instance.host]);
} else {
silencedHosts = meta.silencedHosts.filter((x) => x !== instance!.host);
}
await os.api("admin/update-meta", {
silencedHosts,
});
}
async function toggleSuspend(v) {
await os.api("admin/federation/update-instance", {
host: instance.host,