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:
zawa-ch 2023-12-18 20:59:20 +09:00 committed by GitHub
parent f6ff3b1f1a
commit 4e2d802967
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 648 additions and 71 deletions

View File

@ -33,6 +33,7 @@
- Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加 - Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加
- Enhance: アイコンデコレーションを複数設定できるように - Enhance: アイコンデコレーションを複数設定できるように
- Enhance: アイコンデコレーションの位置を微調整できるように - Enhance: アイコンデコレーションの位置を微調整できるように
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
### Client ### Client

4
locales/index.d.ts vendored
View File

@ -884,8 +884,8 @@ export interface Locale {
"classic": string; "classic": string;
"muteThread": string; "muteThread": string;
"unmuteThread": string; "unmuteThread": string;
"ffVisibility": string; "followingVisibility": string;
"ffVisibilityDescription": string; "followersVisibility": string;
"continueThread": string; "continueThread": string;
"deleteAccountConfirm": string; "deleteAccountConfirm": string;
"incorrectPassword": string; "incorrectPassword": string;

View File

@ -881,8 +881,8 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を
classic: "クラシック" classic: "クラシック"
muteThread: "スレッドをミュート" muteThread: "スレッドをミュート"
unmuteThread: "スレッドのミュートを解除" unmuteThread: "スレッドのミュートを解除"
ffVisibility: "つながりの公開範囲" followingVisibility: "フォローの公開範囲"
ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。" followersVisibility: "フォロワーの公開範囲"
continueThread: "さらにスレッドを見る" continueThread: "さらにスレッドを見る"
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
incorrectPassword: "パスワードが間違っています。" incorrectPassword: "パスワードが間違っています。"

View 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"`);
}
}

View File

@ -332,13 +332,13 @@ export class UserEntityService implements OnModuleInit {
const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const followingCount = profile == null ? null : const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followingCount : (profile.followingVisibility === 'public') || isMe ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : (profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null; null;
const followersCount = profile == null ? null : const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followersCount : (profile.followersVisibility === 'public') || isMe ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null; null;
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
@ -417,7 +417,8 @@ export class UserEntityService implements OnModuleInit {
pinnedPageId: profile!.pinnedPageId, pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
publicReactions: profile!.publicReactions, publicReactions: profile!.publicReactions,
ffVisibility: profile!.ffVisibility, followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
twoFactorEnabled: profile!.twoFactorEnabled, twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin, usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled securityKeys: profile!.twoFactorEnabled

View File

@ -4,7 +4,7 @@
*/ */
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; 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 { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
import { MiPage } from './Page.js'; import { MiPage } from './Page.js';
@ -94,10 +94,16 @@ export class MiUserProfile {
public publicReactions: boolean; public publicReactions: boolean;
@Column('enum', { @Column('enum', {
enum: ffVisibility, enum: followingVisibilities,
default: 'public', 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', { @Column('varchar', {
length: 128, nullable: true, length: 128, nullable: true,

View File

@ -311,7 +311,12 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,
}, },
ffVisibility: { followingVisibility: {
type: 'string',
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
followersVisibility: {
type: 'string', type: 'string',
nullable: false, optional: false, nullable: false, optional: false,
enum: ['public', 'followers', 'private'], enum: ['public', 'followers', 'private'],

View File

@ -195,11 +195,11 @@ export class ActivityPubServerService {
//#region Check ff visibility //#region Check ff visibility
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') { if (profile.followersVisibility === 'private') {
reply.code(403); reply.code(403);
reply.header('Cache-Control', 'public, max-age=30'); reply.header('Cache-Control', 'public, max-age=30');
return; return;
} else if (profile.ffVisibility === 'followers') { } else if (profile.followersVisibility === 'followers') {
reply.code(403); reply.code(403);
reply.header('Cache-Control', 'public, max-age=30'); reply.header('Cache-Control', 'public, max-age=30');
return; return;
@ -287,11 +287,11 @@ export class ActivityPubServerService {
//#region Check ff visibility //#region Check ff visibility
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') { if (profile.followingVisibility === 'private') {
reply.code(403); reply.code(403);
reply.header('Cache-Control', 'public, max-age=30'); reply.header('Cache-Control', 'public, max-age=30');
return; return;
} else if (profile.ffVisibility === 'followers') { } else if (profile.followingVisibility === 'followers') {
reply.code(403); reply.code(403);
reply.header('Cache-Control', 'public, max-age=30'); reply.header('Cache-Control', 'public, max-age=30');
return; return;

View File

@ -176,7 +176,8 @@ export const paramDef = {
receiveAnnouncementEmail: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' },
autoSensitive: { 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 }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: muteWords, mutedWords: muteWords,
hardMutedWords: 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.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.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える // TODO: ちゃんと数える

View File

@ -93,11 +93,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') { if (profile.followersVisibility === 'private') {
if (me == null || (me.id !== user.id)) { if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} }
} else if (profile.ffVisibility === 'followers') { } else if (profile.followersVisibility === 'followers') {
if (me == null) { if (me == null) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) { } else if (me.id !== user.id) {

View File

@ -101,11 +101,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') { if (profile.followingVisibility === 'private') {
if (me == null || (me.id !== user.id)) { if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} }
} else if (profile.ffVisibility === 'followers') { } else if (profile.followingVisibility === 'followers') {
if (me == null) { if (me == null) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) { } else if (me.id !== user.id) {

View File

@ -60,7 +60,7 @@ export class FeedService {
title: `${author.name} (@${user.username}@${this.config.host})`, title: `${author.name} (@${user.username}@${this.config.host})`,
updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined, updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined,
generator: 'Misskey', 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, link: author.link,
image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
feedLinks: { feedLinks: {

View File

@ -25,7 +25,8 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
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; export const followingVisibilities = ['public', 'followers', 'private'] as const;
export const followersVisibilities = ['public', 'followers', 'private'] as const;
export const moderationLogTypes = [ export const moderationLogTypes = [
'updateServerSettings', 'updateServerSettings',

View File

@ -26,9 +26,10 @@ describe('FF visibility', () => {
await app.close(); await app.close();
}); });
test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await api('/i/update', { await api('/i/update', {
ffVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('/users/following', {
@ -44,9 +45,88 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); 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', { await api('/i/update', {
ffVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('/users/following', {
@ -62,9 +142,88 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); 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', { await api('/i/update', {
ffVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('/users/following', {
@ -78,9 +237,82 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 400); 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', { await api('/i/update', {
ffVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('/following/create', {
@ -100,9 +332,106 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); 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', { await api('/i/update', {
ffVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('/users/following', {
@ -118,9 +447,88 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); 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', { await api('/i/update', {
ffVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('/users/following', {
@ -134,36 +542,129 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 400); 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', () => { describe('AP', () => {
test('ffVisibility が public 以外ならばAPからは取得できない', async () => { test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
{ {
await api('/i/update', { await api('/i/update', {
ffVisibility: 'public', followingVisibility: 'public',
}, alice); }, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); 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(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); assert.strictEqual(followersRes.status, 200);
} }
{ {
await api('/i/update', { await api('/i/update', {
ffVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403); assert.strictEqual(followersRes.status, 403);
} }
{ {
await api('/i/update', { await api('/i/update', {
ffVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403); assert.strictEqual(followersRes.status, 403);
} }
}); });

View File

@ -112,7 +112,8 @@ describe('ユーザー', () => {
pinnedPageId: user.pinnedPageId, pinnedPageId: user.pinnedPageId,
pinnedPage: user.pinnedPage, pinnedPage: user.pinnedPage,
publicReactions: user.publicReactions, publicReactions: user.publicReactions,
ffVisibility: user.ffVisibility, followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
twoFactorEnabled: user.twoFactorEnabled, twoFactorEnabled: user.twoFactorEnabled,
usePasswordLessLogin: user.usePasswordLessLogin, usePasswordLessLogin: user.usePasswordLessLogin,
securityKeys: user.securityKeys, securityKeys: user.securityKeys,
@ -386,7 +387,8 @@ describe('ユーザー', () => {
assert.strictEqual(response.pinnedPageId, null); assert.strictEqual(response.pinnedPageId, null);
assert.strictEqual(response.pinnedPage, null); assert.strictEqual(response.pinnedPage, null);
assert.strictEqual(response.publicReactions, true); 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.twoFactorEnabled, false);
assert.strictEqual(response.usePasswordLessLogin, false); assert.strictEqual(response.usePasswordLessLogin, false);
assert.strictEqual(response.securityKeys, false); assert.strictEqual(response.securityKeys, false);
@ -495,9 +497,12 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ alwaysMarkNsfw: false }) }, { parameters: (): object => ({ alwaysMarkNsfw: false }) },
{ parameters: (): object => ({ autoSensitive: true }) }, { parameters: (): object => ({ autoSensitive: true }) },
{ parameters: (): object => ({ autoSensitive: false }) }, { parameters: (): object => ({ autoSensitive: false }) },
{ parameters: (): object => ({ ffVisibility: 'private' }) }, { parameters: (): object => ({ followingVisibility: 'private' }) },
{ parameters: (): object => ({ ffVisibility: 'followers' }) }, { parameters: (): object => ({ followingVisibility: 'followers' }) },
{ parameters: (): object => ({ ffVisibility: 'public' }) }, { 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: Array(19).fill(['xxxxx']) }) },
{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: (): object => ({ mutedWords: [] }) }, { parameters: (): object => ({ mutedWords: [] }) },

View File

@ -82,7 +82,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
birthday: '2014-06-20', birthday: '2014-06-20',
createdAt: '2016-12-28T22:49:51.000Z', createdAt: '2016-12-28T22:49:51.000Z',
description: 'I am a cool user!', description: 'I am a cool user!',
ffVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public',
roles: [], roles: [],
fields: [ fields: [
{ {

View File

@ -22,10 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.statusItem"> <div :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span> <p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span>
</div> </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> <p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ number(user.followingCount) }}</span>
</div> </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> <p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
</div> </div>
</div> </div>
@ -40,7 +40,7 @@ import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
defineProps<{ defineProps<{
user: Misskey.entities.UserDetailed; user: Misskey.entities.UserDetailed;

View File

@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div> <div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
<div>{{ number(user.notesCount) }}</div> <div>{{ number(user.notesCount) }}</div>
</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 :class="$style.statusItemLabel">{{ i18n.ts.following }}</div>
<div>{{ number(user.followingCount) }}</div> <div>{{ number(user.followingCount) }}</div>
</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 :class="$style.statusItemLabel">{{ i18n.ts.followers }}</div>
<div>{{ number(user.followersCount) }}</div> <div>{{ number(user.followersCount) }}</div>
</div> </div>
@ -65,7 +65,7 @@ import number from '@/filters/number.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
const props = defineProps<{ const props = defineProps<{
showing: boolean; showing: boolean;

View File

@ -13,12 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template> <template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
</MkSwitch> </MkSwitch>
<MkSelect v-model="ffVisibility" @update:modelValue="save()"> <MkSelect v-model="followingVisibility" @update:modelValue="save()">
<template #label>{{ i18n.ts.ffVisibility }}</template> <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="public">{{ i18n.ts._ffVisibility.public }}</option>
<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option> <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
<option value="private">{{ i18n.ts._ffVisibility.private }}</option> <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
<template #caption>{{ i18n.ts.ffVisibilityDescription }}</template>
</MkSelect> </MkSelect>
<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> <MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
@ -84,7 +90,8 @@ const preventAiLearning = ref($i.preventAiLearning);
const isExplorable = ref($i.isExplorable); const isExplorable = ref($i.isExplorable);
const hideOnlineStatus = ref($i.hideOnlineStatus); const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions); 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 defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
@ -100,7 +107,8 @@ function save() {
isExplorable: !!isExplorable.value, isExplorable: !!isExplorable.value,
hideOnlineStatus: !!hideOnlineStatus.value, hideOnlineStatus: !!hideOnlineStatus.value,
publicReactions: !!publicReactions.value, publicReactions: !!publicReactions.value,
ffVisibility: ffVisibility.value, followingVisibility: followingVisibility.value,
followersVisibility: followersVisibility.value,
}); });
} }

View File

@ -110,11 +110,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<b>{{ number(user.notesCount) }}</b> <b>{{ number(user.notesCount) }}</b>
<span>{{ i18n.ts.notes }}</span> <span>{{ i18n.ts.notes }}</span>
</MkA> </MkA>
<MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'following')"> <MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')">
<b>{{ number(user.followingCount) }}</b> <b>{{ number(user.followingCount) }}</b>
<span>{{ i18n.ts.following }}</span> <span>{{ i18n.ts.following }}</span>
</MkA> </MkA>
<MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'followers')"> <MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')">
<b>{{ number(user.followersCount) }}</b> <b>{{ number(user.followersCount) }}</b>
<span>{{ i18n.ts.followers }}</span> <span>{{ i18n.ts.followers }}</span>
</MkA> </MkA>
@ -173,7 +173,7 @@ import { dateString } from '@/filters/date.js';
import { confetti } from '@/scripts/confetti.js'; import { confetti } from '@/scripts/confetti.js';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import { api } from '@/os.js'; import { api } from '@/os.js';
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
function calcAge(birthdate: string): number { function calcAge(birthdate: string): number {
const date = new Date(birthdate); const date = new Date(birthdate);

View File

@ -6,11 +6,19 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { $i } from '@/account.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 ($i && $i.id === user.id) return true;
if (user.ffVisibility === 'private') return false; if (user.followingVisibility === 'private') return false;
if (user.ffVisibility === 'followers' && !user.isFollowing) 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; return true;
} }

View File

@ -4,7 +4,9 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
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; export const followingVisibilities = ['public', 'followers', 'private'] as const;
export const followersVisibilities = ['public', 'followers', 'private'] as const;
export const permissions = [ export const permissions = [
'read:account', 'read:account',

View File

@ -16,7 +16,8 @@ export const permissions = consts.permissions;
export const notificationTypes = consts.notificationTypes; export const notificationTypes = consts.notificationTypes;
export const noteVisibilities = consts.noteVisibilities; export const noteVisibilities = consts.noteVisibilities;
export const mutedNoteReasons = consts.mutedNoteReasons; 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; export const moderationLogTypes = consts.moderationLogTypes;
// api extractor not supported yet // api extractor not supported yet