diff options
| author | Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> | 2023-05-19 10:06:12 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-19 10:06:12 +0900 |
| commit | dddbc1c894f53d6891ca7760dd9382c19931661a (patch) | |
| tree | edc57ec92a68d547c9b31e5aa139dcf61ba5095a /packages/backend/src/server/api | |
| parent | perf: MkImgWithBlurhashとMkMediaImageを最適化 (#10782) (diff) | |
| download | misskey-dddbc1c894f53d6891ca7760dd9382c19931661a.tar.gz misskey-dddbc1c894f53d6891ca7760dd9382c19931661a.tar.bz2 misskey-dddbc1c894f53d6891ca7760dd9382c19931661a.zip | |
feat: 公開リスト (#10842)
* feat: まず公開できるように (misskey-dev/misskey#10447)
* feat: 公開したリストのページを作成 (misskey-dev/misskey#10447)
* feat: いいねできるように
* feat: インポートに対応
* wip
* wip
* CHANGELOGを編集
* add note
* refactor
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Diffstat (limited to 'packages/backend/src/server/api')
8 files changed, 370 insertions, 14 deletions
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ee1aae5b6c..1e32e9988d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -321,6 +321,9 @@ import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; import * as ep___users_reactions from './endpoints/users/reactions.js'; @@ -659,6 +662,9 @@ const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default }; const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default }; const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; +const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default }; +const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default }; +const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; @@ -1001,6 +1007,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_push, $users_lists_show, $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, $users_notes, $users_pages, $users_reactions, @@ -1335,6 +1344,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_push, $users_lists_show, $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, $users_notes, $users_pages, $users_reactions, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 09bd7cbff4..7e678a6404 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -320,6 +320,9 @@ import * as ep___users_lists_list from './endpoints/users/lists/list.js'; import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; @@ -656,7 +659,10 @@ const eps = [ ['users/lists/pull', ep___users_lists_pull], ['users/lists/push', ep___users_lists_push], ['users/lists/show', ep___users_lists_show], + ['users/lists/favorite', ep___users_lists_favorite], + ['users/lists/unfavorite', ep___users_lists_unfavorite], ['users/lists/update', ep___users_lists_update], + ['users/lists/create-from-public', ep___users_lists_create_from_public], ['users/notes', ep___users_notes], ['users/pages', ep___users_pages], ['users/reactions', ep___users_reactions], diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts new file mode 100644 index 0000000000..8591e4ab96 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -0,0 +1,148 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleService } from '@/core/RoleService.js'; +import { UserListService } from '@/core/UserListService.js'; + +export const meta = { + requireCredential: true, + prohibitMoved: true, + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserList', + }, + + errors: { + tooManyUserLists: { + message: 'You cannot create user list any more.', + code: 'TOO_MANY_USERLISTS', + id: 'e9c105b2-c595-47de-97fb-7f7c2c33e92f', + }, + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '9292f798-6175-4f7d-93f4-b6742279667d', + }, + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '13c457db-a8cb-4d88-b70a-211ceeeabb5f', + }, + + alreadyAdded: { + message: 'That user has already been added to that list.', + code: 'ALREADY_ADDED', + id: 'c3ad6fdb-692b-47ee-a455-7bd12c7af615', + }, + + youHaveBeenBlocked: { + message: 'You cannot push this user because you have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'a2497f2a-2389-439c-8626-5298540530f4', + }, + + tooManyUsers: { + message: 'You can not push users any more.', + code: 'TOO_MANY_USERS', + id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 100 }, + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['name', 'listId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userListService: UserListService, + private userListEntityService: UserListEntityService, + private idService: IdService, + private getterService: GetterService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const list = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + if (list === null) throw new ApiError(meta.errors.noSuchList); + const currentCount = await this.userListsRepository.countBy({ + userId: me.id, + }); + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { + throw new ApiError(meta.errors.tooManyUserLists); + } + + const userList = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + + const users = (await this.userListJoiningsRepository.findBy({ + userListId: ps.listId, + })).map(x => x.userId); + + for (const user of users) { + const currentUser = await this.getterService.getUser(user).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + if (currentUser.id !== me.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: currentUser.id, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + const exist = await this.userListJoiningsRepository.findOneBy({ + userListId: userList.id, + userId: currentUser.id, + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyAdded); + } + + try { + await this.userListService.push(currentUser, userList, me); + } catch (err) { + if (err instanceof UserListService.TooManyUsersError) { + throw new ApiError(meta.errors.tooManyUsers); + } + throw err; + } + } + return await this.userListEntityService.pack(userList); + }); + } +} + diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts new file mode 100644 index 0000000000..263852fde1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, + errors: { + noSuchList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: '7dbaf3cf-7b42-4b8f-b431-b3919e580dbe', + }, + + alreadyFavorited: { + message: 'The list has already been favorited.', + code: 'ALREADY_FAVORITED', + id: '6425bba0-985b-461e-af1b-518070e72081', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor ( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + + if (userList === null) { + throw new ApiError(meta.errors.noSuchList); + } + + const exist = await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, + }); + + if (exist !== null) { + throw new ApiError(meta.errors.alreadyFavorited); + } + + await this.userListFavoritesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + userListId: ps.listId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 2104c4377d..eab29944b2 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,13 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { ApiError } from '@/server/api/error.js'; import { DI } from '@/di-symbols.js'; export const meta = { tags: ['lists', 'account'], - requireCredential: true, + requireCredential: false, kind: 'read:account', @@ -22,26 +23,58 @@ export const meta = { ref: 'UserList', }, }, + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'a8af4a82-0980-4cc4-a6af-8b0ffd54465e', + }, + remoteUser: { + message: 'Not allowed to load the remote user\'s list', + code: 'REMOTE_USER_NOT_ALLOWED', + id: '53858f1b-3315-4a01-81b7-db9b48d4b79a', + }, + invalidParam: { + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: 'ab36de0e-29e9-48cb-9732-d82f1281620d', + }, + }, } as const; export const paramDef = { type: 'object', - properties: {}, + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, required: [], } as const; -// eslint-disable-next-line import/no-default-export -@Injectable() +@Injectable() // eslint-disable-next-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { - const userLists = await this.userListsRepository.findBy({ + if (typeof ps.userId !== 'undefined') { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user === null) throw new ApiError(meta.errors.noSuchUser); + if (user.host !== null) throw new ApiError(meta.errors.remoteUser); + } else if (me === null) { + throw new ApiError(meta.errors.invalidParam); + } + + const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? { userId: me.id, + } : { + userId: ps.userId, + isPublic: true, }); return await Promise.all(userLists.map(x => this.userListEntityService.pack(x))); diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 77f9cba808..8077841c8c 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -8,7 +8,7 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['lists', 'account'], - requireCredential: true, + requireCredential: false, kind: 'read:account', @@ -33,31 +33,54 @@ export const paramDef = { type: 'object', properties: { listId: { type: 'string', format: 'misskey:id' }, + forPublic: { type: 'boolean', default: false }, }, required: ['listId'], } as const; -// eslint-disable-next-line import/no-default-export -@Injectable() +@Injectable() // eslint-disable-next-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { + const additionalProperties: Partial<{ likedCount: number, isLiked: boolean }> = {}; // Fetch the list - const userList = await this.userListsRepository.findOneBy({ + const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? { id: ps.listId, userId: me.id, + } : { + id: ps.listId, + isPublic: true, }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - return await this.userListEntityService.pack(userList); + if (ps.forPublic && userList.isPublic) { + additionalProperties.likedCount = await this.userListFavoritesRepository.countBy({ + userListId: ps.listId, + }); + if (me !== null) { + additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, + }) !== null); + } else { + additionalProperties.isLiked = false; + } + } + return { + ...await this.userListEntityService.pack(userList), + ...additionalProperties, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts new file mode 100644 index 0000000000..be8e317816 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, + errors: { + noSuchList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: 'baedb33e-76b8-4b0c-86a8-9375c0a7b94b', + }, + + notFavorited: { + message: 'You have not favorited the list.', + code: 'ALREADY_FAVORITED', + id: '835c4b27-463d-4cfa-969b-a9058678d465', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor ( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + + if (userList === null) { + throw new ApiError(meta.errors.noSuchList); + } + + const exist = await this.userListFavoritesRepository.findOneBy({ + userListId: ps.listId, + userId: me.id, + }); + + if (exist === null) { + throw new ApiError(meta.errors.notFavorited); + } + + await this.userListFavoritesRepository.delete({ id: exist.id }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 6453d7d980..b0a95a2f28 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -34,8 +34,9 @@ export const paramDef = { properties: { listId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, + isPublic: { type: 'boolean' }, }, - required: ['listId', 'name'], + required: ['listId'], } as const; // eslint-disable-next-line import/no-default-export @@ -48,7 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { - // Fetch the list const userList = await this.userListsRepository.findOneBy({ id: ps.listId, userId: me.id, @@ -60,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { await this.userListsRepository.update(userList.id, { name: ps.name, + isPublic: ps.isPublic, }); return await this.userListEntityService.pack(userList.id); |