From c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 18 May 2019 20:36:33 +0900 Subject: ユーザーグループ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #3218 --- src/models/entities/messaging-message.ts | 24 ++++++++++- src/models/entities/user-group-joining.ts | 41 +++++++++++++++++++ src/models/entities/user-group.ts | 46 +++++++++++++++++++++ src/models/index.ts | 6 ++- src/models/repositories/messaging-message.ts | 38 +++++++++++++---- src/models/repositories/user-group.ts | 61 ++++++++++++++++++++++++++++ src/models/repositories/user.ts | 35 ++++++++++++---- 7 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 src/models/entities/user-group-joining.ts create mode 100644 src/models/entities/user-group.ts create mode 100644 src/models/repositories/user-group.ts (limited to 'src/models') diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts index d3c3eab3a2..c18897a37d 100644 --- a/src/models/entities/messaging-message.ts +++ b/src/models/entities/messaging-message.ts @@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { User } from './user'; import { DriveFile } from './drive-file'; import { id } from '../id'; +import { UserGroup } from './user-group'; @Entity() export class MessagingMessage { @@ -29,10 +30,10 @@ export class MessagingMessage { @Index() @Column({ - ...id(), + ...id(), nullable: true, comment: 'The recipient user ID.' }) - public recipientId: User['id']; + public recipientId: User['id'] | null; @ManyToOne(type => User, { onDelete: 'CASCADE' @@ -40,6 +41,19 @@ export class MessagingMessage { @JoinColumn() public recipient: User | null; + @Index() + @Column({ + ...id(), nullable: true, + comment: 'The recipient group ID.' + }) + public groupId: UserGroup['id'] | null; + + @ManyToOne(type => UserGroup, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public group: UserGroup | null; + @Column('varchar', { length: 4096, nullable: true }) @@ -50,6 +64,12 @@ export class MessagingMessage { }) public isRead: boolean; + @Column({ + ...id(), + array: true, default: '{}' + }) + public reads: User['id'][]; + @Column({ ...id(), nullable: true, diff --git a/src/models/entities/user-group-joining.ts b/src/models/entities/user-group-joining.ts new file mode 100644 index 0000000000..17b534f42f --- /dev/null +++ b/src/models/entities/user-group-joining.ts @@ -0,0 +1,41 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { UserGroup } from './user-group'; +import { id } from '../id'; + +@Entity() +export class UserGroupJoining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroupJoining.' + }) + 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.ts b/src/models/entities/user-group.ts new file mode 100644 index 0000000000..f4bac03223 --- /dev/null +++ b/src/models/entities/user-group.ts @@ -0,0 +1,46 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class UserGroup { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroup.' + }) + public createdAt: Date; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Index() + @Column({ + ...id(), + comment: 'The ID of owner.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('boolean', { + default: false, + }) + public isPrivate: boolean; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/index.ts b/src/models/index.ts index a63bb2c2b5..c05d7febe5 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -6,7 +6,6 @@ import { PollVote } from './entities/poll-vote'; import { Meta } from './entities/meta'; import { SwSubscription } from './entities/sw-subscription'; import { NoteWatching } from './entities/note-watching'; -import { UserListJoining } from './entities/user-list-joining'; import { NoteUnread } from './entities/note-unread'; import { RegistrationTicket } from './entities/registration-tickets'; import { UserRepository } from './repositories/user'; @@ -20,6 +19,9 @@ import { SigninRepository } from './repositories/signin'; import { MessagingMessageRepository } from './repositories/messaging-message'; import { ReversiGameRepository } from './repositories/games/reversi/game'; 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 { FollowRequestRepository } from './repositories/follow-request'; import { MutingRepository } from './repositories/muting'; import { BlockingRepository } from './repositories/blocking'; @@ -52,6 +54,8 @@ export const UserKeypairs = getRepository(UserKeypair); export const UserPublickeys = getRepository(UserPublickey); export const UserLists = getCustomRepository(UserListRepository); export const UserListJoinings = getRepository(UserListJoining); +export const UserGroups = getCustomRepository(UserGroupRepository); +export const UserGroupJoinings = getRepository(UserGroupJoining); export const UserNotePinings = getRepository(UserNotePining); export const Followings = getCustomRepository(FollowingRepository); export const FollowRequests = getCustomRepository(FollowRequestRepository); diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts index 33f95bbd5f..a64ed07328 100644 --- a/src/models/repositories/messaging-message.ts +++ b/src/models/repositories/messaging-message.ts @@ -1,6 +1,6 @@ import { EntityRepository, Repository } from 'typeorm'; import { MessagingMessage } from '../entities/messaging-message'; -import { Users, DriveFiles } from '..'; +import { Users, DriveFiles, UserGroups } from '..'; import { ensure } from '../../prelude/ensure'; import { types, bool, SchemaType } from '../../misc/schema'; @@ -16,11 +16,13 @@ export class MessagingMessageRepository extends Repository { src: MessagingMessage['id'] | MessagingMessage, me?: any, options?: { - populateRecipient: boolean + populateRecipient?: boolean, + populateGroup?: boolean, } ): Promise { const opts = options || { - populateRecipient: true + populateRecipient: true, + populateGroup: true, }; const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure); @@ -32,10 +34,13 @@ export class MessagingMessageRepository extends Repository { userId: message.userId, user: await Users.pack(message.user || message.userId, me), recipientId: message.recipientId, - recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined, + recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined, + groupId: message.recipientId, + group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined, fileId: message.fileId, file: message.fileId ? await DriveFiles.pack(message.fileId) : null, - isRead: message.isRead + isRead: message.isRead, + reads: message.reads, }; } } @@ -83,17 +88,36 @@ export const packedMessagingMessageSchema = { }, recipientId: { type: types.string, - optional: bool.false, nullable: bool.false, + optional: bool.false, nullable: bool.true, format: 'id', }, recipient: { type: types.object, - optional: bool.true, nullable: bool.false, + optional: bool.true, nullable: bool.true, ref: 'User' }, + groupId: { + type: types.string, + optional: bool.false, nullable: bool.true, + format: 'id', + }, + group: { + type: types.object, + optional: bool.true, nullable: bool.true, + ref: 'UserGroup' + }, isRead: { type: types.boolean, optional: bool.true, nullable: bool.false, }, + reads: { + type: types.array, + optional: bool.true, nullable: bool.false, + items: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'id' + } + }, }, }; diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts new file mode 100644 index 0000000000..8bb1ae8330 --- /dev/null +++ b/src/models/repositories/user-group.ts @@ -0,0 +1,61 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { UserGroup } from '../entities/user-group'; +import { ensure } from '../../prelude/ensure'; +import { UserGroupJoinings } from '..'; +import { bool, types, SchemaType } from '../../misc/schema'; + +export type PackedUserGroup = SchemaType; + +@EntityRepository(UserGroup) +export class UserGroupRepository extends Repository { + public async pack( + src: UserGroup['id'] | UserGroup, + ): Promise { + const userGroup = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + const users = await UserGroupJoinings.find({ + userGroupId: userGroup.id + }); + + return { + id: userGroup.id, + createdAt: userGroup.createdAt.toISOString(), + name: userGroup.name, + userIds: users.map(x => x.userId) + }; + } +} + +export const packedUserGroupSchema = { + type: types.object, + optional: bool.false, nullable: bool.false, + properties: { + id: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'id', + description: 'The unique identifier for this UserGroup.', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'date-time', + description: 'The date that the UserGroup was created.' + }, + name: { + type: types.string, + optional: bool.false, nullable: bool.false, + description: 'The name of the UserGroup.' + }, + userIds: { + type: types.array, + nullable: bool.false, optional: bool.true, + items: { + type: types.string, + nullable: bool.false, optional: bool.false, + format: 'id', + } + }, + }, +}; diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 330220fb72..f81fa6bc77 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -1,6 +1,6 @@ import { EntityRepository, Repository, In } from 'typeorm'; import { User, ILocalUser, IRemoteUser } from '../entities/user'; -import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..'; +import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..'; import { ensure } from '../../prelude/ensure'; import config from '../../config'; import { SchemaType, bool, types } from '../../misc/schema'; @@ -54,6 +54,31 @@ export class UserRepository extends Repository { }; } + public async getHasUnreadMessagingMessage(userId: User['id']): Promise { + const joinings = await UserGroupJoinings.find({ userId: userId }); + + const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') + .where(`message.groupId = :groupId`, { groupId: j.userGroupId }) + .andWhere('message.userId != :userId', { userId: userId }) + .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) + .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない + .getOne().then(x => x != null))); + + const [withUser, withGroups] = await Promise.all([ + // TODO: ミュートを考慮 + MessagingMessages.count({ + where: { + recipientId: userId, + isRead: false + }, + take: 1 + }).then(count => count > 0), + groupQs + ]); + + return withUser || withGroups.some(x => x); + } + public async pack( src: User['id'] | User, me?: User['id'] | User | null | undefined, @@ -151,13 +176,7 @@ export class UserRepository extends Repository { autoWatch: profile!.autoWatch, alwaysMarkNsfw: profile!.alwaysMarkNsfw, carefulBot: profile!.carefulBot, - hasUnreadMessagingMessage: MessagingMessages.count({ - where: { - recipientId: user.id, - isRead: false - }, - take: 1 - }).then(count => count > 0), + hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), hasUnreadNotification: Notifications.count({ where: { notifieeId: user.id, -- cgit v1.2.3-freya