diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2019-05-18 20:36:33 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2019-05-18 20:36:33 +0900 |
| commit | c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b (patch) | |
| tree | c2e1671787c00daa8963c879dba6fbdab6f02d66 /src/server/api | |
| parent | Fix bug (diff) | |
| download | sharkey-c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b.tar.gz sharkey-c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b.tar.bz2 sharkey-c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b.zip | |
ユーザーグループ
Resolve #3218
Diffstat (limited to 'src/server/api')
| -rw-r--r-- | src/server/api/common/read-messaging-message.ts | 80 | ||||
| -rw-r--r-- | src/server/api/endpoints/messaging/history.ts | 51 | ||||
| -rw-r--r-- | src/server/api/endpoints/messaging/messages.ts | 105 | ||||
| -rw-r--r-- | src/server/api/endpoints/messaging/messages/create.ts | 125 | ||||
| -rw-r--r-- | src/server/api/endpoints/messaging/messages/delete.ts | 12 | ||||
| -rw-r--r-- | src/server/api/endpoints/messaging/messages/read.ts | 17 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/create.ts | 51 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/delete.ts | 49 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/joined.ts | 33 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/owned.ts | 33 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/pull.ts | 68 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/push.ts | 90 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/show.ts | 53 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/lists/push.ts | 2 | ||||
| -rw-r--r-- | src/server/api/kinds.ts | 2 | ||||
| -rw-r--r-- | src/server/api/openapi/schemas.ts | 2 | ||||
| -rw-r--r-- | src/server/api/stream/channels/messaging.ts | 31 |
17 files changed, 721 insertions, 83 deletions
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 2cb5a1f87f..544d890197 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,21 +1,33 @@ -import { publishMainStream } from '../../../services/stream'; +import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream'; import { publishMessagingStream } from '../../../services/stream'; import { publishMessagingIndexStream } from '../../../services/stream'; import { User } from '../../../models/entities/user'; import { MessagingMessage } from '../../../models/entities/messaging-message'; -import { MessagingMessages } from '../../../models'; +import { MessagingMessages, UserGroupJoinings, Users } from '../../../models'; import { In } from 'typeorm'; +import { IdentifiableError } from '../../../misc/identifiable-error'; +import { UserGroup } from '../../../models/entities/user-group'; /** * Mark messages as read */ -export default async ( +export async function readUserMessagingMessage( userId: User['id'], otherpartyId: User['id'], messageIds: MessagingMessage['id'][] -) => { +) { if (messageIds.length === 0) return; + const messages = await MessagingMessages.find({ + id: In(messageIds) + }); + + for (const message of messages) { + if (message.recipientId !== userId) { + throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).'); + } + } + // Update documents await MessagingMessages.update({ id: In(messageIds), @@ -30,14 +42,62 @@ export default async ( publishMessagingStream(otherpartyId, userId, 'read', messageIds); publishMessagingIndexStream(userId, 'read', messageIds); - // Calc count of my unread messages - const count = await MessagingMessages.count({ - recipientId: userId, - isRead: false + if (!Users.getHasUnreadMessagingMessage(userId)) { + // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 + publishMainStream(userId, 'readAllMessagingMessages'); + } +} + +/** + * Mark messages as read + */ +export async function readGroupMessagingMessage( + userId: User['id'], + groupId: UserGroup['id'], + messageIds: MessagingMessage['id'][] +) { + if (messageIds.length === 0) return; + + // check joined + const joining = await UserGroupJoinings.findOne({ + userId: userId, + userGroupId: groupId + }); + + if (joining == null) { + throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); + } + + const messages = await MessagingMessages.find({ + id: In(messageIds) + }); + + const reads = []; + + for (const message of messages) { + if (message.userId === userId) continue; + if (message.reads.includes(userId)) continue; + + // Update document + await MessagingMessages.createQueryBuilder().update() + .set({ + reads: (() => `array_append("reads", '${joining.userId}')`) as any + }) + .where('id = :id', { id: message.id }) + .execute(); + + reads.push(message.id); + } + + // Publish event + publishGroupMessagingStream(groupId, 'read', { + ids: reads, + userId: userId }); + publishMessagingIndexStream(userId, 'read', reads); - if (count == 0) { + if (!Users.getHasUnreadMessagingMessage(userId)) { // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllMessagingMessages'); } -}; +} diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index 27e38bbdec..833ec37e4c 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -1,13 +1,13 @@ import $ from 'cafy'; import define from '../../define'; import { MessagingMessage } from '../../../../models/entities/messaging-message'; -import { MessagingMessages, Mutings } from '../../../../models'; +import { MessagingMessages, Mutings, UserGroupJoinings } from '../../../../models'; import { Brackets } from 'typeorm'; import { types, bool } from '../../../../misc/schema'; export const meta = { desc: { - 'ja-JP': 'Messagingの履歴を取得します。', + 'ja-JP': 'トークの履歴を取得します。', 'en-US': 'Show messaging history.' }, @@ -21,6 +21,11 @@ export const meta = { limit: { validator: $.optional.num.range(1, 100), default: 10 + }, + + group: { + validator: $.optional.bool, + default: false } }, @@ -40,26 +45,46 @@ export default define(meta, async (ps, user) => { muterId: user.id, }); + const groups = ps.group ? await UserGroupJoinings.find({ + userId: user.id, + }).then(xs => xs.map(x => x.userGroupId)) : []; + + if (ps.group && groups.length === 0) { + return []; + } + const history: MessagingMessage[] = []; for (let i = 0; i < ps.limit!; i++) { - const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId); + const found = ps.group + ? history.map(m => m.groupId!) + : history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!); const query = MessagingMessages.createQueryBuilder('message') - .where(new Brackets(qb => { qb + .orderBy('message.createdAt', 'DESC'); + + if (ps.group) { + query.where(`message.groupId IN (:...groups)`, { groups: groups }); + + if (found.length > 0) { + query.andWhere(`message.groupId NOT IN (:...found)`, { found: found }); + } + } else { + query.where(new Brackets(qb => { qb .where(`message.userId = :userId`, { userId: user.id }) .orWhere(`message.recipientId = :userId`, { userId: user.id }); - })) - .orderBy('message.createdAt', 'DESC'); + })); + query.andWhere(`message.groupId IS NULL`); - if (found.length > 0) { - query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); - query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); - } + if (found.length > 0) { + query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); + query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); + } - if (mute.length > 0) { - query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); - query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + if (mute.length > 0) { + query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + } } const message = await query.getOne(); diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index 0d5295bff3..c1e79cd130 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -1,16 +1,17 @@ import $ from 'cafy'; import { ID } from '../../../../misc/cafy-id'; -import read from '../../common/read-messaging-message'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; -import { MessagingMessages } from '../../../../models'; +import { MessagingMessages, UserGroups, UserGroupJoinings } from '../../../../models'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { types, bool } from '../../../../misc/schema'; +import { Brackets } from 'typeorm'; +import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; export const meta = { desc: { - 'ja-JP': '指定したユーザーとのMessagingのメッセージ一覧を取得します。', + 'ja-JP': 'トークメッセージ一覧を取得します。', 'en-US': 'Get messages of messaging.' }, @@ -22,13 +23,21 @@ export const meta = { params: { userId: { - validator: $.type(ID), + validator: $.optional.type(ID), desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' } }, + groupId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '対象のグループのID', + 'en-US': 'Target group ID' + } + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 @@ -64,27 +73,85 @@ export const meta = { code: 'NO_SUCH_USER', id: '11795c64-40ea-4198-b06e-3c873ed9039d' }, + + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f' + }, + + groupAccessDenied: { + message: 'You can not read messages of groups that you have not joined.', + code: 'GROUP_ACCESS_DENIED', + id: 'a053a8dd-a491-4718-8f87-50775aad9284' + }, } }; export default define(meta, async (ps, user) => { - // Fetch recipient - const recipient = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); + if (ps.userId != null) { + // Fetch recipient (user) + const recipient = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); - const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id }); + const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(new Brackets(qb => { qb + .where('message.userId = :meId') + .andWhere('message.recipientId = :recipientId'); + })) + .orWhere(new Brackets(qb => { qb + .where('message.userId = :recipientId') + .andWhere('message.recipientId = :meId'); + })); + })) + .setParameter('meId', user.id) + .setParameter('recipientId', recipient.id); - const messages = await query.getMany(); + const messages = await query.take(ps.limit!).getMany(); - // Mark all as read - if (ps.markAsRead) { - read(user.id, recipient.id, messages.map(x => x.id)); - } + // Mark all as read + if (ps.markAsRead) { + readUserMessagingMessage(user.id, recipient.id, messages.map(x => x.id)); + } - return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { - populateRecipient: false - }))); + return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { + populateRecipient: false + }))); + } else if (ps.groupId != null) { + // Fetch recipient (group) + const recipientGroup = await UserGroups.findOne(ps.groupId); + + if (recipientGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // check joined + const joining = await UserGroupJoinings.findOne({ + userId: user.id, + userGroupId: recipientGroup.id + }); + + if (joining == null) { + throw new ApiError(meta.errors.groupAccessDenied); + } + + const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id }); + + const messages = await query.take(ps.limit!).getMany(); + + // Mark all as read + if (ps.markAsRead) { + readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id)); + } + + return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { + populateGroup: false + }))); + } else { + throw new Error(); + } }); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index 388852b9cd..f5d7cf2b38 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -1,19 +1,22 @@ import $ from 'cafy'; import { ID } from '../../../../../misc/cafy-id'; -import { publishMainStream } from '../../../../../services/stream'; +import { publishMainStream, publishGroupMessagingStream } from '../../../../../services/stream'; import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream'; import pushSw from '../../../../../services/push-notification'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; -import { MessagingMessages, DriveFiles, Mutings } from '../../../../../models'; +import { MessagingMessages, DriveFiles, Mutings, UserGroups, UserGroupJoinings } from '../../../../../models'; import { MessagingMessage } from '../../../../../models/entities/messaging-message'; import { genId } from '../../../../../misc/gen-id'; import { types, bool } from '../../../../../misc/schema'; +import { User } from '../../../../../models/entities/user'; +import { UserGroup } from '../../../../../models/entities/user-group'; +import { Not } from 'typeorm'; export const meta = { desc: { - 'ja-JP': '指定したユーザーへMessagingのメッセージを送信します。', + 'ja-JP': 'トークメッセージを送信します。', 'en-US': 'Create a message of messaging.' }, @@ -25,13 +28,21 @@ export const meta = { params: { userId: { - validator: $.type(ID), + validator: $.optional.type(ID), desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' } }, + groupId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '対象のグループのID', + 'en-US': 'Target group ID' + } + }, + text: { validator: $.optional.str.pipe(MessagingMessages.isValidText) }, @@ -60,6 +71,18 @@ export const meta = { id: '11795c64-40ea-4198-b06e-3c873ed9039d' }, + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537' + }, + + groupAccessDenied: { + message: 'You can not send messages to groups that you have not joined.', + code: 'GROUP_ACCESS_DENIED', + id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd' + }, + noSuchFile: { message: 'No such file.', code: 'NO_SUCH_FILE', @@ -75,16 +98,38 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Myself - if (ps.userId === user.id) { - throw new ApiError(meta.errors.recipientIsYourself); - } + let recipientUser: User | undefined; + let recipientGroup: UserGroup | undefined; - // Fetch recipient - const recipient = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); + if (ps.userId != null) { + // Myself + if (ps.userId === user.id) { + throw new ApiError(meta.errors.recipientIsYourself); + } + + // Fetch recipient (user) + recipientUser = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + } else if (ps.groupId != null) { + // Fetch recipient (group) + recipientGroup = await UserGroups.findOne(ps.groupId); + + if (recipientGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // check joined + const joining = await UserGroupJoinings.findOne({ + userId: user.id, + userGroupId: recipientGroup.id + }); + + if (joining == null) { + throw new ApiError(meta.errors.groupAccessDenied); + } + } let file = null; if (ps.fileId != null) { @@ -107,32 +152,49 @@ export default define(meta, async (ps, user) => { id: genId(), createdAt: new Date(), fileId: file ? file.id : null, - recipientId: recipient.id, + recipientId: recipientUser ? recipientUser.id : null, + groupId: recipientGroup ? recipientGroup.id : null, text: ps.text ? ps.text.trim() : null, userId: user.id, - isRead: false + isRead: false, + reads: [] as any[] } as MessagingMessage); const messageObj = await MessagingMessages.pack(message); - // 自分のストリーム - publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); - publishMessagingIndexStream(message.userId, 'message', messageObj); - publishMainStream(message.userId, 'messagingMessage', messageObj); + if (recipientUser) { + // 自分のストリーム + publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj); + publishMessagingIndexStream(message.userId, 'message', messageObj); + publishMainStream(message.userId, 'messagingMessage', messageObj); - // 相手のストリーム - publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); - publishMessagingIndexStream(message.recipientId, 'message', messageObj); - publishMainStream(message.recipientId, 'messagingMessage', messageObj); + // 相手のストリーム + publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj); + publishMessagingIndexStream(recipientUser.id, 'message', messageObj); + publishMainStream(recipientUser.id, 'messagingMessage', messageObj); + } else if (recipientGroup) { + // グループのストリーム + publishGroupMessagingStream(recipientGroup.id, 'message', messageObj); + + // メンバーのストリーム + const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id }); + for (const joining of joinings) { + publishMessagingIndexStream(joining.userId, 'message', messageObj); + publishMainStream(joining.userId, 'messagingMessage', messageObj); + } + } // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する setTimeout(async () => { - const freshMessage = await MessagingMessages.findOne({ id: message.id }); + const freshMessage = await MessagingMessages.findOne(message.id); if (freshMessage == null) return; // メッセージが削除されている場合もある - if (!freshMessage.isRead) { + + if (recipientUser) { + if (freshMessage.isRead) return; // 既読 + //#region ただしミュートされているなら発行しない const mute = await Mutings.find({ - muterId: recipient.id, + muterId: recipientUser.id, }); const mutedUserIds = mute.map(m => m.muteeId.toString()); if (mutedUserIds.indexOf(user.id) != -1) { @@ -140,8 +202,15 @@ export default define(meta, async (ps, user) => { } //#endregion - publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj); - pushSw(message.recipientId, 'unreadMessagingMessage', messageObj); + publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj); + pushSw(recipientUser.id, 'unreadMessagingMessage', messageObj); + } else if (recipientGroup) { + const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id, userId: Not(user.id) }); + for (const joining of joinings) { + if (freshMessage.reads.includes(joining.userId)) return; // 既読 + publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj); + pushSw(joining.userId, 'unreadMessagingMessage', messageObj); + } } }, 2000); diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts index 6a896cd8d1..fb1bb42a56 100644 --- a/src/server/api/endpoints/messaging/messages/delete.ts +++ b/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 { publishMessagingStream } from '../../../../../services/stream'; +import { publishMessagingStream, publishGroupMessagingStream } from '../../../../../services/stream'; import * as ms from 'ms'; import { ApiError } from '../../../error'; import { MessagingMessages } from '../../../../../models'; @@ -10,7 +10,7 @@ export const meta = { stability: 'stable', desc: { - 'ja-JP': '指定したメッセージを削除します。', + 'ja-JP': '指定したトークメッセージを削除します。', 'en-US': 'Delete a message.' }, @@ -57,6 +57,10 @@ export default define(meta, async (ps, user) => { await MessagingMessages.delete(message.id); - publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); - publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); + if (message.recipientId) { + publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); + publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); + } else if (message.groupId) { + publishGroupMessagingStream(message.groupId, 'deleted', message.id); + } }); diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts index 50b7f39870..dd3449af15 100644 --- a/src/server/api/endpoints/messaging/messages/read.ts +++ b/src/server/api/endpoints/messaging/messages/read.ts @@ -1,13 +1,13 @@ import $ from 'cafy'; import { ID } from '../../../../../misc/cafy-id'; -import read from '../../../common/read-messaging-message'; import define from '../../../define'; import { ApiError } from '../../../error'; import { MessagingMessages } from '../../../../../models'; +import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message'; export const meta = { desc: { - 'ja-JP': '指定した自分宛てのメッセージを既読にします。', + 'ja-JP': '指定した自分宛てのトークメッセージを既読にします。', 'en-US': 'Mark as read a message of messaging.' }, @@ -39,12 +39,21 @@ export const meta = { export default define(meta, async (ps, user) => { const message = await MessagingMessages.findOne({ id: ps.messageId, - recipientId: user.id }); if (message == null) { throw new ApiError(meta.errors.noSuchMessage); } - read(user.id, message.userId, [message.id]); + if (message.recipientId) { + await readUserMessagingMessage(user.id, message.recipientId, [message.id]).catch(e => { + if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); + throw e; + }); + } else if (message.groupId) { + await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => { + if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); + throw e; + }); + } }); diff --git a/src/server/api/endpoints/users/groups/create.ts b/src/server/api/endpoints/users/groups/create.ts new file mode 100644 index 0000000000..ee6cade8d0 --- /dev/null +++ b/src/server/api/endpoints/users/groups/create.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { UserGroups, UserGroupJoinings } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; +import { UserGroup } from '../../../../../models/entities/user-group'; +import { types, bool } from '../../../../../misc/schema'; +import { UserGroupJoining } from '../../../../../models/entities/user-group-joining'; + +export const meta = { + desc: { + 'ja-JP': 'ユーザーグループを作成します。', + 'en-US': 'Create a user group.' + }, + + tags: ['groups'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + name: { + validator: $.str.range(1, 100) + } + }, + + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + }, +}; + +export default define(meta, async (ps, user) => { + const userGroup = await UserGroups.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + } as UserGroup); + + // Push the owner + await UserGroupJoinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: userGroup.id + } as UserGroupJoining); + + return await UserGroups.pack(userGroup); +}); diff --git a/src/server/api/endpoints/users/groups/delete.ts b/src/server/api/endpoints/users/groups/delete.ts new file mode 100644 index 0000000000..4f89c324a1 --- /dev/null +++ b/src/server/api/endpoints/users/groups/delete.ts @@ -0,0 +1,49 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups } from '../../../../../models'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループを削除します。', + 'en-US': 'Delete a user group' + }, + + tags: ['groups'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象となるユーザーグループのID', + 'en-US': 'ID of target user group' + } + } + }, + + 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/src/server/api/endpoints/users/groups/joined.ts b/src/server/api/endpoints/users/groups/joined.ts new file mode 100644 index 0000000000..14561fce05 --- /dev/null +++ b/src/server/api/endpoints/users/groups/joined.ts @@ -0,0 +1,33 @@ +import define from '../../../define'; +import { UserGroups, UserGroupJoinings } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; + +export const meta = { + desc: { + 'ja-JP': '自分の所属するユーザーグループ一覧を取得します。' + }, + + tags: ['groups', 'account'], + + requireCredential: true, + + kind: 'read:user-groups', + + res: { + type: types.array, + optional: bool.false, nullable: bool.false, + items: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + } + }, +}; + +export default define(meta, async (ps, me) => { + const joinings = await UserGroupJoinings.find({ + userId: me.id, + }); + + return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId))); +}); diff --git a/src/server/api/endpoints/users/groups/owned.ts b/src/server/api/endpoints/users/groups/owned.ts new file mode 100644 index 0000000000..6cf39a142b --- /dev/null +++ b/src/server/api/endpoints/users/groups/owned.ts @@ -0,0 +1,33 @@ +import define from '../../../define'; +import { UserGroups } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; + +export const meta = { + desc: { + 'ja-JP': '自分の作成したユーザーグループ一覧を取得します。' + }, + + tags: ['groups', 'account'], + + requireCredential: true, + + kind: 'read:user-groups', + + res: { + type: types.array, + optional: bool.false, nullable: bool.false, + items: { + type: types.object, + optional: bool.false, nullable: bool.false, + 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/src/server/api/endpoints/users/groups/pull.ts b/src/server/api/endpoints/users/groups/pull.ts new file mode 100644 index 0000000000..5fc0c2fa5e --- /dev/null +++ b/src/server/api/endpoints/users/groups/pull.ts @@ -0,0 +1,68 @@ +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'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループから指定したユーザーを削除します。', + 'en-US': 'Remove 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: '4662487c-05b1-4b78-86e5-fd46998aba74' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '0b5cc374-3681-41da-861e-8bc1146f7a55' + } + } +}; + +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; + }); + + // Pull the user + await UserGroupJoinings.delete({ userId: user.id }); +}); diff --git a/src/server/api/endpoints/users/groups/push.ts b/src/server/api/endpoints/users/groups/push.ts new file mode 100644 index 0000000000..5371580db0 --- /dev/null +++ b/src/server/api/endpoints/users/groups/push.ts @@ -0,0 +1,90 @@ +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); +}); diff --git a/src/server/api/endpoints/users/groups/show.ts b/src/server/api/endpoints/users/groups/show.ts new file mode 100644 index 0000000000..5f2c839881 --- /dev/null +++ b/src/server/api/endpoints/users/groups/show.ts @@ -0,0 +1,53 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループの情報を取得します。', + 'en-US': 'Show a user group.' + }, + + tags: ['groups', 'account'], + + requireCredential: true, + + kind: 'read:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + }, + + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + 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, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + return await UserGroups.pack(userGroup); +}); diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts index 2763b3a19c..bdc8403083 100644 --- a/src/server/api/endpoints/users/lists/push.ts +++ b/src/server/api/endpoints/users/lists/push.ts @@ -80,5 +80,5 @@ export default define(meta, async (ps, me) => { } // Push the user - pushUserToUserList(user, userList); + await pushUserToUserList(user, userList); }); diff --git a/src/server/api/kinds.ts b/src/server/api/kinds.ts index 76d5a8a61a..be3c30f7d9 100644 --- a/src/server/api/kinds.ts +++ b/src/server/api/kinds.ts @@ -23,4 +23,6 @@ export const kinds = [ 'write:pages', 'write:page-likes', 'read:page-likes', + 'read:user-groups', + 'write:user-groups', ]; diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts index 628bba511f..32f69bdef3 100644 --- a/src/server/api/openapi/schemas.ts +++ b/src/server/api/openapi/schemas.ts @@ -13,6 +13,7 @@ import { packedBlockingSchema } from '../../../models/repositories/blocking'; import { packedNoteReactionSchema } from '../../../models/repositories/note-reaction'; import { packedHashtagSchema } from '../../../models/repositories/hashtag'; import { packedPageSchema } from '../../../models/repositories/page'; +import { packedUserGroupSchema } from '../../../models/repositories/user-group'; export function convertSchemaToOpenApiSchema(schema: Schema) { const res: any = schema; @@ -66,6 +67,7 @@ export const schemas = { User: convertSchemaToOpenApiSchema(packedUserSchema), UserList: convertSchemaToOpenApiSchema(packedUserListSchema), + UserGroup: convertSchemaToOpenApiSchema(packedUserGroupSchema), App: convertSchemaToOpenApiSchema(packedAppSchema), MessagingMessage: convertSchemaToOpenApiSchema(packedMessagingMessageSchema), Note: convertSchemaToOpenApiSchema(packedNoteSchema), diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts index ce766e28e9..1e5e94c1c8 100644 --- a/src/server/api/stream/channels/messaging.ts +++ b/src/server/api/stream/channels/messaging.ts @@ -1,20 +1,39 @@ import autobind from 'autobind-decorator'; -import read from '../../common/read-messaging-message'; +import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; import Channel from '../channel'; +import { UserGroupJoinings } from '../../../../models'; export default class extends Channel { public readonly chName = 'messaging'; public static shouldShare = false; public static requireCredential = true; - private otherpartyId: string; + private otherpartyId: string | null; + private groupId: string | null; @autobind public async init(params: any) { this.otherpartyId = params.otherparty as string; + this.groupId = params.group as string; + + // Check joining + if (this.groupId) { + const joining = await UserGroupJoinings.findOne({ + userId: this.user!.id, + userGroupId: this.groupId + }); + + if (joining == null) { + return; + } + } + + const subCh = this.otherpartyId + ? `messagingStream:${this.user!.id}-${this.otherpartyId}` + : `messagingStream:${this.groupId}`; // Subscribe messaging stream - this.subscriber.on(`messagingStream:${this.user!.id}-${this.otherpartyId}`, data => { + this.subscriber.on(subCh, data => { this.send(data); }); } @@ -23,7 +42,11 @@ export default class extends Channel { public onMessage(type: string, body: any) { switch (type) { case 'read': - read(this.user!.id, this.otherpartyId, [body.id]); + if (this.otherpartyId) { + readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); + } else if (this.groupId) { + readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); + } break; } } |