import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Brackets, LessThan, MoreThan, Repository } from 'typeorm'; import { DirectMessage, FriendRequest, FriendRequestStatus, Friendship, PlayerTravelUnlock, SocialNotification, UserBlock, UserReport, } from './social.entities'; import { SocialStore } from './social.store'; @Injectable() export class SocialDatabaseStore implements SocialStore { constructor( @InjectRepository(Friendship) private readonly friendships: Repository, @InjectRepository(FriendRequest) private readonly requests: Repository, @InjectRepository(UserBlock) private readonly blocks: Repository, @InjectRepository(DirectMessage) private readonly messages: Repository, @InjectRepository(UserReport) private readonly reports: Repository, @InjectRepository(PlayerTravelUnlock) private readonly unlocks: Repository, @InjectRepository(SocialNotification) private readonly notifications: Repository, ) {} async listFriendships(userId: bigint): Promise { return this.friendships.find({ where: [{ user_low_id: userId }, { user_high_id: userId }], order: { created_at: 'DESC' } }); } async findFriendship(userLowId: bigint, userHighId: bigint): Promise { return this.friendships.findOne({ where: { user_low_id: userLowId, user_high_id: userHighId } }); } async createFriendship(userLowId: bigint, userHighId: bigint): Promise { const existing = await this.findFriendship(userLowId, userHighId); return existing || this.friendships.save(this.friendships.create({ user_low_id: userLowId, user_high_id: userHighId })); } async deleteFriendship(userLowId: bigint, userHighId: bigint): Promise { await this.friendships.delete({ user_low_id: userLowId, user_high_id: userHighId }); } async findPendingFriendRequest(requesterId: bigint, recipientId: bigint): Promise { return this.requests.findOne({ where: { requester_id: requesterId, recipient_id: recipientId, status: FriendRequestStatus.PENDING, expires_at: MoreThan(new Date()) } }); } async createFriendRequest(requesterId: bigint, recipientId: bigint, expiresAt: Date): Promise { return this.requests.save(this.requests.create({ requester_id: requesterId, recipient_id: recipientId, expires_at: expiresAt, status: FriendRequestStatus.PENDING })); } async findFriendRequest(id: bigint): Promise { return this.requests.findOne({ where: { id } }); } async saveFriendRequest(request: FriendRequest): Promise { return this.requests.save(request); } async cancelPendingRequestsBetween(userA: bigint, userB: bigint): Promise { await this.requests.createQueryBuilder().update(FriendRequest).set({ status: FriendRequestStatus.CANCELLED, responded_at: new Date() }).where('status = :status', { status: FriendRequestStatus.PENDING }).andWhere(new Brackets((qb) => qb.where('(requester_id = :a AND recipient_id = :b)', { a: userA, b: userB }).orWhere('(requester_id = :b AND recipient_id = :a)', { a: userA, b: userB }))).execute(); } async listFriendRequests(userId: bigint): Promise { return this.requests.find({ where: { recipient_id: userId, status: FriendRequestStatus.PENDING, expires_at: MoreThan(new Date()) }, order: { created_at: 'DESC' } }); } async createBlock(userId: bigint, blockedUserId: bigint): Promise { const found = await this.blocks.findOne({ where: { user_id: userId, blocked_user_id: blockedUserId } }); return found || this.blocks.save(this.blocks.create({ user_id: userId, blocked_user_id: blockedUserId })); } async deleteBlock(userId: bigint, blockedUserId: bigint): Promise { await this.blocks.delete({ user_id: userId, blocked_user_id: blockedUserId }); } async isBlocked(userId: bigint, blockedUserId: bigint): Promise { return (await this.blocks.count({ where: { user_id: userId, blocked_user_id: blockedUserId } })) > 0; } async listBlocks(userId: bigint): Promise { return this.blocks.find({ where: { user_id: userId }, order: { created_at: 'DESC' } }); } async createDirectMessage(input: Pick): Promise { return this.messages.save(this.messages.create(input)); } async listDirectMessages(userA: bigint, userB: bigint, limit: number, before?: Date): Promise { const query = this.messages.createQueryBuilder('message').where('message.expires_at > :now', { now: new Date() }).andWhere(new Brackets((qb) => qb.where('(message.sender_id = :a AND message.recipient_id = :b)', { a: userA, b: userB }).orWhere('(message.sender_id = :b AND message.recipient_id = :a)', { a: userA, b: userB }))); if (before) query.andWhere('message.created_at < :before', { before }); return query.orderBy('message.created_at', 'DESC').take(limit).getMany(); } async markDirectMessagesRead(readerId: bigint, otherUserId: bigint): Promise { const result = await this.messages.createQueryBuilder().update(DirectMessage).set({ read_at: new Date() }).where('sender_id = :otherUserId AND recipient_id = :readerId AND read_at IS NULL', { readerId, otherUserId }).execute(); return result.affected || 0; } async listConversations(userId: bigint): Promise { const rows = await this.messages.createQueryBuilder('message').where('(message.sender_id = :userId OR message.recipient_id = :userId)', { userId }).andWhere('message.expires_at > :now', { now: new Date() }).orderBy('message.created_at', 'DESC').getMany(); const seen = new Set(); return rows.filter((row) => { const other = (row.sender_id === userId ? row.recipient_id : row.sender_id).toString(); if (seen.has(other)) return false; seen.add(other); return true; }); } async countUnreadDirectMessages(userId: bigint): Promise { return this.messages.count({ where: { recipient_id: userId, read_at: null, expires_at: MoreThan(new Date()) } }); } async createReport(input: Pick): Promise { return this.reports.save(this.reports.create(input)); } async createUnlock(userId: bigint, destinationId: string): Promise { const found = await this.unlocks.findOne({ where: { user_id: userId, destination_id: destinationId } }); return found || this.unlocks.save(this.unlocks.create({ user_id: userId, destination_id: destinationId })); } async listUnlocks(userId: bigint): Promise { return this.unlocks.find({ where: { user_id: userId }, order: { discovered_at: 'ASC' } }); } async createNotification(input: Pick): Promise { return this.notifications.save(this.notifications.create(input)); } async listNotifications(userId: bigint, limit: number, before?: Date): Promise { const where: any = { user_id: userId, expires_at: MoreThan(new Date()) }; if (before) where.created_at = LessThan(before); return this.notifications.find({ where, order: { created_at: 'DESC' }, take: limit }); } async markNotificationRead(userId: bigint, id: bigint): Promise { const notification = await this.notifications.findOne({ where: { id, user_id: userId } }); if (!notification) return null; notification.read_at = new Date(); return this.notifications.save(notification); } async markAllNotificationsRead(userId: bigint): Promise { const result = await this.notifications.createQueryBuilder().update(SocialNotification).set({ read_at: new Date() }).where('user_id = :userId AND read_at IS NULL', { userId }).execute(); return result.affected || 0; } async countUnreadNotifications(userId: bigint): Promise { return this.notifications.count({ where: { user_id: userId, read_at: null, expires_at: MoreThan(new Date()) } }); } async cleanupExpired(now: Date): Promise { await Promise.all([this.requests.delete({ status: FriendRequestStatus.PENDING, expires_at: LessThan(now) }), this.messages.delete({ expires_at: LessThan(now) }), this.notifications.delete({ expires_at: LessThan(now) })]); } }