From 0e4a111f81cceed275d9bec2695f6e401fb654d8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 12 Nov 2021 02:02:25 +0900 Subject: refactoring Resolve #7779 --- .../src/server/api/endpoints/users/clips.ts | 40 ++++++ .../src/server/api/endpoints/users/followers.ts | 104 +++++++++++++++ .../src/server/api/endpoints/users/following.ts | 104 +++++++++++++++ .../server/api/endpoints/users/gallery/posts.ts | 39 ++++++ .../users/get-frequently-replied-users.ts | 105 +++++++++++++++ .../server/api/endpoints/users/groups/create.ts | 45 +++++++ .../server/api/endpoints/users/groups/delete.ts | 40 ++++++ .../endpoints/users/groups/invitations/accept.ts | 54 ++++++++ .../endpoints/users/groups/invitations/reject.ts | 44 +++++++ .../server/api/endpoints/users/groups/invite.ts | 102 +++++++++++++++ .../server/api/endpoints/users/groups/joined.ts | 36 ++++++ .../src/server/api/endpoints/users/groups/leave.ts | 50 +++++++ .../src/server/api/endpoints/users/groups/owned.ts | 28 ++++ .../src/server/api/endpoints/users/groups/pull.ts | 69 ++++++++++ .../src/server/api/endpoints/users/groups/show.ts | 55 ++++++++ .../server/api/endpoints/users/groups/transfer.ts | 83 ++++++++++++ .../server/api/endpoints/users/groups/update.ts | 55 ++++++++ .../src/server/api/endpoints/users/lists/create.ts | 36 ++++++ .../src/server/api/endpoints/users/lists/delete.ts | 40 ++++++ .../src/server/api/endpoints/users/lists/list.ts | 28 ++++ .../src/server/api/endpoints/users/lists/pull.ts | 62 +++++++++ .../src/server/api/endpoints/users/lists/push.ts | 92 +++++++++++++ .../src/server/api/endpoints/users/lists/show.ts | 47 +++++++ .../src/server/api/endpoints/users/lists/update.ts | 55 ++++++++ .../src/server/api/endpoints/users/notes.ts | 144 +++++++++++++++++++++ .../src/server/api/endpoints/users/pages.ts | 40 ++++++ .../src/server/api/endpoints/users/reactions.ts | 79 +++++++++++ .../server/api/endpoints/users/recommendation.ts | 63 +++++++++ .../src/server/api/endpoints/users/relation.ts | 111 ++++++++++++++++ .../src/server/api/endpoints/users/report-abuse.ts | 90 +++++++++++++ .../endpoints/users/search-by-username-and-host.ts | 116 +++++++++++++++++ .../src/server/api/endpoints/users/search.ts | 127 ++++++++++++++++++ .../backend/src/server/api/endpoints/users/show.ts | 105 +++++++++++++++ .../src/server/api/endpoints/users/stats.ts | 144 +++++++++++++++++++++ 34 files changed, 2432 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/users/clips.ts create mode 100644 packages/backend/src/server/api/endpoints/users/followers.ts create mode 100644 packages/backend/src/server/api/endpoints/users/following.ts create mode 100644 packages/backend/src/server/api/endpoints/users/gallery/posts.ts create mode 100644 packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/create.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/delete.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/invite.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/joined.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/leave.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/owned.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/pull.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/show.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/transfer.ts create mode 100644 packages/backend/src/server/api/endpoints/users/groups/update.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/create.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/delete.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/list.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/pull.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/push.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/show.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/update.ts create mode 100644 packages/backend/src/server/api/endpoints/users/notes.ts create mode 100644 packages/backend/src/server/api/endpoints/users/pages.ts create mode 100644 packages/backend/src/server/api/endpoints/users/reactions.ts create mode 100644 packages/backend/src/server/api/endpoints/users/recommendation.ts create mode 100644 packages/backend/src/server/api/endpoints/users/relation.ts create mode 100644 packages/backend/src/server/api/endpoints/users/report-abuse.ts create mode 100644 packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts create mode 100644 packages/backend/src/server/api/endpoints/users/search.ts create mode 100644 packages/backend/src/server/api/endpoints/users/show.ts create mode 100644 packages/backend/src/server/api/endpoints/users/stats.ts (limited to 'packages/backend/src/server/api/endpoints/users') diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts new file mode 100644 index 0000000000..8feca9422a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { Clips } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users', 'clips'], + + params: { + userId: { + validator: $.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId) + .andWhere(`clip.userId = :userId`, { userId: ps.userId }) + .andWhere('clip.isPublic = true'); + + const clips = await query + .take(ps.limit!) + .getMany(); + + return await Clips.packMany(clips); +}); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts new file mode 100644 index 0000000000..6d042a2861 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -0,0 +1,104 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Users, Followings, UserProfiles } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { toPunyNullable } from '@/misc/convert-host'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + userId: { + validator: $.optional.type(ID), + }, + + username: { + validator: $.optional.str + }, + + host: { + validator: $.optional.nullable.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Following', + } + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '27fa5435-88ab-43de-9360-387de88727cd' + }, + + forbidden: { + message: 'Forbidden.', + code: 'FORBIDDEN', + id: '3c6a84db-d619-26af-ca14-06232a21df8a' + }, + } +}; + +export default define(meta, async (ps, me) => { + const user = await Users.findOne(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) }); + + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const profile = await UserProfiles.findOneOrFail(user.id); + + if (profile.ffVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.ffVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const following = await Followings.findOne({ + followeeId: user.id, + followerId: me.id, + }); + if (following == null) { + throw new ApiError(meta.errors.forbidden); + } + } + } + + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followeeId = :userId`, { userId: user.id }) + .innerJoinAndSelect('following.follower', 'follower'); + + const followings = await query + .take(ps.limit!) + .getMany(); + + return await Followings.packMany(followings, me, { populateFollower: true }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts new file mode 100644 index 0000000000..1033117ef8 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -0,0 +1,104 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Users, Followings, UserProfiles } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { toPunyNullable } from '@/misc/convert-host'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + userId: { + validator: $.optional.type(ID), + }, + + username: { + validator: $.optional.str + }, + + host: { + validator: $.optional.nullable.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Following', + } + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '63e4aba4-4156-4e53-be25-c9559e42d71b' + }, + + forbidden: { + message: 'Forbidden.', + code: 'FORBIDDEN', + id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba' + }, + } +}; + +export default define(meta, async (ps, me) => { + const user = await Users.findOne(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) }); + + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const profile = await UserProfiles.findOneOrFail(user.id); + + if (profile.ffVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.ffVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const following = await Followings.findOne({ + followeeId: user.id, + followerId: me.id, + }); + if (following == null) { + throw new ApiError(meta.errors.forbidden); + } + } + } + + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followerId = :userId`, { userId: user.id }) + .innerJoinAndSelect('following.followee', 'followee'); + + const followings = await query + .take(ps.limit!) + .getMany(); + + return await Followings.packMany(followings, me, { populateFollowee: true }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts new file mode 100644 index 0000000000..845de1089c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -0,0 +1,39 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { GalleryPosts } from '@/models/index'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; + +export const meta = { + tags: ['users', 'gallery'], + + params: { + userId: { + validator: $.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId) + .andWhere(`post.userId = :userId`, { userId: ps.userId }); + + const posts = await query + .take(ps.limit!) + .getMany(); + + return await GalleryPosts.packMany(posts, user); +}); diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts new file mode 100644 index 0000000000..32ebfd683a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -0,0 +1,105 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { maximum } from '@/prelude/array'; +import { ApiError } from '../../error'; +import { getUser } from '../../common/getters'; +import { Not, In, IsNull } from 'typeorm'; +import { Notes, Users } from '@/models/index'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + userId: { + validator: $.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User', + } + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'e6965129-7b2a-40a4-bae2-cd84cd434822' + } + } +}; + +export default define(meta, async (ps, me) => { + // Lookup user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + // Fetch recent notes + const recentNotes = await Notes.find({ + where: { + userId: user.id, + replyId: Not(IsNull()) + }, + order: { + id: -1 + }, + take: 1000, + select: ['replyId'] + }); + + // 投稿が少なかったら中断 + if (recentNotes.length === 0) { + return []; + } + + // TODO ミュートを考慮 + const replyTargetNotes = await Notes.find({ + where: { + id: In(recentNotes.map(p => p.replyId)), + }, + select: ['userId'] + }); + + const repliedUsers: any = {}; + + // Extract replies from recent notes + for (const userId of replyTargetNotes.map(x => x.userId.toString())) { + if (repliedUsers[userId]) { + repliedUsers[userId]++; + } else { + repliedUsers[userId] = 1; + } + } + + // Calc peak + const peak = maximum(Object.values(repliedUsers)); + + // Sort replies by frequency + const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); + + // Extract top replied users + const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit!); + + // Make replies object (includes weights) + const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ + user: await Users.pack(user, me, { detail: true }), + weight: repliedUsers[user] / peak + }))); + + return repliesObj; +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts new file mode 100644 index 0000000000..dc1ee3879e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -0,0 +1,45 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { UserGroups, UserGroupJoinings } from '@/models/index'; +import { genId } from '@/misc/gen-id'; +import { UserGroup } from '@/models/entities/user-group'; +import { UserGroupJoining } from '@/models/entities/user-group-joining'; + +export const meta = { + tags: ['groups'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + name: { + validator: $.str.range(1, 100) + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserGroup', + }, +}; + +export default define(meta, async (ps, user) => { + const userGroup = await UserGroups.insert({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + } as UserGroup).then(x => UserGroups.findOneOrFail(x.identifiers[0])); + + // Push the owner + await UserGroupJoinings.insert({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: userGroup.id + } as UserGroupJoining); + + return await UserGroups.pack(userGroup); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts new file mode 100644 index 0000000000..7da1b4a273 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups } from '@/models/index'; + +export const meta = { + tags: ['groups'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '63dbd64c-cd77-413f-8e08-61781e210b38' + } + } +}; + +export default define(meta, async (ps, user) => { + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: user.id + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + await UserGroups.delete(userGroup.id); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts new file mode 100644 index 0000000000..09e6ae2647 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -0,0 +1,54 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../../define'; +import { ApiError } from '../../../../error'; +import { UserGroupJoinings, UserGroupInvitations } from '@/models/index'; +import { genId } from '@/misc/gen-id'; +import { UserGroupJoining } from '@/models/entities/user-group-joining'; + +export const meta = { + tags: ['groups', 'users'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + invitationId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchInvitation: { + message: 'No such invitation.', + code: 'NO_SUCH_INVITATION', + id: '98c11eca-c890-4f42-9806-c8c8303ebb5e' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Fetch the invitation + const invitation = await UserGroupInvitations.findOne({ + id: ps.invitationId, + }); + + if (invitation == null) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + if (invitation.userId !== user.id) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + // Push the user + await UserGroupJoinings.insert({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: invitation.userGroupId + } as UserGroupJoining); + + UserGroupInvitations.delete(invitation.id); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts new file mode 100644 index 0000000000..741fcefb35 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -0,0 +1,44 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../../define'; +import { ApiError } from '../../../../error'; +import { UserGroupInvitations } from '@/models/index'; + +export const meta = { + tags: ['groups', 'users'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + invitationId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchInvitation: { + message: 'No such invitation.', + code: 'NO_SUCH_INVITATION', + id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Fetch the invitation + const invitation = await UserGroupInvitations.findOne({ + id: ps.invitationId, + }); + + if (invitation == null) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + if (invitation.userId !== user.id) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + await UserGroupInvitations.delete(invitation.id); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts new file mode 100644 index 0000000000..f1ee8bf8b7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -0,0 +1,102 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index'; +import { genId } from '@/misc/gen-id'; +import { UserGroupInvitation } from '@/models/entities/user-group-invitation'; +import { createNotification } from '@/services/create-notification'; + +export const meta = { + tags: ['groups', 'users'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '583f8bc0-8eee-4b78-9299-1e14fc91e409' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'da52de61-002c-475b-90e1-ba64f9cf13a8' + }, + + alreadyAdded: { + message: 'That user has already been added to that group.', + code: 'ALREADY_ADDED', + id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c' + }, + + alreadyInvited: { + message: 'That user has already been invited to that group.', + code: 'ALREADY_INVITED', + id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6' + } + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + const joining = await UserGroupJoinings.findOne({ + userGroupId: userGroup.id, + userId: user.id + }); + + if (joining) { + throw new ApiError(meta.errors.alreadyAdded); + } + + const existInvitation = await UserGroupInvitations.findOne({ + userGroupId: userGroup.id, + userId: user.id + }); + + if (existInvitation) { + throw new ApiError(meta.errors.alreadyInvited); + } + + const invitation = await UserGroupInvitations.insert({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: userGroup.id + } as UserGroupInvitation).then(x => UserGroupInvitations.findOneOrFail(x.identifiers[0])); + + // 通知を作成 + createNotification(user.id, 'groupInvited', { + notifierId: me.id, + userGroupInvitationId: invitation.id + }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts new file mode 100644 index 0000000000..d5e8fe4032 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts @@ -0,0 +1,36 @@ +import define from '../../../define'; +import { UserGroups, UserGroupJoinings } from '@/models/index'; +import { Not, In } from 'typeorm'; + +export const meta = { + tags: ['groups', 'account'], + + requireCredential: true as const, + + kind: 'read:user-groups', + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserGroup', + } + }, +}; + +export default define(meta, async (ps, me) => { + const ownedGroups = await UserGroups.find({ + userId: me.id, + }); + + const joinings = await UserGroupJoinings.find({ + userId: me.id, + ...(ownedGroups.length > 0 ? { + userGroupId: Not(In(ownedGroups.map(x => x.id))) + } : {}) + }); + + return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId))); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts new file mode 100644 index 0000000000..0e52f2abdf --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -0,0 +1,50 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups, UserGroupJoinings } from '@/models/index'; + +export const meta = { + tags: ['groups', 'users'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '62780270-1f67-5dc0-daca-3eb510612e31' + }, + + youAreOwner: { + message: 'Your are the owner.', + code: 'YOU_ARE_OWNER', + id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + if (me.id === userGroup.userId) { + throw new ApiError(meta.errors.youAreOwner); + } + + await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts new file mode 100644 index 0000000000..17de370dbc --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts @@ -0,0 +1,28 @@ +import define from '../../../define'; +import { UserGroups } from '@/models/index'; + +export const meta = { + tags: ['groups', 'account'], + + requireCredential: true as const, + + kind: 'read:user-groups', + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserGroup', + } + }, +}; + +export default define(meta, async (ps, me) => { + const userGroups = await UserGroups.find({ + userId: me.id, + }); + + return await Promise.all(userGroups.map(x => UserGroups.pack(x))); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts new file mode 100644 index 0000000000..ce4d2e2881 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -0,0 +1,69 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { UserGroups, UserGroupJoinings } from '@/models/index'; + +export const meta = { + tags: ['groups', 'users'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '4662487c-05b1-4b78-86e5-fd46998aba74' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '0b5cc374-3681-41da-861e-8bc1146f7a55' + }, + + isOwner: { + message: 'The user is the owner.', + code: 'IS_OWNER', + id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + if (user.id === userGroup.userId) { + throw new ApiError(meta.errors.isOwner); + } + + // Pull the user + await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: user.id }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts new file mode 100644 index 0000000000..3c030bf3a5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -0,0 +1,55 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups, UserGroupJoinings } from '@/models/index'; + +export const meta = { + tags: ['groups', 'account'], + + requireCredential: true as const, + + kind: 'read:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserGroup', + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + const joining = await UserGroupJoinings.findOne({ + userId: me.id, + userGroupId: userGroup.id + }); + + if (joining == null && userGroup.userId !== me.id) { + throw new ApiError(meta.errors.noSuchGroup); + } + + return await UserGroups.pack(userGroup); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts new file mode 100644 index 0000000000..17c42e1127 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -0,0 +1,83 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { UserGroups, UserGroupJoinings } from '@/models/index'; + +export const meta = { + tags: ['groups', 'users'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + }, + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserGroup', + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9' + }, + + noSuchGroupMember: { + message: 'No such group member.', + code: 'NO_SUCH_GROUP_MEMBER', + id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + const joining = await UserGroupJoinings.findOne({ + userGroupId: userGroup.id, + userId: user.id + }); + + if (joining == null) { + throw new ApiError(meta.errors.noSuchGroupMember); + } + + await UserGroups.update(userGroup.id, { + userId: ps.userId + }); + + return await UserGroups.pack(userGroup.id); +}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts new file mode 100644 index 0000000000..127bbc47a1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -0,0 +1,55 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups } from '@/models/index'; + +export const meta = { + tags: ['groups'], + + requireCredential: true as const, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + name: { + validator: $.str.range(1, 100), + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserGroup', + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + await UserGroups.update(userGroup.id, { + name: ps.name + }); + + return await UserGroups.pack(userGroup.id); +}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts new file mode 100644 index 0000000000..e0bfe611fc --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -0,0 +1,36 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { UserLists } from '@/models/index'; +import { genId } from '@/misc/gen-id'; +import { UserList } from '@/models/entities/user-list'; + +export const meta = { + tags: ['lists'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + name: { + validator: $.str.range(1, 100) + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserList', + }, +}; + +export default define(meta, async (ps, user) => { + const userList = await UserLists.insert({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + } as UserList).then(x => UserLists.findOneOrFail(x.identifiers[0])); + + return await UserLists.pack(userList); +}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts new file mode 100644 index 0000000000..5fe3bfb03d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserLists } from '@/models/index'; + +export const meta = { + tags: ['lists'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + listId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '78436795-db79-42f5-b1e2-55ea2cf19166' + } + } +}; + +export default define(meta, async (ps, user) => { + const userList = await UserLists.findOne({ + id: ps.listId, + userId: user.id + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + await UserLists.delete(userList.id); +}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts new file mode 100644 index 0000000000..cf0c92bb84 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -0,0 +1,28 @@ +import define from '../../../define'; +import { UserLists } from '@/models/index'; + +export const meta = { + tags: ['lists', 'account'], + + requireCredential: true as const, + + kind: 'read:account', + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserList', + } + }, +}; + +export default define(meta, async (ps, me) => { + const userLists = await UserLists.find({ + userId: me.id, + }); + + return await Promise.all(userLists.map(x => UserLists.pack(x))); +}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts new file mode 100644 index 0000000000..d4357fc5e7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import { publishUserListStream } from '@/services/stream'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { UserLists, UserListJoinings, Users } from '@/models/index'; + +export const meta = { + tags: ['lists', 'users'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + listId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '588e7f72-c744-4a61-b180-d354e912bda2' + } + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the list + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + // Pull the user + await UserListJoinings.delete({ userListId: userList.id, userId: user.id }); + + publishUserListStream(userList.id, 'userRemoved', await Users.pack(user)); +}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts new file mode 100644 index 0000000000..8e21059d3d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -0,0 +1,92 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { pushUserToUserList } from '@/services/user-list/push'; +import { UserLists, UserListJoinings, Blockings } from '@/models/index'; + +export const meta = { + tags: ['lists', 'users'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + listId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '2214501d-ac96-4049-b717-91e42272a711' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a' + }, + + alreadyAdded: { + message: 'That user has already been added to that list.', + code: 'ALREADY_ADDED', + id: '1de7c884-1595-49e9-857e-61f12f4d4fc5' + }, + + youHaveBeenBlocked: { + message: 'You cannot push this user because you have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the list + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + // Check blocking + if (user.id !== me.id) { + const block = await Blockings.findOne({ + blockerId: user.id, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + const exist = await UserListJoinings.findOne({ + userListId: userList.id, + userId: user.id + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyAdded); + } + + // Push the user + await pushUserToUserList(user, userList); +}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts new file mode 100644 index 0000000000..f9a35cdab3 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -0,0 +1,47 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserLists } from '@/models/index'; + +export const meta = { + tags: ['lists', 'account'], + + requireCredential: true as const, + + kind: 'read:account', + + params: { + listId: { + validator: $.type(ID), + }, + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserList', + }, + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the list + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + return await UserLists.pack(userList); +}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts new file mode 100644 index 0000000000..1185af5043 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -0,0 +1,55 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserLists } from '@/models/index'; + +export const meta = { + tags: ['lists'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + listId: { + validator: $.type(ID), + }, + + name: { + validator: $.str.range(1, 100), + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'UserList', + }, + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '796666fe-3dff-4d39-becb-8a5932c1d5b7' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Fetch the list + const userList = await UserLists.findOne({ + id: ps.listId, + userId: user.id + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + await UserLists.update(userList.id, { + name: ps.name + }); + + return await UserLists.pack(userList.id); +}); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts new file mode 100644 index 0000000000..0afbad9d04 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -0,0 +1,144 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { getUser } from '../../common/getters'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { Notes } from '@/models/index'; +import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; +import { Brackets } from 'typeorm'; +import { generateBlockedUserQuery } from '../../common/generate-block-query'; + +export const meta = { + tags: ['users', 'notes'], + + params: { + userId: { + validator: $.type(ID), + }, + + includeReplies: { + validator: $.optional.bool, + default: true, + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10, + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + sinceDate: { + validator: $.optional.num, + }, + + untilDate: { + validator: $.optional.num, + }, + + includeMyRenotes: { + validator: $.optional.bool, + default: true, + }, + + withFiles: { + validator: $.optional.bool, + default: false, + }, + + fileType: { + validator: $.optional.arr($.str), + }, + + excludeNsfw: { + validator: $.optional.bool, + default: false, + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Note', + } + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b' + } + } +}; + +export default define(meta, async (ps, me) => { + // Lookup user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + //#region Construct query + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.userId = :userId', { userId: user.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + generateVisibilityQuery(query, me); + if (me) generateMutedUserQuery(query, me, user); + if (me) generateBlockedUserQuery(query, me); + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); + + if (ps.excludeNsfw) { + query.andWhere('note.cw IS NULL'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + } + } + + if (!ps.includeReplies) { + query.andWhere('note.replyId IS NULL'); + } + + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :userId', { userId: user.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + //#endregion + + const timeline = await query.take(ps.limit!).getMany(); + + return await Notes.packMany(timeline, me); +}); diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts new file mode 100644 index 0000000000..24e9e207fd --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { Pages } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users', 'pages'], + + params: { + userId: { + validator: $.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) + .andWhere(`page.userId = :userId`, { userId: ps.userId }) + .andWhere('page.visibility = \'public\''); + + const pages = await query + .take(ps.limit!) + .getMany(); + + return await Pages.packMany(pages); +}); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts new file mode 100644 index 0000000000..fe5e4d84a9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -0,0 +1,79 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { NoteReactions, UserProfiles } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { ApiError } from '../../error'; + +export const meta = { + tags: ['users', 'reactions'], + + requireCredential: false as const, + + params: { + userId: { + validator: $.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10, + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + sinceDate: { + validator: $.optional.num, + }, + + untilDate: { + validator: $.optional.num, + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'NoteReaction', + } + }, + + errors: { + reactionsNotPublic: { + message: 'Reactions of the user is not public.', + code: 'REACTIONS_NOT_PUBLIC', + id: '673a7dd2-6924-1093-e0c0-e68456ceae5c' + }, + } +}; + +export default define(meta, async (ps, me) => { + const profile = await UserProfiles.findOneOrFail(ps.userId); + + if (me == null || (me.id !== ps.userId && !profile.publicReactions)) { + throw new ApiError(meta.errors.reactionsNotPublic); + } + + const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere(`reaction.userId = :userId`, { userId: ps.userId }) + .leftJoinAndSelect('reaction.note', 'note'); + + generateVisibilityQuery(query, me); + + const reactions = await query + .take(ps.limit!) + .getMany(); + + return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true }))); +}); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts new file mode 100644 index 0000000000..dde6bb1037 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -0,0 +1,63 @@ +import * as ms from 'ms'; +import $ from 'cafy'; +import define from '../../define'; +import { Users, Followings } from '@/models/index'; +import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query'; +import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query'; + +export const meta = { + tags: ['users'], + + requireCredential: true as const, + + kind: 'read:account', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + offset: { + validator: $.optional.num.min(0), + default: 0 + } + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = Users.createQueryBuilder('user') + .where('user.isLocked = FALSE') + .andWhere('user.isExplorable = TRUE') + .andWhere('user.host IS NULL') + .andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) + .andWhere('user.id != :meId', { meId: me.id }) + .orderBy('user.followersCount', 'DESC'); + + generateMutedUserQueryForUsers(query, me); + generateBlockQueryForUsers(query, me); + generateBlockedUserQuery(query, me); + + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + query + .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`); + + query.setParameters(followingQuery.getParameters()); + + const users = await query.take(ps.limit!).skip(ps.offset).getMany(); + + return await Users.packMany(users, me, { detail: true }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts new file mode 100644 index 0000000000..32d76a5322 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -0,0 +1,111 @@ +import $ from 'cafy'; +import define from '../../define'; +import { ID } from '@/misc/cafy-id'; +import { Users } from '@/models/index'; + +export const meta = { + tags: ['users'], + + requireCredential: true as const, + + params: { + userId: { + validator: $.either($.type(ID), $.arr($.type(ID)).unique()), + } + }, + + res: { + oneOf: [ + { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id' + }, + isFollowing: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + hasPendingFollowRequestFromYou: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + hasPendingFollowRequestToYou: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isFollowed: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isBlocking: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isBlocked: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isMuted: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + } + } + }, + { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id' + }, + isFollowing: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + hasPendingFollowRequestFromYou: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + hasPendingFollowRequestToYou: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isFollowed: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isBlocking: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isBlocked: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, + isMuted: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + } + } + } + } + ] + } +}; + +export default define(meta, async (ps, me) => { + const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; + + const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id))); + + return Array.isArray(ps.userId) ? relations : relations[0]; +}); diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts new file mode 100644 index 0000000000..2c8672cd47 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -0,0 +1,90 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { publishAdminStream } from '@/services/stream'; +import { ApiError } from '../../error'; +import { getUser } from '../../common/getters'; +import { AbuseUserReports, Users } from '@/models/index'; +import { genId } from '@/misc/gen-id'; + +export const meta = { + tags: ['users'], + + requireCredential: true as const, + + params: { + userId: { + validator: $.type(ID), + }, + + comment: { + validator: $.str.range(1, 2048), + }, + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '1acefcb5-0959-43fd-9685-b48305736cb5' + }, + + cannotReportYourself: { + message: 'Cannot report yourself.', + code: 'CANNOT_REPORT_YOURSELF', + id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f' + }, + + cannotReportAdmin: { + message: 'Cannot report the admin.', + code: 'CANNOT_REPORT_THE_ADMIN', + id: '35e166f5-05fb-4f87-a2d5-adb42676d48f' + } + } +}; + +export default define(meta, async (ps, me) => { + // Lookup user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + if (user.id === me.id) { + throw new ApiError(meta.errors.cannotReportYourself); + } + + if (user.isAdmin) { + throw new ApiError(meta.errors.cannotReportAdmin); + } + + const report = await AbuseUserReports.save({ + id: genId(), + createdAt: new Date(), + targetUserId: user.id, + targetUserHost: user.host, + reporterId: me.id, + reporterHost: null, + comment: ps.comment, + }); + + // Publish event to moderators + setTimeout(async () => { + const moderators = await Users.find({ + where: [{ + isAdmin: true + }, { + isModerator: true + }] + }); + + for (const moderator of moderators) { + publishAdminStream(moderator.id, 'newAbuseUserReport', { + id: report.id, + targetUserId: report.targetUserId, + reporterId: report.reporterId, + comment: report.comment + }); + } + }, 1); +}); diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts new file mode 100644 index 0000000000..1ec5e1a743 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -0,0 +1,116 @@ +import $ from 'cafy'; +import define from '../../define'; +import { Followings, Users } from '@/models/index'; +import { Brackets } from 'typeorm'; +import { USER_ACTIVE_THRESHOLD } from '@/const'; +import { User } from '@/models/entities/user'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + username: { + validator: $.optional.nullable.str, + }, + + host: { + validator: $.optional.nullable.str, + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10, + }, + + detail: { + validator: $.optional.bool, + default: true, + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User', + } + }, +}; + +export default define(meta, async (ps, me) => { + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + if (ps.host) { + const q = Users.createQueryBuilder('user') + .where('user.isSuspended = FALSE') + .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); + + if (ps.username) { + q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }); + } + + q.andWhere('user.updatedAt IS NOT NULL'); + q.orderBy('user.updatedAt', 'DESC'); + + const users = await q.take(ps.limit!).getMany(); + + return await Users.packMany(users, me, { detail: ps.detail }); + } else if (ps.username) { + let users: User[] = []; + + if (me) { + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = Users.createQueryBuilder('user') + .where(`user.id IN (${ followingQuery.getQuery() })`) + .andWhere(`user.id != :meId`, { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })); + + query.setParameters(followingQuery.getParameters()); + + users = await query + .orderBy('user.usernameLower', 'ASC') + .take(ps.limit!) + .getMany(); + + if (users.length < ps.limit!) { + const otherQuery = await Users.createQueryBuilder('user') + .where(`user.id NOT IN (${ followingQuery.getQuery() })`) + .andWhere(`user.id != :meId`, { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL'); + + otherQuery.setParameters(followingQuery.getParameters()); + + const otherUsers = await otherQuery + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit! - users.length) + .getMany(); + + users = users.concat(otherUsers); + } + } else { + users = await Users.createQueryBuilder('user') + .where('user.isSuspended = FALSE') + .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL') + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit! - users.length) + .getMany(); + } + + return await Users.packMany(users, me, { detail: ps.detail }); + } +}); diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts new file mode 100644 index 0000000000..9aa988d9ed --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -0,0 +1,127 @@ +import $ from 'cafy'; +import define from '../../define'; +import { UserProfiles, Users } from '@/models/index'; +import { User } from '@/models/entities/user'; +import { Brackets } from 'typeorm'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + query: { + validator: $.str, + }, + + offset: { + validator: $.optional.num.min(0), + default: 0, + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10, + }, + + origin: { + validator: $.optional.str.or(['local', 'remote', 'combined']), + default: 'combined', + }, + + detail: { + validator: $.optional.bool, + default: true, + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User', + } + }, +}; + +export default define(meta, async (ps, me) => { + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + const isUsername = ps.query.startsWith('@'); + + let users: User[] = []; + + if (isUsername) { + const usernameQuery = Users.createQueryBuilder('user') + .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + usernameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + usernameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await usernameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit!) + .skip(ps.offset) + .getMany(); + } else { + const nameQuery = Users.createQueryBuilder('user') + .where('user.name ILIKE :query', { query: '%' + ps.query + '%' }) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + nameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + nameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await nameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit!) + .skip(ps.offset) + .getMany(); + + if (users.length < ps.limit!) { + const profQuery = UserProfiles.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' }); + + if (ps.origin === 'local') { + profQuery.andWhere('prof.userHost IS NULL'); + } else if (ps.origin === 'remote') { + profQuery.andWhere('prof.userHost IS NOT NULL'); + } + + const query = Users.createQueryBuilder('user') + .where(`user.id IN (${ profQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE') + .setParameters(profQuery.getParameters()); + + users = users.concat(await query + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .take(ps.limit!) + .skip(ps.offset) + .getMany() + ); + } + } + + return await Users.packMany(users, me, { detail: ps.detail }); +}); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts new file mode 100644 index 0000000000..f056983636 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -0,0 +1,105 @@ +import $ from 'cafy'; +import { resolveUser } from '@/remote/resolve-user'; +import define from '../../define'; +import { apiLogger } from '../../logger'; +import { ApiError } from '../../error'; +import { ID } from '@/misc/cafy-id'; +import { Users } from '@/models/index'; +import { In } from 'typeorm'; +import { User } from '@/models/entities/user'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + userId: { + validator: $.optional.type(ID), + }, + + userIds: { + validator: $.optional.arr($.type(ID)).unique(), + }, + + username: { + validator: $.optional.str + }, + + host: { + validator: $.optional.nullable.str + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User', + }, + + errors: { + failedToResolveRemoteUser: { + message: 'Failed to resolve remote user.', + code: 'FAILED_TO_RESOLVE_REMOTE_USER', + id: 'ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c', + kind: 'server' as const + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '4362f8dc-731f-4ad8-a694-be5a88922a24' + }, + } +}; + +export default define(meta, async (ps, me) => { + let user; + + const isAdminOrModerator = me && (me.isAdmin || me.isModerator); + + if (ps.userIds) { + if (ps.userIds.length === 0) { + return []; + } + + const users = await Users.find(isAdminOrModerator ? { + id: In(ps.userIds) + } : { + id: In(ps.userIds), + isSuspended: false + }); + + // リクエストされた通りに並べ替え + const _users: User[] = []; + for (const id of ps.userIds) { + _users.push(users.find(x => x.id === id)!); + } + + return await Promise.all(_users.map(u => Users.pack(u, me, { + detail: true + }))); + } else { + // Lookup user + if (typeof ps.host === 'string' && typeof ps.username === 'string') { + user = await resolveUser(ps.username, ps.host).catch(e => { + apiLogger.warn(`failed to resolve remote user: ${e}`); + throw new ApiError(meta.errors.failedToResolveRemoteUser); + }); + } else { + const q: any = ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: null }; + + user = await Users.findOne(q); + } + + if (user == null || (!isAdminOrModerator && user.isSuspended)) { + throw new ApiError(meta.errors.noSuchUser); + } + + return await Users.pack(user, me, { + detail: true + }); + } +}); diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts new file mode 100644 index 0000000000..ef8afd5625 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -0,0 +1,144 @@ +import $ from 'cafy'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { ID } from '@/misc/cafy-id'; +import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '@/models/index'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + userId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '9e638e45-3b25-4ef7-8f95-07e8498f1819' + }, + } +}; + +export default define(meta, async (ps, me) => { + const user = await Users.findOne(ps.userId); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const [ + notesCount, + repliesCount, + renotesCount, + repliedCount, + renotedCount, + pollVotesCount, + pollVotedCount, + localFollowingCount, + remoteFollowingCount, + localFollowersCount, + remoteFollowersCount, + sentReactionsCount, + receivedReactionsCount, + noteFavoritesCount, + pageLikesCount, + pageLikedCount, + driveFilesCount, + driveUsage, + reversiCount, + ] = await Promise.all([ + Notes.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + Notes.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.replyId IS NOT NULL') + .getCount(), + Notes.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.renoteId IS NOT NULL') + .getCount(), + Notes.createQueryBuilder('note') + .where('note.replyUserId = :userId', { userId: user.id }) + .getCount(), + Notes.createQueryBuilder('note') + .where('note.renoteUserId = :userId', { userId: user.id }) + .getCount(), + PollVotes.createQueryBuilder('vote') + .where('vote.userId = :userId', { userId: user.id }) + .getCount(), + PollVotes.createQueryBuilder('vote') + .innerJoin('vote.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + Followings.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NULL') + .getCount(), + Followings.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NOT NULL') + .getCount(), + Followings.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NULL') + .getCount(), + Followings.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NOT NULL') + .getCount(), + NoteReactions.createQueryBuilder('reaction') + .where('reaction.userId = :userId', { userId: user.id }) + .getCount(), + NoteReactions.createQueryBuilder('reaction') + .innerJoin('reaction.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + NoteFavorites.createQueryBuilder('favorite') + .where('favorite.userId = :userId', { userId: user.id }) + .getCount(), + PageLikes.createQueryBuilder('like') + .where('like.userId = :userId', { userId: user.id }) + .getCount(), + PageLikes.createQueryBuilder('like') + .innerJoin('like.page', 'page') + .where('page.userId = :userId', { userId: user.id }) + .getCount(), + DriveFiles.createQueryBuilder('file') + .where('file.userId = :userId', { userId: user.id }) + .getCount(), + DriveFiles.calcDriveUsageOf(user), + ReversiGames.createQueryBuilder('game') + .where('game.user1Id = :userId', { userId: user.id }) + .orWhere('game.user2Id = :userId', { userId: user.id }) + .getCount(), + ]); + + return { + notesCount, + repliesCount, + renotesCount, + repliedCount, + renotedCount, + pollVotesCount, + pollVotedCount, + localFollowingCount, + remoteFollowingCount, + localFollowersCount, + remoteFollowersCount, + followingCount: localFollowingCount + remoteFollowingCount, + followersCount: localFollowersCount + remoteFollowersCount, + sentReactionsCount, + receivedReactionsCount, + noteFavoritesCount, + pageLikesCount, + pageLikedCount, + driveFilesCount, + driveUsage, + reversiCount, + }; +}); -- cgit v1.2.3-freya From 9b876b30b27d6a02129447f6584d95a497d94c32 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 12 Nov 2021 19:47:04 +0900 Subject: update ms to 3.0.0 --- packages/backend/@types/ms.d.ts | 12 ------------ packages/backend/package.json | 2 +- packages/backend/src/server/api/endpoints/ap/get.ts | 2 +- packages/backend/src/server/api/endpoints/ap/show.ts | 2 +- packages/backend/src/server/api/endpoints/blocking/create.ts | 2 +- packages/backend/src/server/api/endpoints/blocking/delete.ts | 2 +- .../backend/src/server/api/endpoints/drive/files/create.ts | 2 +- .../src/server/api/endpoints/drive/files/upload-from-url.ts | 2 +- .../backend/src/server/api/endpoints/following/create.ts | 2 +- .../backend/src/server/api/endpoints/following/delete.ts | 2 +- .../backend/src/server/api/endpoints/gallery/posts/create.ts | 2 +- .../backend/src/server/api/endpoints/gallery/posts/update.ts | 2 +- .../backend/src/server/api/endpoints/i/export-blocking.ts | 2 +- .../backend/src/server/api/endpoints/i/export-following.ts | 2 +- packages/backend/src/server/api/endpoints/i/export-mute.ts | 2 +- packages/backend/src/server/api/endpoints/i/export-notes.ts | 2 +- .../backend/src/server/api/endpoints/i/export-user-lists.ts | 2 +- .../backend/src/server/api/endpoints/i/import-blocking.ts | 2 +- .../backend/src/server/api/endpoints/i/import-following.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-muting.ts | 2 +- .../backend/src/server/api/endpoints/i/import-user-lists.ts | 2 +- packages/backend/src/server/api/endpoints/i/update-email.ts | 2 +- .../src/server/api/endpoints/messaging/messages/delete.ts | 2 +- packages/backend/src/server/api/endpoints/notes/create.ts | 2 +- packages/backend/src/server/api/endpoints/notes/delete.ts | 2 +- .../src/server/api/endpoints/notes/reactions/delete.ts | 2 +- packages/backend/src/server/api/endpoints/notes/unrenote.ts | 2 +- packages/backend/src/server/api/endpoints/pages/create.ts | 2 +- packages/backend/src/server/api/endpoints/pages/update.ts | 2 +- .../src/server/api/endpoints/request-reset-password.ts | 2 +- .../backend/src/server/api/endpoints/users/recommendation.ts | 2 +- packages/backend/src/server/web/index.ts | 2 +- packages/backend/yarn.lock | 5 +++++ 33 files changed, 36 insertions(+), 43 deletions(-) delete mode 100644 packages/backend/@types/ms.d.ts (limited to 'packages/backend/src/server/api/endpoints/users') diff --git a/packages/backend/@types/ms.d.ts b/packages/backend/@types/ms.d.ts deleted file mode 100644 index 2f0156d104..0000000000 --- a/packages/backend/@types/ms.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare module 'ms' { - interface IMSOptions { - long: boolean; - } - - function ms(value: string): number; - function ms(value: number, options?: IMSOptions): string; - - namespace ms {} // Hack - - export = ms; -} diff --git a/packages/backend/package.json b/packages/backend/package.json index 4376dc4887..b1337489fa 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -135,7 +135,7 @@ "mfm-js": "0.20.0", "misskey-js": "0.0.8", "mocha": "8.4.0", - "ms": "2.1.3", + "ms": "3.0.0-canary.1", "multer": "1.4.3", "nested-property": "4.0.0", "node-fetch": "2.6.1", diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index 78919f43b0..2f97a24774 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import define from '../../define'; import Resolver from '@/remote/activitypub/resolver'; import { ApiError } from '../../error'; -import * as ms from 'ms'; +import ms from 'ms'; export const meta = { tags: ['federation'], diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 2280d93724..32685d44bd 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -11,7 +11,7 @@ import { Note } from '@/models/entities/note'; import { User } from '@/models/entities/user'; import { fetchMeta } from '@/misc/fetch-meta'; import { isActor, isPost, getApId } from '@/remote/activitypub/type'; -import * as ms from 'ms'; +import ms from 'ms'; export const meta = { tags: ['federation'], diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 2953252394..4d33c09479 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; +import ms from 'ms'; import create from '@/services/blocking/create'; import define from '../../define'; import { ApiError } from '../../error'; diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index a66e46fdf0..ae5dab020a 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; +import ms from 'ms'; import deleteBlocking from '@/services/blocking/delete'; import define from '../../define'; import { ApiError } from '../../error'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 2abc104e6c..89755d9498 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -1,4 +1,4 @@ -import * as ms from 'ms'; +import ms from 'ms'; import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import create from '@/services/drive/add-file'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index 9f10a42d24..adb5126fbe 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; +import ms from 'ms'; import uploadFromUrl from '@/services/drive/upload-from-url'; import define from '../../../define'; import { DriveFiles } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index ba9ca1092d..92e86bf6b2 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; +import ms from 'ms'; import create from '@/services/following/create'; import define from '../../define'; import { ApiError } from '../../error'; diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 0b0158b86e..030d30c9b5 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; -import * as ms from 'ms'; +import ms from 'ms'; import deleteFollowing from '@/services/following/delete'; import define from '../../define'; import { ApiError } from '../../error'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 38b487e6ea..34af72695d 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import * as ms from 'ms'; +import ms from 'ms'; import define from '../../../define'; import { ID } from '../../../../../misc/cafy-id'; import { DriveFiles, GalleryPosts } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 54eea130d3..f94606acf2 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import * as ms from 'ms'; +import ms from 'ms'; import define from '../../../define'; import { ID } from '../../../../../misc/cafy-id'; import { DriveFiles, GalleryPosts } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts index e4797da0c1..e276ecf384 100644 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts @@ -1,6 +1,6 @@ import define from '../../define'; import { createExportBlockingJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; export const meta = { secure: true, diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts index b0f154cda8..a351161111 100644 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ b/packages/backend/src/server/api/endpoints/i/export-following.ts @@ -1,6 +1,6 @@ import define from '../../define'; import { createExportFollowingJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; export const meta = { secure: true, diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts index 46d547fa53..b176c7ee8d 100644 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts @@ -1,6 +1,6 @@ import define from '../../define'; import { createExportMuteJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; export const meta = { secure: true, diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts index 441bf16896..8cba04552e 100644 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts @@ -1,6 +1,6 @@ import define from '../../define'; import { createExportNotesJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; export const meta = { secure: true, diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts index 24043a862a..44d43c0bea 100644 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts @@ -1,6 +1,6 @@ import define from '../../define'; import { createExportUserListsJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; export const meta = { secure: true, diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index d44d0b6077..4822bd5868 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; import { createImportBlockingJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; import { ApiError } from '../../error'; import { DriveFiles } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index b3de397661..19aa2fa933 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; import { createImportFollowingJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; import { ApiError } from '../../error'; import { DriveFiles } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index c17434c587..c474dd7186 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; import { createImportMutingJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; import { ApiError } from '../../error'; import { DriveFiles } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index 9069a019a9..ceccdd1852 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; import { createImportUserListsJob } from '@/queue/index'; -import * as ms from 'ms'; +import ms from 'ms'; import { ApiError } from '../../error'; import { DriveFiles } from '@/models/index'; 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 9b6fb9c410..ff5a9f292c 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -3,7 +3,7 @@ import { publishMainStream } from '@/services/stream'; import define from '../../define'; import rndstr from 'rndstr'; import config from '@/config/index'; -import * as ms from 'ms'; +import ms from 'ms'; import * as bcrypt from 'bcryptjs'; import { Users, UserProfiles } from '@/models/index'; import { sendEmail } from '@/services/send-email'; diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts index bd4890fc8a..25bf676383 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../../define'; -import * as ms from 'ms'; +import ms from 'ms'; import { ApiError } from '../../../error'; import { MessagingMessages } from '@/models/index'; import { deleteMessage } from '@/services/messages/delete'; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 8257ea1d0f..e9584e7b8b 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import * as ms from 'ms'; +import ms from 'ms'; import { length } from 'stringz'; import create from '@/services/note/create'; import define from '../../define'; diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 7163a2b9d2..7f0d59669e 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import deleteNote from '@/services/note/delete'; import define from '../../define'; -import * as ms from 'ms'; +import ms from 'ms'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; import { Users } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index 69550f96de..ea851458d2 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../../define'; -import * as ms from 'ms'; +import ms from 'ms'; import deleteReaction from '@/services/note/reaction/delete'; import { getNote } from '../../../common/getters'; import { ApiError } from '../../../error'; diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index dce43d9d9c..d3fba66095 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import deleteNote from '@/services/note/delete'; import define from '../../define'; -import * as ms from 'ms'; +import ms from 'ms'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; import { Notes, Users } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index c23978f093..0ec287c592 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import * as ms from 'ms'; +import ms from 'ms'; import define from '../../define'; import { ID } from '@/misc/cafy-id'; import { Pages, DriveFiles } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index b3a7f26963..4aaf2aed5d 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import * as ms from 'ms'; +import ms from 'ms'; import define from '../../define'; import { ApiError } from '../../error'; import { Pages, DriveFiles } from '@/models/index'; diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index f9928c2ee6..7bb50bf6a6 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -3,7 +3,7 @@ import { publishMainStream } from '@/services/stream'; import define from '../define'; import rndstr from 'rndstr'; import config from '@/config/index'; -import * as ms from 'ms'; +import ms from 'ms'; import { Users, UserProfiles, PasswordResetRequests } from '@/models/index'; import { sendEmail } from '@/services/send-email'; import { ApiError } from '../error'; diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index dde6bb1037..7c775c4dcf 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,4 +1,4 @@ -import * as ms from 'ms'; +import ms from 'ms'; import $ from 'cafy'; import define from '../../define'; import { Users, Followings } from '@/models/index'; diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index fc95a36a87..d80d73f252 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -3,7 +3,7 @@ */ import { dirname } from 'path'; -import * as ms from 'ms'; +import ms from 'ms'; import * as Koa from 'koa'; import * as Router from '@koa/router'; import * as send from 'koa-send'; diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 0346380ee1..311530db66 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -5421,6 +5421,11 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +ms@3.0.0-canary.1: + version "3.0.0-canary.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-3.0.0-canary.1.tgz#c7b34fbce381492fd0b345d1cf56e14d67b77b80" + integrity sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g== + multer@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b" -- cgit v1.2.3-freya From 9ea7d75aa477f6cd1cc6d78a8efd4dbded479dbf Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 12 Nov 2021 21:11:15 +0900 Subject: feat: 通報があったときに管理者へEメールで通知されるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #7025 --- CHANGELOG.md | 1 + packages/backend/package.json | 2 + .../src/server/api/endpoints/users/report-abuse.ts | 29 +++++++---- packages/backend/yarn.lock | 60 +++++++++++++++++++++- 4 files changed, 82 insertions(+), 10 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/users') diff --git a/CHANGELOG.md b/CHANGELOG.md index e5fd4e88a7..49d411cd49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - インスタンスプロフィールレンダリング ready - 通知のリアクションアイコンをホバーで拡大できるように - 返信の際にメンションを含めるように +- 通報があったときに管理者へEメールで通知されるように - メールアドレスのバリデーションを強化 ### Bugfixes diff --git a/packages/backend/package.json b/packages/backend/package.json index b1337489fa..6c41245e66 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -59,6 +59,7 @@ "@types/redis": "2.8.32", "@types/rename": "1.0.4", "@types/request-stats": "3.0.0", + "@types/sanitize-html": "2.5.0", "@types/seedrandom": "2.4.28", "@types/sharp": "0.29.3", "@types/sinonjs__fake-timers": "6.0.4", @@ -163,6 +164,7 @@ "require-all": "3.0.0", "rndstr": "1.0.0", "s-age": "1.1.2", + "sanitize-html": "2.5.3", "seedrandom": "3.0.5", "sharp": "0.29.2", "speakeasy": "2.0.0", diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 2c8672cd47..a1d8376651 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,4 +1,5 @@ import $ from 'cafy'; +import * as sanitizeHtml from 'sanitize-html'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; import { publishAdminStream } from '@/services/stream'; @@ -6,6 +7,8 @@ import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; import { AbuseUserReports, Users } from '@/models/index'; import { genId } from '@/misc/gen-id'; +import { sendEmail } from '@/services/send-email'; +import { fetchMeta } from '@/misc/fetch-meta'; export const meta = { tags: ['users'], @@ -26,23 +29,24 @@ export const meta = { noSuchUser: { message: 'No such user.', code: 'NO_SUCH_USER', - id: '1acefcb5-0959-43fd-9685-b48305736cb5' + id: '1acefcb5-0959-43fd-9685-b48305736cb5', }, cannotReportYourself: { message: 'Cannot report yourself.', code: 'CANNOT_REPORT_YOURSELF', - id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f' + id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f', }, cannotReportAdmin: { message: 'Cannot report the admin.', code: 'CANNOT_REPORT_THE_ADMIN', - id: '35e166f5-05fb-4f87-a2d5-adb42676d48f' - } - } + id: '35e166f5-05fb-4f87-a2d5-adb42676d48f', + }, + }, }; +// eslint-disable-next-line import/no-default-export export default define(meta, async (ps, me) => { // Lookup user const user = await getUser(ps.userId).catch(e => { @@ -72,10 +76,10 @@ export default define(meta, async (ps, me) => { setTimeout(async () => { const moderators = await Users.find({ where: [{ - isAdmin: true + isAdmin: true, }, { - isModerator: true - }] + isModerator: true, + }], }); for (const moderator of moderators) { @@ -83,8 +87,15 @@ export default define(meta, async (ps, me) => { id: report.id, targetUserId: report.targetUserId, reporterId: report.reporterId, - comment: report.comment + comment: report.comment, }); } + + const meta = await fetchMeta(); + if (meta.email) { + sendEmail(meta.email, 'New abuse report', + sanitizeHtml(ps.comment), + sanitizeHtml(ps.comment)); + } }, 1); }); diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 311530db66..aa8794f7b7 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -769,6 +769,13 @@ resolved "https://registry.yarnpkg.com/@types/rsvp/-/rsvp-4.0.4.tgz#55e93e7054027f1ad4b4ebc1e60e59eb091e2d32" integrity sha512-J3Ol++HCC7/hwZhanDvggFYU/GtxHxE/e7cGRWxR04BF7Tt3TqJZ84BkzQgDxmX0uu8IagiyfmfoUlBACh2Ilg== +"@types/sanitize-html@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.5.0.tgz#bfef58fbcf2674b20ffcc23c3506faa68c3a13e3" + integrity sha512-PeFIEZsO9m1+ACJlXUaimgrR+5DEDiIXhz7Hso307jmq5Yz0lb5kDp8LiTr5dMMMliC/jNNx/qds7Zoxa4zexw== + dependencies: + htmlparser2 "^6.0.0" + "@types/seedrandom@2.4.28": version "2.4.28" resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" @@ -2631,6 +2638,11 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + defer-to-connect@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" @@ -2831,7 +2843,7 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -domutils@^2.6.0: +domutils@^2.5.2, domutils@^2.6.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== @@ -3889,6 +3901,16 @@ htmlparser2@^3.9.1: inherits "^2.0.1" readable-stream "^3.1.1" +htmlparser2@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-assert@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" @@ -4358,6 +4380,11 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -5471,6 +5498,11 @@ nanoid@^3.1.23: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== +nanoid@^3.1.30: + version "3.1.30" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" + integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== + napi-build-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" @@ -5940,6 +5972,11 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + parse5-htmlparser2-tree-adapter@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" @@ -6371,6 +6408,15 @@ postcss@^8.2.15: nanoid "^3.1.23" source-map-js "^0.6.2" +postcss@^8.3.11: + version "8.3.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858" + integrity sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^0.6.2" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -7049,6 +7095,18 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-html@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.5.3.tgz#91aa3dc760b072cdf92f9c6973747569b1ba1cd8" + integrity sha512-DGATXd1fs/Rm287/i5FBKVYSBBUL0iAaztOA1/RFhEs4yqo39/X52i/q/CwsfCUG5cilmXSBmnQmyWfnKhBlOg== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^6.0.0" + is-plain-object "^5.0.0" + parse-srcset "^1.0.2" + postcss "^8.3.11" + sax@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" -- cgit v1.2.3-freya