Add for leave family
This commit is contained in:
@@ -907,6 +907,11 @@ export class AuthGrpcController {
|
||||
return this.family.removeMember(command.requesterId, command.memberId);
|
||||
}
|
||||
|
||||
@GrpcMethod('FamilyService', 'LeaveFamilyGroup')
|
||||
leaveFamilyGroup(command: { requesterId: string; groupId: string; newOwnerUserId?: string }) {
|
||||
return this.family.leaveFamily(command.requesterId, command.groupId, command.newOwnerUserId);
|
||||
}
|
||||
|
||||
@GrpcMethod('FamilyService', 'SendFamilyInvite')
|
||||
async sendFamilyInvite(command: { requesterId: string; groupId: string; target?: string; inviteeUserId?: string }) {
|
||||
const invite = await this.family.sendInvite(command.requesterId, command.groupId, {
|
||||
|
||||
@@ -940,12 +940,16 @@ export class ChatService {
|
||||
viewerId?: string
|
||||
) {
|
||||
const viewerMembership = viewerId ? room.members.find((member) => member.userId === viewerId) : undefined;
|
||||
const activeFamilyUserIds = new Set(familyRoleByUserId.keys());
|
||||
const peerMember =
|
||||
room.type === 'BOT'
|
||||
? room.members.find((member) => Boolean(member.user.linkedBotId))
|
||||
: viewerId && (room.type === 'DIRECT' || room.type === 'E2E')
|
||||
? room.members.find((member) => member.userId !== viewerId)
|
||||
: undefined;
|
||||
const peerLeftFamily = Boolean(
|
||||
peerMember && !activeFamilyUserIds.has(peerMember.userId) && !peerMember.user.linkedBotId
|
||||
);
|
||||
|
||||
return {
|
||||
id: room.id,
|
||||
@@ -960,6 +964,7 @@ export class ChatService {
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
isPinned: Boolean(viewerMembership?.pinnedAt),
|
||||
pinnedAt: viewerMembership?.pinnedAt?.toISOString(),
|
||||
peerLeftFamily,
|
||||
members: room.members.map((member) => ({
|
||||
id: member.id,
|
||||
userId: member.userId,
|
||||
@@ -970,7 +975,10 @@ export class ChatService {
|
||||
notificationsMuted: member.notificationsMuted,
|
||||
isPinned: Boolean(member.pinnedAt),
|
||||
pinnedAt: member.pinnedAt?.toISOString(),
|
||||
familyRole: familyRoleByUserId.get(member.userId) ?? 'member',
|
||||
inFamily: activeFamilyUserIds.has(member.userId),
|
||||
familyRole: activeFamilyUserIds.has(member.userId)
|
||||
? (familyRoleByUserId.get(member.userId) ?? 'member')
|
||||
: 'member',
|
||||
isChatCreator: room.createdById === member.userId
|
||||
})),
|
||||
lastMessage
|
||||
|
||||
@@ -212,12 +212,76 @@ export class FamilyService {
|
||||
|
||||
for (const member of memberships) {
|
||||
|
||||
await this.removeMember(userId, member.id);
|
||||
await this.leaveFamily(userId, member.groupId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async leaveFamily(requesterId: string, groupId: string, newOwnerUserId?: string) {
|
||||
|
||||
const group = await this.getGroupWithMembers(groupId);
|
||||
|
||||
this.assertMember(group, requesterId);
|
||||
|
||||
const member = group.members.find((item) => item.userId === requesterId);
|
||||
|
||||
if (!member) {
|
||||
|
||||
throw new NotFoundException('Участник не найден');
|
||||
|
||||
}
|
||||
|
||||
const isOwner = group.ownerId === requesterId;
|
||||
|
||||
const otherHumans = this.getHumanMembers(group.members).filter((item) => item.userId !== requesterId);
|
||||
|
||||
if (isOwner && otherHumans.length === 0) {
|
||||
|
||||
await this.destroyGroupWithCleanup(group);
|
||||
|
||||
return { count: 1, deleted: true };
|
||||
|
||||
}
|
||||
|
||||
if (isOwner && otherHumans.length > 0) {
|
||||
|
||||
if (!newOwnerUserId) {
|
||||
|
||||
throw new BadRequestException('Перед выходом передайте управление семьёй другому участнику');
|
||||
|
||||
}
|
||||
|
||||
if (!otherHumans.some((item) => item.userId === newOwnerUserId)) {
|
||||
|
||||
throw new BadRequestException('Новый глава семьи должен быть участником семьи');
|
||||
|
||||
}
|
||||
|
||||
await this.transferOwnership(group, requesterId, newOwnerUserId);
|
||||
|
||||
const updatedGroup = await this.getGroupWithMembers(groupId);
|
||||
|
||||
const updatedMember = updatedGroup.members.find((item) => item.userId === requesterId);
|
||||
|
||||
if (!updatedMember) {
|
||||
|
||||
throw new NotFoundException('Участник не найден');
|
||||
|
||||
}
|
||||
|
||||
await this.executeMemberLeave(updatedGroup, updatedMember, { voluntary: true });
|
||||
|
||||
return { count: 1, deleted: false };
|
||||
|
||||
}
|
||||
|
||||
await this.executeMemberLeave(group, member, { voluntary: true });
|
||||
|
||||
return { count: 1, deleted: false };
|
||||
|
||||
}
|
||||
|
||||
private async destroyGroupWithCleanup(group: {
|
||||
|
||||
id: string;
|
||||
@@ -399,7 +463,7 @@ export class FamilyService {
|
||||
|
||||
where: { id: memberId },
|
||||
|
||||
include: { group: { include: { members: true } } }
|
||||
include: { group: { include: { members: { include: this.memberUserInclude() } } } }
|
||||
|
||||
});
|
||||
|
||||
@@ -413,7 +477,13 @@ export class FamilyService {
|
||||
|
||||
const isOwner = member.group.ownerId === requesterId;
|
||||
|
||||
if (!isSelf && !isOwner) {
|
||||
if (isSelf) {
|
||||
|
||||
throw new BadRequestException('Для выхода из семьи используйте соответствующую функцию');
|
||||
|
||||
}
|
||||
|
||||
if (!isOwner) {
|
||||
|
||||
throw new ForbiddenException('Недостаточно прав для удаления участника');
|
||||
|
||||
@@ -425,17 +495,175 @@ export class FamilyService {
|
||||
|
||||
}
|
||||
|
||||
await this.executeMemberLeave(member.group, member, { voluntary: false });
|
||||
|
||||
return { count: 1 };
|
||||
|
||||
}
|
||||
|
||||
private async transferOwnership(
|
||||
|
||||
group: {
|
||||
|
||||
id: string;
|
||||
|
||||
name: string;
|
||||
|
||||
members: Array<{ userId: string; user?: { displayName: string } }>;
|
||||
|
||||
},
|
||||
|
||||
fromUserId: string,
|
||||
|
||||
toUserId: string
|
||||
|
||||
) {
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
|
||||
await tx.chatRoomMember.deleteMany({ where: { userId: member.userId, room: { groupId: member.groupId } } });
|
||||
await tx.familyGroup.update({ where: { id: group.id }, data: { ownerId: toUserId } });
|
||||
|
||||
await tx.familyMember.delete({ where: { id: memberId } });
|
||||
await tx.familyMember.updateMany({ where: { groupId: group.id, userId: fromUserId }, data: { role: 'member' } });
|
||||
|
||||
await tx.familyMember.updateMany({ where: { groupId: group.id, userId: toUserId }, data: { role: 'owner' } });
|
||||
|
||||
});
|
||||
|
||||
return { count: 1 };
|
||||
const oldOwnerName = group.members.find((item) => item.userId === fromUserId)?.user?.displayName ?? 'Участник';
|
||||
|
||||
const title = 'Вы стали главой семьи';
|
||||
|
||||
const body = `${oldOwnerName} передал(а) вам управление семьёй «${group.name}»`;
|
||||
|
||||
await this.notifications.create(
|
||||
|
||||
toUserId,
|
||||
|
||||
'family_ownership_transferred',
|
||||
|
||||
title,
|
||||
|
||||
body,
|
||||
|
||||
{ groupId: group.id, groupName: group.name, previousOwnerId: fromUserId }
|
||||
|
||||
);
|
||||
|
||||
await this.notifications.publishRealtime(
|
||||
|
||||
toUserId,
|
||||
|
||||
'family_ownership_transferred',
|
||||
|
||||
title,
|
||||
|
||||
body,
|
||||
|
||||
{ groupId: group.id, groupName: group.name, previousOwnerId: fromUserId }
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private async executeMemberLeave(
|
||||
|
||||
group: {
|
||||
|
||||
id: string;
|
||||
|
||||
name: string;
|
||||
|
||||
members: Array<{ id: string; userId: string; role: string; user?: { displayName: string; linkedBotId?: string | null } }>;
|
||||
|
||||
},
|
||||
|
||||
member: { id: string; userId: string; user?: { displayName: string } },
|
||||
|
||||
options: { voluntary: boolean }
|
||||
|
||||
) {
|
||||
|
||||
const displayName = member.user?.displayName ?? (await this.loadMemberUser(member.userId))?.displayName ?? 'Участник';
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
|
||||
await tx.chatRoomMember.deleteMany({ where: { userId: member.userId, room: { groupId: group.id } } });
|
||||
|
||||
await tx.familyMember.delete({ where: { id: member.id } });
|
||||
|
||||
});
|
||||
|
||||
const recipients = group.members.filter(
|
||||
|
||||
(item) => item.userId !== member.userId && item.role !== 'bot' && !item.user?.linkedBotId
|
||||
|
||||
);
|
||||
|
||||
const eventType = options.voluntary ? 'family_member_left' : 'family_member_removed';
|
||||
|
||||
const title = options.voluntary ? 'Участник покинул семью' : 'Участник удалён из семьи';
|
||||
|
||||
const body = options.voluntary
|
||||
|
||||
? `${displayName} покинул(а) семью «${group.name}»`
|
||||
|
||||
: `${displayName} был(а) удалён(а) из семьи «${group.name}»`;
|
||||
|
||||
for (const recipient of recipients) {
|
||||
|
||||
await this.notifications.create(
|
||||
|
||||
recipient.userId,
|
||||
|
||||
eventType,
|
||||
|
||||
title,
|
||||
|
||||
body,
|
||||
|
||||
{ groupId: group.id, groupName: group.name, userId: member.userId, userName: displayName }
|
||||
|
||||
);
|
||||
|
||||
await this.notifications.publishRealtime(
|
||||
|
||||
recipient.userId,
|
||||
|
||||
eventType,
|
||||
|
||||
title,
|
||||
|
||||
body,
|
||||
|
||||
{ groupId: group.id, groupName: group.name, userId: member.userId, userName: displayName }
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
await this.notifications.publishRealtime(
|
||||
|
||||
member.userId,
|
||||
|
||||
'family_left_self',
|
||||
|
||||
'Вы покинули семью',
|
||||
|
||||
`Вы покинули семью «${group.name}»`,
|
||||
|
||||
{ groupId: group.id, groupName: group.name }
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private getHumanMembers(
|
||||
|
||||
members: Array<{ userId: string; role: string; user?: { linkedBotId?: string | null } }>
|
||||
|
||||
) {
|
||||
|
||||
return members.filter((member) => member.role !== 'bot' && !member.user?.linkedBotId);
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user