enhance: “つながりの公開範囲”がフォロー・フォロワー個別設定できるように (#12702)
* Enhance: “つながりの公開範囲”がフォロー・フォロワー個別設定できるように (#12072) * refactor: crowdin 編集部分のコミットを打ち消し https://github.com/misskey-dev/misskey/pull/12702#issuecomment-1859417158 * refactor: オブジェクトの名前修正 https://github.com/misskey-dev/misskey/pull/12702#issuecomment-1859417158 * fix: 設定項目の説明を削除 名称が具体的になって必要なくなったため https://github.com/misskey-dev/misskey/pull/12702#discussion_r1429932463
This commit is contained in:
		
							parent
							
								
									f6ff3b1f1a
								
							
						
					
					
						commit
						4e2d802967
					
				
					 23 changed files with 648 additions and 71 deletions
				
			
		| 
						 | 
				
			
			@ -33,6 +33,7 @@
 | 
			
		|||
- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加
 | 
			
		||||
- Enhance: アイコンデコレーションを複数設定できるように
 | 
			
		||||
- Enhance: アイコンデコレーションの位置を微調整できるように
 | 
			
		||||
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
 | 
			
		||||
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -884,8 +884,8 @@ export interface Locale {
 | 
			
		|||
    "classic": string;
 | 
			
		||||
    "muteThread": string;
 | 
			
		||||
    "unmuteThread": string;
 | 
			
		||||
    "ffVisibility": string;
 | 
			
		||||
    "ffVisibilityDescription": string;
 | 
			
		||||
    "followingVisibility": string;
 | 
			
		||||
    "followersVisibility": string;
 | 
			
		||||
    "continueThread": string;
 | 
			
		||||
    "deleteAccountConfirm": string;
 | 
			
		||||
    "incorrectPassword": string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -881,8 +881,8 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を
 | 
			
		|||
classic: "クラシック"
 | 
			
		||||
muteThread: "スレッドをミュート"
 | 
			
		||||
unmuteThread: "スレッドのミュートを解除"
 | 
			
		||||
ffVisibility: "つながりの公開範囲"
 | 
			
		||||
ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。"
 | 
			
		||||
followingVisibility: "フォローの公開範囲"
 | 
			
		||||
followersVisibility: "フォロワーの公開範囲"
 | 
			
		||||
continueThread: "さらにスレッドを見る"
 | 
			
		||||
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
 | 
			
		||||
incorrectPassword: "パスワードが間違っています。"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										35
									
								
								packages/backend/migration/1702718871541-ffVisibility.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								packages/backend/migration/1702718871541-ffVisibility.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class ffVisibility1702718871541 {
 | 
			
		||||
	constructor() {
 | 
			
		||||
			this.name = 'ffVisibility1702718871541';
 | 
			
		||||
	}
 | 
			
		||||
	async up(queryRunner) {
 | 
			
		||||
		await queryRunner.query(`CREATE TYPE "public"."user_profile_followingvisibility_enum" AS ENUM('public', 'followers', 'private')`);
 | 
			
		||||
		await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum") WITH INOUT AS ASSIGNMENT`);
 | 
			
		||||
		await queryRunner.query(`CREATE TYPE "public"."user_profile_followersVisibility_enum" AS ENUM('public', 'followers', 'private')`);
 | 
			
		||||
		await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum") WITH INOUT AS ASSIGNMENT`);
 | 
			
		||||
		await queryRunner.query(`ALTER TABLE "user_profile" ADD "followingVisibility" "public"."user_profile_followingvisibility_enum" NOT NULL DEFAULT 'public'`);
 | 
			
		||||
		await queryRunner.query(`ALTER TABLE "user_profile" ADD "followersVisibility" "public"."user_profile_followersVisibility_enum" NOT NULL DEFAULT 'public'`);
 | 
			
		||||
		await queryRunner.query(`UPDATE "user_profile" SET "followingVisibility" = "ffVisibility"`);
 | 
			
		||||
		await queryRunner.query(`UPDATE "user_profile" SET "followersVisibility" = "ffVisibility"`);
 | 
			
		||||
		await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum")`);
 | 
			
		||||
		await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum")`);
 | 
			
		||||
		await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`);
 | 
			
		||||
		await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`);
 | 
			
		||||
	}
 | 
			
		||||
	async down(queryRunner) {
 | 
			
		||||
		await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`);
 | 
			
		||||
		await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`);
 | 
			
		||||
		await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`);
 | 
			
		||||
		await queryRunner.query(`UPDATE "user_profile" SET ffVisibility = "user_profile"."followingVisibility"`);
 | 
			
		||||
		await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`);
 | 
			
		||||
		await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`);
 | 
			
		||||
		await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`);
 | 
			
		||||
		await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`);
 | 
			
		||||
		await queryRunner.query(`DROP TYPE "public"."user_profile_followingvisibility_enum"`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -332,13 +332,13 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
		const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
 | 
			
		||||
 | 
			
		||||
		const followingCount = profile == null ? null :
 | 
			
		||||
			(profile.ffVisibility === 'public') || isMe ? user.followingCount :
 | 
			
		||||
			(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
 | 
			
		||||
			(profile.followingVisibility === 'public') || isMe ? user.followingCount :
 | 
			
		||||
			(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
 | 
			
		||||
			null;
 | 
			
		||||
 | 
			
		||||
		const followersCount = profile == null ? null :
 | 
			
		||||
			(profile.ffVisibility === 'public') || isMe ? user.followersCount :
 | 
			
		||||
			(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
 | 
			
		||||
			(profile.followersVisibility === 'public') || isMe ? user.followersCount :
 | 
			
		||||
			(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
 | 
			
		||||
			null;
 | 
			
		||||
 | 
			
		||||
		const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
 | 
			
		||||
| 
						 | 
				
			
			@ -417,7 +417,8 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
				pinnedPageId: profile!.pinnedPageId,
 | 
			
		||||
				pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
 | 
			
		||||
				publicReactions: profile!.publicReactions,
 | 
			
		||||
				ffVisibility: profile!.ffVisibility,
 | 
			
		||||
				followersVisibility: profile!.followersVisibility,
 | 
			
		||||
				followingVisibility: profile!.followingVisibility,
 | 
			
		||||
				twoFactorEnabled: profile!.twoFactorEnabled,
 | 
			
		||||
				usePasswordLessLogin: profile!.usePasswordLessLogin,
 | 
			
		||||
				securityKeys: profile!.twoFactorEnabled
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
 | 
			
		||||
import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js';
 | 
			
		||||
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
import { MiPage } from './Page.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -94,10 +94,16 @@ export class MiUserProfile {
 | 
			
		|||
	public publicReactions: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('enum', {
 | 
			
		||||
		enum: ffVisibility,
 | 
			
		||||
		enum: followingVisibilities,
 | 
			
		||||
		default: 'public',
 | 
			
		||||
	})
 | 
			
		||||
	public ffVisibility: typeof ffVisibility[number];
 | 
			
		||||
	public followingVisibility: typeof followingVisibilities[number];
 | 
			
		||||
 | 
			
		||||
	@Column('enum', {
 | 
			
		||||
		enum: followersVisibilities,
 | 
			
		||||
		default: 'public',
 | 
			
		||||
	})
 | 
			
		||||
	public followersVisibility: typeof followersVisibilities[number];
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128, nullable: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -311,7 +311,12 @@ export const packedUserDetailedNotMeOnlySchema = {
 | 
			
		|||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		ffVisibility: {
 | 
			
		||||
		followingVisibility: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
			enum: ['public', 'followers', 'private'],
 | 
			
		||||
		},
 | 
			
		||||
		followersVisibility: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
			enum: ['public', 'followers', 'private'],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -195,11 +195,11 @@ export class ActivityPubServerService {
 | 
			
		|||
		//#region Check ff visibility
 | 
			
		||||
		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
			
		||||
 | 
			
		||||
		if (profile.ffVisibility === 'private') {
 | 
			
		||||
		if (profile.followersVisibility === 'private') {
 | 
			
		||||
			reply.code(403);
 | 
			
		||||
			reply.header('Cache-Control', 'public, max-age=30');
 | 
			
		||||
			return;
 | 
			
		||||
		} else if (profile.ffVisibility === 'followers') {
 | 
			
		||||
		} else if (profile.followersVisibility === 'followers') {
 | 
			
		||||
			reply.code(403);
 | 
			
		||||
			reply.header('Cache-Control', 'public, max-age=30');
 | 
			
		||||
			return;
 | 
			
		||||
| 
						 | 
				
			
			@ -287,11 +287,11 @@ export class ActivityPubServerService {
 | 
			
		|||
		//#region Check ff visibility
 | 
			
		||||
		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
			
		||||
 | 
			
		||||
		if (profile.ffVisibility === 'private') {
 | 
			
		||||
		if (profile.followingVisibility === 'private') {
 | 
			
		||||
			reply.code(403);
 | 
			
		||||
			reply.header('Cache-Control', 'public, max-age=30');
 | 
			
		||||
			return;
 | 
			
		||||
		} else if (profile.ffVisibility === 'followers') {
 | 
			
		||||
		} else if (profile.followingVisibility === 'followers') {
 | 
			
		||||
			reply.code(403);
 | 
			
		||||
			reply.header('Cache-Control', 'public, max-age=30');
 | 
			
		||||
			return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -176,7 +176,8 @@ export const paramDef = {
 | 
			
		|||
		receiveAnnouncementEmail: { type: 'boolean' },
 | 
			
		||||
		alwaysMarkNsfw: { type: 'boolean' },
 | 
			
		||||
		autoSensitive: { type: 'boolean' },
 | 
			
		||||
		ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
 | 
			
		||||
		followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
 | 
			
		||||
		followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
 | 
			
		||||
		pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
		mutedWords: muteWords,
 | 
			
		||||
		hardMutedWords: muteWords,
 | 
			
		||||
| 
						 | 
				
			
			@ -241,7 +242,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
			if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
 | 
			
		||||
			if (ps.location !== undefined) profileUpdates.location = ps.location;
 | 
			
		||||
			if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
 | 
			
		||||
			if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
 | 
			
		||||
			if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
 | 
			
		||||
			if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
 | 
			
		||||
 | 
			
		||||
			function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
 | 
			
		||||
				// TODO: ちゃんと数える
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,11 +93,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
			
		||||
 | 
			
		||||
			if (profile.ffVisibility === 'private') {
 | 
			
		||||
			if (profile.followersVisibility === 'private') {
 | 
			
		||||
				if (me == null || (me.id !== user.id)) {
 | 
			
		||||
					throw new ApiError(meta.errors.forbidden);
 | 
			
		||||
				}
 | 
			
		||||
			} else if (profile.ffVisibility === 'followers') {
 | 
			
		||||
			} else if (profile.followersVisibility === 'followers') {
 | 
			
		||||
				if (me == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.forbidden);
 | 
			
		||||
				} else if (me.id !== user.id) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,11 +101,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
			
		||||
 | 
			
		||||
			if (profile.ffVisibility === 'private') {
 | 
			
		||||
			if (profile.followingVisibility === 'private') {
 | 
			
		||||
				if (me == null || (me.id !== user.id)) {
 | 
			
		||||
					throw new ApiError(meta.errors.forbidden);
 | 
			
		||||
				}
 | 
			
		||||
			} else if (profile.ffVisibility === 'followers') {
 | 
			
		||||
			} else if (profile.followingVisibility === 'followers') {
 | 
			
		||||
				if (me == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.forbidden);
 | 
			
		||||
				} else if (me.id !== user.id) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ export class FeedService {
 | 
			
		|||
			title: `${author.name} (@${user.username}@${this.config.host})`,
 | 
			
		||||
			updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined,
 | 
			
		||||
			generator: 'Misskey',
 | 
			
		||||
			description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
 | 
			
		||||
			description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
 | 
			
		||||
			link: author.link,
 | 
			
		||||
			image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
 | 
			
		||||
			feedLinks: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,8 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
 | 
			
		|||
 | 
			
		||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
 | 
			
		||||
 | 
			
		||||
export const ffVisibility = ['public', 'followers', 'private'] as const;
 | 
			
		||||
export const followingVisibilities = ['public', 'followers', 'private'] as const;
 | 
			
		||||
export const followersVisibilities = ['public', 'followers', 'private'] as const;
 | 
			
		||||
 | 
			
		||||
export const moderationLogTypes = [
 | 
			
		||||
	'updateServerSettings',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,9 +26,10 @@ describe('FF visibility', () => {
 | 
			
		|||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
 | 
			
		||||
	test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
 | 
			
		||||
		await api('/i/update', {
 | 
			
		||||
			ffVisibility: 'public',
 | 
			
		||||
			followingVisibility: 'public',
 | 
			
		||||
			followersVisibility: 'public',
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		const followingRes = await api('/users/following', {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,9 +45,88 @@ describe('FF visibility', () => {
 | 
			
		|||
		assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
 | 
			
		||||
	test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
 | 
			
		||||
		await api('/i/update', {
 | 
			
		||||
			ffVisibility: 'followers',
 | 
			
		||||
			followingVisibility: 'followers',
 | 
			
		||||
			followersVisibility: 'followers',
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		const followingRes = await api('/users/following', {
 | 
			
		||||
| 
						 | 
				
			
			@ -62,9 +142,88 @@ describe('FF visibility', () => {
 | 
			
		|||
		assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
 | 
			
		||||
	test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
 | 
			
		||||
		await api('/i/update', {
 | 
			
		||||
			ffVisibility: 'followers',
 | 
			
		||||
			followingVisibility: 'followers',
 | 
			
		||||
			followersVisibility: 'followers',
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		const followingRes = await api('/users/following', {
 | 
			
		||||
| 
						 | 
				
			
			@ -78,9 +237,82 @@ describe('FF visibility', () => {
 | 
			
		|||
		assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
 | 
			
		||||
	test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
 | 
			
		||||
		await api('/i/update', {
 | 
			
		||||
			ffVisibility: 'followers',
 | 
			
		||||
			followingVisibility: 'followers',
 | 
			
		||||
			followersVisibility: 'followers',
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		await api('/following/create', {
 | 
			
		||||
| 
						 | 
				
			
			@ -100,9 +332,106 @@ describe('FF visibility', () => {
 | 
			
		|||
		assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
 | 
			
		||||
	test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
			await api('/following/create', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
			await api('/following/create', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
			await api('/following/create', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
			await api('/following/create', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
			await api('/following/create', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
			await api('/following/create', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
 | 
			
		||||
		await api('/i/update', {
 | 
			
		||||
			ffVisibility: 'private',
 | 
			
		||||
			followingVisibility: 'private',
 | 
			
		||||
			followersVisibility: 'private',
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		const followingRes = await api('/users/following', {
 | 
			
		||||
| 
						 | 
				
			
			@ -118,9 +447,88 @@ describe('FF visibility', () => {
 | 
			
		|||
		assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
 | 
			
		||||
	test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followingRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			assert.strictEqual(Array.isArray(followersRes.body), true);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
 | 
			
		||||
		await api('/i/update', {
 | 
			
		||||
			ffVisibility: 'private',
 | 
			
		||||
			followingVisibility: 'private',
 | 
			
		||||
			followersVisibility: 'private',
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		const followingRes = await api('/users/following', {
 | 
			
		||||
| 
						 | 
				
			
			@ -134,36 +542,129 @@ describe('FF visibility', () => {
 | 
			
		|||
		assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'public',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'followers',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followingRes = await api('/users/following', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followingRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => {
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'public',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'followers',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
		{
 | 
			
		||||
			await api('/i/update', {
 | 
			
		||||
				followingVisibility: 'private',
 | 
			
		||||
				followersVisibility: 'private',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const followersRes = await api('/users/followers', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
			}, bob);
 | 
			
		||||
			assert.strictEqual(followersRes.status, 400);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('AP', () => {
 | 
			
		||||
		test('ffVisibility が public 以外ならばAPからは取得できない', async () => {
 | 
			
		||||
		test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
 | 
			
		||||
			{
 | 
			
		||||
				await api('/i/update', {
 | 
			
		||||
					ffVisibility: 'public',
 | 
			
		||||
					followingVisibility: 'public',
 | 
			
		||||
				}, alice);
 | 
			
		||||
 | 
			
		||||
				const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
 | 
			
		||||
				const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
 | 
			
		||||
				assert.strictEqual(followingRes.status, 200);
 | 
			
		||||
			}
 | 
			
		||||
			{
 | 
			
		||||
				await api('/i/update', {
 | 
			
		||||
					followingVisibility: 'followers',
 | 
			
		||||
				}, alice);
 | 
			
		||||
 | 
			
		||||
				const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
 | 
			
		||||
				assert.strictEqual(followingRes.status, 403);
 | 
			
		||||
			}
 | 
			
		||||
			{
 | 
			
		||||
				await api('/i/update', {
 | 
			
		||||
					followingVisibility: 'private',
 | 
			
		||||
				}, alice);
 | 
			
		||||
 | 
			
		||||
				const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
 | 
			
		||||
				assert.strictEqual(followingRes.status, 403);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => {
 | 
			
		||||
			{
 | 
			
		||||
				await api('/i/update', {
 | 
			
		||||
					followersVisibility: 'public',
 | 
			
		||||
				}, alice);
 | 
			
		||||
 | 
			
		||||
				const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
 | 
			
		||||
				assert.strictEqual(followersRes.status, 200);
 | 
			
		||||
			}
 | 
			
		||||
			{
 | 
			
		||||
				await api('/i/update', {
 | 
			
		||||
					ffVisibility: 'followers',
 | 
			
		||||
					followersVisibility: 'followers',
 | 
			
		||||
				}, alice);
 | 
			
		||||
 | 
			
		||||
				const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
 | 
			
		||||
				const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
 | 
			
		||||
				assert.strictEqual(followingRes.status, 403);
 | 
			
		||||
				assert.strictEqual(followersRes.status, 403);
 | 
			
		||||
			}
 | 
			
		||||
			{
 | 
			
		||||
				await api('/i/update', {
 | 
			
		||||
					ffVisibility: 'private',
 | 
			
		||||
					followersVisibility: 'private',
 | 
			
		||||
				}, alice);
 | 
			
		||||
 | 
			
		||||
				const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
 | 
			
		||||
				const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
 | 
			
		||||
				assert.strictEqual(followingRes.status, 403);
 | 
			
		||||
				assert.strictEqual(followersRes.status, 403);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -112,7 +112,8 @@ describe('ユーザー', () => {
 | 
			
		|||
			pinnedPageId: user.pinnedPageId,
 | 
			
		||||
			pinnedPage: user.pinnedPage,
 | 
			
		||||
			publicReactions: user.publicReactions,
 | 
			
		||||
			ffVisibility: user.ffVisibility,
 | 
			
		||||
			followingVisibility: user.followingVisibility,
 | 
			
		||||
			followersVisibility: user.followersVisibility,
 | 
			
		||||
			twoFactorEnabled: user.twoFactorEnabled,
 | 
			
		||||
			usePasswordLessLogin: user.usePasswordLessLogin,
 | 
			
		||||
			securityKeys: user.securityKeys,
 | 
			
		||||
| 
						 | 
				
			
			@ -386,7 +387,8 @@ describe('ユーザー', () => {
 | 
			
		|||
		assert.strictEqual(response.pinnedPageId, null);
 | 
			
		||||
		assert.strictEqual(response.pinnedPage, null);
 | 
			
		||||
		assert.strictEqual(response.publicReactions, true);
 | 
			
		||||
		assert.strictEqual(response.ffVisibility, 'public');
 | 
			
		||||
		assert.strictEqual(response.followingVisibility, 'public');
 | 
			
		||||
		assert.strictEqual(response.followersVisibility, 'public');
 | 
			
		||||
		assert.strictEqual(response.twoFactorEnabled, false);
 | 
			
		||||
		assert.strictEqual(response.usePasswordLessLogin, false);
 | 
			
		||||
		assert.strictEqual(response.securityKeys, false);
 | 
			
		||||
| 
						 | 
				
			
			@ -495,9 +497,12 @@ describe('ユーザー', () => {
 | 
			
		|||
		{ parameters: (): object => ({ alwaysMarkNsfw: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ autoSensitive: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ autoSensitive: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ ffVisibility: 'private' }) },
 | 
			
		||||
		{ parameters: (): object => ({ ffVisibility: 'followers' }) },
 | 
			
		||||
		{ parameters: (): object => ({ ffVisibility: 'public' }) },
 | 
			
		||||
		{ parameters: (): object => ({ followingVisibility: 'private' }) },
 | 
			
		||||
		{ parameters: (): object => ({ followingVisibility: 'followers' }) },
 | 
			
		||||
		{ parameters: (): object => ({ followingVisibility: 'public' }) },
 | 
			
		||||
		{ parameters: (): object => ({ followersVisibility: 'private' }) },
 | 
			
		||||
		{ parameters: (): object => ({ followersVisibility: 'followers' }) },
 | 
			
		||||
		{ parameters: (): object => ({ followersVisibility: 'public' }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedWords: [] }) },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,7 +82,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
 | 
			
		|||
		birthday: '2014-06-20',
 | 
			
		||||
		createdAt: '2016-12-28T22:49:51.000Z',
 | 
			
		||||
		description: 'I am a cool user!',
 | 
			
		||||
		ffVisibility: 'public',
 | 
			
		||||
		followingVisibility: 'public',
 | 
			
		||||
		followersVisibility: 'public',
 | 
			
		||||
		roles: [],
 | 
			
		||||
		fields: [
 | 
			
		||||
			{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,10 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<div :class="$style.statusItem">
 | 
			
		||||
			<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
		<div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
			<p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ number(user.followingCount) }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
		<div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
			<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +40,7 @@ import number from '@/filters/number.js';
 | 
			
		|||
import { userPage } from '@/filters/user.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 | 
			
		||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
	user: Misskey.entities.UserDetailed;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
					<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
 | 
			
		||||
					<div>{{ number(user.notesCount) }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
				<div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
					<div :class="$style.statusItemLabel">{{ i18n.ts.following }}</div>
 | 
			
		||||
					<div>{{ number(user.followingCount) }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
				<div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem">
 | 
			
		||||
					<div :class="$style.statusItemLabel">{{ i18n.ts.followers }}</div>
 | 
			
		||||
					<div>{{ number(user.followersCount) }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +65,7 @@ import number from '@/filters/number.js';
 | 
			
		|||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 | 
			
		||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	showing: boolean;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,12 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
 | 
			
		||||
	</MkSwitch>
 | 
			
		||||
 | 
			
		||||
	<MkSelect v-model="ffVisibility" @update:modelValue="save()">
 | 
			
		||||
		<template #label>{{ i18n.ts.ffVisibility }}</template>
 | 
			
		||||
	<MkSelect v-model="followingVisibility" @update:modelValue="save()">
 | 
			
		||||
		<template #label>{{ i18n.ts.followingVisibility }}</template>
 | 
			
		||||
		<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
 | 
			
		||||
		<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
 | 
			
		||||
		<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
 | 
			
		||||
	</MkSelect>
 | 
			
		||||
 | 
			
		||||
	<MkSelect v-model="followersVisibility" @update:modelValue="save()">
 | 
			
		||||
		<template #label>{{ i18n.ts.followersVisibility }}</template>
 | 
			
		||||
		<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
 | 
			
		||||
		<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
 | 
			
		||||
		<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
 | 
			
		||||
		<template #caption>{{ i18n.ts.ffVisibilityDescription }}</template>
 | 
			
		||||
	</MkSelect>
 | 
			
		||||
 | 
			
		||||
	<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +90,8 @@ const preventAiLearning = ref($i.preventAiLearning);
 | 
			
		|||
const isExplorable = ref($i.isExplorable);
 | 
			
		||||
const hideOnlineStatus = ref($i.hideOnlineStatus);
 | 
			
		||||
const publicReactions = ref($i.publicReactions);
 | 
			
		||||
const ffVisibility = ref($i.ffVisibility);
 | 
			
		||||
const followingVisibility = ref($i?.followingVisibility);
 | 
			
		||||
const followersVisibility = ref($i?.followersVisibility);
 | 
			
		||||
 | 
			
		||||
const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
 | 
			
		||||
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +107,8 @@ function save() {
 | 
			
		|||
		isExplorable: !!isExplorable.value,
 | 
			
		||||
		hideOnlineStatus: !!hideOnlineStatus.value,
 | 
			
		||||
		publicReactions: !!publicReactions.value,
 | 
			
		||||
		ffVisibility: ffVisibility.value,
 | 
			
		||||
		followingVisibility: followingVisibility.value,
 | 
			
		||||
		followersVisibility: followersVisibility.value,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -110,11 +110,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
							<b>{{ number(user.notesCount) }}</b>
 | 
			
		||||
							<span>{{ i18n.ts.notes }}</span>
 | 
			
		||||
						</MkA>
 | 
			
		||||
						<MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'following')">
 | 
			
		||||
						<MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')">
 | 
			
		||||
							<b>{{ number(user.followingCount) }}</b>
 | 
			
		||||
							<span>{{ i18n.ts.following }}</span>
 | 
			
		||||
						</MkA>
 | 
			
		||||
						<MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'followers')">
 | 
			
		||||
						<MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')">
 | 
			
		||||
							<b>{{ number(user.followersCount) }}</b>
 | 
			
		||||
							<span>{{ i18n.ts.followers }}</span>
 | 
			
		||||
						</MkA>
 | 
			
		||||
| 
						 | 
				
			
			@ -173,7 +173,7 @@ import { dateString } from '@/filters/date.js';
 | 
			
		|||
import { confetti } from '@/scripts/confetti.js';
 | 
			
		||||
import MkNotes from '@/components/MkNotes.vue';
 | 
			
		||||
import { api } from '@/os.js';
 | 
			
		||||
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 | 
			
		||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 | 
			
		||||
 | 
			
		||||
function calcAge(birthdate: string): number {
 | 
			
		||||
	const date = new Date(birthdate);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,11 +6,19 @@
 | 
			
		|||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
 | 
			
		||||
export function isFfVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
 | 
			
		||||
export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
 | 
			
		||||
	if ($i && $i.id === user.id) return true;
 | 
			
		||||
 | 
			
		||||
	if (user.ffVisibility === 'private') return false;
 | 
			
		||||
	if (user.ffVisibility === 'followers' && !user.isFollowing) return false;
 | 
			
		||||
	if (user.followingVisibility === 'private') return false;
 | 
			
		||||
	if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
 | 
			
		||||
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
 | 
			
		||||
	if ($i && $i.id === user.id) return true;
 | 
			
		||||
 | 
			
		||||
	if (user.followersVisibility === 'private') return false;
 | 
			
		||||
	if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
 | 
			
		||||
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,9 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
 | 
			
		|||
 | 
			
		||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
 | 
			
		||||
 | 
			
		||||
export const ffVisibility = ['public', 'followers', 'private'] as const;
 | 
			
		||||
export const followingVisibilities = ['public', 'followers', 'private'] as const;
 | 
			
		||||
 | 
			
		||||
export const followersVisibilities = ['public', 'followers', 'private'] as const;
 | 
			
		||||
 | 
			
		||||
export const permissions = [
 | 
			
		||||
	'read:account',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,8 @@ export const permissions = consts.permissions;
 | 
			
		|||
export const notificationTypes = consts.notificationTypes;
 | 
			
		||||
export const noteVisibilities = consts.noteVisibilities;
 | 
			
		||||
export const mutedNoteReasons = consts.mutedNoteReasons;
 | 
			
		||||
export const ffVisibility = consts.ffVisibility;
 | 
			
		||||
export const followingVisibilities = consts.followingVisibilities;
 | 
			
		||||
export const followersVisibilities = consts.followersVisibilities;
 | 
			
		||||
export const moderationLogTypes = consts.moderationLogTypes;
 | 
			
		||||
 | 
			
		||||
// api extractor not supported yet
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue