Add for leave family

This commit is contained in:
lendry
2026-06-26 23:41:20 +03:00
parent f1bba24faa
commit 886b7e9ade
9 changed files with 489 additions and 21 deletions

View File

@@ -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);
}