Merge branch 'develop' into re-ed25519
This commit is contained in:
		
						commit
						c137167c2c
					
				|  | @ -4,6 +4,8 @@ | |||
| - Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 | ||||
|   - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください | ||||
| - Feat: パスキーでログインボタンを実装 (#14574) | ||||
| - Feat: フォローされた際のメッセージを設定できるように | ||||
| - Feat: 連合をホワイトリスト制にできるように | ||||
| - Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) | ||||
| - Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように   | ||||
|   (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) | ||||
|  | @ -19,6 +21,7 @@ | |||
| - Enhance: ScratchpadにUIインスペクターを追加 | ||||
| - Enhance: Play編集画面の項目の並びを少しリデザイン | ||||
| - Enhance: 各種メニューをドロワー表示するかどうか設定可能に | ||||
| - Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加 | ||||
| - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 | ||||
| - Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正 | ||||
| - Fix: 月の違う同じ日はセパレータが表示されないのを修正 | ||||
|  |  | |||
|  | @ -960,6 +960,14 @@ export interface Locale extends ILocale { | |||
|      * メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。 | ||||
|      */ | ||||
|     "mediaSilencedInstancesDescription": string; | ||||
|     /** | ||||
|      * 連合を許可するサーバー | ||||
|      */ | ||||
|     "federationAllowedHosts": string; | ||||
|     /** | ||||
|      * 連合を許可するサーバーのホストを改行で区切って設定します。 | ||||
|      */ | ||||
|     "federationAllowedHostsDescription": string; | ||||
|     /** | ||||
|      * ミュートとブロック | ||||
|      */ | ||||
|  | @ -8725,6 +8733,18 @@ export interface Locale extends ILocale { | |||
|          * 最大{max}つまでデコレーションを付けられます。 | ||||
|          */ | ||||
|         "avatarDecorationMax": ParameterizedString<"max">; | ||||
|         /** | ||||
|          * フォローされた時のメッセージ | ||||
|          */ | ||||
|         "followedMessage": string; | ||||
|         /** | ||||
|          * フォローされた時に相手に表示する短いメッセージを設定できます。 | ||||
|          */ | ||||
|         "followedMessageDescription": string; | ||||
|         /** | ||||
|          * フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。 | ||||
|          */ | ||||
|         "followedMessageDescriptionForLockedAccount": string; | ||||
|     }; | ||||
|     "_exportOrImport": { | ||||
|         /** | ||||
|  |  | |||
|  | @ -236,6 +236,8 @@ silencedInstances: "サイレンスしたサーバー" | |||
| silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。" | ||||
| mediaSilencedInstances: "メディアサイレンスしたサーバー" | ||||
| mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。" | ||||
| federationAllowedHosts: "連合を許可するサーバー" | ||||
| federationAllowedHostsDescription: "連合を許可するサーバーのホストを改行で区切って設定します。" | ||||
| muteAndBlock: "ミュートとブロック" | ||||
| mutedUsers: "ミュートしたユーザー" | ||||
| blockedUsers: "ブロックしたユーザー" | ||||
|  | @ -2297,6 +2299,9 @@ _profile: | |||
|   changeBanner: "バナー画像を変更" | ||||
|   verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" | ||||
|   avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" | ||||
|   followedMessage: "フォローされた時のメッセージ" | ||||
|   followedMessageDescription: "フォローされた時に相手に表示する短いメッセージを設定できます。" | ||||
|   followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。" | ||||
| 
 | ||||
| _exportOrImport: | ||||
|   allNotes: "全てのノート" | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
| 	"name": "misskey", | ||||
| 	"version": "2024.9.0-alpha.10", | ||||
| 	"version": "2024.9.0-alpha.12", | ||||
| 	"codename": "nasubi", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|  |  | |||
|  | @ -0,0 +1,16 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| export class FollowedMessage1723944246767 { | ||||
| 	name = 'FollowedMessage1723944246767'; | ||||
| 
 | ||||
| 	async up(queryRunner) { | ||||
| 		await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)'); | ||||
| 	} | ||||
| 
 | ||||
| 	async down(queryRunner) { | ||||
| 		await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"'); | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| export class UserScore1727491883993 { | ||||
|     name = 'UserScore1727491883993' | ||||
| 
 | ||||
|     async up(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`); | ||||
|     } | ||||
| 
 | ||||
|     async down(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,18 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| export class MetaFederation1727512908322 { | ||||
|     name = 'MetaFederation1727512908322' | ||||
| 
 | ||||
|     async up(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`); | ||||
|         await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); | ||||
|     } | ||||
| 
 | ||||
|     async down(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`); | ||||
|         await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`); | ||||
|     } | ||||
| } | ||||
|  | @ -275,16 +275,19 @@ export class UserFollowingService implements OnModuleInit { | |||
| 				followeeId: followee.id, | ||||
| 				followerId: follower.id, | ||||
| 			}); | ||||
| 
 | ||||
| 			// 通知を作成
 | ||||
| 			if (follower.host === null) { | ||||
| 				this.notificationService.createNotification(follower.id, 'followRequestAccepted', { | ||||
| 				}, followee.id); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (alreadyFollowed) return; | ||||
| 
 | ||||
| 		// 通知を作成
 | ||||
| 		if (follower.host === null) { | ||||
| 			const profile = await this.cacheService.userProfileCache.fetch(followee.id); | ||||
| 
 | ||||
| 			this.notificationService.createNotification(follower.id, 'followRequestAccepted', { | ||||
| 				message: profile.followedMessage, | ||||
| 			}, followee.id); | ||||
| 		} | ||||
| 
 | ||||
| 		this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); | ||||
| 
 | ||||
| 		const [followeeUser, followerUser] = await Promise.all([ | ||||
|  |  | |||
|  | @ -10,12 +10,16 @@ import RE2 from 're2'; | |||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { MiMeta } from '@/models/Meta.js'; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class UtilityService { | ||||
| 	constructor( | ||||
| 		@Inject(DI.config) | ||||
| 		private config: Config, | ||||
| 
 | ||||
| 		@Inject(DI.meta) | ||||
| 		private meta: MiMeta, | ||||
| 	) { | ||||
| 	} | ||||
| 
 | ||||
|  | @ -105,4 +109,19 @@ export class UtilityService { | |||
| 		if (host == null) return null; | ||||
| 		return toASCII(host.toLowerCase()); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public isFederationAllowedHost(host: string): boolean { | ||||
| 		if (this.meta.federation === 'none') return false; | ||||
| 		if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false; | ||||
| 		if (this.isBlockedHost(this.meta.blockedHosts, host)) return false; | ||||
| 
 | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public isFederationAllowedUri(uri: string): boolean { | ||||
| 		const host = this.extractDbHost(uri); | ||||
| 		return this.isFederationAllowedHost(host); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -68,6 +68,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser { | |||
| 		isHibernated: false, | ||||
| 		isDeleted: false, | ||||
| 		emojis: [], | ||||
| 		score: 0, | ||||
| 		host: null, | ||||
| 		inbox: null, | ||||
| 		sharedInbox: null, | ||||
|  |  | |||
|  | @ -283,8 +283,8 @@ export class ApInboxService { | |||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		// アナウンス先をブロックしてたら中断
 | ||||
| 		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; | ||||
| 		// アナウンス先が許可されているかチェック
 | ||||
| 		if (!this.utilityService.isFederationAllowedUri(uri)) return; | ||||
| 
 | ||||
| 		const unlock = await this.appLockService.getApLock(uri); | ||||
| 
 | ||||
|  |  | |||
|  | @ -494,6 +494,7 @@ export class ApRendererService { | |||
| 			name: user.name, | ||||
| 			summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, | ||||
| 			_misskey_summary: profile.description, | ||||
| 			_misskey_followedMessage: profile.followedMessage, | ||||
| 			icon: avatar ? this.renderImage(avatar) : null, | ||||
| 			image: banner ? this.renderImage(banner) : null, | ||||
| 			tag, | ||||
|  |  | |||
|  | @ -95,7 +95,7 @@ export class Resolver { | |||
| 			return await this.resolveLocal(value); | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { | ||||
| 		if (!this.utilityService.isFederationAllowedHost(host)) { | ||||
| 			throw new Error('Instance is blocked'); | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -555,6 +555,7 @@ const extension_context_definition = { | |||
| 	'_misskey_reaction': 'misskey:_misskey_reaction', | ||||
| 	'_misskey_votes': 'misskey:_misskey_votes', | ||||
| 	'_misskey_summary': 'misskey:_misskey_summary', | ||||
| 	'_misskey_followedMessage': 'misskey:_misskey_followedMessage', | ||||
| 	'isCat': 'misskey:isCat', | ||||
| 	// vcard
 | ||||
| 	vcard: 'http://www.w3.org/2006/vcard/ns#', | ||||
|  |  | |||
|  | @ -336,8 +336,7 @@ export class ApNoteService { | |||
| 	public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> { | ||||
| 		const uri = getApId(value); | ||||
| 
 | ||||
| 		// ブロックしていたら中断
 | ||||
| 		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) { | ||||
| 		if (!this.utilityService.isFederationAllowedUri(uri)) { | ||||
| 			throw new StatusError('blocked host', 451); | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ import type { ApNoteService } from './ApNoteService.js'; | |||
| import type { ApMfmService } from '../ApMfmService.js'; | ||||
| import type { ApResolverService, Resolver } from '../ApResolverService.js'; | ||||
| import type { ApLoggerService } from '../ApLoggerService.js'; | ||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-imports
 | ||||
| 
 | ||||
| import type { ApImageService } from './ApImageService.js'; | ||||
| import type { IActor, IKey, IObject, ICollection, IOrderedCollection } from '../type.js'; | ||||
| 
 | ||||
|  | @ -361,8 +361,8 @@ export class ApPersonService implements OnModuleInit { | |||
| 						this.logger.error('error occurred while fetching following/followers collection', { stack: err }); | ||||
| 					} | ||||
| 					return 'private'; | ||||
| 				}) | ||||
| 			) | ||||
| 				}), | ||||
| 			), | ||||
| 		); | ||||
| 
 | ||||
| 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); | ||||
|  | @ -424,6 +424,7 @@ export class ApPersonService implements OnModuleInit { | |||
| 				await transactionalEntityManager.save(new MiUserProfile({ | ||||
| 					userId: user.id, | ||||
| 					description: _description, | ||||
| 					followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, | ||||
| 					url, | ||||
| 					fields, | ||||
| 					followingVisibility, | ||||
|  | @ -552,8 +553,8 @@ export class ApPersonService implements OnModuleInit { | |||
| 						return undefined; | ||||
| 					} | ||||
| 					return 'private'; | ||||
| 				}) | ||||
| 			) | ||||
| 				}), | ||||
| 			), | ||||
| 		); | ||||
| 
 | ||||
| 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); | ||||
|  | @ -642,6 +643,7 @@ export class ApPersonService implements OnModuleInit { | |||
| 			url, | ||||
| 			fields, | ||||
| 			description: _description, | ||||
| 			followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, | ||||
| 			followingVisibility, | ||||
| 			followersVisibility, | ||||
| 			birthday: bday?.[0] ?? null, | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ export interface IObject { | |||
| 	name?: string | null; | ||||
| 	summary?: string; | ||||
| 	_misskey_summary?: string; | ||||
| 	_misskey_followedMessage?: string | null; | ||||
| 	published?: string; | ||||
| 	cc?: ApObject; | ||||
| 	to?: ApObject; | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit { | |||
| 	async #packInternal <T extends MiNotification | MiGroupedNotification> ( | ||||
| 		src: T, | ||||
| 		meId: MiUser['id'], | ||||
| 		// eslint-disable-next-line @typescript-eslint/ban-types
 | ||||
| 		  | ||||
| 		options: { | ||||
| 			checkValidNotifier?: boolean; | ||||
| 		}, | ||||
|  | @ -159,6 +159,9 @@ export class NotificationEntityService implements OnModuleInit { | |||
| 			...(notification.type === 'roleAssigned' ? { | ||||
| 				role: role, | ||||
| 			} : {}), | ||||
| 			...(notification.type === 'followRequestAccepted' ? { | ||||
| 				message: notification.message, | ||||
| 			} : {}), | ||||
| 			...(notification.type === 'achievementEarned' ? { | ||||
| 				achievement: notification.achievement, | ||||
| 			} : {}), | ||||
|  | @ -233,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit { | |||
| 	public async pack( | ||||
| 		src: MiNotification | MiGroupedNotification, | ||||
| 		meId: MiUser['id'], | ||||
| 		// eslint-disable-next-line @typescript-eslint/ban-types
 | ||||
| 		  | ||||
| 		options: { | ||||
| 			checkValidNotifier?: boolean; | ||||
| 		}, | ||||
|  |  | |||
|  | @ -508,7 +508,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 					name: r.name, | ||||
| 					iconUrl: r.iconUrl, | ||||
| 					displayOrder: r.displayOrder, | ||||
| 				})) | ||||
| 				})), | ||||
| 			) : undefined, | ||||
| 
 | ||||
| 			...(isDetailed ? { | ||||
|  | @ -567,6 +567,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 			...(isDetailed && isMe ? { | ||||
| 				avatarId: user.avatarId, | ||||
| 				bannerId: user.bannerId, | ||||
| 				followedMessage: profile!.followedMessage, | ||||
| 				isModerator: isModerator, | ||||
| 				isAdmin: isAdmin, | ||||
| 				injectFeaturedNote: profile!.injectFeaturedNote, | ||||
|  | @ -635,6 +636,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 				isRenoteMuted: relation.isRenoteMuted, | ||||
| 				notify: relation.following?.notify ?? 'none', | ||||
| 				withReplies: relation.following?.withReplies ?? false, | ||||
| 				followedMessage: relation.isFollowing ? profile!.followedMessage : undefined, | ||||
| 			} : {}), | ||||
| 		} as Promiseable<Packed<S>>; | ||||
| 
 | ||||
|  |  | |||
|  | @ -630,4 +630,17 @@ export class MiMeta { | |||
| 		nullable: true, | ||||
| 	}) | ||||
| 	public urlPreviewUserAgent: string | null; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 128, | ||||
| 		default: 'all', | ||||
| 	}) | ||||
| 	public federation: 'all' | 'specified' | 'none'; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 1024, | ||||
| 		array: true, | ||||
| 		default: '{}', | ||||
| 	}) | ||||
| 	public federationHosts: string[]; | ||||
| } | ||||
|  |  | |||
|  | @ -69,6 +69,7 @@ export type MiNotification = { | |||
| 	id: string; | ||||
| 	createdAt: string; | ||||
| 	notifierId: MiUser['id']; | ||||
| 	message: string | null; | ||||
| } | { | ||||
| 	type: 'roleAssigned'; | ||||
| 	id: string; | ||||
|  |  | |||
|  | @ -155,6 +155,11 @@ export class MiUser { | |||
| 	}) | ||||
| 	public tags: string[]; | ||||
| 
 | ||||
| 	@Column('integer', { | ||||
| 		default: 0, | ||||
| 	}) | ||||
| 	public score: number; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 		comment: 'Whether the User is suspended.', | ||||
|  | @ -289,5 +294,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr | |||
| export const passwordSchema = { type: 'string', minLength: 1 } as const; | ||||
| export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; | ||||
| export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const; | ||||
| export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const; | ||||
| export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; | ||||
| export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; | ||||
|  |  | |||
|  | @ -42,6 +42,14 @@ export class MiUserProfile { | |||
| 	}) | ||||
| 	public description: string | null; | ||||
| 
 | ||||
| 	// フォローされた際のメッセージ
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 256, nullable: true, | ||||
| 	}) | ||||
| 	public followedMessage: string | null; | ||||
| 
 | ||||
| 	// TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
 | ||||
| 
 | ||||
| 	@Column('jsonb', { | ||||
| 		default: [], | ||||
| 	}) | ||||
|  |  | |||
|  | @ -267,6 +267,10 @@ export const packedNotificationSchema = { | |||
| 				optional: false, nullable: false, | ||||
| 				format: 'id', | ||||
| 			}, | ||||
| 			message: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, { | ||||
| 		type: 'object', | ||||
|  |  | |||
|  | @ -370,6 +370,10 @@ export const packedUserDetailedNotMeOnlySchema = { | |||
| 				ref: 'RoleLite', | ||||
| 			}, | ||||
| 		}, | ||||
| 		followedMessage: { | ||||
| 			type: 'string', | ||||
| 			nullable: true, optional: true, | ||||
| 		}, | ||||
| 		memo: { | ||||
| 			type: 'string', | ||||
| 			nullable: true, optional: false, | ||||
|  | @ -437,6 +441,10 @@ export const packedMeDetailedOnlySchema = { | |||
| 			nullable: true, optional: false, | ||||
| 			format: 'id', | ||||
| 		}, | ||||
| 		followedMessage: { | ||||
| 			type: 'string', | ||||
| 			nullable: true, optional: false, | ||||
| 		}, | ||||
| 		isModerator: { | ||||
| 			type: 'boolean', | ||||
| 			nullable: true, optional: false, | ||||
|  |  | |||
|  | @ -53,8 +53,7 @@ export class DeliverProcessorService { | |||
| 	public async process(job: Bull.Job<DeliverJobData>): Promise<string> { | ||||
| 		const { host } = new URL(job.data.to); | ||||
| 
 | ||||
| 		// ブロックしてたら中断
 | ||||
| 		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.toPuny(host))) { | ||||
| 		if (!this.utilityService.isFederationAllowedUri(job.data.to)) { | ||||
| 			return 'skip (blocked)'; | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -79,8 +79,7 @@ export class InboxProcessorService implements OnApplicationShutdown { | |||
| 
 | ||||
| 		const host = this.utilityService.toPuny(new URL(actorUri).hostname); | ||||
| 
 | ||||
| 		// ブロックしてたら中断
 | ||||
| 		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { | ||||
| 		if (!this.utilityService.isFederationAllowedHost(host)) { | ||||
| 			return `Blocked request: ${host}`; | ||||
| 		} | ||||
| 
 | ||||
|  | @ -166,7 +165,7 @@ export class InboxProcessorService implements OnApplicationShutdown { | |||
| 
 | ||||
| 				// ブロックしてたら中断
 | ||||
| 				const ldHost = this.utilityService.extractDbHost(authUser.user.uri); | ||||
| 				if (this.utilityService.isBlockedHost(this.meta.blockedHosts, ldHost)) { | ||||
| 				if (this.utilityService.isFederationAllowedHost(ldHost)) { | ||||
| 					throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); | ||||
| 				} | ||||
| 
 | ||||
|  |  | |||
|  | @ -495,6 +495,18 @@ export const meta = { | |||
| 				type: 'string', | ||||
| 				optional: false, nullable: true, | ||||
| 			}, | ||||
| 			federation: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			federationHosts: { | ||||
| 				type: 'array', | ||||
| 				optional: false, nullable: false, | ||||
| 				items: { | ||||
| 					type: 'string', | ||||
| 					optional: false, nullable: false, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
|  | @ -630,6 +642,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, | ||||
| 				urlPreviewUserAgent: instance.urlPreviewUserAgent, | ||||
| 				urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, | ||||
| 				federation: instance.federation, | ||||
| 				federationHosts: instance.federationHosts, | ||||
| 			}; | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -34,6 +34,10 @@ export const meta = { | |||
| 				type: 'boolean', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			followedMessage: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: true, | ||||
| 			}, | ||||
| 			autoAcceptFollowed: { | ||||
| 				type: 'boolean', | ||||
| 				optional: false, nullable: false, | ||||
|  | @ -279,6 +283,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 			return { | ||||
| 				email: profile.email, | ||||
| 				emailVerified: profile.emailVerified, | ||||
| 				followedMessage: profile.followedMessage, | ||||
| 				autoAcceptFollowed: profile.autoAcceptFollowed, | ||||
| 				noCrawle: profile.noCrawle, | ||||
| 				preventAiLearning: profile.preventAiLearning, | ||||
|  |  | |||
|  | @ -168,6 +168,16 @@ export const paramDef = { | |||
| 		urlPreviewRequireContentLength: { type: 'boolean' }, | ||||
| 		urlPreviewUserAgent: { type: 'string', nullable: true }, | ||||
| 		urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, | ||||
| 		federation: { | ||||
| 			type: 'string', | ||||
| 			enum: ['all', 'none', 'specified'], | ||||
| 		}, | ||||
| 		federationHosts: { | ||||
| 			type: 'array', | ||||
| 			items: { | ||||
| 				type: 'string', | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	required: [], | ||||
| } as const; | ||||
|  | @ -637,6 +647,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				set.urlPreviewSummaryProxyUrl = value === '' ? null : value; | ||||
| 			} | ||||
| 
 | ||||
| 			if (ps.federation !== undefined) { | ||||
| 				set.federation = ps.federation; | ||||
| 			} | ||||
| 
 | ||||
| 			if (Array.isArray(ps.federationHosts)) { | ||||
| 				set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); | ||||
| 			} | ||||
| 
 | ||||
| 			const before = await this.metaService.fetch(true); | ||||
| 
 | ||||
| 			await this.metaService.update(set); | ||||
|  |  | |||
|  | @ -19,8 +19,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; | |||
| import { UtilityService } from '@/core/UtilityService.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
| import { MiMeta } from '@/models/_.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['federation'], | ||||
|  | @ -89,9 +87,6 @@ export const paramDef = { | |||
| @Injectable() | ||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||
| 	constructor( | ||||
| 		@Inject(DI.meta) | ||||
| 		private serverSettings: MiMeta, | ||||
| 
 | ||||
| 		private utilityService: UtilityService, | ||||
| 		private userEntityService: UserEntityService, | ||||
| 		private noteEntityService: NoteEntityService, | ||||
|  | @ -115,8 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 	 */ | ||||
| 	@bindThis | ||||
| 	private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { | ||||
| 		// ブロックしてたら中断
 | ||||
| 		if (this.utilityService.isBlockedHost(this.serverSettings.blockedHosts, this.utilityService.extractDbHost(uri))) return null; | ||||
| 		if (!this.utilityService.isFederationAllowedUri(uri)) return null; | ||||
| 
 | ||||
| 		let local = await this.mergePack(me, ...await Promise.all([ | ||||
| 			this.apDbResolverService.getUserFromApId(uri), | ||||
|  |  | |||
|  | @ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js'; | |||
| import * as Acct from '@/misc/acct.js'; | ||||
| import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; | ||||
| import type { MiLocalUser, MiUser } from '@/models/User.js'; | ||||
| import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; | ||||
| import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; | ||||
| import type { MiUserProfile } from '@/models/UserProfile.js'; | ||||
| import { notificationTypes } from '@/types.js'; | ||||
| import { normalizeForSearch } from '@/misc/normalize-for-search.js'; | ||||
| import { langmap } from '@/misc/langmap.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
|  | @ -134,6 +133,7 @@ export const paramDef = { | |||
| 	properties: { | ||||
| 		name: { ...nameSchema, nullable: true }, | ||||
| 		description: { ...descriptionSchema, nullable: true }, | ||||
| 		followedMessage: { ...followedMessageSchema, nullable: true }, | ||||
| 		location: { ...locationSchema, nullable: true }, | ||||
| 		birthday: { ...birthdaySchema, nullable: true }, | ||||
| 		lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, | ||||
|  | @ -267,6 +267,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				} | ||||
| 			} | ||||
| 			if (ps.description !== undefined) profileUpdates.description = ps.description; | ||||
| 			if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage; | ||||
| 			if (ps.lang !== undefined) profileUpdates.lang = ps.lang; | ||||
| 			if (ps.location !== undefined) profileUpdates.location = ps.location; | ||||
| 			if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; | ||||
|  |  | |||
|  | @ -7,9 +7,9 @@ process.env.NODE_ENV = 'test'; | |||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import { inspect } from 'node:util'; | ||||
| import { DEFAULT_POLICIES } from '@/core/RoleService.js'; | ||||
| import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; | ||||
| import type * as misskey from 'misskey-js'; | ||||
| import { DEFAULT_POLICIES } from '@/core/RoleService.js'; | ||||
| 
 | ||||
| describe('ユーザー', () => { | ||||
| 	// エンティティとしてのユーザーを主眼においたテストを記述する
 | ||||
|  | @ -105,6 +105,7 @@ describe('ユーザー', () => { | |||
| 			isRenoteMuted: user.isRenoteMuted ?? false, | ||||
| 			notify: user.notify ?? 'none', | ||||
| 			withReplies: user.withReplies ?? false, | ||||
| 			followedMessage: user.isFollowing ? (user.followedMessage ?? null) : undefined, | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -114,6 +115,7 @@ describe('ユーザー', () => { | |||
| 			...userDetailedNotMe(user), | ||||
| 			avatarId: user.avatarId, | ||||
| 			bannerId: user.bannerId, | ||||
| 			followedMessage: user.followedMessage, | ||||
| 			isModerator: user.isModerator, | ||||
| 			isAdmin: user.isAdmin, | ||||
| 			injectFeaturedNote: user.injectFeaturedNote, | ||||
|  | @ -350,6 +352,7 @@ describe('ユーザー', () => { | |||
| 		// MeDetailedOnly
 | ||||
| 		assert.strictEqual(response.avatarId, null); | ||||
| 		assert.strictEqual(response.bannerId, null); | ||||
| 		assert.strictEqual(response.followedMessage, null); | ||||
| 		assert.strictEqual(response.isModerator, false); | ||||
| 		assert.strictEqual(response.isAdmin, false); | ||||
| 		assert.strictEqual(response.injectFeaturedNote, true); | ||||
|  | @ -413,6 +416,8 @@ describe('ユーザー', () => { | |||
| 		{ parameters: () => ({ description: 'x'.repeat(1500) }) }, | ||||
| 		{ parameters: () => ({ description: 'x' }) }, | ||||
| 		{ parameters: () => ({ description: 'My description' }) }, | ||||
| 		{ parameters: () => ({ followedMessage: null }) }, | ||||
| 		{ parameters: () => ({ followedMessage: 'Thank you' }) }, | ||||
| 		{ parameters: () => ({ location: null }) }, | ||||
| 		{ parameters: () => ({ location: 'x'.repeat(50) }) }, | ||||
| 		{ parameters: () => ({ location: 'x' }) }, | ||||
|  |  | |||
|  | @ -0,0 +1,182 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { IncomingHttpHeaders } from 'node:http'; | ||||
| import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import { FastifyReply, FastifyRequest } from 'fastify'; | ||||
| import { AuthenticationResponseJSON } from '@simplewebauthn/types'; | ||||
| import { HttpHeader } from 'fastify/types/utils.js'; | ||||
| import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; | ||||
| import { MiUser } from '@/models/User.js'; | ||||
| import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; | ||||
| import { IdService } from '@/core/IdService.js'; | ||||
| import { GlobalModule } from '@/GlobalModule.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { CoreModule } from '@/core/CoreModule.js'; | ||||
| import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js'; | ||||
| import { RateLimiterService } from '@/server/api/RateLimiterService.js'; | ||||
| import { WebAuthnService } from '@/core/WebAuthnService.js'; | ||||
| import { SigninService } from '@/server/api/SigninService.js'; | ||||
| import { IdentifiableError } from '@/misc/identifiable-error.js'; | ||||
| 
 | ||||
| const moduleMocker = new ModuleMocker(global); | ||||
| 
 | ||||
| class FakeLimiter { | ||||
| 	public async limit() { | ||||
| 		return; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| class FakeSigninService { | ||||
| 	public signin(..._args: any): any { | ||||
| 		return true; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| class DummyFastifyReply { | ||||
| 	public statusCode: number; | ||||
| 	code(num: number): void { | ||||
| 		this.statusCode = num; | ||||
| 	} | ||||
| 	header(_key: HttpHeader, _value: any): void { | ||||
| 	} | ||||
| } | ||||
| class DummyFastifyRequest { | ||||
| 	public ip: string; | ||||
| 	public body: {credential: any, context: string}; | ||||
| 	public headers: IncomingHttpHeaders = { 'accept': 'application/json' }; | ||||
| 	constructor(body?: any) { | ||||
| 		this.ip = '0.0.0.0'; | ||||
| 		this.body = body; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type ApiFastifyRequestType = FastifyRequest<{ | ||||
| 	Body: { | ||||
| 		credential?: AuthenticationResponseJSON; | ||||
| 		context?: string; | ||||
| 	}; | ||||
| }>; | ||||
| 
 | ||||
| describe('SigninWithPasskeyApiService', () => { | ||||
| 	let app: TestingModule; | ||||
| 	let passkeyApiService: SigninWithPasskeyApiService; | ||||
| 	let usersRepository: UsersRepository; | ||||
| 	let userProfilesRepository: UserProfilesRepository; | ||||
| 	let webAuthnService: WebAuthnService; | ||||
| 	let idService: IdService; | ||||
| 	let FakeWebauthnVerify: ()=>Promise<string>; | ||||
| 
 | ||||
| 	async function createUser(data: Partial<MiUser> = {}) { | ||||
| 		const user = await usersRepository | ||||
| 			.save({ | ||||
| 				...data, | ||||
| 			}); | ||||
| 		return user; | ||||
| 	} | ||||
| 
 | ||||
| 	async function createUserProfile(data: Partial<MiUserProfile> = {}) { | ||||
| 		const userProfile = await userProfilesRepository | ||||
| 			.save({ ...data }, | ||||
| 			); | ||||
| 		return userProfile; | ||||
| 	} | ||||
| 
 | ||||
| 	beforeAll(async () => { | ||||
| 		app = await Test.createTestingModule({ | ||||
| 			imports: [GlobalModule, CoreModule], | ||||
| 			providers: [ | ||||
| 				SigninWithPasskeyApiService,  | ||||
| 				{ provide: RateLimiterService, useClass: FakeLimiter },  | ||||
| 				{ provide: SigninService, useClass: FakeSigninService }, | ||||
| 			], | ||||
| 		}).useMocker((token) => { | ||||
| 			if (typeof token === 'function') { | ||||
| 				const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>; | ||||
| 				const Mock = moduleMocker.generateFromMetadata(mockMetadata); | ||||
| 				return new Mock(); | ||||
| 			} | ||||
| 		}).compile(); | ||||
| 		passkeyApiService = app.get<SigninWithPasskeyApiService>(SigninWithPasskeyApiService); | ||||
| 		usersRepository = app.get<UsersRepository>(DI.usersRepository); | ||||
| 		userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository); | ||||
| 		webAuthnService = app.get<WebAuthnService>(WebAuthnService); | ||||
| 		idService = app.get<IdService>(IdService); | ||||
| 	}); | ||||
| 
 | ||||
| 	beforeEach(async () => { | ||||
| 		const uid = idService.gen(); | ||||
| 		FakeWebauthnVerify = async () => { | ||||
| 			return uid; | ||||
| 		}; | ||||
| 		jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify); | ||||
| 
 | ||||
| 		const dummyUser = { | ||||
| 			id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null, | ||||
| 		 }; | ||||
| 		const dummyProfile = { | ||||
| 			userId: uid, | ||||
| 			password: 'qwerty', | ||||
| 			usePasswordLessLogin: true, | ||||
| 		}; | ||||
| 		await createUser(dummyUser); | ||||
| 		await createUserProfile(dummyProfile); | ||||
| 	}); | ||||
| 
 | ||||
| 	afterAll(async () => { | ||||
| 		await app.close(); | ||||
| 	}); | ||||
| 
 | ||||
| 	describe('Get Passkey Options', () => { | ||||
| 		it('Should return passkey Auth Options', async () => { | ||||
| 			const req = new DummyFastifyRequest({}) as ApiFastifyRequestType; | ||||
| 			const res = new DummyFastifyReply() as unknown as FastifyReply; | ||||
| 			const res_body = await passkeyApiService.signin(req, res); | ||||
| 			expect(res.statusCode).toBe(200); | ||||
| 			expect((res_body as any).option).toBeDefined(); | ||||
| 			expect(typeof (res_body as any).context).toBe('string'); | ||||
| 		}); | ||||
| 	}); | ||||
| 	describe('Try Passkey Auth', () => { | ||||
| 		it('Should Success', async () => { | ||||
| 			const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType; | ||||
| 			const res = new DummyFastifyReply() as FastifyReply; | ||||
| 			const res_body = await passkeyApiService.signin(req, res); | ||||
| 			expect((res_body as any).signinResponse).toBeDefined(); | ||||
| 		}); | ||||
| 
 | ||||
| 		it('Should return 400 Without Auth Context', async () => { | ||||
| 			const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType; | ||||
| 			const res = new DummyFastifyReply() as FastifyReply; | ||||
| 			const res_body = await passkeyApiService.signin(req, res); | ||||
| 			expect(res.statusCode).toBe(400); | ||||
| 			expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1'); | ||||
| 		}); | ||||
| 
 | ||||
| 		it('Should return 403 When Challenge Verify fail', async () => { | ||||
| 			const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; | ||||
| 			const res = new DummyFastifyReply() as FastifyReply; | ||||
| 			jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication') | ||||
| 				.mockImplementation(async () => { | ||||
| 					throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); | ||||
| 				}); | ||||
| 			const res_body = await passkeyApiService.signin(req, res); | ||||
| 			expect(res.statusCode).toBe(403); | ||||
| 			expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); | ||||
| 		}); | ||||
| 
 | ||||
| 		it('Should return 403 When The user not Enabled Passwordless login', async () => { | ||||
| 			const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; | ||||
| 			const res = new DummyFastifyReply() as FastifyReply; | ||||
| 			const userId = await FakeWebauthnVerify(); | ||||
| 			const data = { userId: userId, usePasswordLessLogin: false }; | ||||
| 			await userProfilesRepository.update({ userId: userId }, data); | ||||
| 			const res_body = await passkeyApiService.signin(req, res); | ||||
| 			expect(res.statusCode).toBe(403); | ||||
| 			expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912'); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|  | @ -12,59 +12,46 @@ | |||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@discordapp/twemoji": "15.1.0", | ||||
| 		"@github/webauthn-json": "2.1.1", | ||||
| 		"@rollup/plugin-json": "6.1.0", | ||||
| 		"@rollup/plugin-replace": "5.0.7", | ||||
| 		"@rollup/pluginutils": "5.1.0", | ||||
| 		"@rollup/pluginutils": "5.1.2", | ||||
| 		"@tabler/icons-webfont": "3.3.0", | ||||
| 		"@twemoji/parser": "15.1.1", | ||||
| 		"@vitejs/plugin-vue": "5.1.4", | ||||
| 		"@vue/compiler-sfc": "3.5.7", | ||||
| 		"@vue/compiler-sfc": "3.5.10", | ||||
| 		"astring": "1.9.0", | ||||
| 		"buraha": "0.0.1", | ||||
| 		"compare-versions": "6.1.1", | ||||
| 		"date-fns": "2.30.0", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"estree-walker": "3.0.3", | ||||
| 		"eventemitter3": "5.0.1", | ||||
| 		"idb-keyval": "6.2.1", | ||||
| 		"is-file-animated": "1.0.2", | ||||
| 		"mfm-js": "0.24.0", | ||||
| 		"misskey-js": "workspace:*", | ||||
| 		"frontend-shared": "workspace:*", | ||||
| 		"punycode": "2.3.1", | ||||
| 		"rollup": "4.22.2", | ||||
| 		"sanitize-html": "2.13.0", | ||||
| 		"rollup": "4.22.5", | ||||
| 		"sass": "1.79.3", | ||||
| 		"shiki": "1.12.0", | ||||
| 		"strict-event-emitter-types": "2.0.0", | ||||
| 		"throttle-debounce": "5.0.2", | ||||
| 		"tinycolor2": "1.6.0", | ||||
| 		"tsc-alias": "1.8.10", | ||||
| 		"tsconfig-paths": "4.2.0", | ||||
| 		"typescript": "5.6.2", | ||||
| 		"uuid": "10.0.0", | ||||
| 		"json5": "2.2.3", | ||||
| 		"vite": "5.4.7", | ||||
| 		"vue": "3.5.7" | ||||
| 		"vite": "5.4.8", | ||||
| 		"vue": "3.5.10" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@misskey-dev/summaly": "5.1.0", | ||||
| 		"@testing-library/vue": "8.1.0", | ||||
| 		"@types/escape-regexp": "0.0.3", | ||||
| 		"@types/estree": "1.0.6", | ||||
| 		"@types/micromatch": "4.0.9", | ||||
| 		"@types/node": "20.14.12", | ||||
| 		"@types/punycode": "2.1.4", | ||||
| 		"@types/sanitize-html": "2.13.0", | ||||
| 		"@types/throttle-debounce": "5.0.2", | ||||
| 		"@types/tinycolor2": "1.4.6", | ||||
| 		"@types/uuid": "10.0.0", | ||||
| 		"@types/ws": "8.5.12", | ||||
| 		"@typescript-eslint/eslint-plugin": "7.17.0", | ||||
| 		"@typescript-eslint/parser": "7.17.0", | ||||
| 		"@vitest/coverage-v8": "1.6.0", | ||||
| 		"@vue/runtime-core": "3.5.7", | ||||
| 		"@vue/runtime-core": "3.5.10", | ||||
| 		"acorn": "8.12.1", | ||||
| 		"cross-env": "7.0.3", | ||||
| 		"eslint-plugin-import": "2.30.0", | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
| 		accentDarken: ':darken<10<@accent', | ||||
| 		accentLighten: ':lighten<10<@accent', | ||||
| 		accentedBg: ':alpha<0.15<@accent', | ||||
| 		love: '#dd2e44', | ||||
| 		focus: ':alpha<0.3<@accent', | ||||
| 		bg: '#000', | ||||
| 		acrylicBg: ':alpha<0.5<@bg', | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
| 		accentDarken: ':darken<10<@accent', | ||||
| 		accentLighten: ':lighten<10<@accent', | ||||
| 		accentedBg: ':alpha<0.15<@accent', | ||||
| 		love: '#dd2e44', | ||||
| 		focus: ':alpha<0.3<@accent', | ||||
| 		bg: '#fff', | ||||
| 		acrylicBg: ':alpha<0.5<@bg', | ||||
|  |  | |||
|  | @ -23,12 +23,12 @@ | |||
| 		"@misskey-dev/browser-image-resizer": "2024.1.0", | ||||
| 		"@rollup/plugin-json": "6.1.0", | ||||
| 		"@rollup/plugin-replace": "5.0.7", | ||||
| 		"@rollup/pluginutils": "5.1.0", | ||||
| 		"@rollup/pluginutils": "5.1.2", | ||||
| 		"@syuilo/aiscript": "0.19.0", | ||||
| 		"@tabler/icons-webfont": "3.3.0", | ||||
| 		"@twemoji/parser": "15.1.1", | ||||
| 		"@vitejs/plugin-vue": "5.1.4", | ||||
| 		"@vue/compiler-sfc": "3.5.7", | ||||
| 		"@vue/compiler-sfc": "3.5.10", | ||||
| 		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11", | ||||
| 		"astring": "1.9.0", | ||||
| 		"broadcast-channel": "7.0.0", | ||||
|  | @ -39,11 +39,10 @@ | |||
| 		"chartjs-chart-matrix": "2.0.1", | ||||
| 		"chartjs-plugin-gradient": "0.6.1", | ||||
| 		"chartjs-plugin-zoom": "2.0.1", | ||||
| 		"chromatic": "11.10.2", | ||||
| 		"chromatic": "11.10.4", | ||||
| 		"compare-versions": "6.1.1", | ||||
| 		"cropperjs": "2.0.0-rc.2", | ||||
| 		"date-fns": "2.30.0", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"estree-walker": "3.0.3", | ||||
| 		"eventemitter3": "5.0.1", | ||||
| 		"idb-keyval": "6.2.1", | ||||
|  | @ -58,13 +57,13 @@ | |||
| 		"frontend-shared": "workspace:*", | ||||
| 		"photoswipe": "5.4.4", | ||||
| 		"punycode": "2.3.1", | ||||
| 		"rollup": "4.22.2", | ||||
| 		"rollup": "4.22.5", | ||||
| 		"sanitize-html": "2.13.0", | ||||
| 		"sass": "1.79.3", | ||||
| 		"shiki": "1.12.0", | ||||
| 		"strict-event-emitter-types": "2.0.0", | ||||
| 		"textarea-caret": "3.1.0", | ||||
| 		"three": "0.168.0", | ||||
| 		"three": "0.169.0", | ||||
| 		"throttle-debounce": "5.0.2", | ||||
| 		"tinycolor2": "1.6.0", | ||||
| 		"tsc-alias": "1.8.10", | ||||
|  | @ -72,32 +71,31 @@ | |||
| 		"typescript": "5.6.2", | ||||
| 		"uuid": "10.0.0", | ||||
| 		"v-code-diff": "1.13.1", | ||||
| 		"vite": "5.4.7", | ||||
| 		"vue": "3.5.7", | ||||
| 		"vite": "5.4.8", | ||||
| 		"vue": "3.5.10", | ||||
| 		"vuedraggable": "next" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@misskey-dev/summaly": "5.1.0", | ||||
| 		"@storybook/addon-actions": "8.3.2", | ||||
| 		"@storybook/addon-essentials": "8.3.2", | ||||
| 		"@storybook/addon-interactions": "8.3.2", | ||||
| 		"@storybook/addon-links": "8.3.2", | ||||
| 		"@storybook/addon-mdx-gfm": "8.3.2", | ||||
| 		"@storybook/addon-storysource": "8.3.2", | ||||
| 		"@storybook/blocks": "8.3.2", | ||||
| 		"@storybook/components": "8.3.2", | ||||
| 		"@storybook/core-events": "8.3.2", | ||||
| 		"@storybook/manager-api": "8.3.2", | ||||
| 		"@storybook/preview-api": "8.3.2", | ||||
| 		"@storybook/react": "8.3.2", | ||||
| 		"@storybook/react-vite": "8.3.2", | ||||
| 		"@storybook/test": "8.3.2", | ||||
| 		"@storybook/theming": "8.3.2", | ||||
| 		"@storybook/types": "8.3.2", | ||||
| 		"@storybook/vue3": "8.3.2", | ||||
| 		"@storybook/vue3-vite": "8.3.2", | ||||
| 		"@storybook/addon-actions": "8.3.3", | ||||
| 		"@storybook/addon-essentials": "8.3.3", | ||||
| 		"@storybook/addon-interactions": "8.3.3", | ||||
| 		"@storybook/addon-links": "8.3.3", | ||||
| 		"@storybook/addon-mdx-gfm": "8.3.3", | ||||
| 		"@storybook/addon-storysource": "8.3.3", | ||||
| 		"@storybook/blocks": "8.3.3", | ||||
| 		"@storybook/components": "8.3.3", | ||||
| 		"@storybook/core-events": "8.3.3", | ||||
| 		"@storybook/manager-api": "8.3.3", | ||||
| 		"@storybook/preview-api": "8.3.3", | ||||
| 		"@storybook/react": "8.3.3", | ||||
| 		"@storybook/react-vite": "8.3.3", | ||||
| 		"@storybook/test": "8.3.3", | ||||
| 		"@storybook/theming": "8.3.3", | ||||
| 		"@storybook/types": "8.3.3", | ||||
| 		"@storybook/vue3": "8.3.3", | ||||
| 		"@storybook/vue3-vite": "8.3.3", | ||||
| 		"@testing-library/vue": "8.1.0", | ||||
| 		"@types/escape-regexp": "0.0.3", | ||||
| 		"@types/estree": "1.0.6", | ||||
| 		"@types/matter-js": "0.19.7", | ||||
| 		"@types/micromatch": "4.0.9", | ||||
|  | @ -112,10 +110,10 @@ | |||
| 		"@typescript-eslint/eslint-plugin": "7.17.0", | ||||
| 		"@typescript-eslint/parser": "7.17.0", | ||||
| 		"@vitest/coverage-v8": "1.6.0", | ||||
| 		"@vue/runtime-core": "3.5.7", | ||||
| 		"@vue/runtime-core": "3.5.10", | ||||
| 		"acorn": "8.12.1", | ||||
| 		"cross-env": "7.0.3", | ||||
| 		"cypress": "13.14.2", | ||||
| 		"cypress": "13.15.0", | ||||
| 		"eslint-plugin-import": "2.30.0", | ||||
| 		"eslint-plugin-vue": "9.28.0", | ||||
| 		"fast-glob": "3.3.2", | ||||
|  | @ -130,7 +128,7 @@ | |||
| 		"react-dom": "18.3.1", | ||||
| 		"seedrandom": "3.0.5", | ||||
| 		"start-server-and-test": "2.0.8", | ||||
| 		"storybook": "8.3.2", | ||||
| 		"storybook": "8.3.3", | ||||
| 		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", | ||||
| 		"vite-plugin-turbosnap": "1.0.3", | ||||
| 		"vitest": "1.6.0", | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> | ||||
| 		</template> | ||||
| 	</MkFolder> | ||||
| 	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> | ||||
| 	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="containerStyle"> | ||||
| 		<template v-for="child in c.children" :key="child"> | ||||
| 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/> | ||||
| 		</template> | ||||
|  | @ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { Ref, ref } from 'vue'; | ||||
| import { Ref, ref, computed } from 'vue'; | ||||
| import * as os from '@/os.js'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkInput from '@/components/MkInput.vue'; | ||||
|  | @ -97,6 +97,29 @@ function g(id) { | |||
| 	} as AsUiRoot; | ||||
| } | ||||
| 
 | ||||
| const containerStyle = computed(() => { | ||||
| 	if (c.type !== 'container') return undefined; | ||||
| 
 | ||||
| 	// width, color, styleのうち一つでも指定があれば、枠線がちゃんと表示されるようにwidthとstyleのデフォルト値を設定 | ||||
| 	// radiusは単に角を丸める用途もあるため除外 | ||||
| 	const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle; | ||||
| 
 | ||||
| 	const border = isBordered ? { | ||||
| 		borderWidth: c.borderWidth ?? '1px', | ||||
| 		borderColor: c.borderColor ?? 'var(--divider)', | ||||
| 		borderStyle: c.borderStyle ?? 'solid', | ||||
| 	} : undefined; | ||||
| 
 | ||||
| 	return { | ||||
| 		textAlign: c.align, | ||||
| 		backgroundColor: c.bgColor, | ||||
| 		color: c.fgColor, | ||||
| 		padding: c.padding ? `${c.padding}px` : 0, | ||||
| 		borderRadius: (c.borderRadius ?? (c.rounded ? 8 : 0)) + 'px', | ||||
| 		...border, | ||||
| 	}; | ||||
| }); | ||||
| 
 | ||||
| const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false); | ||||
| 
 | ||||
| function onSwitchUpdate(v) { | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> | ||||
| 		</template> | ||||
| 		<template v-else-if="isFollowing"> | ||||
| 			<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> | ||||
| 			<span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i> | ||||
| 		</template> | ||||
| 		<template v-else-if="!isFollowing && user.isLocked"> | ||||
| 			<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> | ||||
|  |  | |||
|  | @ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 					<i class="ti ti-ban"></i> | ||||
| 				</button> | ||||
| 				<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()"> | ||||
| 					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> | ||||
| 					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> | ||||
| 					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> | ||||
| 					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> | ||||
| 					<i v-else class="ti ti-plus"></i> | ||||
|  |  | |||
|  | @ -128,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				<i class="ti ti-ban"></i> | ||||
| 			</button> | ||||
| 			<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> | ||||
| 				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> | ||||
| 				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> | ||||
| 				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> | ||||
| 				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> | ||||
| 				<i v-else class="ti ti-plus"></i> | ||||
|  |  | |||
|  | @ -5,14 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 
 | ||||
| <template> | ||||
| <header :class="$style.root"> | ||||
| 	<div v-if="mock" :class="$style.name"> | ||||
| 		<MkUserName :user="note.user"/> | ||||
| 	</div> | ||||
| 	<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> | ||||
| 		<MkUserName :user="note.user"/> | ||||
| 	</MkA> | ||||
| 	<div v-if="note.user.isBot" :class="$style.isBot">bot</div> | ||||
| 	<div :class="$style.username"><MkAcct :user="note.user"/></div> | ||||
| 	<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0" style="min-width: 0;"> | ||||
| 		<div style="display: flex; white-space: nowrap; align-items: baseline;"> | ||||
| 			<div v-if="mock" :class="$style.name"> | ||||
| 				<MkUserName :user="note.user"/> | ||||
| 			</div> | ||||
| 			<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> | ||||
| 				<MkUserName :user="note.user"/> | ||||
| 			</MkA> | ||||
| 			<div v-if="note.user.isBot" :class="$style.isBot">bot</div> | ||||
| 			<div :class="$style.username"><MkAcct :user="note.user"/></div> | ||||
| 		</div> | ||||
| 	</component> | ||||
| 	<div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> | ||||
| 		<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> | ||||
| 	</div> | ||||
|  | @ -40,6 +44,7 @@ import * as Misskey from 'misskey-js'; | |||
| import { i18n } from '@/i18n.js'; | ||||
| import { notePage } from '@/filters/note.js'; | ||||
| import { userPage } from '@/filters/user.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| 
 | ||||
| defineProps<{ | ||||
| 	note: Misskey.entities.Note; | ||||
|  |  | |||
|  | @ -108,7 +108,14 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			<template v-else-if="notification.type === 'follow'"> | ||||
| 				<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> | ||||
| 			</template> | ||||
| 			<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> | ||||
| 			<template v-else-if="notification.type === 'followRequestAccepted'"> | ||||
| 				<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div> | ||||
| 				<div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;"> | ||||
| 					<i class="ti ti-quote" :class="$style.quote"></i> | ||||
| 					<span>{{ notification.message }}</span> | ||||
| 					<i class="ti ti-quote" :class="$style.quote"></i> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 			<template v-else-if="notification.type === 'receiveFollowRequest'"> | ||||
| 				<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span> | ||||
| 				<div v-if="full && !followRequestDone" :class="$style.followRequestCommands"> | ||||
|  | @ -211,6 +218,14 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) | |||
| 	overflow-wrap: break-word; | ||||
| 	display: flex; | ||||
| 	contain: content; | ||||
| 
 | ||||
| 	--eventFollow: #36aed2; | ||||
| 	--eventRenote: #36d298; | ||||
| 	--eventReply: #007aff; | ||||
| 	--eventReactionHeart: var(--love); | ||||
| 	--eventReaction: #e99a0b; | ||||
| 	--eventAchievement: #cb9a11; | ||||
| 	--eventOther: #88a6b7; | ||||
| } | ||||
| 
 | ||||
| .head { | ||||
|  |  | |||
|  | @ -4,11 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| --> | ||||
| 
 | ||||
| <template> | ||||
| <MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3"> | ||||
| 	<span>@{{ user.username }}</span> | ||||
| 	<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> | ||||
| </MkCondensedLine> | ||||
| <span v-else> | ||||
| <span> | ||||
| 	<span>@{{ user.username }}</span> | ||||
| 	<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -210,6 +210,31 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 					</div> | ||||
| 				</MkFolder> | ||||
| 
 | ||||
| 				<MkFolder> | ||||
| 					<template #icon><i class="ti ti-planet"></i></template> | ||||
| 					<template #label>{{ i18n.ts.federation }}</template> | ||||
| 					<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template> | ||||
| 					<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template> | ||||
| 					<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template> | ||||
| 					<template v-if="federationForm.modified.value" #footer> | ||||
| 						<MkFormFooter :form="federationForm"/> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<div class="_gaps"> | ||||
| 						<MkRadios v-model="federationForm.state.federation"> | ||||
| 							<template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template> | ||||
| 							<option value="all">{{ i18n.ts.all }}</option> | ||||
| 							<option value="specified">{{ i18n.ts.specifyHost }}</option> | ||||
| 							<option value="none">{{ i18n.ts.none }}</option> | ||||
| 						</MkRadios> | ||||
| 
 | ||||
| 						<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts"> | ||||
| 							<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> | ||||
| 					</div> | ||||
| 				</MkFolder> | ||||
| 
 | ||||
| 				<MkFolder> | ||||
| 					<template #icon><i class="ti ti-ghost"></i></template> | ||||
| 					<template #label>{{ i18n.ts.proxyAccount }}</template> | ||||
|  | @ -248,6 +273,7 @@ import MkFolder from '@/components/MkFolder.vue'; | |||
| import MkKeyValue from '@/components/MkKeyValue.vue'; | ||||
| import { useForm } from '@/scripts/use-form.js'; | ||||
| import MkFormFooter from '@/components/MkFormFooter.vue'; | ||||
| import MkRadios from '@/components/MkRadios.vue'; | ||||
| 
 | ||||
| const meta = await misskeyApi('admin/meta'); | ||||
| 
 | ||||
|  | @ -341,6 +367,17 @@ const urlPreviewForm = useForm({ | |||
| 	fetchInstance(true); | ||||
| }); | ||||
| 
 | ||||
| const federationForm = useForm({ | ||||
| 	federation: meta.federation, | ||||
| 	federationHosts: meta.federationHosts.join('\n'), | ||||
| }, async (state) => { | ||||
| 	await os.apiWithDialog('admin/update-meta', { | ||||
| 		federation: state.federation, | ||||
| 		federationHosts: state.federationHosts.split('\n'), | ||||
| 	}); | ||||
| 	fetchInstance(true); | ||||
| }); | ||||
| 
 | ||||
| function chooseProxyAccount() { | ||||
| 	os.selectUser({ localOnly: true }).then(user => { | ||||
| 		proxyAccount.value = user; | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			<MkContainer :foldable="true" :expanded="false"> | ||||
| 				<template #header>{{ i18n.ts.uiInspector }}</template> | ||||
| 				<div :class="$style.uiInspector"> | ||||
| 					<div v-for="c in components" :key="c.value.id"> | ||||
| 					<div v-for="c in components" :key="c.value.id" :class="{ [$style.uiInspectorUnShown]: !showns.has(c.value.id) }"> | ||||
| 						<div :class="$style.uiInspectorType">{{ c.value.type }}</div> | ||||
| 						<div :class="$style.uiInspectorId">{{ c.value.id }}</div> | ||||
| 						<button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))"> | ||||
|  | @ -180,6 +180,20 @@ const headerActions = computed(() => []); | |||
| 
 | ||||
| const headerTabs = computed(() => []); | ||||
| 
 | ||||
| const showns = computed(() => { | ||||
| 	const result = new Set<string>(); | ||||
| 	(function addChildrenToResult(c: AsUiComponent) { | ||||
| 		result.add(c.id); | ||||
| 		if (c.children) { | ||||
| 			const childComponents = components.value.filter(v => c.children.includes(v.value.id)); | ||||
| 			for (const child of childComponents) { | ||||
| 				addChildrenToResult(child.value); | ||||
| 			} | ||||
| 		} | ||||
| 	})(root.value); | ||||
| 	return result; | ||||
| }); | ||||
| 
 | ||||
| definePageMetadata(() => ({ | ||||
| 	title: i18n.ts.scratchpad, | ||||
| 	icon: 'ti ti-terminal-2', | ||||
|  | @ -227,6 +241,10 @@ definePageMetadata(() => ({ | |||
| 	padding: 16px; | ||||
| } | ||||
| 
 | ||||
| .uiInspectorUnShown { | ||||
| 	color: var(--fgTransparent); | ||||
| } | ||||
| 
 | ||||
| .uiInspectorType { | ||||
| 	display: inline-block; | ||||
| 	border: hidden; | ||||
|  |  | |||
|  | @ -51,8 +51,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				<template #label>{{ i18n.ts.experimentalFeatures }}</template> | ||||
| 
 | ||||
| 				<div class="_gaps_m"> | ||||
| 					<MkSwitch v-model="enableCondensedLineForAcct"> | ||||
| 						<template #label>Enable condensed line for acct</template> | ||||
| 					<MkSwitch v-model="enableCondensedLine"> | ||||
| 						<template #label>Enable condensed line</template> | ||||
| 					</MkSwitch> | ||||
| 				</div> | ||||
| 			</MkFolder> | ||||
|  | @ -104,7 +104,7 @@ import FormSection from '@/components/form/section.vue'; | |||
| const $i = signinRequired(); | ||||
| 
 | ||||
| const reportError = computed(defaultStore.makeGetterSetter('reportError')); | ||||
| const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); | ||||
| const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine')); | ||||
| const devMode = computed(defaultStore.makeGetterSetter('devMode')); | ||||
| const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); | ||||
| 
 | ||||
|  | @ -142,12 +142,6 @@ async function updateRepliesAll(withReplies: boolean) { | |||
| 	misskeyApi('following/update-all', { withReplies }); | ||||
| } | ||||
| 
 | ||||
| watch([ | ||||
| 	enableCondensedLineForAcct, | ||||
| ], async () => { | ||||
| 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); | ||||
| }); | ||||
| 
 | ||||
| const headerActions = computed(() => []); | ||||
| 
 | ||||
| const headerTabs = computed(() => []); | ||||
|  |  | |||
|  | @ -103,7 +103,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ | |||
| 	'mediaListWithOneImageAppearance', | ||||
| 	'notificationPosition', | ||||
| 	'notificationStackAxis', | ||||
| 	'enableCondensedLineForAcct', | ||||
| 	'keepScreenOn', | ||||
| 	'defaultWithReplies', | ||||
| 	'disableStreamingTimeline', | ||||
|  |  | |||
|  | @ -88,14 +88,13 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 		<template #caption>{{ i18n.ts._profile.metadataDescription }}</template> | ||||
| 	</FormSlot> | ||||
| 
 | ||||
| 	<MkFolder> | ||||
| 		<template #label>{{ i18n.ts.advancedSettings }}</template> | ||||
| 
 | ||||
| 		<div class="_gaps_m"> | ||||
| 			<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> | ||||
| 			<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> | ||||
| 		</div> | ||||
| 	</MkFolder> | ||||
| 	<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false"> | ||||
| 		<template #label>{{ i18n.ts._profile.followedMessage }}<span class="_beta">{{ i18n.ts.beta }}</span></template> | ||||
| 		<template #caption> | ||||
| 			<div>{{ i18n.ts._profile.followedMessageDescription }}</div> | ||||
| 			<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div> | ||||
| 		</template> | ||||
| 	</MkInput> | ||||
| 
 | ||||
| 	<MkSelect v-model="reactionAcceptance"> | ||||
| 		<template #label>{{ i18n.ts.reactionAcceptance }}</template> | ||||
|  | @ -105,6 +104,15 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 		<option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option> | ||||
| 		<option value="likeOnly">{{ i18n.ts.likeOnly }}</option> | ||||
| 	</MkSelect> | ||||
| 
 | ||||
| 	<MkFolder> | ||||
| 		<template #label>{{ i18n.ts.advancedSettings }}</template> | ||||
| 
 | ||||
| 		<div class="_gaps_m"> | ||||
| 			<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> | ||||
| 			<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> | ||||
| 		</div> | ||||
| 	</MkFolder> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -138,6 +146,7 @@ const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAccep | |||
| const profile = reactive({ | ||||
| 	name: $i.name, | ||||
| 	description: $i.description, | ||||
| 	followedMessage: $i.followedMessage, | ||||
| 	location: $i.location, | ||||
| 	birthday: $i.birthday, | ||||
| 	lang: $i.lang, | ||||
|  | @ -185,6 +194,8 @@ function save() { | |||
| 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | ||||
| 		description: profile.description || null, | ||||
| 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | ||||
| 		followedMessage: profile.followedMessage || null, | ||||
| 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | ||||
| 		location: profile.location || null, | ||||
| 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | ||||
| 		birthday: profile.birthday || null, | ||||
|  |  | |||
|  | @ -47,6 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 							<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div v-if="user.followedMessage != null" class="followedMessage"> | ||||
| 						<div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;"> | ||||
| 							<Mfm :text="user.followedMessage" :author="user"/> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div v-if="user.roles.length > 0" class="roles"> | ||||
| 						<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> | ||||
| 							<MkA v-adaptive-bg :to="`/roles/${role.id}`"> | ||||
|  | @ -460,6 +465,11 @@ onUnmounted(() => { | |||
| 					box-shadow: 1px 1px 3px rgba(#000, 0.2); | ||||
| 				} | ||||
| 
 | ||||
| 				> .followedMessage { | ||||
| 					padding: 24px 24px 0 154px; | ||||
| 					font-size: 0.9em; | ||||
| 				} | ||||
| 
 | ||||
| 				> .roles { | ||||
| 					padding: 24px 24px 0 154px; | ||||
| 					font-size: 0.95em; | ||||
|  | @ -642,6 +652,10 @@ onUnmounted(() => { | |||
| 					margin: auto; | ||||
| 				} | ||||
| 
 | ||||
| 				> .followedMessage { | ||||
| 					padding: 16px 16px 0 16px; | ||||
| 				} | ||||
| 
 | ||||
| 				> .roles { | ||||
| 					padding: 16px 16px 0 16px; | ||||
| 					justify-content: center; | ||||
|  |  | |||
|  | @ -27,6 +27,8 @@ export type AsUiContainer = AsUiComponentBase & { | |||
| 	font?: 'serif' | 'sans-serif' | 'monospace'; | ||||
| 	borderWidth?: number; | ||||
| 	borderColor?: string; | ||||
| 	borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'; | ||||
| 	borderRadius?: number; | ||||
| 	padding?: number; | ||||
| 	rounded?: boolean; | ||||
| 	hidden?: boolean; | ||||
|  | @ -173,6 +175,10 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, | |||
| 	if (borderWidth) utils.assertNumber(borderWidth); | ||||
| 	const borderColor = def.value.get('borderColor'); | ||||
| 	if (borderColor) utils.assertString(borderColor); | ||||
| 	const borderStyle = def.value.get('borderStyle'); | ||||
| 	if (borderStyle) utils.assertString(borderStyle); | ||||
| 	const borderRadius = def.value.get('borderRadius'); | ||||
| 	if (borderRadius) utils.assertNumber(borderRadius); | ||||
| 	const padding = def.value.get('padding'); | ||||
| 	if (padding) utils.assertNumber(padding); | ||||
| 	const rounded = def.value.get('rounded'); | ||||
|  | @ -191,6 +197,8 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, | |||
| 		font: font?.value, | ||||
| 		borderWidth: borderWidth?.value, | ||||
| 		borderColor: borderColor?.value, | ||||
| 		borderStyle: borderStyle?.value, | ||||
| 		borderRadius: borderRadius?.value, | ||||
| 		padding: padding?.value, | ||||
| 		rounded: rounded?.value, | ||||
| 		hidden: hidden?.value, | ||||
|  |  | |||
|  | @ -392,9 +392,9 @@ export const defaultStore = markRaw(new Storage('base', { | |||
| 		where: 'device', | ||||
| 		default: 'horizontal' as 'vertical' | 'horizontal', | ||||
| 	}, | ||||
| 	enableCondensedLineForAcct: { | ||||
| 	enableCondensedLine: { | ||||
| 		where: 'device', | ||||
| 		default: false, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	additionalUnicodeEmojiIndexes: { | ||||
| 		where: 'device', | ||||
|  |  | |||
|  | @ -18,13 +18,6 @@ | |||
| 	--minBottomSpacing: var(--minBottomSpacingMobile); | ||||
| 
 | ||||
| 	//--ad: rgb(255 169 0 / 10%); | ||||
| 	--eventFollow: #36aed2; | ||||
| 	--eventRenote: #36d298; | ||||
| 	--eventReply: #007aff; | ||||
| 	--eventReactionHeart: #dd2e44; | ||||
| 	--eventReaction: #e99a0b; | ||||
| 	--eventAchievement: #cb9a11; | ||||
| 	--eventOther: #88a6b7; | ||||
| 
 | ||||
| 	@media (max-width: 500px) { | ||||
| 		--margin: var(--marginHalf); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	"type": "module", | ||||
| 	"name": "misskey-js", | ||||
| 	"version": "2024.9.0-alpha.10", | ||||
| 	"version": "2024.9.0-alpha.12", | ||||
| 	"description": "Misskey SDK for JavaScript", | ||||
| 	"license": "MIT", | ||||
| 	"main": "./built/index.js", | ||||
|  |  | |||
|  | @ -3789,6 +3789,7 @@ export type components = { | |||
|       /** @default false */ | ||||
|       securityKeys: boolean; | ||||
|       roles: components['schemas']['RoleLite'][]; | ||||
|       followedMessage?: string | null; | ||||
|       memo: string | null; | ||||
|       moderationNote?: string; | ||||
|       isFollowing?: boolean; | ||||
|  | @ -3808,6 +3809,7 @@ export type components = { | |||
|       avatarId: string | null; | ||||
|       /** Format: id */ | ||||
|       bannerId: string | null; | ||||
|       followedMessage: string | null; | ||||
|       isModerator: boolean | null; | ||||
|       isAdmin: boolean | null; | ||||
|       injectFeaturedNote: boolean; | ||||
|  | @ -4247,7 +4249,7 @@ export type components = { | |||
|       user: components['schemas']['UserLite']; | ||||
|       /** Format: id */ | ||||
|       userId: string; | ||||
|     } | { | ||||
|     } | ({ | ||||
|       /** Format: id */ | ||||
|       id: string; | ||||
|       /** Format: date-time */ | ||||
|  | @ -4257,7 +4259,8 @@ export type components = { | |||
|       user: components['schemas']['UserLite']; | ||||
|       /** Format: id */ | ||||
|       userId: string; | ||||
|     } | { | ||||
|       message: string | null; | ||||
|     }) | { | ||||
|       /** Format: id */ | ||||
|       id: string; | ||||
|       /** Format: date-time */ | ||||
|  | @ -8936,6 +8939,7 @@ export type operations = { | |||
|           'application/json': { | ||||
|             email: string | null; | ||||
|             emailVerified: boolean; | ||||
|             followedMessage: string | null; | ||||
|             autoAcceptFollowed: boolean; | ||||
|             noCrawle: boolean; | ||||
|             preventAiLearning: boolean; | ||||
|  | @ -19676,6 +19680,7 @@ export type operations = { | |||
|         'application/json': { | ||||
|           name?: string | null; | ||||
|           description?: string | null; | ||||
|           followedMessage?: string | null; | ||||
|           location?: string | null; | ||||
|           birthday?: string | null; | ||||
|           /** @enum {string|null} */ | ||||
|  |  | |||
							
								
								
									
										1173
									
								
								pnpm-lock.yaml
								
								
								
								
							
							
						
						
									
										1173
									
								
								pnpm-lock.yaml
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue