diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-07 18:04:32 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-11-07 18:04:32 +0900 |
| commit | a28c515ef63a6f9c188cf0a7f544db1afa8e1331 (patch) | |
| tree | 4b207f6998e0697ab5c732c04769b069dfd054c7 /src | |
| parent | perf: delete-account処理を軽くする (#7958) (diff) | |
| download | misskey-a28c515ef63a6f9c188cf0a7f544db1afa8e1331.tar.gz misskey-a28c515ef63a6f9c188cf0a7f544db1afa8e1331.tar.bz2 misskey-a28c515ef63a6f9c188cf0a7f544db1afa8e1331.zip | |
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
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/pages/settings/privacy.vue | 12 | ||||
| -rw-r--r-- | src/models/entities/user-profile.ts | 8 | ||||
| -rw-r--r-- | src/models/repositories/user.ts | 15 | ||||
| -rw-r--r-- | src/server/activitypub/followers.ts | 16 | ||||
| -rw-r--r-- | src/server/activitypub/following.ts | 16 | ||||
| -rw-r--r-- | src/server/api/endpoints/i/update.ts | 5 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/followers.ts | 30 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/following.ts | 30 | ||||
| -rw-r--r-- | src/server/web/feed.ts | 2 | ||||
| -rw-r--r-- | src/types.ts | 2 |
10 files changed, 126 insertions, 10 deletions
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue index 2a60ae1f46..5e0c259ca3 100644 --- a/src/client/pages/settings/privacy.vue +++ b/src/client/pages/settings/privacy.vue @@ -9,6 +9,15 @@ {{ $ts.makeReactionsPublic }} <template #desc>{{ $ts.makeReactionsPublicDescription }}</template> </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()"> {{ $ts.hideOnlineStatus }} <template #desc>{{ $ts.hideOnlineStatusDescription }}</template> @@ -69,6 +78,7 @@ export default defineComponent({ isExplorable: false, hideOnlineStatus: false, publicReactions: false, + ffVisibility: 'public', } }, @@ -86,6 +96,7 @@ export default defineComponent({ this.isExplorable = this.$i.isExplorable; this.hideOnlineStatus = this.$i.hideOnlineStatus; this.publicReactions = this.$i.publicReactions; + this.ffVisibility = this.$i.ffVisibility; }, mounted() { @@ -101,6 +112,7 @@ export default defineComponent({ isExplorable: !!this.isExplorable, hideOnlineStatus: !!this.hideOnlineStatus, publicReactions: !!this.publicReactions, + ffVisibility: this.ffVisibility, }); } } diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts index 1f450f223d..8a8cacfd52 100644 --- a/src/models/entities/user-profile.ts +++ b/src/models/entities/user-profile.ts @@ -2,7 +2,7 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'type import { id } from '../id'; import { User } from './user'; import { Page } from './page'; -import { notificationTypes } from '@/types'; +import { ffVisibility, notificationTypes } from '@/types'; // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも // ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン @@ -80,6 +80,12 @@ export class UserProfile { }) public publicReactions: boolean; + @Column('enum', { + enum: ffVisibility, + default: 'public', + }) + public ffVisibility: typeof ffVisibility[number]; + @Column('varchar', { length: 128, nullable: true, }) diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 9598e87191..fc0860970c 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -187,6 +187,16 @@ export class UserRepository extends Repository<User> { .getMany() : []; 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 packed = { @@ -230,8 +240,8 @@ export class UserRepository extends Repository<User> { birthday: profile!.birthday, lang: profile!.lang, fields: profile!.fields, - followersCount: user.followersCount, - followingCount: user.followingCount, + followersCount: followersCount || 0, + followingCount: followingCount || 0, notesCount: user.notesCount, pinnedNoteIds: pins.map(pin => pin.noteId), pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), me, { @@ -240,6 +250,7 @@ export class UserRepository extends Repository<User> { pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null, publicReactions: profile!.publicReactions, + ffVisibility: profile!.ffVisibility, twoFactorEnabled: profile!.twoFactorEnabled, usePasswordLessLogin: profile!.usePasswordLessLogin, securityKeys: profile!.twoFactorEnabled diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts index 8b6a066bf0..baf2d23460 100644 --- a/src/server/activitypub/followers.ts +++ b/src/server/activitypub/followers.ts @@ -8,7 +8,7 @@ import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-colle import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; -import { Users, Followings } from '@/models/index'; +import { Users, Followings, UserProfiles } from '@/models/index'; import { LessThan } from 'typeorm'; export default async (ctx: Router.RouterContext) => { @@ -38,6 +38,20 @@ export default async (ctx: Router.RouterContext) => { 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 partOf = `${config.url}/users/${userId}/followers`; diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts index 5fc5d68a9c..b9eb806c3c 100644 --- a/src/server/activitypub/following.ts +++ b/src/server/activitypub/following.ts @@ -8,7 +8,7 @@ import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-colle import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; -import { Users, Followings } from '@/models/index'; +import { Users, Followings, UserProfiles } from '@/models/index'; import { LessThan, FindConditions } from 'typeorm'; import { Following } from '@/models/entities/following'; @@ -39,6 +39,20 @@ export default async (ctx: Router.RouterContext) => { 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 partOf = `${config.url}/users/${userId}/following`; diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 3b8b1579ea..d0f201ab60 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -72,6 +72,10 @@ export const meta = { validator: $.optional.bool, }, + ffVisibility: { + validator: $.optional.str, + }, + carefulBot: { 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.location !== undefined) profileUpdates.location = ps.location; 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.bannerId !== undefined) updates.bannerId = ps.bannerId; if (ps.mutedWords !== undefined) { diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index e54b6078ee..6d042a2861 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; 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 { toPunyNullable } from '@/misc/convert-host'; @@ -53,7 +53,13 @@ export const meta = { message: 'No such user.', code: 'NO_SUCH_USER', 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); } + 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) .andWhere(`following.followeeId = :userId`, { userId: user.id }) .innerJoinAndSelect('following.follower', 'follower'); diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index f2ef7f47e1..1033117ef8 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; 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 { toPunyNullable } from '@/misc/convert-host'; @@ -53,7 +53,13 @@ export const meta = { message: 'No such user.', code: 'NO_SUCH_USER', 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); } + 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) .andWhere(`following.followerId = :userId`, { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); diff --git a/src/server/web/feed.ts b/src/server/web/feed.ts index 4b6de517b7..1d4c47dafb 100644 --- a/src/server/web/feed.ts +++ b/src/server/web/feed.ts @@ -27,7 +27,7 @@ export default async function(user: User) { title: `${author.name} (@${user.username}@${config.host})`, updated: notes[0].createdAt, 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, image: user.avatarUrl ? user.avatarUrl : undefined, feedLinks: { diff --git a/src/types.ts b/src/types.ts index d8eb442810..20f6f8bb88 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,3 +3,5 @@ export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; + +export const ffVisibility = ['public', 'followers', 'private'] as const; |