From 864827f788cd1671a4db2ebc159c1c8ab031b7ad Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 23 Nov 2023 18:56:20 +0900 Subject: Hard mute (#12376) * feat(backend,misskey-js): hard mute storage in backend * fix(backend,misskey-js): mute word record type * chore(frontend): generalize XWordMute * feat(frontend): configure hard mute * feat(frontend): hard mute notes on the timelines * lint(backend,frontend): fix lint failure * chore(misskey-js): update api.md * fix(backend): test failure * chore(frontend): check word mute for reply * chore: limit hard mute count --- .../backend/src/server/api/endpoints/i/update.ts | 34 +++++++++++++++++----- 1 file changed, 27 insertions(+), 7 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b00aa87bee..8ba29c5658 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -123,6 +123,11 @@ export const meta = { }, } as const; +const muteWords = { type: 'array', items: { oneOf: [ + { type: 'array', items: { type: 'string' } }, + { type: 'string' } +] } } as const; + export const paramDef = { type: 'object', properties: { @@ -171,7 +176,8 @@ export const paramDef = { autoSensitive: { type: 'boolean' }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, - mutedWords: { type: 'array' }, + mutedWords: muteWords, + hardMutedWords: muteWords, mutedInstances: { type: 'array', items: { type: 'string', } }, @@ -234,16 +240,20 @@ export default class extends Endpoint { // eslint- 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.mutedWords !== undefined) { + + function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える const length = JSON.stringify(ps.mutedWords).length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } + } + + function validateMuteWordRegex(mutedWords: (string[] | string)[]) { + for (const mutedWord of mutedWords) { + if (typeof mutedWord !== "string") continue; - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); + const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { @@ -251,11 +261,21 @@ export default class extends Endpoint { // eslint- } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } - }); + } + } + + if (ps.mutedWords !== undefined) { + checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } + if (ps.hardMutedWords !== undefined) { + checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.hardMutedWords); + profileUpdates.hardMutedWords = ps.hardMutedWords; + } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; -- cgit v1.2.3-freya From 536f08c401e82b9d34e2435f62ab2ac96473e77b Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 24 Nov 2023 15:09:25 +0900 Subject: fix: hard mute limit not applied (#12428) * fix: hard mute limit not applied * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/i/update.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/CHANGELOG.md b/CHANGELOG.md index c49aa0e899..5678da25a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### General - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) +- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 8ba29c5658..b045c01189 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -243,7 +243,7 @@ export default class extends Endpoint { // eslint- function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える - const length = JSON.stringify(ps.mutedWords).length; + const length = JSON.stringify(mutedWords).length; if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } -- cgit v1.2.3-freya From e17d741f4b14e3d675f2818fab3ec30c72a39586 Mon Sep 17 00:00:00 2001 From: おさむのひと <46447427+samunohito@users.noreply.github.com> Date: Sun, 3 Dec 2023 12:45:18 +0900 Subject: enhance(misskey-js) misskey-jsのストリーミングAPI定義をバックエンドに追従 (#12552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (enhance) misskey-jsのストリーミングAPI定義をバックエンドに追従 * fix ci * fix ci --- packages/backend/src/misc/json-schema.ts | 2 + packages/backend/src/models/json-schema/signin.ts | 26 +++ .../src/server/api/endpoints/i/signin-history.ts | 11 +- packages/misskey-js/etc/misskey-js.api.md | 180 +++++++++++++++++++-- packages/misskey-js/src/autogen/endpoint.ts | 4 +- packages/misskey-js/src/autogen/entities.ts | 4 +- packages/misskey-js/src/autogen/models.ts | 5 +- packages/misskey-js/src/autogen/types.ts | 42 +++-- packages/misskey-js/src/entities.ts | 56 ++++++- packages/misskey-js/src/streaming.types.ts | 140 +++++++++++++--- 10 files changed, 414 insertions(+), 56 deletions(-) create mode 100644 packages/backend/src/models/json-schema/signin.ts (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 80c1041c62..7e7fc447c3 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -36,6 +36,7 @@ import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; +import { packedSigninSchema } from '@/models/json-schema/signin.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -71,6 +72,7 @@ export const refs = { EmojiSimple: packedEmojiSimpleSchema, EmojiDetailed: packedEmojiDetailedSchema, Flash: packedFlashSchema, + Signin: packedSigninSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts new file mode 100644 index 0000000000..d27d2490c5 --- /dev/null +++ b/packages/backend/src/models/json-schema/signin.ts @@ -0,0 +1,26 @@ +export const packedSigninSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + ip: { + type: 'string', + optional: false, nullable: false, + }, + headers: { + type: 'object', + optional: false, nullable: false, + }, + success: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 139bede7bc..f82e3f9b28 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -12,8 +12,17 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - secure: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Signin', + }, + }, } as const; export const paramDef = { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 4e6e2adc02..e2e3349c08 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -292,6 +292,11 @@ type AdminUpdateUserNoteRequest = operations['admin/update-user-note']['requestB // @public (undocumented) type Announcement = components['schemas']['Announcement']; +// @public (undocumented) +type AnnouncementCreated = { + announcement: Announcement; +}; + // @public (undocumented) type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json']; @@ -488,9 +493,7 @@ export type Channels = { unreadAntenna: (payload: Antenna) => void; readAllAnnouncements: () => void; myTokenRegenerated: () => void; - reversiNoInvites: () => void; - reversiInvited: (payload: FIXME) => void; - signin: (payload: FIXME) => void; + signin: (payload: Signin) => void; registryUpdated: (payload: { scope?: string[]; key: string; @@ -498,41 +501,116 @@ export type Channels = { }) => void; driveFileCreated: (payload: DriveFile) => void; readAntenna: (payload: Antenna) => void; + receiveFollowRequest: (payload: User) => void; + announcementCreated: (payload: AnnouncementCreated) => void; }; receives: null; }; homeTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; localTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; hybridTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; globalTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + userList: { + params: { + listId: string; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + hashtag: { + params: { + q?: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + roleTimeline: { + params: { + roleId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + antenna: { + params: { + antennaId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + channel: { + params: { + channelId: string; + }; events: { note: (payload: Note) => void; }; receives: null; }; + drive: { + params: null; + events: { + fileCreated: (payload: DriveFile) => void; + fileDeleted: (payload: DriveFile['id']) => void; + fileUpdated: (payload: DriveFile) => void; + folderCreated: (payload: DriveFolder) => void; + folderDeleted: (payload: DriveFolder['id']) => void; + folderUpdated: (payload: DriveFile) => void; + }; + receives: null; + }; serverStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: ServerStats) => void; + statsLog: (payload: ServerStatsLog) => void; }; receives: { requestLog: { @@ -544,7 +622,8 @@ export type Channels = { queueStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: QueueStats) => void; + statsLog: (payload: QueueStatsLog) => void; }; receives: { requestLog: { @@ -553,6 +632,18 @@ export type Channels = { }; }; }; + admin: { + params: null; + events: { + newAbuseUserReport: { + id: string; + targetUserId: string; + reporterId: string; + comment: string; + }; + }; + receives: null; + }; }; // @public (undocumented) @@ -846,6 +937,16 @@ type EmailAddressAvailableRequest = operations['email-address/available']['reque // @public (undocumented) type EmailAddressAvailableResponse = operations['email-address/available']['responses']['200']['content']['application/json']; +// @public (undocumented) +type EmojiAdded = { + emoji: EmojiDetailed; +}; + +// @public (undocumented) +type EmojiDeleted = { + emojis: EmojiDetailed[]; +}; + // @public (undocumented) type EmojiDetailed = components['schemas']['EmojiDetailed']; @@ -861,6 +962,11 @@ type EmojiSimple = components['schemas']['EmojiSimple']; // @public (undocumented) type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json']; +// @public (undocumented) +type EmojiUpdated = { + emojis: EmojiDetailed[]; +}; + // @public (undocumented) type EmptyRequest = Record | undefined; @@ -902,6 +1008,14 @@ declare namespace entities { DateString, PageEvent, ModerationLog, + ServerStats, + ServerStatsLog, + QueueStats, + QueueStatsLog, + EmojiAdded, + EmojiUpdated, + EmojiDeleted, + AnnouncementCreated, EmptyRequest, EmptyResponse, AdminMetaResponse, @@ -1404,7 +1518,8 @@ declare namespace entities { GalleryPost, EmojiSimple, EmojiDetailed, - Flash + Flash, + Signin } } export { entities } @@ -2154,6 +2269,25 @@ type PromoReadRequest = operations['promo/read']['requestBody']['content']['appl // @public (undocumented) type QueueCount = components['schemas']['QueueCount']; +// @public (undocumented) +type QueueStats = { + deliver: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; + }; + inbox: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; + }; +}; + +// @public (undocumented) +type QueueStatsLog = string[]; + // @public (undocumented) type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json']; @@ -2190,6 +2324,29 @@ type RolesShowRequest = operations['roles/show']['requestBody']['content']['appl // @public (undocumented) type RolesUsersRequest = operations['roles/users']['requestBody']['content']['application/json']; +// @public (undocumented) +type ServerStats = { + cpu: number; + mem: { + used: number; + active: number; + }; + net: { + rx: number; + tx: number; + }; + fs: { + r: number; + w: number; + }; +}; + +// @public (undocumented) +type ServerStatsLog = string[]; + +// @public (undocumented) +type Signin = components['schemas']['Signin']; + // @public (undocumented) type StatsResponse = operations['stats']['responses']['200']['content']['application/json']; @@ -2448,8 +2605,7 @@ type UsersUpdateMemoRequest = operations['users/update-memo']['requestBody']['co // Warnings were encountered during analysis: // -// src/entities.ts:24:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts -// src/streaming.types.ts:31:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts +// src/entities.ts:25:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 64739a65d0..b46e69f691 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* - * version: 2023.11.1 - * generatedAt: 2023-11-27T02:24:45.113Z + * version: 2023.12.0-beta.1 + * generatedAt: 2023-12-03T02:04:45.058Z */ import type { diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 133a30b4ca..a51ee037d0 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* - * version: 2023.11.1 - * generatedAt: 2023-11-27T02:24:45.111Z + * version: 2023.12.0-beta.1 + * generatedAt: 2023-12-03T02:04:45.053Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index bc7ab1f3b9..c2b27b2c58 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* - * version: 2023.11.1 - * generatedAt: 2023-11-27T02:24:45.109Z + * version: 2023.12.0-beta.1 + * generatedAt: 2023-12-03T02:04:45.051Z */ import { components } from './types.js'; @@ -37,3 +37,4 @@ export type GalleryPost = components['schemas']['GalleryPost']; export type EmojiSimple = components['schemas']['EmojiSimple']; export type EmojiDetailed = components['schemas']['EmojiDetailed']; export type Flash = components['schemas']['Flash']; +export type Signin = components['schemas']['Signin']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index c7f5c6c82d..44ed4dbaa8 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2,8 +2,8 @@ /* eslint @typescript-eslint/no-explicit-any: 0 */ /* - * version: 2023.11.1 - * generatedAt: 2023-11-27T02:24:44.994Z + * version: 2023.12.0-beta.1 + * generatedAt: 2023-12-03T02:04:44.864Z */ /** @@ -3388,6 +3388,7 @@ export type components = { uri?: string; url?: string; reactionAndUserPairCache?: string[]; + clippedCount?: number; myReaction?: Record | null; }; NoteReaction: { @@ -3786,6 +3787,14 @@ export type components = { likedCount: number | null; isLiked?: boolean; }; + Signin: { + id: string; + /** Format: date-time */ + createdAt: string; + ip: string; + headers: Record; + success: boolean; + }; }; responses: never; parameters: never; @@ -6405,10 +6414,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - /** @example GR6S02ERUA5VR */ - code: string; - }[]; + 'application/json': components['schemas']['InviteCode'][]; }; }; /** @description Client error */ @@ -6471,7 +6477,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': Record[]; + 'application/json': components['schemas']['InviteCode'][]; }; }; /** @description Client error */ @@ -9600,6 +9606,8 @@ export type operations = { untilId?: string; sinceDate?: number; untilDate?: number; + /** @default false */ + allowPartial?: boolean; }; }; }; @@ -15893,10 +15901,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - /** @example GR6S02ERUA5VR */ - code: string; - }; + 'application/json': components['schemas']['InviteCode']; }; }; /** @description Client error */ @@ -17349,6 +17354,8 @@ export type operations = { untilId?: string; sinceDate?: number; untilDate?: number; + /** @default false */ + allowPartial?: boolean; /** @default true */ includeMyRenotes?: boolean; /** @default true */ @@ -17419,14 +17426,14 @@ export type operations = { withRenotes?: boolean; /** @default false */ withReplies?: boolean; - /** @default false */ - excludeNsfw?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ sinceId?: string; /** Format: misskey:id */ untilId?: string; + /** @default false */ + allowPartial?: boolean; sinceDate?: number; untilDate?: number; }; @@ -18317,6 +18324,8 @@ export type operations = { untilId?: string; sinceDate?: number; untilDate?: number; + /** @default false */ + allowPartial?: boolean; /** @default true */ includeMyRenotes?: boolean; /** @default true */ @@ -18502,6 +18511,8 @@ export type operations = { untilId?: string; sinceDate?: number; untilDate?: number; + /** @default false */ + allowPartial?: boolean; /** @default true */ includeMyRenotes?: boolean; /** @default true */ @@ -20799,6 +20810,7 @@ export type operations = { username?: string; /** @description The local host is represented with `null`. */ host?: string | null; + birthday?: string | null; }; }; }; @@ -21704,9 +21716,9 @@ export type operations = { sinceDate?: number; untilDate?: number; /** @default false */ - withFiles?: boolean; + allowPartial?: boolean; /** @default false */ - excludeNsfw?: boolean; + withFiles?: boolean; }; }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index b1d6b5c10f..99f433cc02 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -1,5 +1,6 @@ -import { ModerationLogPayloads, notificationTypes } from './consts.js'; -import { Page, User, UserDetailed } from './autogen/models'; +import { ModerationLogPayloads } from './consts.js'; +import { Announcement, EmojiDetailed, Page, User, UserDetailed } from './autogen/models'; + export * from './autogen/entities'; export * from './autogen/models'; @@ -131,3 +132,54 @@ export type ModerationLog = { type: 'unsetUserBanner'; info: ModerationLogPayloads['unsetUserBanner']; }); + +export type ServerStats = { + cpu: number; + mem: { + used: number; + active: number; + }; + net: { + rx: number; + tx: number; + }; + fs: { + r: number; + w: number; + } +}; + +export type ServerStatsLog = string[]; + +export type QueueStats = { + deliver: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; + }; + inbox: { + activeSincePrevTick: number; + active: number; + waiting: number; + delayed: number; + }; +}; + +export type QueueStatsLog = string[]; + +export type EmojiAdded = { + emoji: EmojiDetailed +}; + +export type EmojiUpdated = { + emojis: EmojiDetailed[] +}; + +export type EmojiDeleted = { + emojis: EmojiDetailed[] +}; + +export type AnnouncementCreated = { + announcement: Announcement; +}; diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 7981c280f6..6f575ce585 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -1,7 +1,23 @@ -import { Antenna, DriveFile, EmojiDetailed, MeDetailed, Note, User, Notification } from './autogen/models.js'; -import { PageEvent } from './entities.js'; - -type FIXME = any; +import { + Antenna, + DriveFile, + DriveFolder, + MeDetailed, + Note, + Notification, + Signin, + User, +} from './autogen/models.js'; +import { + AnnouncementCreated, + EmojiAdded, EmojiDeleted, + EmojiUpdated, + PageEvent, + QueueStats, + QueueStatsLog, + ServerStats, + ServerStatsLog, +} from './entities.js'; export type Channels = { main: { @@ -27,9 +43,7 @@ export type Channels = { unreadAntenna: (payload: Antenna) => void; readAllAnnouncements: () => void; myTokenRegenerated: () => void; - reversiNoInvites: () => void; - reversiInvited: (payload: FIXME) => void; - signin: (payload: FIXME) => void; + signin: (payload: Signin) => void; registryUpdated: (payload: { scope?: string[]; key: string; @@ -37,41 +51,116 @@ export type Channels = { }) => void; driveFileCreated: (payload: DriveFile) => void; readAntenna: (payload: Antenna) => void; + receiveFollowRequest: (payload: User) => void; + announcementCreated: (payload: AnnouncementCreated) => void; }; receives: null; }; homeTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; localTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; hybridTimeline: { - params: null; + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; events: { note: (payload: Note) => void; }; receives: null; }; globalTimeline: { - params: null; + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + userList: { + params: { + listId: string; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + hashtag: { + params: { + q?: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + roleTimeline: { + params: { + roleId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + antenna: { + params: { + antennaId: string; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; + channel: { + params: { + channelId: string; + }; events: { note: (payload: Note) => void; }; receives: null; }; + drive: { + params: null; + events: { + fileCreated: (payload: DriveFile) => void; + fileDeleted: (payload: DriveFile['id']) => void; + fileUpdated: (payload: DriveFile) => void; + folderCreated: (payload: DriveFolder) => void; + folderDeleted: (payload: DriveFolder['id']) => void; + folderUpdated: (payload: DriveFile) => void; + }; + receives: null; + }; serverStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: ServerStats) => void; + statsLog: (payload: ServerStatsLog) => void; }; receives: { requestLog: { @@ -83,7 +172,8 @@ export type Channels = { queueStats: { params: null; events: { - stats: (payload: FIXME) => void; + stats: (payload: QueueStats) => void; + statsLog: (payload: QueueStatsLog) => void; }; receives: { requestLog: { @@ -92,30 +182,39 @@ export type Channels = { }; }; }; + admin: { + params: null; + events: { + newAbuseUserReport: { + id: string; + targetUserId: string; + reporterId: string; + comment: string; + } + }; + receives: null; + } }; export type NoteUpdatedEvent = { - id: Note['id']; type: 'reacted'; body: { reaction: string; + emoji: string | null; userId: User['id']; }; } | { - id: Note['id']; type: 'unreacted'; body: { reaction: string; userId: User['id']; }; } | { - id: Note['id']; type: 'deleted'; body: { deletedAt: string; }; } | { - id: Note['id']; type: 'pollVoted'; body: { choice: number; @@ -125,7 +224,8 @@ export type NoteUpdatedEvent = { export type BroadcastEvents = { noteUpdated: (payload: NoteUpdatedEvent) => void; - emojiAdded: (payload: { - emoji: EmojiDetailed; - }) => void; + emojiAdded: (payload: EmojiAdded) => void; + emojiUpdated: (payload: EmojiUpdated) => void; + emojiDeleted: (payload: EmojiDeleted) => void; + announcementCreated: (payload: AnnouncementCreated) => void; }; -- cgit v1.2.3-freya From 5472f4b934c8ca8c702152a4a927b4ac94cf3fdb Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 13 Dec 2023 16:56:19 +0900 Subject: enhance: アイコンデコレーションを複数設定できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 5 ++ locales/ja-JP.yml | 5 ++ packages/backend/src/core/RoleService.ts | 3 ++ packages/backend/src/models/json-schema/role.ts | 1 + packages/backend/src/models/json-schema/user.ts | 4 ++ .../backend/src/server/api/endpoints/i/update.ts | 10 ++-- packages/frontend/src/account.ts | 2 +- .../frontend/src/components/MkDrive.folder.vue | 9 ++-- packages/frontend/src/components/MkWindow.vue | 2 +- packages/frontend/src/components/global/MkA.vue | 2 +- .../frontend/src/components/global/MkAvatar.vue | 53 ++++++++-------------- packages/frontend/src/const.ts | 1 + packages/frontend/src/pages/admin/roles.editor.vue | 22 ++++++++- packages/frontend/src/pages/admin/roles.vue | 7 +++ .../settings/profile.avatar-decoration-dialog.vue | 11 +++-- packages/frontend/src/pages/settings/profile.vue | 39 ++++++++++++---- 17 files changed, 115 insertions(+), 62 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1434cde4..e5ff09edec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) - Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 +- Enhance: アイコンデコレーションを複数設定できるように - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/locales/index.d.ts b/locales/index.d.ts index 846a6d503d..d32023f5ac 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -264,6 +264,7 @@ export interface Locale { "removeAreYouSure": string; "deleteAreYouSure": string; "resetAreYouSure": string; + "areYouSure": string; "saved": string; "messaging": string; "upload": string; @@ -1160,6 +1161,7 @@ export interface Locale { "avatarDecorations": string; "attach": string; "detach": string; + "detachAll": string; "angle": string; "flip": string; "showAvatarDecorations": string; @@ -1173,6 +1175,7 @@ export interface Locale { "doReaction": string; "code": string; "reloadRequiredToApplySettings": string; + "remainingN": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1701,6 +1704,7 @@ export interface Locale { "canHideAds": string; "canSearchNotes": string; "canUseTranslator": string; + "avatarDecorationLimit": string; }; "_condition": { "isLocal": string; @@ -2181,6 +2185,7 @@ export interface Locale { "changeAvatar": string; "changeBanner": string; "verifiedLinkDescription": string; + "avatarDecorationMax": string; }; "_exportOrImport": { "allNotes": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0d84440bc8..2ac57fd311 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -261,6 +261,7 @@ removed: "削除しました" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" resetAreYouSure: "リセットしますか?" +areYouSure: "よろしいですか?" saved: "保存しました" messaging: "チャット" upload: "アップロード" @@ -1157,6 +1158,7 @@ tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" avatarDecorations: "アイコンデコレーション" attach: "付ける" detach: "外す" +detachAll: "全て外す" angle: "角度" flip: "反転" showAvatarDecorations: "アイコンのデコレーションを表示" @@ -1170,6 +1172,7 @@ cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述 doReaction: "リアクションする" code: "コード" reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。" +remainingN: "残り: {n}" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -1610,6 +1613,7 @@ _role: canHideAds: "広告の非表示" canSearchNotes: "ノート検索の利用" canUseTranslator: "翻訳機能の利用" + avatarDecorationLimit: "アイコンデコレーションの最大取付個数" _condition: isLocal: "ローカルユーザー" isRemote: "リモートユーザー" @@ -2084,6 +2088,7 @@ _profile: changeAvatar: "アイコン画像を変更" changeBanner: "バナー画像を変更" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" + avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" _exportOrImport: allNotes: "全てのノート" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 29e48aa8ca..4de719d6a0 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -47,6 +47,7 @@ export type RolePolicies = { userListLimit: number; userEachUserListsLimit: number; rateLimitFactor: number; + avatarDecorationLimit: number; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -73,6 +74,7 @@ export const DEFAULT_POLICIES: RolePolicies = { userListLimit: 10, userEachUserListsLimit: 50, rateLimitFactor: 1, + avatarDecorationLimit: 1, }; @Injectable() @@ -326,6 +328,7 @@ export class RoleService implements OnApplicationShutdown { userListLimit: calc('userListLimit', vs => Math.max(...vs)), userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), + avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), }; } diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index dd2f32b14d..b0c6804bb8 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -145,6 +145,7 @@ export const packedRoleSchema = { userEachUserListsLimit: rolePolicyValue, canManageAvatarDecorations: rolePolicyValue, canUseTranslator: rolePolicyValue, + avatarDecorationLimit: rolePolicyValue, }, }, usersCount: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index c6b2707b80..c6b96b85f0 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -672,6 +672,10 @@ export const packedMeDetailedOnlySchema = { type: 'number', nullable: false, optional: false, }, + avatarDecorationLimit: { + type: 'number', + nullable: false, optional: false, + }, }, }, //#region secrets diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b045c01189..399e6b88cb 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -125,7 +125,7 @@ export const meta = { const muteWords = { type: 'array', items: { oneOf: [ { type: 'array', items: { type: 'string' } }, - { type: 'string' } + { type: 'string' }, ] } } as const; export const paramDef = { @@ -137,7 +137,7 @@ export const paramDef = { birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, - avatarDecorations: { type: 'array', maxItems: 1, items: { + avatarDecorations: { type: 'array', maxItems: 16, items: { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, @@ -251,7 +251,7 @@ export default class extends Endpoint { // eslint- function validateMuteWordRegex(mutedWords: (string[] | string)[]) { for (const mutedWord of mutedWords) { - if (typeof mutedWord !== "string") continue; + if (typeof mutedWord !== 'string') continue; const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); @@ -329,12 +329,14 @@ export default class extends Endpoint { // eslint- if (ps.avatarDecorations) { const decorations = await this.avatarDecorationService.getAll(true); - const myRoles = await this.roleService.getUserRoles(user.id); + const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); + if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, angle: d.angle ?? 0, diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 0e4e4b50ff..a6af298024 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -284,7 +284,7 @@ export async function openAccountMenu(opts: { text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, - }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { + }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { type: 'parent' as const, icon: 'ti ti-plus', text: i18n.ts.addAccount, diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 5322664664..b0c14d1f0b 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -250,7 +251,7 @@ function setAsUploadFolder() { } function onContextmenu(ev: MouseEvent) { - let menu; + let menu: MenuItem[]; menu = [{ text: i18n.ts.openInWindow, icon: 'ti ti-app-window', @@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) { }, { }, 'closed'); }, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.rename, icon: 'ti ti-forms', action: rename, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.delete, icon: 'ti ti-trash', danger: true, action: deleteFolder, }]; if (defaultStore.state.devMode) { - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFolderId, action: () => { diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 1150a29e03..7c8ffcccf9 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; -import { MenuItem } from '@/types/menu'; +import { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 809dae421a..5552e96ee0 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -57,7 +57,7 @@ function onContextmenu(ev) { action: () => { router.push(props.to, 'forcePage'); }, - }, null, { + }, { type: 'divider' }, { icon: 'ti ti-external-link', text: i18n.ts.openInNewTab, action: () => { diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index c7e50e275a..6aa9a42037 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -23,16 +23,18 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -57,19 +59,14 @@ const props = withDefaults(defineProps<{ link?: boolean; preview?: boolean; indicator?: boolean; - decoration?: { - url: string; - angle?: number; - flipH?: boolean; - flipV?: boolean; - }; + decorations?: Misskey.entities.UserDetailed['avatarDecorations'][number][]; forceShowDecoration?: boolean; }>(), { target: null, link: false, preview: false, indicator: false, - decoration: undefined, + decorations: undefined, forceShowDecoration: false, }); @@ -92,27 +89,13 @@ function onClick(ev: MouseEvent): void { emit('click', ev); } -function getDecorationAngle() { - let angle; - if (props.decoration) { - angle = props.decoration.angle ?? 0; - } else if (props.user.avatarDecorations.length > 0) { - angle = props.user.avatarDecorations[0].angle ?? 0; - } else { - angle = 0; - } +function getDecorationAngle(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) { + const angle = decoration.angle ?? 0; return angle === 0 ? undefined : `${angle * 360}deg`; } -function getDecorationScale() { - let scaleX; - if (props.decoration) { - scaleX = props.decoration.flipH ? -1 : 1; - } else if (props.user.avatarDecorations.length > 0) { - scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1; - } else { - scaleX = 1; - } +function getDecorationScale(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) { + const scaleX = decoration.flipH ? -1 : 1; return scaleX === 1 ? undefined : `${scaleX} 1`; } diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 397f804822..f016b7aa02 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -81,6 +81,7 @@ export const ROLE_POLICIES = [ 'userListLimit', 'userEachUserListsLimit', 'rateLimitFactor', + 'avatarDecorationLimit', ] as const; // なんか動かない diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index a8e0e8bbd1..5ded8d6931 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -531,6 +531,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
+ + + + + + + + + +
+
@@ -549,7 +569,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import { i18n } from '@/i18n.js'; -import { ROLE_POLICIES } from '@/const'; +import { ROLE_POLICIES } from '@/const.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index db4595b150..1bb91a0a5b 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -192,6 +192,13 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + {{ i18n.ts.save }} diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue index 4d571bc9ba..c27a21217b 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ decoration.name }}
- +
@@ -54,6 +54,7 @@ const props = defineProps<{ decoration: { id: string; url: string; + name: string; } }>(); @@ -77,18 +78,18 @@ async function attach() { flipH: flipH.value, }; await os.apiWithDialog('i/update', { - avatarDecorations: [decoration], + avatarDecorations: [...$i.avatarDecorations, decoration], }); - $i.avatarDecorations = [decoration]; + $i.avatarDecorations = [...$i.avatarDecorations, decoration]; dialog.value.close(); } async function detach() { await os.apiWithDialog('i/update', { - avatarDecorations: [], + avatarDecorations: $i.avatarDecorations.filter(x => x.id !== props.decoration.id), }); - $i.avatarDecorations = []; + $i.avatarDecorations = $i.avatarDecorations.filter(x => x.id !== props.decoration.id); dialog.value.close(); } diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index ba75b539e1..a5d3835b93 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -87,16 +87,22 @@ SPDX-License-Identifier: AGPL-3.0-only -
-
-
{{ avatarDecoration.name }}
- - +
+ {{ i18n.t('_profile.avatarDecorationMax', { max: $i?.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i?.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }}) + + {{ i18n.ts.detachAll }} + +
+
+
{{ avatarDecoration.name }}
+ + +
@@ -273,6 +279,19 @@ function openDecoration(avatarDecoration) { }, {}, 'closed'); } +function detachAllDecorations() { + os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }).then(async ({ canceled }) => { + if (canceled) return; + await os.apiWithDialog('i/update', { + avatarDecorations: [], + }); + $i.avatarDecorations = []; + }); +} + const headerActions = computed(() => []); const headerTabs = computed(() => []); -- cgit v1.2.3-freya From 417852779f871197f6aec2fec0fd56a2380836e4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 14 Dec 2023 20:58:08 +0900 Subject: enhance: アイコンデコレーションの位置を微調整できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../backend/src/core/entities/UserEntityService.ts | 2 ++ packages/backend/src/models/User.ts | 6 ++++-- packages/backend/src/server/api/endpoints/i/update.ts | 4 ++++ packages/frontend/src/components/global/MkAvatar.vue | 7 +++++++ .../settings/profile.avatar-decoration.decoration.vue | 4 +++- .../settings/profile.avatar-decoration.dialog.vue | 18 ++++++++++++++++++ .../src/pages/settings/profile.avatar-decoration.vue | 6 ++++++ 8 files changed, 45 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0581466d7c..599bd463fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) - Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 - Enhance: アイコンデコレーションを複数設定できるように +- Enhance: アイコンデコレーションの位置を微調整できるように - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 917f4e06d0..fb7aa0c244 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -362,6 +362,8 @@ export class UserEntityService implements OnModuleInit { id: ud.id, angle: ud.angle || undefined, flipH: ud.flipH || undefined, + offsetX: ud.offsetX || undefined, + offsetY: ud.offsetY || undefined, url: decorations.find(d => d.id === ud.id)!.url, }))) : [], isBot: user.isBot, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index c3762fcd3e..219497a125 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -143,8 +143,10 @@ export class MiUser { }) public avatarDecorations: { id: string; - angle: number; - flipH: boolean; + angle?: number; + flipH?: boolean; + offsetX?: number; + offsetY?: number; }[]; @Index() diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 399e6b88cb..a56f50115b 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -143,6 +143,8 @@ export const paramDef = { id: { type: 'string', format: 'misskey:id' }, angle: { type: 'number', nullable: true, maximum: 0.5, minimum: -0.5 }, flipH: { type: 'boolean', nullable: true }, + offsetX: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, + offsetY: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, }, required: ['id'], } }, @@ -341,6 +343,8 @@ export default class extends Endpoint { // eslint- id: d.id, angle: d.angle ?? 0, flipH: d.flipH ?? false, + offsetX: d.offsetX ?? 0, + offsetY: d.offsetY ?? 0, })); } diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 9d13c03290..af5b6e44f5 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only :style="{ rotate: getDecorationAngle(decoration), scale: getDecorationScale(decoration), + translate: getDecorationOffset(decoration), }" alt="" > @@ -99,6 +100,12 @@ function getDecorationScale(decoration: Omit) { + const offsetX = decoration.offsetX ?? 0; + const offsetY = decoration.offsetY ?? 0; + return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`; +} + const color = ref(); watch(() => props.user.avatarBlurhash, () => { diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue index c113868238..9c95b5547e 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click="emit('click')" >
{{ decoration.name }}
- +
@@ -28,6 +28,8 @@ const props = defineProps<{ }; angle?: number; flipH?: boolean; + offsetX?: number; + offsetY?: number; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue index 26cacf3c37..77e6b28fad 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue @@ -23,6 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + @@ -64,10 +70,14 @@ const emit = defineEmits<{ (ev: 'attach', payload: { angle: number; flipH: boolean; + offsetX: number; + offsetY: number; }): void; (ev: 'update', payload: { angle: number; flipH: boolean; + offsetX: number; + offsetY: number; }): void; (ev: 'detach'): void; }>(); @@ -76,6 +86,8 @@ const dialog = shallowRef>(); const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0); const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0); const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); +const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0); +const offsetY = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetY : null) ?? 0); const decorationsForPreview = computed(() => { const decoration = { @@ -83,6 +95,8 @@ const decorationsForPreview = computed(() => { url: props.decoration.url, angle: angle.value, flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, }; const decorations = [...$i.avatarDecorations]; if (props.usingIndex != null) { @@ -101,6 +115,8 @@ async function update() { emit('update', { angle: angle.value, flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, }); dialog.value.close(); } @@ -109,6 +125,8 @@ async function attach() { emit('attach', { angle: angle.value, flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, }); dialog.value.close(); } diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue index bfef6e0325..8579acfed8 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue @@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only :decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)" :angle="avatarDecoration.angle" :flipH="avatarDecoration.flipH" + :offsetX="avatarDecoration.offsetX" + :offsetY="avatarDecoration.offsetY" :active="true" @click="openDecoration(avatarDecoration, i)" /> @@ -66,6 +68,8 @@ function openDecoration(avatarDecoration, index?: number) { id: avatarDecoration.id, angle: payload.angle, flipH: payload.flipH, + offsetX: payload.offsetX, + offsetY: payload.offsetY, }; const update = [...$i.avatarDecorations, decoration]; await os.apiWithDialog('i/update', { @@ -78,6 +82,8 @@ function openDecoration(avatarDecoration, index?: number) { id: avatarDecoration.id, angle: payload.angle, flipH: payload.flipH, + offsetX: payload.offsetX, + offsetY: payload.offsetY, }; const update = [...$i.avatarDecorations]; update[index] = decoration; -- cgit v1.2.3-freya From 4e2d8029678951ca3b8b9b40e62901b0c67618ed Mon Sep 17 00:00:00 2001 From: zawa-ch Date: Mon, 18 Dec 2023 20:59:20 +0900 Subject: enhance: “つながりの公開範囲”がフォロー・フォロワー個別設定できるように (#12702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- CHANGELOG.md | 1 + locales/index.d.ts | 4 +- locales/ja-JP.yml | 4 +- .../migration/1702718871541-ffVisibility.js | 35 ++ .../backend/src/core/entities/UserEntityService.ts | 11 +- packages/backend/src/models/UserProfile.ts | 12 +- packages/backend/src/models/json-schema/user.ts | 7 +- .../backend/src/server/ActivityPubServerService.ts | 8 +- .../backend/src/server/api/endpoints/i/update.ts | 6 +- .../src/server/api/endpoints/users/followers.ts | 4 +- .../src/server/api/endpoints/users/following.ts | 4 +- packages/backend/src/server/web/FeedService.ts | 2 +- packages/backend/src/types.ts | 3 +- packages/backend/test/e2e/ff-visibility.ts | 543 ++++++++++++++++++++- packages/backend/test/e2e/users.ts | 15 +- packages/frontend/.storybook/fakes.ts | 3 +- packages/frontend/src/components/MkUserInfo.vue | 6 +- packages/frontend/src/components/MkUserPopup.vue | 6 +- packages/frontend/src/pages/settings/privacy.vue | 18 +- packages/frontend/src/pages/user/home.vue | 6 +- packages/frontend/src/scripts/isFfVisibleForMe.ts | 14 +- packages/misskey-js/src/consts.ts | 4 +- packages/misskey-js/src/index.ts | 3 +- 23 files changed, 648 insertions(+), 71 deletions(-) create mode 100644 packages/backend/migration/1702718871541-ffVisibility.js (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a99a6ca2c..dd8c492782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 - Enhance: アイコンデコレーションを複数設定できるように - Enhance: アイコンデコレーションの位置を微調整できるように +- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/locales/index.d.ts b/locales/index.d.ts index cd15bd968f..25a16d4a4d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -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; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5537db9d56..308b7ae67d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -881,8 +881,8 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を classic: "クラシック" muteThread: "スレッドをミュート" unmuteThread: "スレッドのミュートを解除" -ffVisibility: "つながりの公開範囲" -ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。" +followingVisibility: "フォローの公開範囲" +followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" incorrectPassword: "パスワードが間違っています。" diff --git a/packages/backend/migration/1702718871541-ffVisibility.js b/packages/backend/migration/1702718871541-ffVisibility.js new file mode 100644 index 0000000000..24b1873134 --- /dev/null +++ b/packages/backend/migration/1702718871541-ffVisibility.js @@ -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"`); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index fb7aa0c244..ef815a388a 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -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 diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 6659a01412..328dbeaa1c 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -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, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 7a3ca58269..1b86b1bf10 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -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'], diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 2bc7e1136a..68e426b5bc 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -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; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a56f50115b..eed3082258 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -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 { // 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: ちゃんと数える diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index b22fd2ff7a..5706e46b96 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -93,11 +93,11 @@ export default class extends Endpoint { // 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) { diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index ead7ba8c40..794fb04f10 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -101,11 +101,11 @@ export default class extends Endpoint { // 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) { diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index dd4304e6ef..dfda85aac9 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -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: { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 1fb3d6a6ce..e085407de0 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -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', diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 7841e057bf..1fbd45c741 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -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); - assert.strictEqual(followersRes.status, 200); } { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: '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', + followingVisibility: '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); + } + }); + + 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', { + followersVisibility: 'followers', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followersRes.status, 403); + } + { + await api('/i/update', { + followersVisibility: 'private', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followersRes.status, 403); } }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 2ce8fbc129..9c4cbac368 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -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: [] }) }, diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index c2e6ee52f3..2960489c77 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -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: [ { diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index eaebbf03e7..762b9b4316 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -22,10 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts.notes }}

{{ number(user.notesCount) }}
-
+

{{ i18n.ts.following }}

{{ number(user.followingCount) }}
-
+

{{ i18n.ts.followers }}

{{ number(user.followersCount) }}
@@ -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; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index b703369433..df8252fb14 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.notes }}
{{ number(user.notesCount) }}
-
+
{{ i18n.ts.following }}
{{ number(user.followingCount) }}
-
+
{{ i18n.ts.followers }}
{{ number(user.followersCount) }}
@@ -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; diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 971881ea24..67a2f2cb40 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -13,12 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only - - + + + + + + + + + - @@ -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, }); } diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index a87e03e761..a9497f4fe0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -110,11 +110,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ number(user.notesCount) }} {{ i18n.ts.notes }} - + {{ number(user.followingCount) }} {{ i18n.ts.following }} - + {{ number(user.followersCount) }} {{ i18n.ts.followers }} @@ -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); diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index 0567f3b34a..dc0e90d20a 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -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; } diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index a8f0b96d5d..83d313a5fe 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -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', diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index e78501fdfd..54cae8ec03 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -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 -- cgit v1.2.3-freya From 79ca93cefb8c892556b49fe4055d397e2a56adcf Mon Sep 17 00:00:00 2001 From: GrapeApple0 <84321396+GrapeApple0@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:57:05 +0900 Subject: enhance: api.jsonのレスポンスの内容を実際の内容に合わせる (#12723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create packedAdSchema * admin/emoji/add * admin/get-user-ips * admin/roles/users * admin/get-index-stats * admin/accounts/find-by-email * fix type of admin/ad/list * federation/stats * endpoints * get-online-users-count * i/2fa/register-key * i/2fa/key-done * i/2fa/register * i/apps * i/authorized-apps * i/registry/get-all * i/registry/get * i/registry/get-detail * i/registry/key-with-type * i/registry/scopes-with-domain * i/update-email * i/move * i/webhooks/create * fix miss type * i/webhooks/show * i/webhooks/list * flash/create * roles/users * server-info * test * users/lists/get-memberships * users/achievements * fetch-rss * fetch-external-resources --- packages/backend/src/misc/json-schema.ts | 2 + packages/backend/src/models/json-schema/ad.ts | 64 ++++++++++ .../api/endpoints/admin/accounts/find-by-email.ts | 5 + .../src/server/api/endpoints/admin/ad/create.ts | 19 ++- .../src/server/api/endpoints/admin/ad/list.ts | 24 +++- .../src/server/api/endpoints/admin/emoji/add.ts | 2 + .../server/api/endpoints/admin/get-index-stats.ts | 10 ++ .../src/server/api/endpoints/admin/get-user-ips.ts | 19 +++ .../src/server/api/endpoints/admin/roles/users.ts | 16 ++- .../backend/src/server/api/endpoints/endpoint.ts | 17 +++ .../src/server/api/endpoints/federation/stats.ts | 86 +++++++++++++ .../api/endpoints/fetch-external-resources.ts | 12 ++ .../backend/src/server/api/endpoints/fetch-rss.ts | 12 ++ .../src/server/api/endpoints/flash/create.ts | 6 + .../server/api/endpoints/get-online-users-count.ts | 10 ++ .../src/server/api/endpoints/i/2fa/key-done.ts | 10 ++ .../src/server/api/endpoints/i/2fa/register-key.ts | 134 +++++++++++++++++++++ .../src/server/api/endpoints/i/2fa/register.ts | 13 ++ .../backend/src/server/api/endpoints/i/apps.ts | 33 ++++- .../src/server/api/endpoints/i/authorized-apps.ts | 30 +++++ .../backend/src/server/api/endpoints/i/move.ts | 4 + .../src/server/api/endpoints/i/registry/get-all.ts | 4 + .../server/api/endpoints/i/registry/get-detail.ts | 4 + .../src/server/api/endpoints/i/registry/get.ts | 4 + .../api/endpoints/i/registry/keys-with-type.ts | 4 + .../api/endpoints/i/registry/scopes-with-domain.ts | 22 ++++ .../src/server/api/endpoints/i/update-email.ts | 5 + .../src/server/api/endpoints/i/webhooks/create.ts | 39 +++++- .../src/server/api/endpoints/i/webhooks/list.ts | 45 ++++++- .../src/server/api/endpoints/i/webhooks/show.ts | 40 +++++- .../src/server/api/endpoints/roles/users.ts | 19 +++ .../src/server/api/endpoints/server-info.ts | 47 ++++++++ packages/backend/src/server/api/endpoints/test.ts | 24 ++++ .../src/server/api/endpoints/users/achievements.ts | 15 +++ .../api/endpoints/users/lists/get-memberships.ts | 29 +++++ 35 files changed, 822 insertions(+), 7 deletions(-) create mode 100644 packages/backend/src/models/json-schema/ad.ts (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 49f35b9b74..176978d35f 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -38,6 +38,7 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; +import { packedAdSchema } from '@/models/json-schema/ad.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -49,6 +50,7 @@ export const refs = { User: packedUserSchema, UserList: packedUserListSchema, + Ad: packedAdSchema, Announcement: packedAnnouncementSchema, App: packedAppSchema, Note: packedNoteSchema, diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts new file mode 100644 index 0000000000..649ffcd4dc --- /dev/null +++ b/packages/backend/src/models/json-schema/ad.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedAdSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, + nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + expiresAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + startsAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + place: { + type: 'string', + optional: false, + nullable: false, + }, + priority: { + type: 'string', + optional: false, + nullable: false, + }, + ratio: { + type: 'number', + optional: false, + nullable: false, + }, + url: { + type: 'string', + optional: false, + nullable: false, + }, + imageUrl: { + type: 'string', + optional: false, + nullable: false, + }, + memo: { + type: 'string', + optional: false, + nullable: false, + }, + dayOfWeek: { + type: 'integer', + optional: false, + nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts index 7dc9ca830b..bc292fd53a 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts @@ -25,6 +25,11 @@ export const meta = { id: 'cb865949-8af5-4062-a88c-ef55e8786d1d', }, }, + res: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index cbe9727c46..087ae4befc 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -17,6 +17,12 @@ export const meta = { requireCredential: true, requireModerator: true, + res: { + type: 'object', + optional: false, + nullable: false, + ref: 'Ad', + }, } as const; export const paramDef = { @@ -63,7 +69,18 @@ export default class extends Endpoint { // eslint- ad: ad, }); - return ad; + return { + id: ad.id, + expiresAt: ad.expiresAt.toISOString(), + startsAt: ad.startsAt.toISOString(), + dayOfWeek: ad.dayOfWeek, + url: ad.url, + imageUrl: ad.imageUrl, + priority: ad.priority, + ratio: ad.ratio, + place: ad.place, + memo: ad.memo, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 3bda9fcb02..12528917dc 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -16,6 +16,17 @@ export const meta = { requireCredential: true, requireModerator: true, + res: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + ref: 'Ad', + }, + }, } as const; export const paramDef = { @@ -46,7 +57,18 @@ export default class extends Endpoint { // eslint- } const ads = await query.limit(ps.limit).getMany(); - return ads; + return ads.map(ad => ({ + id: ad.id, + expiresAt: ad.expiresAt.toISOString(), + startsAt: ad.startsAt.toISOString(), + dayOfWeek: ad.dayOfWeek, + url: ad.url, + imageUrl: ad.imageUrl, + memo: ad.memo, + place: ad.place, + priority: ad.priority, + ratio: ad.ratio, + })); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 360926594a..76ff1c6b94 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -31,6 +31,8 @@ export const meta = { id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', }, }, + + ref: 'EmojiDetailed', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index 2de85f655a..b81d9857d7 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -15,6 +15,16 @@ export const meta = { kind: 'read:admin', tags: ['admin'], + res: { + type: 'array', + items: { + type: 'object', + properties: { + tablename: { type: 'string' }, + indexname: { type: 'string' }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts index 6a404c0c77..76c32f2a9f 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -16,6 +16,25 @@ export const meta = { requireCredential: true, requireModerator: true, + res: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + properties: { + ip: { type: 'string' }, + createdAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 53145a32d6..6a0f7f9987 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -28,6 +28,20 @@ export const meta = { id: '224eff5e-2488-4b18-b3e7-f50d94421648', }, }, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + createdAt: { type: 'string', format: 'date-time' }, + user: { ref: 'UserDetailed' }, + expiresAt: { type: 'string', format: 'date-time', nullable: true }, + }, + required: ['id', 'createdAt', 'user'], + }, + } } as const; export const paramDef = { @@ -80,7 +94,7 @@ export default class extends Endpoint { // eslint- id: assign.id, createdAt: this.idService.parse(assign.id).date.toISOString(), user: await this.userEntityService.pack(assign.user!, me, { detail: true }), - expiresAt: assign.expiresAt, + expiresAt: assign.expiresAt?.toISOString() ?? null, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index cecaded20a..66ac8f664f 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -11,6 +11,23 @@ export const meta = { requireCredential: false, tags: ['meta'], + + res: { + type: 'object', + nullable: true, + properties: { + params: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + type: { type: 'string' }, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index e3ffea7b7e..6548142d41 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -18,6 +18,92 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + res: { + type: 'object', + optional: false, + nullable: false, + properties: { + topSubInstances: { + type: 'array', + optional: false, + nullable: false, + items: { + properties: { + id: { type: 'string' }, + firstRetrievedAt: { type: 'string' }, + host: { type: 'string' }, + usersCount: { type: 'number' }, + notesCount: { type: 'number' }, + followingCount: { type: 'number' }, + followersCount: { type: 'number' }, + isNotResponding: { type: 'boolean' }, + isSuspended: { type: 'boolean' }, + isBlocked: { type: 'boolean' }, + softwareName: { type: 'string' }, + softwareVersion: { type: 'string' }, + openRegistrations: { type: 'boolean' }, + name: { type: 'string' }, + description: { type: 'string' }, + maintainerName: { type: 'string' }, + maintainerEmail: { type: 'string' }, + isSilenced: { type: 'boolean' }, + iconUrl: { type: 'string' }, + faviconUrl: { type: 'string' }, + themeColor: { type: 'string' }, + infoUpdatedAt: { + type: 'string', + nullable: true, + }, + latestRequestReceivedAt: { + type: 'string', + nullable: true, + }, + } + }, + }, + otherFollowersCount: { type: 'number' }, + topPubInstances: { + type: 'array', + optional: false, + nullable: false, + items: { + properties: { + id: { type: 'string' }, + firstRetrievedAt: { type: 'string' }, + host: { type: 'string' }, + usersCount: { type: 'number' }, + notesCount: { type: 'number' }, + followingCount: { type: 'number' }, + followersCount: { type: 'number' }, + isNotResponding: { type: 'boolean' }, + isSuspended: { type: 'boolean' }, + isBlocked: { type: 'boolean' }, + softwareName: { type: 'string' }, + softwareVersion: { type: 'string' }, + openRegistrations: { type: 'boolean' }, + name: { type: 'string' }, + description: { type: 'string' }, + maintainerName: { type: 'string' }, + maintainerEmail: { type: 'string' }, + isSilenced: { type: 'boolean' }, + iconUrl: { type: 'string' }, + faviconUrl: { type: 'string' }, + themeColor: { type: 'string' }, + infoUpdatedAt: { + type: 'string', + nullable: true, + }, + latestRequestReceivedAt: { + type: 'string', + nullable: true, + }, + } + }, + }, + otherFollowingCount: { type: 'number' }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts index d7b46cc666..6391a2f580 100644 --- a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts +++ b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts @@ -32,6 +32,18 @@ export const meta = { id: '693ba8ba-b486-40df-a174-72f8279b56a4', }, }, + + res: { + type: 'object', + properties: { + type: { + type: 'string', + }, + data: { + type: 'string', + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 37859d8330..b2dee83fe9 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -16,6 +16,18 @@ export const meta = { requireCredential: false, allowGet: true, cacheSec: 60 * 3, + + res: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'object', + }, + } + } + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 4fa65ac9aa..674f323734 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -27,6 +27,12 @@ export const meta = { errors: { }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Flash', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index 8a61168f25..737d637b7e 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -16,6 +16,16 @@ export const meta = { requireCredential: false, allowGet: true, cacheSec: 60 * 1, + res: { + type: 'object', + optional: false, nullable: false, + properties: { + count: { + type: 'number', + nullable: false, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 6d530aba3b..a7be47fd0f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -32,6 +32,16 @@ export const meta = { id: '798d6847-b1ed-4f9c-b1f9-163c42655995', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index c39005f2dd..0fac96d58f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -36,6 +36,140 @@ export const meta = { id: 'bf32b864-449b-47b8-974e-f9a5468546f1', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + rp: { + type: 'object', + properties: { + id: { + type: 'string', + nullable: true, + }, + }, + }, + user: { + type: 'object', + properties: { + id: { + type: 'string', + }, + name: { + type: 'string', + }, + displayName: { + type: 'string', + }, + }, + }, + challenge: { + type: 'string', + }, + pubKeyCredParams: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + }, + alg: { + type: 'number', + }, + }, + }, + }, + timeout: { + type: 'number', + nullable: true, + }, + excludeCredentials: { + type: 'array', + nullable: true, + items: { + type: 'object', + properties: { + id: { + type: 'string', + }, + type: { + type: 'string', + }, + transports: { + type: 'array', + items: { + type: 'string', + enum: [ + "ble", + "cable", + "hybrid", + "internal", + "nfc", + "smart-card", + "usb", + ], + }, + }, + }, + }, + }, + authenticatorSelection: { + type: 'object', + nullable: true, + properties: { + authenticatorAttachment: { + type: 'string', + enum: [ + "cross-platform", + "platform", + ], + }, + requireResidentKey: { + type: 'boolean', + }, + userVerification: { + type: 'string', + enum: [ + "discouraged", + "preferred", + "required", + ], + }, + }, + }, + attestation: { + type: 'string', + nullable: true, + enum: [ + "direct", + "enterprise", + "indirect", + "none", + ], + }, + extensions: { + type: 'object', + nullable: true, + properties: { + appid: { + type: 'string', + nullable: true, + }, + credProps: { + type: 'boolean', + nullable: true, + }, + hmacCreateSecret: { + type: 'boolean', + nullable: true, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index b358c812ee..cc083cbf7b 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -26,6 +26,19 @@ export const meta = { id: '78d6c839-20c9-4c66-b90a-fc0542168b48', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + qr: { type: 'string' }, + url: { type: 'string' }, + secret: { type: 'string' }, + label: { type: 'string' }, + issuer: { type: 'string' }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 09f6540a77..ef89f93181 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -13,6 +13,37 @@ export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + name: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + lastUsedAt: { + type: 'string', + format: 'date-time', + }, + permission: { + type: 'array', + uniqueItems: true, + items: { + type: 'string' + }, + } + }, + }, + }, } as const; export const paramDef = { @@ -50,7 +81,7 @@ export default class extends Endpoint { // eslint- id: token.id, name: token.name ?? token.app?.name, createdAt: this.idService.parse(token.id).date.toISOString(), - lastUsedAt: token.lastUsedAt, + lastUsedAt: token.lastUsedAt?.toISOString(), permission: token.permission, }))); }); diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 32061c2aa4..a0ed371fb8 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -14,6 +14,36 @@ export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + name: { + type: 'string', + }, + callbackUrl: { + type: 'string', + nullable: true, + }, + permission: { + type: 'array', + uniqueItems: true, + items: { + type: 'string' + }, + }, + isAuthorized: { + type: 'boolean', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 86b726e054..f3ba720c2b 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -64,6 +64,10 @@ export const meta = { id: 'b234a14e-9ebe-4581-8000-074b3c215962', }, }, + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 29fa0a29cc..bd6e85a074 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 5b460b45d6..2352beb130 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -18,6 +18,10 @@ export const meta = { id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a', }, }, + + res: { + type: 'object', + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index e8c28298ef..4155a43e0d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -18,6 +18,10 @@ export const meta = { id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', }, }, + + res: { + type: 'object', + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 8953ee5d3d..b411cdd3d9 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts index 1ff994b82c..0aca2a26fe 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -10,6 +10,28 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + scopes: { + type: 'array', + items: { + type: 'array', + items: { + type: 'string', + } + } + }, + domain: { + type: 'string', + nullable: true, + }, + }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index a36b3a732b..52977f5a07 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -40,6 +40,11 @@ export const meta = { id: 'a2defefb-f220-8849-0af6-17f816099323', }, }, + + res: { + type: 'object', + ref: 'UserDetailed', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index f00dba4a85..bdc9f9ea8b 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -27,6 +27,33 @@ export const meta = { id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', }, }, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + }, } as const; export const paramDef = { @@ -73,7 +100,17 @@ export default class extends Endpoint { // eslint- this.globalEventService.publishInternalEvent('webhookCreated', webhook); - return webhook; + return { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index aa8921fe24..afb2d0509e 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -14,6 +15,36 @@ export const meta = { requireCredential: true, kind: 'read:account', + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + } + } } as const; export const paramDef = { @@ -33,7 +64,19 @@ export default class extends Endpoint { // eslint- userId: me.id, }); - return webhooks; + return webhooks.map(webhook => ( + { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + } + )); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index f1294bb5c8..5c6dd908b4 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -23,6 +24,33 @@ export const meta = { id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', }, }, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + }, } as const; export const paramDef = { @@ -49,7 +77,17 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchWebhook); } - return webhook; + return { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index caaa3735e9..d304d075b2 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -24,6 +24,25 @@ export const meta = { id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', }, }, + + res: { + type: 'array', + items: { + type: 'object', + nullable: false, + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + user: { + type: 'object', + ref: 'User' + }, + }, + required: ['id', 'user'], + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index c8cb63e6b3..079f2d7f1d 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -15,6 +15,53 @@ export const meta = { cacheSec: 60 * 1, tags: ['meta'], + res: { + type: 'object', + optional: false, nullable: false, + properties: { + machine: { + type: 'string', + nullable: false, + }, + cpu: { + type: 'object', + nullable: false, + properties: { + model: { + type: 'string', + nullable: false, + }, + cores: { + type: 'number', + nullable: false, + }, + }, + }, + mem: { + type: 'object', + properties: { + total: { + type: 'number', + nullable: false, + }, + }, + }, + fs: { + type: 'object', + nullable: false, + properties: { + total: { + type: 'number', + nullable: false, + }, + used: { + type: 'number', + nullable: false, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 6d6d44f752..949867c572 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -12,6 +12,30 @@ export const meta = { description: 'Endpoint for testing input validation.', requireCredential: false, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + required: { + type: 'boolean', + }, + string: { + type: 'string', + }, + default: { + type: 'string', + }, + nullableDefault: { + type: 'string', + default: 'hello', + nullable: true, + }, + } + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index e4845d57bf..d6ad718dfa 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -10,6 +10,21 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + unlockedAt: { + type: 'number', + }, + }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts index ae8b4e9b81..985141515e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -25,6 +25,35 @@ export const meta = { id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', }, }, + + res: { + type: 'array', + items: { + type: 'object', + nullable: false, + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + user: { + type: 'object', + ref: 'User', + }, + withReplies: { + type: 'boolean', + }, + }, + }, + }, } as const; export const paramDef = { -- cgit v1.2.3-freya