diff --git a/CHANGELOG.md b/CHANGELOG.md index 2705d33fb5..aba43b9e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## Unreleased + +### General +- + +### Client +- + +### Server +- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775` +- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727` + ## 2025.4.1 ### General diff --git a/locales/index.d.ts b/locales/index.d.ts index 762a863446..b65bfe4e67 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -898,6 +898,10 @@ export interface Locale extends ILocale { * ソフトウェア */ "software": string; + /** + * ソフトウェア名 + */ + "softwareName": string; /** * バージョン */ @@ -5879,6 +5883,10 @@ export interface Locale extends ILocale { * サーバー応答なしのため停止中 */ "autoSuspendedForNotResponding": string; + /** + * 配信停止中のソフトウェアであるため停止中 + */ + "softwareSuspended": string; }; }; "_bubbleGame": { @@ -6364,6 +6372,14 @@ export interface Locale extends ILocale { * 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。 */ "thisSettingWillAutomaticallyOffWhenModeratorsInactive": string; + /** + * 配信停止中のソフトウェア + */ + "deliverSuspendedSoftware": string; + /** + * 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。 + */ + "deliverSuspendedSoftwareDescription": string; }; "_accountMigration": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7931d1faef..f9fa456a77 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -220,6 +220,7 @@ silenceThisInstance: "サーバーをサイレンス" mediaSilenceThisInstance: "サーバーをメディアサイレンス" operations: "操作" software: "ソフトウェア" +softwareName: "ソフトウェア名" version: "バージョン" metadata: "メタデータ" withNFiles: "{n}つのファイル" @@ -1479,6 +1480,7 @@ _delivery: manuallySuspended: "手動停止中" goneSuspended: "サーバー削除のため停止中" autoSuspendedForNotResponding: "サーバー応答なしのため停止中" + softwareSuspended: "配信停止中のソフトウェアであるため停止中" _bubbleGame: howToPlay: "遊び方" @@ -1617,6 +1619,8 @@ _serverSettings: openRegistration: "アカウントの作成をオープンにする" openRegistrationWarning: "登録を開放することはリスクが伴います。サーバーを常に監視し、トラブルが発生した際にすぐに対応できる体制がある場合のみオンにすることを推奨します。" thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。" + deliverSuspendedSoftware: "配信停止中のソフトウェア" + deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" diff --git a/package.json b/package.json index e2f2d62e00..8649d1eac7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2025.4.1-rc.0", + "version": "2025.4.1", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js b/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js new file mode 100644 index 0000000000..19983a72bd --- /dev/null +++ b/packages/backend/migration/1743403874305-DeliverSuspendedSoftware.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class DeliverSuspendedSoftware1743403874305 { + name = 'DeliverSuspendedSoftware1743403874305' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "deliverSuspendedSoftware" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deliverSuspendedSoftware"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index c4de44df18..3c6dcc6523 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -169,6 +169,7 @@ "sanitize-html": "2.16.0", "secure-json-parse": "3.0.2", "sharp": "0.34.1", + "semver": "7.7.1", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 1ffeb4b3a4..6253f792ed 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -36,6 +36,7 @@ type TimelineOptions = { excludeNoFiles?: boolean; excludeReplies?: boolean; excludePureRenotes: boolean; + ignoreAuthorFromUserSuspension?: boolean; dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, }; @@ -139,6 +140,23 @@ export class FanoutTimelineEndpointService { }; } + { + const parentFilter = filter; + filter = (note) => { + const noteJoined = note as MiNote & { + renoteUser: MiUser | null; + replyUser: MiUser | null; + }; + if (!ps.ignoreAuthorFromUserSuspension) { + if (note.user!.isSuspended) return false; + } + if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false; + if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false; + + return parentFilter(note); + }; + } + const redisTimeline: MiNote[] = []; let readFromRedis = 0; let lastSuccessfulRate = 1; // rateをキャッシュする? diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 119eb49c02..e219efaf3d 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -287,4 +287,26 @@ export class QueryService { .andWhere(instanceSuspension('renoteUser')); } } + + // Requirements: user replyUser renoteUser must be joined + @bindThis + public generateSuspendedUserQueryForNote(q: SelectQueryBuilder, excludeAuthor?: boolean): void { + if (excludeAuthor) { + const brakets = (user: string) => new Brackets(qb => qb + .where(`note.${user}Id IS NULL`) + .orWhere(`user.id = ${user}.id`) + .orWhere(`${user}.isSuspended = FALSE`)); + q + .andWhere(brakets('replyUser')) + .andWhere(brakets('renoteUser')); + } else { + const brakets = (user: string) => new Brackets(qb => qb + .where(`note.${user}Id IS NULL`) + .orWhere(`${user}.isSuspended = FALSE`)); + q + .andWhere('user.isSuspended = FALSE') + .andWhere(brakets('replyUser')) + .andWhere(brakets('renoteUser')); + } + } } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index d94281920e..20a776ded8 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -235,6 +235,7 @@ export class SearchService { this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); @@ -297,11 +298,17 @@ export class SearchService { ]) : [new Set(), new Set()]; - const query = this.notesRepository.createQueryBuilder('note'); + const query = this.notesRepository.createQueryBuilder('note') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); query.where('note.id IN (:...noteIds)', { noteIds: res.hits.map(x => x.id) }); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); const notes = (await query.getMany()).filter(note => { if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 23fb928ac9..67ec6cc7b0 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -6,10 +6,12 @@ import { URL, domainToASCII } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; +import semver from 'semver'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { MiMeta } from '@/models/Meta.js'; +import { MiMeta, SoftwareSuspension } from '@/models/Meta.js'; +import { MiInstance } from '@/models/Instance.js'; @Injectable() export class UtilityService { @@ -143,4 +145,20 @@ export class UtilityService { const host = this.extractDbHost(uri); return this.isFederationAllowedHost(host); } + + @bindThis + public isDeliverSuspendedSoftware(software: Pick): SoftwareSuspension | undefined { + if (software.softwareName == null) return undefined; + if (software.softwareVersion == null) { + // software version is null; suspend iff versionRange is * + return this.meta.deliverSuspendedSoftware.find(x => + x.software === software.softwareName + && x.versionRange.trim() === '*'); + } else { + const softwareVersion = software.softwareVersion; + return this.meta.deliverSuspendedSoftware.find(x => + x.software === software.softwareName + && semver.satisfies(softwareVersion, x.versionRange, { includePrerelease: true })); + } + } } diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 284537b986..3688cfb363 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -31,6 +31,7 @@ export class InstanceEntityService { me?: { id: MiUser['id']; } | null | undefined, ): Promise> { const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; + const softwareSuspended = this.utilityService.isDeliverSuspendedSoftware(instance); return { id: instance.id, @@ -41,8 +42,8 @@ export class InstanceEntityService { followingCount: instance.followingCount, followersCount: instance.followersCount, isNotResponding: instance.isNotResponding, - isSuspended: instance.suspensionState !== 'none', - suspensionState: instance.suspensionState, + isSuspended: instance.suspensionState !== 'none' || Boolean(softwareSuspended), + suspensionState: instance.suspensionState === 'none' && softwareSuspended ? 'softwareSuspended' : instance.suspensionState, isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 1fbf5371bc..46f3b2e3c0 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -664,4 +664,14 @@ export class MiMeta { nullable: true, }) public googleAnalyticsMeasurementId: string | null; + + @Column('jsonb', { + default: [], + }) + public deliverSuspendedSoftware: SoftwareSuspension[]; } + +export type SoftwareSuspension = { + software: string, + versionRange: string, +}; diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index c5ca2b5776..abaf615bcf 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -229,7 +229,6 @@ export class MiNote { comment: '[Denormalized]', }) public renoteUserHost: string | null; - //#endregion constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 912a0399d8..85f84952f1 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -48,7 +48,7 @@ export const packedFederationInstanceSchema = { suspensionState: { type: 'string', nullable: false, optional: false, - enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'], + enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding', 'softwareSuspended'], }, isBlocked: { type: 'boolean', diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 5a16496011..391ccdac05 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -71,6 +71,15 @@ export class DeliverProcessorService { return 'skip (suspended)'; } + const i = await (this.meta.enableStatsForFederatedInstances + ? this.federatedInstanceService.fetchOrRegister(host) + : this.federatedInstanceService.fetch(host)); + + // suspend server by software + if (i != null && this.utilityService.isDeliverSuspendedSoftware(i)) { + return 'skip (software suspended)'; + } + try { await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); @@ -79,10 +88,6 @@ export class DeliverProcessorService { // Update instance stats process.nextTick(async () => { - const i = await (this.meta.enableStatsForFederatedInstances - ? this.federatedInstanceService.fetchOrRegister(host) - : this.federatedInstanceService.fetch(host)); - if (i == null) return; if (i.isNotResponding) { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 53e2b2b237..4a106e7175 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -528,6 +528,24 @@ export const meta = { optional: false, nullable: false, }, }, + deliverSuspendedSoftware: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + software: { + type: 'string', + optional: false, nullable: false, + }, + versionRange: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + }, }, }, } as const; @@ -672,6 +690,7 @@ export default class extends Endpoint { // eslint- urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, federation: instance.federation, federationHosts: instance.federationHosts, + deliverSuspendedSoftware: instance.deliverSuspendedSoftware, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index bc05587668..31eeaa5e38 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -185,6 +185,17 @@ export const paramDef = { type: 'string', }, }, + deliverSuspendedSoftware: { + type: 'array', + items: { + type: 'object', + properties: { + software: { type: 'string' }, + versionRange: { type: 'string' }, + }, + required: ['software', 'versionRange'], + }, + }, }, required: [], } as const; @@ -671,6 +682,10 @@ export default class extends Endpoint { // eslint- set.federation = ps.federation; } + if (ps.deliverSuspendedSoftware !== undefined) { + set.deliverSuspendedSoftware = ps.deliverSuspendedSoftware; + } + if (Array.isArray(ps.federationHosts)) { set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); } diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 4708dab73c..f37cdc6658 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -112,6 +112,7 @@ export default class extends Endpoint { // eslint- // https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255 this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 620cdb0f5d..2401ab8208 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -122,6 +122,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.channel', 'channel'); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 2b65407cea..33f32d1d8a 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -85,9 +85,10 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .andWhere('clipNote.clipId = :clipId', { clipId: clip.id }); + this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + // this.queryService.generateSuspendedUserQueryForNote(query); // To avoid problems with removing notes, ignoring suspended user for now if (me) { - this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); } diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 218a3c1a4c..712a86eb13 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -71,6 +71,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index e7aba2d306..a57c84d432 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -97,6 +97,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.channel', 'channel'); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); const notes = (await query.getMany()).filter(note => { if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 39b519a599..6a3ee817e4 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -244,6 +244,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 8b2d5397b2..d1dc22f233 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -157,6 +157,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index f5cddd5bad..c3722b1b5a 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -73,6 +73,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateMutedNoteThreadQuery(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 178e311ed1..ce2435b8eb 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -73,6 +73,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index d9aaed2f10..f491cc38ab 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -57,6 +57,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 079231d432..d0781bd8dd 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -82,6 +82,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 42752eaeec..e6d6a1b629 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -200,6 +200,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 58a4223207..ec7c4b0f97 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -185,6 +185,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index b0d3f6d2f9..16b0783a01 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -103,6 +103,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts index 053fd60548..90bd11bc25 100644 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -88,6 +88,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.channel', 'channel'); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); const notes = (await query.getMany()).filter(note => { if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false; diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index b0585f75fc..0c64df569d 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -130,6 +130,7 @@ export default class extends Endpoint { // eslint- useDbFallback: true, ignoreAuthorFromMute: true, ignoreAuthorFromInstanceBlock: true, + ignoreAuthorFromUserSuspension: true, excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files excludePureRenotes: !ps.withRenotes, @@ -186,6 +187,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query, true); + this.queryService.generateSuspendedUserQueryForNote(query, true); if (me) { this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId }); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index bb9000a7a0..d6f1ecd8ed 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -99,10 +99,16 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('reaction.userId = :userId', { userId: ps.userId }) - .leftJoinAndSelect('reaction.note', 'note'); + .leftJoinAndSelect('reaction.note', 'note') + .leftJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBlockedHostQueryForNote(query); + this.queryService.generateSuspendedUserQueryForNote(query); const reactions = (await query .limit(ps.limit) diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index 7ae1ee4523..570cc61c4b 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -909,7 +909,7 @@ describe('クリップ', () => { assert.deepStrictEqual(res.map(x => x.id), [aliceNote.id]); }); - test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートはhideされて返ってくる)', async () => { + test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートは含まれない)', async () => { const publicClip = await create({ isPublic: true }); await addNote({ clipId: publicClip.id, noteId: aliceNote.id }); await addNote({ clipId: publicClip.id, noteId: aliceHomeNote.id }); @@ -919,8 +919,6 @@ describe('クリップ', () => { const res = await notes({ clipId: publicClip.id }, { user: undefined }); const expects = [ aliceNote, aliceHomeNote, - // 認証なしだと非公開ノートは結果には含むけどhideされる。 - hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote), ]; assert.deepStrictEqual( res.sort(compareBy(s => s.id)).map(x => x.id), diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index d7454882b2..5914e243c6 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -230,6 +230,31 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + +
+ {{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }} +
+ +
+ + + + + + +
+
+
+
@@ -368,10 +393,12 @@ const urlPreviewForm = useForm({ const federationForm = useForm({ federation: meta.federation, federationHosts: meta.federationHosts.join('\n'), + deliverSuspendedSoftware: meta.deliverSuspendedSoftware, }, async (state) => { await os.apiWithDialog('admin/update-meta', { federation: state.federation, federationHosts: state.federationHosts.split('\n'), + deliverSuspendedSoftware: state.deliverSuspendedSoftware, }); fetchInstance(true); }); @@ -398,4 +425,53 @@ definePage(() => ({ font-size: 0.85em; color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); } + +.metadataRoot { + container-type: inline-size; +} + +.fieldDragItem { + display: flex; + padding: 10px; + align-items: flex-end; + border-radius: 6px; + + /* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */ + @container (max-width: 452px) { + align-items: center; + } +} + +.dragItemHandle { + cursor: grab; + width: 32px; + height: 32px; + margin: 0 8px 0 0; + opacity: 0.5; + flex-shrink: 0; + + &:active { + cursor: grabbing; + } +} + +.dragItemRemove { + @extend .dragItemHandle; + + color: #ff2a2a; + opacity: 1; + cursor: pointer; + + &:hover, &:focus { + opacity: .7; + } + + &:active { + cursor: pointer; + } +} + +.dragItemForm { + flex-grow: 1; +} diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 28ce02b87c..96a43f67e8 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._delivery.stop }} - {{ i18n.ts._delivery.resume }} + {{ i18n.ts._delivery.resume }} {{ i18n.ts.blockThisInstance }} {{ i18n.ts.silenceThisInstance }} {{ i18n.ts.mediaSilenceThisInstance }} @@ -164,7 +164,7 @@ const tab = ref('overview'); const chartSrc = ref('instance-requests'); const meta = ref(null); const instance = ref(null); -const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none'); +const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended'>('none'); const isBlocked = ref(false); const isSilenced = ref(false); const isMediaSilenced = ref(false); diff --git a/packages/frontend/src/use/use-form.ts b/packages/frontend/src/use/use-form.ts index 26cca839c3..1c93557413 100644 --- a/packages/frontend/src/use/use-form.ts +++ b/packages/frontend/src/use/use-form.ts @@ -5,6 +5,7 @@ import { computed, reactive, watch } from 'vue'; import type { Reactive } from 'vue'; +import { deepEqual } from '@/utility/deep-equal'; function copy(v: T): T { return JSON.parse(JSON.stringify(v)); @@ -27,7 +28,7 @@ export function useForm>(initialState: T, save: (n watch([currentState, previousState], () => { for (const key in modifiedStates) { - modifiedStates[key] = currentState[key] !== previousState[key]; + modifiedStates[key] = !deepEqual(currentState[key], previousState[key]); } }, { deep: true }); diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index ee200d6890..9c7c611e5b 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -46,6 +46,7 @@ }, "compileOnSave": false, "include": [ + "./lib/**/*.ts", "./src/**/*.ts", "./src/**/*.vue", "./test/**/*.ts", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 4537d84ff6..63fbebd5b5 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.4.1-rc.0", + "version": "2025.4.1", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index b9d48f02ae..c83e1f1fbe 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4989,7 +4989,7 @@ export type components = { isNotResponding: boolean; isSuspended: boolean; /** @enum {string} */ - suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'; + suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended'; isBlocked: boolean; /** @example misskey */ softwareName: string | null; @@ -8765,6 +8765,10 @@ export type operations = { /** @enum {string} */ federation: 'all' | 'specified' | 'none'; federationHosts: string[]; + deliverSuspendedSoftware: { + software: string; + versionRange: string; + }[]; }; }; }; @@ -11431,6 +11435,10 @@ export type operations = { /** @enum {string} */ federation?: 'all' | 'none' | 'specified'; federationHosts?: string[]; + deliverSuspendedSoftware?: { + software: string; + versionRange: string; + }[]; }; }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3c95d703..0b21d85a28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -390,6 +390,9 @@ importers: secure-json-parse: specifier: 3.0.2 version: 3.0.2 + semver: + specifier: 7.7.1 + version: 7.7.1 sharp: specifier: 0.34.1 version: 0.34.1 @@ -9564,11 +9567,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -17051,7 +17049,7 @@ snapshots: natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.1.2 - semver: 7.6.3 + semver: 7.7.1 vue-eslint-parser: 10.1.3(eslint@9.25.1) xml-name-validator: 4.0.0 @@ -20987,8 +20985,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.6.3: {} - semver@7.7.1: {} send@0.19.0: