summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authorChocolate Pie <106949016+chocolate-pie@users.noreply.github.com>2023-05-19 10:06:12 +0900
committerGitHub <noreply@github.com>2023-05-19 10:06:12 +0900
commitdddbc1c894f53d6891ca7760dd9382c19931661a (patch)
treeedc57ec92a68d547c9b31e5aa139dcf61ba5095a /packages/backend/src/server/api
parentperf: MkImgWithBlurhashとMkMediaImageを最適化 (#10782) (diff)
downloadmisskey-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')
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts12
-rw-r--r--packages/backend/src/server/api/endpoints.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts148
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/favorite.ts70
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/list.ts45
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/show.ts35
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/update.ts5
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);