From e103904a0454d8a05cea60d984bc1ef1e2b9e652 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 19 May 2019 20:41:23 +0900 Subject: Resolve #4941 --- .../app/common/views/components/messaging.vue | 5 +- .../app/common/views/pages/user-group-editor.vue | 12 ++- src/client/app/common/views/pages/user-groups.vue | 92 ++++++++++++++---- src/db/postgre.ts | 2 + src/models/entities/user-group-invite.ts | 42 +++++++++ src/models/entities/user-group-joining.ts | 1 + src/models/index.ts | 2 + src/models/repositories/user-group-invite.ts | 24 +++++ src/server/api/endpoints/i/user-group-invites.ts | 45 +++++++++ .../endpoints/users/groups/invitations/accept.ts | 63 +++++++++++++ .../endpoints/users/groups/invitations/reject.ts | 53 +++++++++++ src/server/api/endpoints/users/groups/invite.ts | 104 +++++++++++++++++++++ src/server/api/endpoints/users/groups/joined.ts | 6 ++ src/server/api/endpoints/users/groups/push.ts | 90 ------------------ 14 files changed, 427 insertions(+), 114 deletions(-) create mode 100644 src/models/entities/user-group-invite.ts create mode 100644 src/models/repositories/user-group-invite.ts create mode 100644 src/server/api/endpoints/i/user-group-invites.ts create mode 100644 src/server/api/endpoints/users/groups/invitations/accept.ts create mode 100644 src/server/api/endpoints/users/groups/invitations/reject.ts create mode 100644 src/server/api/endpoints/users/groups/invite.ts delete mode 100644 src/server/api/endpoints/users/groups/push.ts (limited to 'src') diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index 01d7a5a798..c7c70a4ce5 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -217,12 +217,13 @@ export default Vue.extend({ this.navigate(user); }, async startGroup() { - const groups = await this.$root.api('users/groups/joined'); + const groups1 = await this.$root.api('users/groups/owned'); + const groups2 = await this.$root.api('users/groups/joined'); const { canceled, result: group } = await this.$root.dialog({ type: null, title: this.$t('select-group'), select: { - items: groups.map(group => ({ + items: groups1.concat(groups2).map(group => ({ value: group, text: group.name })) }, diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue index fb9f1a6772..ef79689ae8 100644 --- a/src/client/app/common/views/pages/user-group-editor.vue +++ b/src/client/app/common/views/pages/user-group-editor.vue @@ -16,7 +16,7 @@
- {{ $t('add-user') }} + {{ $t('invite') }}
@@ -134,18 +134,22 @@ export default Vue.extend({ }); }, - async add() { + async invite() { + const t = this.$t('invited'); const { result: user } = await this.$root.dialog({ user: { local: true } }); if (user == null) return; - this.$root.api('users/groups/push', { + this.$root.api('users/groups/invite', { groupId: this.group.id, userId: user.id }).then(() => { - this.fetchUsers(); + this.$root.dialog({ + type: 'success', + text: t + }); }); } } diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue index 336772799b..2a89196e55 100644 --- a/src/client/app/common/views/pages/user-groups.vue +++ b/src/client/app/common/views/pages/user-groups.vue @@ -1,36 +1,70 @@ diff --git a/src/db/postgre.ts b/src/db/postgre.ts index 40b9ce151b..fcec68602f 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -26,6 +26,7 @@ import { UserList } from '../models/entities/user-list'; import { UserListJoining } from '../models/entities/user-list-joining'; import { UserGroup } from '../models/entities/user-group'; import { UserGroupJoining } from '../models/entities/user-group-joining'; +import { UserGroupInvite } from '../models/entities/user-group-invite'; import { Hashtag } from '../models/entities/hashtag'; import { NoteFavorite } from '../models/entities/note-favorite'; import { AbuseUserReport } from '../models/entities/abuse-user-report'; @@ -110,6 +111,7 @@ export function initDb(justBorrow = false, sync = false, log = false) { UserListJoining, UserGroup, UserGroupJoining, + UserGroupInvite, UserNotePining, Following, FollowRequest, diff --git a/src/models/entities/user-group-invite.ts b/src/models/entities/user-group-invite.ts new file mode 100644 index 0000000000..2adf2c024e --- /dev/null +++ b/src/models/entities/user-group-invite.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { UserGroup } from './user-group'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'userGroupId'], { unique: true }) +export class UserGroupInvite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroupInvite.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The group ID.' + }) + public userGroupId: UserGroup['id']; + + @ManyToOne(type => UserGroup, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public userGroup: UserGroup | null; +} diff --git a/src/models/entities/user-group-joining.ts b/src/models/entities/user-group-joining.ts index 17b534f42f..e09c3230f1 100644 --- a/src/models/entities/user-group-joining.ts +++ b/src/models/entities/user-group-joining.ts @@ -4,6 +4,7 @@ import { UserGroup } from './user-group'; import { id } from '../id'; @Entity() +@Index(['userId', 'userGroupId'], { unique: true }) export class UserGroupJoining { @PrimaryColumn(id()) public id: string; diff --git a/src/models/index.ts b/src/models/index.ts index c05d7febe5..a60cd10ef9 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -22,6 +22,7 @@ import { UserListRepository } from './repositories/user-list'; import { UserListJoining } from './entities/user-list-joining'; import { UserGroupRepository } from './repositories/user-group'; import { UserGroupJoining } from './entities/user-group-joining'; +import { UserGroupInviteRepository } from './repositories/user-group-invite'; import { FollowRequestRepository } from './repositories/follow-request'; import { MutingRepository } from './repositories/muting'; import { BlockingRepository } from './repositories/blocking'; @@ -56,6 +57,7 @@ export const UserLists = getCustomRepository(UserListRepository); export const UserListJoinings = getRepository(UserListJoining); export const UserGroups = getCustomRepository(UserGroupRepository); export const UserGroupJoinings = getRepository(UserGroupJoining); +export const UserGroupInvites = getCustomRepository(UserGroupInviteRepository); export const UserNotePinings = getRepository(UserNotePining); export const Followings = getCustomRepository(FollowingRepository); export const FollowRequests = getCustomRepository(FollowRequestRepository); diff --git a/src/models/repositories/user-group-invite.ts b/src/models/repositories/user-group-invite.ts new file mode 100644 index 0000000000..1d4c2aa15f --- /dev/null +++ b/src/models/repositories/user-group-invite.ts @@ -0,0 +1,24 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { UserGroupInvite } from '../entities/user-group-invite'; +import { UserGroups } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(UserGroupInvite) +export class UserGroupInviteRepository extends Repository { + public async pack( + src: UserGroupInvite['id'] | UserGroupInvite, + ) { + const invite = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: invite.id, + group: await UserGroups.pack(invite.userGroup || invite.userGroupId), + }; + } + + public packMany( + invites: any[], + ) { + return Promise.all(invites.map(x => this.pack(x))); + } +} diff --git a/src/server/api/endpoints/i/user-group-invites.ts b/src/server/api/endpoints/i/user-group-invites.ts new file mode 100644 index 0000000000..9d07fa31a4 --- /dev/null +++ b/src/server/api/endpoints/i/user-group-invites.ts @@ -0,0 +1,45 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { UserGroupInvites } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + desc: { + 'ja-JP': 'グループへの招待一覧を取得します。', + 'en-US': 'Get user group invitations.' + }, + + tags: ['account', 'groups'], + + requireCredential: true, + + kind: 'read:user-groups', + + params: { + 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(UserGroupInvites.createQueryBuilder('invite'), ps.sinceId, ps.untilId) + .andWhere(`invite.userId = :meId`, { meId: user.id }) + .leftJoinAndSelect('invite.userGroup', 'user_group'); + + const invites = await query + .take(ps.limit!) + .getMany(); + + return await UserGroupInvites.packMany(invites); +}); diff --git a/src/server/api/endpoints/users/groups/invitations/accept.ts b/src/server/api/endpoints/users/groups/invitations/accept.ts new file mode 100644 index 0000000000..33779dd349 --- /dev/null +++ b/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -0,0 +1,63 @@ +import $ from 'cafy'; +import { ID } from '../../../../../../misc/cafy-id'; +import define from '../../../../define'; +import { ApiError } from '../../../../error'; +import { UserGroupJoinings, UserGroupInvites } from '../../../../../../models'; +import { genId } from '../../../../../../misc/gen-id'; +import { UserGroupJoining } from '../../../../../../models/entities/user-group-joining'; + +export const meta = { + desc: { + 'ja-JP': 'ユーザーグループへの招待を承認します。', + 'en-US': 'Accept invite of a user group.' + }, + + tags: ['groups', 'users'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + inviteId: { + validator: $.type(ID), + desc: { + 'ja-JP': '招待ID', + 'en-US': 'The invite 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 invite = await UserGroupInvites.findOne({ + id: ps.inviteId, + }); + + if (invite == null) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + if (invite.userId !== user.id) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + // Push the user + await UserGroupJoinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: invite.userGroupId + } as UserGroupJoining); + + UserGroupInvites.delete(invite.id); +}); diff --git a/src/server/api/endpoints/users/groups/invitations/reject.ts b/src/server/api/endpoints/users/groups/invitations/reject.ts new file mode 100644 index 0000000000..e9e7bc8b48 --- /dev/null +++ b/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -0,0 +1,53 @@ +import $ from 'cafy'; +import { ID } from '../../../../../../misc/cafy-id'; +import define from '../../../../define'; +import { ApiError } from '../../../../error'; +import { UserGroupInvites } from '../../../../../../models'; + +export const meta = { + desc: { + 'ja-JP': 'ユーザーグループへの招待を拒否します。', + 'en-US': 'Reject invite of a user group.' + }, + + tags: ['groups', 'users'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + inviteId: { + validator: $.type(ID), + desc: { + 'ja-JP': '招待ID', + 'en-US': 'The invite 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 invite = await UserGroupInvites.findOne({ + id: ps.inviteId, + }); + + if (invite == null) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + if (invite.userId !== user.id) { + throw new ApiError(meta.errors.noSuchInvitation); + } + + await UserGroupInvites.delete(invite.id); +}); diff --git a/src/server/api/endpoints/users/groups/invite.ts b/src/server/api/endpoints/users/groups/invite.ts new file mode 100644 index 0000000000..503184a92f --- /dev/null +++ b/src/server/api/endpoints/users/groups/invite.ts @@ -0,0 +1,104 @@ +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, UserGroupInvites } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; +import { UserGroupInvite } from '../../../../../models/entities/user-group-invite'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループに指定したユーザーを招待します。', + 'en-US': 'Invite a user to a user group.' + }, + + tags: ['groups', 'users'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user 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 invite = await UserGroupInvites.findOne({ + userGroupId: userGroup.id, + userId: user.id + }); + + if (invite) { + throw new ApiError(meta.errors.alreadyInvited); + } + + await UserGroupInvites.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: userGroup.id + } as UserGroupInvite); +}); diff --git a/src/server/api/endpoints/users/groups/joined.ts b/src/server/api/endpoints/users/groups/joined.ts index 14561fce05..853a2510de 100644 --- a/src/server/api/endpoints/users/groups/joined.ts +++ b/src/server/api/endpoints/users/groups/joined.ts @@ -1,6 +1,7 @@ import define from '../../../define'; import { UserGroups, UserGroupJoinings } from '../../../../../models'; import { types, bool } from '../../../../../misc/schema'; +import { Not, In } from 'typeorm'; export const meta = { desc: { @@ -25,8 +26,13 @@ export const meta = { }; export default define(meta, async (ps, me) => { + const ownedGroups = await UserGroups.find({ + userId: me.id, + }); + const joinings = await UserGroupJoinings.find({ userId: me.id, + userGroupId: Not(In(ownedGroups.map(x => x.id))) }); return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId))); diff --git a/src/server/api/endpoints/users/groups/push.ts b/src/server/api/endpoints/users/groups/push.ts deleted file mode 100644 index 5371580db0..0000000000 --- a/src/server/api/endpoints/users/groups/push.ts +++ /dev/null @@ -1,90 +0,0 @@ -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'; -import { genId } from '../../../../../misc/gen-id'; -import { UserGroupJoining } from '../../../../../models/entities/user-group-joining'; - -export const meta = { - desc: { - 'ja-JP': '指定したユーザーグループに指定したユーザーを追加します。', - 'en-US': 'Add a user to a user group.' - }, - - tags: ['groups', 'users'], - - requireCredential: true, - - kind: 'write:user-groups', - - params: { - groupId: { - validator: $.type(ID), - }, - - userId: { - validator: $.type(ID), - desc: { - 'ja-JP': '対象のユーザーのID', - 'en-US': 'Target user 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' - } - } -}; - -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 exist = await UserGroupJoinings.findOne({ - userGroupId: userGroup.id, - userId: user.id - }); - - if (exist) { - throw new ApiError(meta.errors.alreadyAdded); - } - - // Push the user - await UserGroupJoinings.save({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: userGroup.id - } as UserGroupJoining); -}); -- cgit v1.2.3-freya