summaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/api/common/read-messaging-message.ts80
-rw-r--r--src/server/api/endpoints/messaging/history.ts51
-rw-r--r--src/server/api/endpoints/messaging/messages.ts105
-rw-r--r--src/server/api/endpoints/messaging/messages/create.ts125
-rw-r--r--src/server/api/endpoints/messaging/messages/delete.ts12
-rw-r--r--src/server/api/endpoints/messaging/messages/read.ts17
-rw-r--r--src/server/api/endpoints/users/groups/create.ts51
-rw-r--r--src/server/api/endpoints/users/groups/delete.ts49
-rw-r--r--src/server/api/endpoints/users/groups/joined.ts33
-rw-r--r--src/server/api/endpoints/users/groups/owned.ts33
-rw-r--r--src/server/api/endpoints/users/groups/pull.ts68
-rw-r--r--src/server/api/endpoints/users/groups/push.ts90
-rw-r--r--src/server/api/endpoints/users/groups/show.ts53
-rw-r--r--src/server/api/endpoints/users/lists/push.ts2
-rw-r--r--src/server/api/kinds.ts2
-rw-r--r--src/server/api/openapi/schemas.ts2
-rw-r--r--src/server/api/stream/channels/messaging.ts31
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;
}
}