Merge branch 'develop' into create-index-concurrently
This commit is contained in:
		
						commit
						1b3c9c0334
					
				|  | @ -7,8 +7,8 @@ | |||
| - | ||||
| 
 | ||||
| ### Server | ||||
| - | ||||
| 
 | ||||
| - Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775` | ||||
| - Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727` | ||||
| 
 | ||||
| ## 2025.4.1 | ||||
| 
 | ||||
|  |  | |||
|  | @ -898,6 +898,10 @@ export interface Locale extends ILocale { | |||
|      * ソフトウェア | ||||
|      */ | ||||
|     "software": string; | ||||
|     /** | ||||
|      * ソフトウェア名 | ||||
|      */ | ||||
|     "softwareName": string; | ||||
|     /** | ||||
|      * バージョン | ||||
|      */ | ||||
|  | @ -5871,6 +5875,10 @@ export interface Locale extends ILocale { | |||
|              * サーバー応答なしのため停止中 | ||||
|              */ | ||||
|             "autoSuspendedForNotResponding": string; | ||||
|             /** | ||||
|              * 配信停止中のソフトウェアであるため停止中 | ||||
|              */ | ||||
|             "softwareSuspended": string; | ||||
|         }; | ||||
|     }; | ||||
|     "_bubbleGame": { | ||||
|  | @ -6356,6 +6364,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": { | ||||
|         /** | ||||
|  |  | |||
|  | @ -220,6 +220,7 @@ silenceThisInstance: "サーバーをサイレンス" | |||
| mediaSilenceThisInstance: "サーバーをメディアサイレンス" | ||||
| operations: "操作" | ||||
| software: "ソフトウェア" | ||||
| softwareName: "ソフトウェア名" | ||||
| version: "バージョン" | ||||
| metadata: "メタデータ" | ||||
| withNFiles: "{n}つのファイル" | ||||
|  | @ -1477,6 +1478,7 @@ _delivery: | |||
|     manuallySuspended: "手動停止中" | ||||
|     goneSuspended: "サーバー削除のため停止中" | ||||
|     autoSuspendedForNotResponding: "サーバー応答なしのため停止中" | ||||
|     softwareSuspended: "配信停止中のソフトウェアであるため停止中" | ||||
| 
 | ||||
| _bubbleGame: | ||||
|   howToPlay: "遊び方" | ||||
|  | @ -1615,6 +1617,8 @@ _serverSettings: | |||
|   openRegistration: "アカウントの作成をオープンにする" | ||||
|   openRegistrationWarning: "登録を開放することはリスクが伴います。サーバーを常に監視し、トラブルが発生した際にすぐに対応できる体制がある場合のみオンにすることを推奨します。" | ||||
|   thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。" | ||||
|   deliverSuspendedSoftware: "配信停止中のソフトウェア" | ||||
|   deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" | ||||
| 
 | ||||
| _accountMigration: | ||||
|   moveFrom: "別のアカウントからこのアカウントに移行" | ||||
|  |  | |||
|  | @ -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"`); | ||||
|     } | ||||
| } | ||||
|  | @ -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", | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ type TimelineOptions = { | |||
| 	excludeNoFiles?: boolean; | ||||
| 	excludeReplies?: boolean; | ||||
| 	excludePureRenotes: boolean; | ||||
| 	ignoreAuthorFromUserSuspension?: boolean; | ||||
| 	dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>, | ||||
| }; | ||||
| 
 | ||||
|  | @ -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をキャッシュする?
 | ||||
|  |  | |||
|  | @ -287,4 +287,26 @@ export class QueryService { | |||
| 				.andWhere(instanceSuspension('renoteUser')); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Requirements: user replyUser renoteUser must be joined
 | ||||
| 	@bindThis | ||||
| 	public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, 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')); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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<string>(), new Set<string>()]; | ||||
| 
 | ||||
| 		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; | ||||
|  |  | |||
|  | @ -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<MiInstance, 'softwareName' | 'softwareVersion'>): 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 })); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ export class InstanceEntityService { | |||
| 		me?: { id: MiUser['id']; } | null | undefined, | ||||
| 	): Promise<Packed<'FederationInstance'>> { | ||||
| 		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, | ||||
|  |  | |||
|  | @ -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, | ||||
| }; | ||||
|  |  | |||
|  | @ -239,7 +239,6 @@ export class MiNote { | |||
| 		comment: '[Denormalized]', | ||||
| 	}) | ||||
| 	public renoteUserHost: string | null; | ||||
| 	//#endregion
 | ||||
| 
 | ||||
| 	constructor(data: Partial<MiNote>) { | ||||
| 		if (data == null) return; | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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<typeof meta, typeof paramDef> { // eslint- | |||
| 				urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, | ||||
| 				federation: instance.federation, | ||||
| 				federationHosts: instance.federationHosts, | ||||
| 				deliverSuspendedSoftware: instance.deliverSuspendedSoftware, | ||||
| 			}; | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -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<typeof meta, typeof paramDef> { // 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()); | ||||
| 			} | ||||
|  |  | |||
|  | @ -112,6 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -122,6 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -85,9 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
| 			} | ||||
|  |  | |||
|  | @ -71,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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; | ||||
|  |  | |||
|  | @ -244,6 +244,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -157,6 +157,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -82,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -200,6 +200,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -185,6 +185,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -103,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 
 | ||||
| 			this.queryService.generateVisibilityQuery(query, me); | ||||
| 			this.queryService.generateBlockedHostQueryForNote(query); | ||||
| 			this.queryService.generateSuspendedUserQueryForNote(query); | ||||
| 			this.queryService.generateMutedUserQueryForNotes(query, me); | ||||
| 			this.queryService.generateBlockedUserQueryForNotes(query, me); | ||||
| 
 | ||||
|  |  | |||
|  | @ -88,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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; | ||||
|  |  | |||
|  | @ -130,6 +130,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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); | ||||
|  |  | |||
|  | @ -99,10 +99,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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) | ||||
|  |  | |||
|  | @ -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), | ||||
|  |  | |||
|  | @ -230,6 +230,31 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 						<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template> | ||||
| 						<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template> | ||||
| 					</MkTextarea> | ||||
| 
 | ||||
| 					<MkFolder> | ||||
| 						<template #icon><i class="ti ti-list"></i></template> | ||||
| 						<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template> | ||||
| 						<template #footer> | ||||
| 							<div class="_buttons"> | ||||
| 								<MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 							</div> | ||||
| 						</template> | ||||
| 
 | ||||
| 						<div :class="$style.metadataRoot" class="_gaps_s"> | ||||
| 							<MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo> | ||||
| 							<div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem"> | ||||
| 								<button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button> | ||||
| 								<div :class="$style.dragItemForm"> | ||||
| 									<FormSplit :minWidth="200"> | ||||
| 										<MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName"> | ||||
| 										</MkInput> | ||||
| 										<MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version"> | ||||
| 										</MkInput> | ||||
| 									</FormSplit> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</MkFolder> | ||||
| 				</div> | ||||
| 			</MkFolder> | ||||
| 
 | ||||
|  | @ -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; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 						</template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton> | ||||
| 					<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton> | ||||
| 					<MkButton v-if="suspensionState !== 'none'" :disabled="!instance || suspensionState == 'softwareSuspended'" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton> | ||||
| 					<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> | ||||
| 					<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> | ||||
| 					<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch> | ||||
|  | @ -164,7 +164,7 @@ const tab = ref('overview'); | |||
| const chartSrc = ref<ChartSrc>('instance-requests'); | ||||
| const meta = ref<Misskey.entities.AdminMetaResponse | null>(null); | ||||
| const instance = ref<Misskey.entities.FederationInstance | null>(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); | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| 
 | ||||
| import { computed, reactive, watch } from 'vue'; | ||||
| import type { Reactive } from 'vue'; | ||||
| import { deepEqual } from '@/utility/deep-equal'; | ||||
| 
 | ||||
| function copy<T>(v: T): T { | ||||
| 	return JSON.parse(JSON.stringify(v)); | ||||
|  | @ -27,7 +28,7 @@ export function useForm<T extends Record<string, any>>(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 }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,6 +46,7 @@ | |||
| 	}, | ||||
| 	"compileOnSave": false, | ||||
| 	"include": [ | ||||
| 		"./lib/**/*.ts", | ||||
| 		"./src/**/*.ts", | ||||
| 		"./src/**/*.vue", | ||||
| 		"./test/**/*.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; | ||||
|             }[]; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue