feat: make possible to configure following/followers visibility (#7959)
* feat: make possible to configure following/followers visibility * add test * ap * add ap test * set Cache-Control * hide following/followers count
This commit is contained in:
		
							parent
							
								
									07526ada45
								
							
						
					
					
						commit
						a28c515ef6
					
				
					 14 changed files with 317 additions and 10 deletions
				
			
		|  | @ -10,6 +10,7 @@ | ||||||
| ## 12.x.x (unreleased) | ## 12.x.x (unreleased) | ||||||
| 
 | 
 | ||||||
| ### Improvements | ### Improvements | ||||||
|  | - フォロー/フォロワーを非公開にできるように | ||||||
| 
 | 
 | ||||||
| ### Bugfixes | ### Bugfixes | ||||||
| - クライアント: 長いメニューが画面からはみ出す問題を修正 | - クライアント: 長いメニューが画面からはみ出す問題を修正 | ||||||
|  |  | ||||||
|  | @ -804,6 +804,13 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を | ||||||
| classic: "クラシック" | classic: "クラシック" | ||||||
| muteThread: "スレッドをミュート" | muteThread: "スレッドをミュート" | ||||||
| unmuteThread: "スレッドのミュートを解除" | unmuteThread: "スレッドのミュートを解除" | ||||||
|  | ffVisibility: "つながりの公開範囲" | ||||||
|  | ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。" | ||||||
|  | 
 | ||||||
|  | _ffVisibility: | ||||||
|  |   public: "公開" | ||||||
|  |   followers: "フォロワーだけに公開" | ||||||
|  |   private: "非公開" | ||||||
| 
 | 
 | ||||||
| _signup: | _signup: | ||||||
|   almostThere: "ほとんど完了です" |   almostThere: "ほとんど完了です" | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								migration/1636197624383-ff-visibility.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								migration/1636197624383-ff-visibility.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class ffVisibility1636197624383 implements MigrationInterface { | ||||||
|  |     name = 'ffVisibility1636197624383' | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         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'`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`); | ||||||
|  |         await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -9,6 +9,15 @@ | ||||||
| 		{{ $ts.makeReactionsPublic }} | 		{{ $ts.makeReactionsPublic }} | ||||||
| 		<template #desc>{{ $ts.makeReactionsPublicDescription }}</template> | 		<template #desc>{{ $ts.makeReactionsPublicDescription }}</template> | ||||||
| 	</FormSwitch> | 	</FormSwitch> | ||||||
|  | 	<FormGroup> | ||||||
|  | 		<template #label>{{ $ts.ffVisibility }}</template> | ||||||
|  | 		<FormSelect v-model="ffVisibility"> | ||||||
|  | 			<option value="public">{{ $ts._ffVisibility.public }}</option> | ||||||
|  | 			<option value="followers">{{ $ts._ffVisibility.followers }}</option> | ||||||
|  | 			<option value="private">{{ $ts._ffVisibility.private }}</option> | ||||||
|  | 		</FormSelect> | ||||||
|  | 		<template #caption>{{ $ts.ffVisibilityDescription }}</template> | ||||||
|  | 	</FormGroup> | ||||||
| 	<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> | 	<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> | ||||||
| 		{{ $ts.hideOnlineStatus }} | 		{{ $ts.hideOnlineStatus }} | ||||||
| 		<template #desc>{{ $ts.hideOnlineStatusDescription }}</template> | 		<template #desc>{{ $ts.hideOnlineStatusDescription }}</template> | ||||||
|  | @ -69,6 +78,7 @@ export default defineComponent({ | ||||||
| 			isExplorable: false, | 			isExplorable: false, | ||||||
| 			hideOnlineStatus: false, | 			hideOnlineStatus: false, | ||||||
| 			publicReactions: false, | 			publicReactions: false, | ||||||
|  | 			ffVisibility: 'public', | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -86,6 +96,7 @@ export default defineComponent({ | ||||||
| 		this.isExplorable = this.$i.isExplorable; | 		this.isExplorable = this.$i.isExplorable; | ||||||
| 		this.hideOnlineStatus = this.$i.hideOnlineStatus; | 		this.hideOnlineStatus = this.$i.hideOnlineStatus; | ||||||
| 		this.publicReactions = this.$i.publicReactions; | 		this.publicReactions = this.$i.publicReactions; | ||||||
|  | 		this.ffVisibility = this.$i.ffVisibility; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  | @ -101,6 +112,7 @@ export default defineComponent({ | ||||||
| 				isExplorable: !!this.isExplorable, | 				isExplorable: !!this.isExplorable, | ||||||
| 				hideOnlineStatus: !!this.hideOnlineStatus, | 				hideOnlineStatus: !!this.hideOnlineStatus, | ||||||
| 				publicReactions: !!this.publicReactions, | 				publicReactions: !!this.publicReactions, | ||||||
|  | 				ffVisibility: this.ffVisibility, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'type | ||||||
| import { id } from '../id'; | import { id } from '../id'; | ||||||
| import { User } from './user'; | import { User } from './user'; | ||||||
| import { Page } from './page'; | import { Page } from './page'; | ||||||
| import { notificationTypes } from '@/types'; | import { ffVisibility, notificationTypes } from '@/types'; | ||||||
| 
 | 
 | ||||||
| // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
 | // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
 | ||||||
| //       ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
 | //       ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
 | ||||||
|  | @ -80,6 +80,12 @@ export class UserProfile { | ||||||
| 	}) | 	}) | ||||||
| 	public publicReactions: boolean; | 	public publicReactions: boolean; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('enum', { | ||||||
|  | 		enum: ffVisibility, | ||||||
|  | 		default: 'public', | ||||||
|  | 	}) | ||||||
|  | 	public ffVisibility: typeof ffVisibility[number]; | ||||||
|  | 
 | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 128, nullable: true, | 		length: 128, nullable: true, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -187,6 +187,16 @@ export class UserRepository extends Repository<User> { | ||||||
| 			.getMany() : []; | 			.getMany() : []; | ||||||
| 		const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null; | 		const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null; | ||||||
| 
 | 
 | ||||||
|  | 		const followingCount = profile == null ? null : | ||||||
|  | 			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount : | ||||||
|  | 			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount : | ||||||
|  | 			null; | ||||||
|  | 
 | ||||||
|  | 		const followersCount = profile == null ? null : | ||||||
|  | 			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount : | ||||||
|  | 			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount : | ||||||
|  | 			null; | ||||||
|  | 
 | ||||||
| 		const falsy = opts.detail ? false : undefined; | 		const falsy = opts.detail ? false : undefined; | ||||||
| 
 | 
 | ||||||
| 		const packed = { | 		const packed = { | ||||||
|  | @ -230,8 +240,8 @@ export class UserRepository extends Repository<User> { | ||||||
| 				birthday: profile!.birthday, | 				birthday: profile!.birthday, | ||||||
| 				lang: profile!.lang, | 				lang: profile!.lang, | ||||||
| 				fields: profile!.fields, | 				fields: profile!.fields, | ||||||
| 				followersCount: user.followersCount, | 				followersCount: followersCount || 0, | ||||||
| 				followingCount: user.followingCount, | 				followingCount: followingCount || 0, | ||||||
| 				notesCount: user.notesCount, | 				notesCount: user.notesCount, | ||||||
| 				pinnedNoteIds: pins.map(pin => pin.noteId), | 				pinnedNoteIds: pins.map(pin => pin.noteId), | ||||||
| 				pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), me, { | 				pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), me, { | ||||||
|  | @ -240,6 +250,7 @@ export class UserRepository extends Repository<User> { | ||||||
| 				pinnedPageId: profile!.pinnedPageId, | 				pinnedPageId: profile!.pinnedPageId, | ||||||
| 				pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null, | 				pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null, | ||||||
| 				publicReactions: profile!.publicReactions, | 				publicReactions: profile!.publicReactions, | ||||||
|  | 				ffVisibility: profile!.ffVisibility, | ||||||
| 				twoFactorEnabled: profile!.twoFactorEnabled, | 				twoFactorEnabled: profile!.twoFactorEnabled, | ||||||
| 				usePasswordLessLogin: profile!.usePasswordLessLogin, | 				usePasswordLessLogin: profile!.usePasswordLessLogin, | ||||||
| 				securityKeys: profile!.twoFactorEnabled | 				securityKeys: profile!.twoFactorEnabled | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-colle | ||||||
| import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page'; | import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page'; | ||||||
| import renderFollowUser from '@/remote/activitypub/renderer/follow-user'; | import renderFollowUser from '@/remote/activitypub/renderer/follow-user'; | ||||||
| import { setResponseType } from '../activitypub'; | import { setResponseType } from '../activitypub'; | ||||||
| import { Users, Followings } from '@/models/index'; | import { Users, Followings, UserProfiles } from '@/models/index'; | ||||||
| import { LessThan } from 'typeorm'; | import { LessThan } from 'typeorm'; | ||||||
| 
 | 
 | ||||||
| export default async (ctx: Router.RouterContext) => { | export default async (ctx: Router.RouterContext) => { | ||||||
|  | @ -38,6 +38,20 @@ export default async (ctx: Router.RouterContext) => { | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	//#region Check ff visibility
 | ||||||
|  | 	const profile = await UserProfiles.findOneOrFail(user.id); | ||||||
|  | 
 | ||||||
|  | 	if (profile.ffVisibility === 'private') { | ||||||
|  | 		ctx.status = 403; | ||||||
|  | 		ctx.set('Cache-Control', 'public, max-age=30'); | ||||||
|  | 		return; | ||||||
|  | 	} else if (profile.ffVisibility === 'followers') { | ||||||
|  | 		ctx.status = 403; | ||||||
|  | 		ctx.set('Cache-Control', 'public, max-age=30'); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 	//#endregion
 | ||||||
|  | 
 | ||||||
| 	const limit = 10; | 	const limit = 10; | ||||||
| 	const partOf = `${config.url}/users/${userId}/followers`; | 	const partOf = `${config.url}/users/${userId}/followers`; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-colle | ||||||
| import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page'; | import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page'; | ||||||
| import renderFollowUser from '@/remote/activitypub/renderer/follow-user'; | import renderFollowUser from '@/remote/activitypub/renderer/follow-user'; | ||||||
| import { setResponseType } from '../activitypub'; | import { setResponseType } from '../activitypub'; | ||||||
| import { Users, Followings } from '@/models/index'; | import { Users, Followings, UserProfiles } from '@/models/index'; | ||||||
| import { LessThan, FindConditions } from 'typeorm'; | import { LessThan, FindConditions } from 'typeorm'; | ||||||
| import { Following } from '@/models/entities/following'; | import { Following } from '@/models/entities/following'; | ||||||
| 
 | 
 | ||||||
|  | @ -39,6 +39,20 @@ export default async (ctx: Router.RouterContext) => { | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	//#region Check ff visibility
 | ||||||
|  | 	const profile = await UserProfiles.findOneOrFail(user.id); | ||||||
|  | 
 | ||||||
|  | 	if (profile.ffVisibility === 'private') { | ||||||
|  | 		ctx.status = 403; | ||||||
|  | 		ctx.set('Cache-Control', 'public, max-age=30'); | ||||||
|  | 		return; | ||||||
|  | 	} else if (profile.ffVisibility === 'followers') { | ||||||
|  | 		ctx.status = 403; | ||||||
|  | 		ctx.set('Cache-Control', 'public, max-age=30'); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 	//#endregion
 | ||||||
|  | 
 | ||||||
| 	const limit = 10; | 	const limit = 10; | ||||||
| 	const partOf = `${config.url}/users/${userId}/following`; | 	const partOf = `${config.url}/users/${userId}/following`; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -72,6 +72,10 @@ export const meta = { | ||||||
| 			validator: $.optional.bool, | 			validator: $.optional.bool, | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		ffVisibility: { | ||||||
|  | 			validator: $.optional.str, | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		carefulBot: { | 		carefulBot: { | ||||||
| 			validator: $.optional.bool, | 			validator: $.optional.bool, | ||||||
| 		}, | 		}, | ||||||
|  | @ -174,6 +178,7 @@ export default define(meta, async (ps, _user, token) => { | ||||||
| 	if (ps.lang !== undefined) profileUpdates.lang = ps.lang; | 	if (ps.lang !== undefined) profileUpdates.lang = ps.lang; | ||||||
| 	if (ps.location !== undefined) profileUpdates.location = ps.location; | 	if (ps.location !== undefined) profileUpdates.location = ps.location; | ||||||
| 	if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; | 	if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; | ||||||
|  | 	if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; | ||||||
| 	if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; | 	if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; | ||||||
| 	if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; | 	if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; | ||||||
| 	if (ps.mutedWords !== undefined) { | 	if (ps.mutedWords !== undefined) { | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import $ from 'cafy'; | ||||||
| import { ID } from '@/misc/cafy-id'; | import { ID } from '@/misc/cafy-id'; | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
| import { ApiError } from '../../error'; | import { ApiError } from '../../error'; | ||||||
| import { Users, Followings } from '@/models/index'; | import { Users, Followings, UserProfiles } from '@/models/index'; | ||||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
| import { toPunyNullable } from '@/misc/convert-host'; | import { toPunyNullable } from '@/misc/convert-host'; | ||||||
| 
 | 
 | ||||||
|  | @ -53,7 +53,13 @@ export const meta = { | ||||||
| 			message: 'No such user.', | 			message: 'No such user.', | ||||||
| 			code: 'NO_SUCH_USER', | 			code: 'NO_SUCH_USER', | ||||||
| 			id: '27fa5435-88ab-43de-9360-387de88727cd' | 			id: '27fa5435-88ab-43de-9360-387de88727cd' | ||||||
| 		} | 		}, | ||||||
|  | 
 | ||||||
|  | 		forbidden: { | ||||||
|  | 			message: 'Forbidden.', | ||||||
|  | 			code: 'FORBIDDEN', | ||||||
|  | 			id: '3c6a84db-d619-26af-ca14-06232a21df8a' | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -66,6 +72,26 @@ export default define(meta, async (ps, me) => { | ||||||
| 		throw new ApiError(meta.errors.noSuchUser); | 		throw new ApiError(meta.errors.noSuchUser); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	const profile = await UserProfiles.findOneOrFail(user.id); | ||||||
|  | 
 | ||||||
|  | 	if (profile.ffVisibility === 'private') { | ||||||
|  | 		if (me == null || (me.id !== user.id)) { | ||||||
|  | 			throw new ApiError(meta.errors.forbidden); | ||||||
|  | 		} | ||||||
|  | 	} else if (profile.ffVisibility === 'followers') { | ||||||
|  | 		if (me == null) { | ||||||
|  | 			throw new ApiError(meta.errors.forbidden); | ||||||
|  | 		} else if (me.id !== user.id) { | ||||||
|  | 			const following = await Followings.findOne({ | ||||||
|  | 				followeeId: user.id, | ||||||
|  | 				followerId: me.id, | ||||||
|  | 			}); | ||||||
|  | 			if (following == null) { | ||||||
|  | 				throw new ApiError(meta.errors.forbidden); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) | 	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) | ||||||
| 		.andWhere(`following.followeeId = :userId`, { userId: user.id }) | 		.andWhere(`following.followeeId = :userId`, { userId: user.id }) | ||||||
| 		.innerJoinAndSelect('following.follower', 'follower'); | 		.innerJoinAndSelect('following.follower', 'follower'); | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import $ from 'cafy'; | ||||||
| import { ID } from '@/misc/cafy-id'; | import { ID } from '@/misc/cafy-id'; | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
| import { ApiError } from '../../error'; | import { ApiError } from '../../error'; | ||||||
| import { Users, Followings } from '@/models/index'; | import { Users, Followings, UserProfiles } from '@/models/index'; | ||||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
| import { toPunyNullable } from '@/misc/convert-host'; | import { toPunyNullable } from '@/misc/convert-host'; | ||||||
| 
 | 
 | ||||||
|  | @ -53,7 +53,13 @@ export const meta = { | ||||||
| 			message: 'No such user.', | 			message: 'No such user.', | ||||||
| 			code: 'NO_SUCH_USER', | 			code: 'NO_SUCH_USER', | ||||||
| 			id: '63e4aba4-4156-4e53-be25-c9559e42d71b' | 			id: '63e4aba4-4156-4e53-be25-c9559e42d71b' | ||||||
| 		} | 		}, | ||||||
|  | 
 | ||||||
|  | 		forbidden: { | ||||||
|  | 			message: 'Forbidden.', | ||||||
|  | 			code: 'FORBIDDEN', | ||||||
|  | 			id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba' | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -66,6 +72,26 @@ export default define(meta, async (ps, me) => { | ||||||
| 		throw new ApiError(meta.errors.noSuchUser); | 		throw new ApiError(meta.errors.noSuchUser); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	const profile = await UserProfiles.findOneOrFail(user.id); | ||||||
|  | 
 | ||||||
|  | 	if (profile.ffVisibility === 'private') { | ||||||
|  | 		if (me == null || (me.id !== user.id)) { | ||||||
|  | 			throw new ApiError(meta.errors.forbidden); | ||||||
|  | 		} | ||||||
|  | 	} else if (profile.ffVisibility === 'followers') { | ||||||
|  | 		if (me == null) { | ||||||
|  | 			throw new ApiError(meta.errors.forbidden); | ||||||
|  | 		} else if (me.id !== user.id) { | ||||||
|  | 			const following = await Followings.findOne({ | ||||||
|  | 				followeeId: user.id, | ||||||
|  | 				followerId: me.id, | ||||||
|  | 			}); | ||||||
|  | 			if (following == null) { | ||||||
|  | 				throw new ApiError(meta.errors.forbidden); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) | 	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) | ||||||
| 		.andWhere(`following.followerId = :userId`, { userId: user.id }) | 		.andWhere(`following.followerId = :userId`, { userId: user.id }) | ||||||
| 		.innerJoinAndSelect('following.followee', 'followee'); | 		.innerJoinAndSelect('following.followee', 'followee'); | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ export default async function(user: User) { | ||||||
| 		title: `${author.name} (@${user.username}@${config.host})`, | 		title: `${author.name} (@${user.username}@${config.host})`, | ||||||
| 		updated: notes[0].createdAt, | 		updated: notes[0].createdAt, | ||||||
| 		generator: 'Misskey', | 		generator: 'Misskey', | ||||||
| 		description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${profile.description ? ` · ${profile.description}` : ''}`, | 		description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, | ||||||
| 		link: author.link, | 		link: author.link, | ||||||
| 		image: user.avatarUrl ? user.avatarUrl : undefined, | 		image: user.avatarUrl ? user.avatarUrl : undefined, | ||||||
| 		feedLinks: { | 		feedLinks: { | ||||||
|  |  | ||||||
|  | @ -3,3 +3,5 @@ export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote | ||||||
| export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; | export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; | ||||||
| 
 | 
 | ||||||
| export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; | export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; | ||||||
|  | 
 | ||||||
|  | export const ffVisibility = ['public', 'followers', 'private'] as const; | ||||||
|  |  | ||||||
							
								
								
									
										167
									
								
								test/ff-visibility.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								test/ff-visibility.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,167 @@ | ||||||
|  | process.env.NODE_ENV = 'test'; | ||||||
|  | 
 | ||||||
|  | import * as assert from 'assert'; | ||||||
|  | import * as childProcess from 'child_process'; | ||||||
|  | import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils'; | ||||||
|  | 
 | ||||||
|  | describe('FF visibility', () => { | ||||||
|  | 	let p: childProcess.ChildProcess; | ||||||
|  | 
 | ||||||
|  | 	let alice: any; | ||||||
|  | 	let bob: any; | ||||||
|  | 	let carol: any; | ||||||
|  | 
 | ||||||
|  | 	before(async () => { | ||||||
|  | 		p = await startServer(); | ||||||
|  | 		alice = await signup({ username: 'alice' }); | ||||||
|  | 		bob = await signup({ username: 'bob' }); | ||||||
|  | 		carol = await signup({ username: 'carol' }); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	after(async () => { | ||||||
|  | 		await shutdownServer(p); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async(async () => { | ||||||
|  | 		await request('/i/update', { | ||||||
|  | 			ffVisibility: 'public', | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		const followingRes = await request('/users/following', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 		const followersRes = await request('/users/followers', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 
 | ||||||
|  | 		assert.strictEqual(followingRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followingRes.body), true); | ||||||
|  | 		assert.strictEqual(followersRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followersRes.body), true); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async(async () => { | ||||||
|  | 		await request('/i/update', { | ||||||
|  | 			ffVisibility: 'followers', | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		const followingRes = await request('/users/following', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, alice); | ||||||
|  | 		const followersRes = await request('/users/followers', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		assert.strictEqual(followingRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followingRes.body), true); | ||||||
|  | 		assert.strictEqual(followersRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followersRes.body), true); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async(async () => { | ||||||
|  | 		await request('/i/update', { | ||||||
|  | 			ffVisibility: 'followers', | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		const followingRes = await request('/users/following', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 		const followersRes = await request('/users/followers', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 
 | ||||||
|  | 		assert.strictEqual(followingRes.status, 400); | ||||||
|  | 		assert.strictEqual(followersRes.status, 400); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async(async () => { | ||||||
|  | 		await request('/i/update', { | ||||||
|  | 			ffVisibility: 'followers', | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		await request('/following/create', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 
 | ||||||
|  | 		const followingRes = await request('/users/following', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 		const followersRes = await request('/users/followers', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 
 | ||||||
|  | 		assert.strictEqual(followingRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followingRes.body), true); | ||||||
|  | 		assert.strictEqual(followersRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followersRes.body), true); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async(async () => { | ||||||
|  | 		await request('/i/update', { | ||||||
|  | 			ffVisibility: 'private', | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		const followingRes = await request('/users/following', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, alice); | ||||||
|  | 		const followersRes = await request('/users/followers', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		assert.strictEqual(followingRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followingRes.body), true); | ||||||
|  | 		assert.strictEqual(followersRes.status, 200); | ||||||
|  | 		assert.strictEqual(Array.isArray(followersRes.body), true); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async(async () => { | ||||||
|  | 		await request('/i/update', { | ||||||
|  | 			ffVisibility: 'private', | ||||||
|  | 		}, alice); | ||||||
|  | 
 | ||||||
|  | 		const followingRes = await request('/users/following', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 		const followersRes = await request('/users/followers', { | ||||||
|  | 			userId: alice.id, | ||||||
|  | 		}, bob); | ||||||
|  | 
 | ||||||
|  | 		assert.strictEqual(followingRes.status, 400); | ||||||
|  | 		assert.strictEqual(followersRes.status, 400); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	describe('AP', () => { | ||||||
|  | 		it('ffVisibility が public 以外ならばAPからは取得できない', async(async () => { | ||||||
|  | 			{ | ||||||
|  | 				await request('/i/update', { | ||||||
|  | 					ffVisibility: '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); | ||||||
|  | 				assert.strictEqual(followersRes.status, 200); | ||||||
|  | 			} | ||||||
|  | 			{ | ||||||
|  | 				await request('/i/update', { | ||||||
|  | 					ffVisibility: 'followers', | ||||||
|  | 				}, alice); | ||||||
|  | 
 | ||||||
|  | 				const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); | ||||||
|  | 				const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); | ||||||
|  | 				assert.strictEqual(followingRes.status, 403); | ||||||
|  | 				assert.strictEqual(followersRes.status, 403); | ||||||
|  | 			} | ||||||
|  | 			{ | ||||||
|  | 				await request('/i/update', { | ||||||
|  | 					ffVisibility: 'private', | ||||||
|  | 				}, alice); | ||||||
|  | 
 | ||||||
|  | 				const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); | ||||||
|  | 				const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); | ||||||
|  | 				assert.strictEqual(followingRes.status, 403); | ||||||
|  | 				assert.strictEqual(followersRes.status, 403); | ||||||
|  | 			} | ||||||
|  | 		})); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue