|
|
import _ from 'lodash';
|
|
|
import { Types } from 'mongoose';
|
|
|
import { isValidStr } from '../../../lib/utils';
|
|
|
import type {
|
|
|
Group,
|
|
|
GroupDocument,
|
|
|
GroupModel,
|
|
|
GroupPanel,
|
|
|
} from '../../../models/group/group';
|
|
|
import {
|
|
|
TcService,
|
|
|
GroupBaseInfo,
|
|
|
TcContext,
|
|
|
TcDbService,
|
|
|
PureContext,
|
|
|
call,
|
|
|
DataNotFoundError,
|
|
|
EntityError,
|
|
|
NoPermissionError,
|
|
|
PERMISSION,
|
|
|
GroupPanelType,
|
|
|
} from 'tailchat-server-sdk';
|
|
|
import moment from 'moment';
|
|
|
|
|
|
interface GroupService
|
|
|
extends TcService,
|
|
|
TcDbService<GroupDocument, GroupModel> {}
|
|
|
class GroupService extends TcService {
|
|
|
get serviceName(): string {
|
|
|
return 'group';
|
|
|
}
|
|
|
|
|
|
onInit(): void {
|
|
|
this.registerLocalDb(require('../../../models/group/group').default);
|
|
|
|
|
|
this.registerAction('createGroup', this.createGroup, {
|
|
|
params: {
|
|
|
name: 'string',
|
|
|
panels: 'array',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('getUserGroups', this.getUserGroups);
|
|
|
this.registerAction(
|
|
|
'getJoinedGroupAndPanelIds',
|
|
|
this.getJoinedGroupAndPanelIds
|
|
|
);
|
|
|
this.registerAction('getGroupBasicInfo', this.getGroupBasicInfo, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('getGroupInfo', this.getGroupInfo, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
cache: {
|
|
|
keys: ['groupId'],
|
|
|
ttl: 60 * 60, // 1 hour
|
|
|
},
|
|
|
visibility: 'public',
|
|
|
});
|
|
|
this.registerAction('updateGroupField', this.updateGroupField, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
fieldName: 'string',
|
|
|
fieldValue: 'any',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('updateGroupConfig', this.updateGroupConfig, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
configName: 'string',
|
|
|
configValue: 'any',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('isGroupOwner', this.isGroupOwner, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('joinGroup', this.joinGroup, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
visibility: 'public',
|
|
|
});
|
|
|
this.registerAction('quitGroup', this.quitGroup, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('isMember', this.isMember, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('appendGroupMemberRoles', this.appendGroupMemberRoles, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
memberIds: { type: 'array', items: 'string' },
|
|
|
roles: { type: 'array', items: 'string' },
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('removeGroupMemberRoles', this.removeGroupMemberRoles, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
memberIds: { type: 'array', items: 'string' },
|
|
|
roles: { type: 'array', items: 'string' },
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('createGroupPanel', this.createGroupPanel, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
name: 'string',
|
|
|
type: 'number',
|
|
|
parentId: { type: 'string', optional: true },
|
|
|
provider: { type: 'string', optional: true },
|
|
|
pluginPanelName: { type: 'string', optional: true },
|
|
|
meta: { type: 'object', optional: true },
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('modifyGroupPanel', this.modifyGroupPanel, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
panelId: 'string',
|
|
|
name: 'string',
|
|
|
type: 'number',
|
|
|
provider: { type: 'string', optional: true },
|
|
|
pluginPanelName: { type: 'string', optional: true },
|
|
|
meta: { type: 'object', optional: true },
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('deleteGroupPanel', this.deleteGroupPanel, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
panelId: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction(
|
|
|
'getGroupLobbyConverseId',
|
|
|
this.getGroupLobbyConverseId,
|
|
|
{
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
}
|
|
|
);
|
|
|
this.registerAction('createGroupRole', this.createGroupRole, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
roleName: 'string',
|
|
|
permissions: { type: 'array', items: 'string' },
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('deleteGroupRole', this.deleteGroupRole, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
roleId: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('updateGroupRoleName', this.updateGroupRoleName, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
roleId: 'string',
|
|
|
roleName: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction(
|
|
|
'updateGroupRolePermission',
|
|
|
this.updateGroupRolePermission,
|
|
|
{
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
roleId: 'string',
|
|
|
permissions: {
|
|
|
type: 'array',
|
|
|
items: 'string',
|
|
|
},
|
|
|
},
|
|
|
}
|
|
|
);
|
|
|
this.registerAction('getPermissions', this.getPermissions, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('getUserAllPermissions', this.getUserAllPermissions, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
userId: 'string',
|
|
|
},
|
|
|
visibility: 'public',
|
|
|
cache: {
|
|
|
keys: ['groupId', 'userId'],
|
|
|
ttl: 60 * 60, // 1 hour
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('muteGroupMember', this.muteGroupMember, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
memberId: 'string',
|
|
|
muteMs: 'number',
|
|
|
},
|
|
|
});
|
|
|
this.registerAction('deleteGroupMember', this.deleteGroupMember, {
|
|
|
params: {
|
|
|
groupId: 'string',
|
|
|
memberId: 'string',
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取群组所有的文字面板id列表
|
|
|
* 用于加入房间
|
|
|
*/
|
|
|
private getGroupTextPanelIds(group: Group): string[] {
|
|
|
// TODO: 先无视权限, 把所有的信息全部显示
|
|
|
const textPanelIds = group.panels
|
|
|
.filter((p) => p.type === GroupPanelType.TEXT)
|
|
|
.map((p) => p.id);
|
|
|
|
|
|
return textPanelIds;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 创建群组
|
|
|
*/
|
|
|
async createGroup(
|
|
|
ctx: TcContext<{
|
|
|
name: string;
|
|
|
panels: GroupPanel[];
|
|
|
}>
|
|
|
) {
|
|
|
const name = ctx.params.name;
|
|
|
const panels = ctx.params.panels;
|
|
|
const userId = ctx.meta.userId;
|
|
|
|
|
|
const group = await this.adapter.model.createGroup({
|
|
|
name,
|
|
|
panels,
|
|
|
owner: userId,
|
|
|
});
|
|
|
|
|
|
const textPanelIds = this.getGroupTextPanelIds(group);
|
|
|
|
|
|
await call(ctx).joinSocketIORoom(
|
|
|
[String(group._id), ...textPanelIds],
|
|
|
userId
|
|
|
);
|
|
|
|
|
|
return this.transformDocuments(ctx, {}, group);
|
|
|
}
|
|
|
|
|
|
async getUserGroups(ctx: TcContext): Promise<Group[]> {
|
|
|
const userId = ctx.meta.userId;
|
|
|
|
|
|
const groups = await this.adapter.model.getUserGroups(userId);
|
|
|
|
|
|
return this.transformDocuments(ctx, {}, groups);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取用户所有加入群组的群组id列表与聊天会话id列表
|
|
|
*/
|
|
|
async getJoinedGroupAndPanelIds(ctx: TcContext): Promise<{
|
|
|
groupIds: string[];
|
|
|
panelIds: string[];
|
|
|
}> {
|
|
|
const groups = await this.getUserGroups(ctx); // TODO: 应该使用call而不是直接调用,为了获取tracer和caching支持。目前moleculer的文档没有显式的声明类似localCall的行为,可以花时间看一下
|
|
|
const panelIds = _.flatten(groups.map((g) => this.getGroupTextPanelIds(g)));
|
|
|
|
|
|
return {
|
|
|
groupIds: groups.map((g) => String(g._id)),
|
|
|
panelIds,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取群组基本信息
|
|
|
*/
|
|
|
async getGroupBasicInfo(
|
|
|
ctx: PureContext<{
|
|
|
groupId: string;
|
|
|
}>
|
|
|
): Promise<GroupBaseInfo> {
|
|
|
const group = await this.adapter.model
|
|
|
.findById(ctx.params.groupId, {
|
|
|
name: 1,
|
|
|
avatar: 1,
|
|
|
owner: 1,
|
|
|
members: 1,
|
|
|
})
|
|
|
.exec();
|
|
|
|
|
|
if (group === null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
const groupMemberCount = group.members.length;
|
|
|
|
|
|
return {
|
|
|
name: group.name,
|
|
|
avatar: group.avatar,
|
|
|
owner: String(group.owner),
|
|
|
memberCount: groupMemberCount,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取群组完整信息
|
|
|
* 仅内部可以访问
|
|
|
*/
|
|
|
async getGroupInfo(ctx: TcContext<{ groupId: string }>): Promise<Group> {
|
|
|
const groupInfo = await this.adapter.model.findById(ctx.params.groupId);
|
|
|
|
|
|
return await this.transformDocuments(ctx, {}, groupInfo);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 修改群组字段
|
|
|
*/
|
|
|
async updateGroupField(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
fieldName: string;
|
|
|
fieldValue: unknown;
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, fieldName, fieldValue } = ctx.params;
|
|
|
const userId = ctx.meta.userId;
|
|
|
const t = ctx.meta.t;
|
|
|
if (
|
|
|
!['name', 'avatar', 'panels', 'roles', 'fallbackPermissions'].includes(
|
|
|
fieldName
|
|
|
)
|
|
|
) {
|
|
|
throw new EntityError(t('该数据不允许修改'));
|
|
|
}
|
|
|
|
|
|
const [isGroupOwner, hasRolePermission] = await call(
|
|
|
ctx
|
|
|
).checkUserPermissions(groupId, userId, [
|
|
|
PERMISSION.core.owner,
|
|
|
PERMISSION.core.manageRoles,
|
|
|
]);
|
|
|
|
|
|
if (fieldName === 'fallbackPermissions') {
|
|
|
if (!hasRolePermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
} else if (!isGroupOwner) {
|
|
|
throw new NoPermissionError(t('不是群组管理员无法编辑'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model.findById(groupId).exec();
|
|
|
|
|
|
group[fieldName] = fieldValue;
|
|
|
await group.save();
|
|
|
|
|
|
if (fieldName === 'fallbackPermissions') {
|
|
|
await this.cleanGroupAllUserPermissionCache(groupId);
|
|
|
}
|
|
|
|
|
|
this.notifyGroupInfoUpdate(ctx, group);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 修改群组配置
|
|
|
*/
|
|
|
async updateGroupConfig(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
configName: string;
|
|
|
configValue: unknown;
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, configName, configValue } = ctx.params;
|
|
|
const userId = ctx.meta.userId;
|
|
|
const t = ctx.meta.t;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.groupConfig]
|
|
|
);
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model.findById(groupId).exec();
|
|
|
group.config[configName] = configValue;
|
|
|
await group.save();
|
|
|
|
|
|
this.notifyGroupInfoUpdate(ctx, group);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 检测用户是否为群组所有者
|
|
|
*/
|
|
|
async isGroupOwner(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
}>
|
|
|
): Promise<boolean> {
|
|
|
const t = ctx.meta.t;
|
|
|
const group = await this.adapter.model.findById(ctx.params.groupId);
|
|
|
if (!group) {
|
|
|
throw new DataNotFoundError(t('没有找到群组'));
|
|
|
}
|
|
|
|
|
|
return String(group.owner) === ctx.meta.userId;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 加入群组
|
|
|
*/
|
|
|
async joinGroup(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
}>
|
|
|
) {
|
|
|
const groupId = ctx.params.groupId;
|
|
|
const userId = ctx.meta.userId;
|
|
|
|
|
|
if (!isValidStr(userId)) {
|
|
|
throw new EntityError('用户id为空');
|
|
|
}
|
|
|
|
|
|
if (!isValidStr(groupId)) {
|
|
|
throw new EntityError('群组id为空');
|
|
|
}
|
|
|
|
|
|
const { members } = await this.adapter.model.findById(groupId, {
|
|
|
members: 1,
|
|
|
});
|
|
|
if (members.findIndex((m) => String(m.userId) === userId) >= 0) {
|
|
|
throw new Error('已加入该群组');
|
|
|
}
|
|
|
|
|
|
const doc = await this.adapter.model
|
|
|
.findByIdAndUpdate(
|
|
|
groupId,
|
|
|
{
|
|
|
$addToSet: {
|
|
|
members: {
|
|
|
userId: new Types.ObjectId(userId),
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
new: true,
|
|
|
}
|
|
|
)
|
|
|
.exec();
|
|
|
|
|
|
const group: Group = await this.transformDocuments(ctx, {}, doc);
|
|
|
|
|
|
this.notifyGroupInfoUpdate(ctx, group); // 推送变更
|
|
|
this.unicastNotify(ctx, userId, 'add', group);
|
|
|
|
|
|
const textPanelIds = this.getGroupTextPanelIds(group);
|
|
|
|
|
|
await call(ctx).joinSocketIORoom(
|
|
|
[String(group._id), ...textPanelIds],
|
|
|
userId
|
|
|
);
|
|
|
|
|
|
return group;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 退出群组
|
|
|
*/
|
|
|
async quitGroup(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
}>
|
|
|
) {
|
|
|
const groupId = ctx.params.groupId;
|
|
|
const userId = ctx.meta.userId;
|
|
|
|
|
|
const group = await this.adapter.findById(groupId);
|
|
|
if (String(group.owner) === userId) {
|
|
|
// 是群组所有人
|
|
|
await this.adapter.removeById(groupId); // TODO: 后续可以考虑改为软删除
|
|
|
await this.roomcastNotify(ctx, groupId, 'remove', { groupId });
|
|
|
await ctx.call('gateway.leaveRoom', {
|
|
|
roomIds: [groupId],
|
|
|
});
|
|
|
} else {
|
|
|
// 是普通群组成员
|
|
|
const doc = await this.adapter.model
|
|
|
.findByIdAndUpdate(
|
|
|
groupId,
|
|
|
{
|
|
|
$pull: {
|
|
|
members: {
|
|
|
userId: new Types.ObjectId(userId),
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
new: true,
|
|
|
}
|
|
|
)
|
|
|
.exec();
|
|
|
|
|
|
const group: Group = await this.transformDocuments(ctx, {}, doc);
|
|
|
|
|
|
await this.memberLeaveGroup(ctx, group, userId);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 检查是否为群组成员
|
|
|
*/
|
|
|
async isMember(ctx: TcContext<{ groupId: string }>) {
|
|
|
const groupId = ctx.params.groupId;
|
|
|
const userId = ctx.meta.userId;
|
|
|
|
|
|
const groupInfo = await call(ctx).getGroupInfo(groupId);
|
|
|
if (!groupInfo) {
|
|
|
// 没有找到群组信息
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
const members = groupInfo.members;
|
|
|
|
|
|
return members.some((m) => String(m.userId) === userId);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 追加群组成员的角色
|
|
|
*/
|
|
|
async appendGroupMemberRoles(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
memberIds: string[];
|
|
|
roles: string[];
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, memberIds, roles } = ctx.params;
|
|
|
const { userId } = ctx.meta;
|
|
|
|
|
|
await Promise.all(
|
|
|
memberIds.map((memberId) =>
|
|
|
this.adapter.model.updateGroupMemberField(
|
|
|
ctx,
|
|
|
groupId,
|
|
|
memberId,
|
|
|
'roles',
|
|
|
(member) =>
|
|
|
(member['roles'] = _.uniq([...member['roles'], ...roles])),
|
|
|
userId
|
|
|
)
|
|
|
)
|
|
|
);
|
|
|
|
|
|
const group = await this.adapter.model.findById(groupId);
|
|
|
|
|
|
await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
await Promise.all(
|
|
|
memberIds.map((memberId) =>
|
|
|
this.cleanGroupUserPermission(groupId, memberId)
|
|
|
)
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 移除群组成员的角色
|
|
|
*/
|
|
|
async removeGroupMemberRoles(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
memberIds: string[];
|
|
|
roles: string[];
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, memberIds, roles } = ctx.params;
|
|
|
const { userId } = ctx.meta;
|
|
|
|
|
|
await Promise.all(
|
|
|
memberIds.map((memberId) =>
|
|
|
this.adapter.model.updateGroupMemberField(
|
|
|
ctx,
|
|
|
groupId,
|
|
|
memberId,
|
|
|
'roles',
|
|
|
(member) => (member['roles'] = _.without(member['roles'], ...roles)),
|
|
|
userId
|
|
|
)
|
|
|
)
|
|
|
);
|
|
|
|
|
|
const group = await this.adapter.model.findById(groupId);
|
|
|
|
|
|
await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
await Promise.all(
|
|
|
memberIds.map((memberId) =>
|
|
|
this.cleanGroupUserPermission(groupId, memberId)
|
|
|
)
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 创建群组面板
|
|
|
*/
|
|
|
async createGroupPanel(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
name: string;
|
|
|
type: number;
|
|
|
parentId?: string;
|
|
|
provider?: string;
|
|
|
pluginPanelName?: string;
|
|
|
meta?: object;
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, name, type, parentId, provider, pluginPanelName, meta } =
|
|
|
ctx.params;
|
|
|
const { t, userId } = ctx.meta;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.managePanel]
|
|
|
);
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const panelId = String(new Types.ObjectId());
|
|
|
|
|
|
const group = await this.adapter.model
|
|
|
.findOneAndUpdate(
|
|
|
{
|
|
|
_id: new Types.ObjectId(groupId),
|
|
|
},
|
|
|
{
|
|
|
$push: {
|
|
|
panels: {
|
|
|
id: panelId,
|
|
|
name,
|
|
|
type,
|
|
|
parentId,
|
|
|
provider,
|
|
|
pluginPanelName,
|
|
|
meta,
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
new: true,
|
|
|
}
|
|
|
)
|
|
|
.exec();
|
|
|
|
|
|
if (type === 0) {
|
|
|
/**
|
|
|
* 如果为文本面板
|
|
|
* 则所有群组成员加入房间
|
|
|
*/
|
|
|
const groupInfo = await call(ctx).getGroupInfo(groupId);
|
|
|
(groupInfo?.members ?? []).map((m) =>
|
|
|
call(ctx).joinSocketIORoom([panelId], m.userId)
|
|
|
);
|
|
|
}
|
|
|
|
|
|
this.notifyGroupInfoUpdate(ctx, group);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 修改群组面板
|
|
|
*/
|
|
|
async modifyGroupPanel(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
panelId: string;
|
|
|
name: string;
|
|
|
type: number;
|
|
|
provider?: string;
|
|
|
pluginPanelName?: string;
|
|
|
meta?: object;
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, panelId, name, type, provider, pluginPanelName, meta } =
|
|
|
ctx.params;
|
|
|
const { t, userId } = ctx.meta;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.managePanel]
|
|
|
);
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const res = await this.adapter.model
|
|
|
.updateOne(
|
|
|
{
|
|
|
_id: new Types.ObjectId(groupId),
|
|
|
},
|
|
|
{
|
|
|
$set: {
|
|
|
'panels.$[element].name': name,
|
|
|
'panels.$[element].type': type,
|
|
|
'panels.$[element].provider': provider,
|
|
|
'panels.$[element].pluginPanelName': pluginPanelName,
|
|
|
'panels.$[element].meta': meta,
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
new: true,
|
|
|
arrayFilters: [{ 'element.id': panelId }],
|
|
|
}
|
|
|
)
|
|
|
.exec();
|
|
|
|
|
|
if (res.modifiedCount === 0) {
|
|
|
throw new Error(t('没有找到该面板'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model.findById(String(groupId));
|
|
|
|
|
|
const json = await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
return json;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 删除群组面板
|
|
|
*/
|
|
|
async deleteGroupPanel(ctx: TcContext<{ groupId: string; panelId: string }>) {
|
|
|
const { groupId, panelId } = ctx.params;
|
|
|
const { t, userId } = ctx.meta;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.managePanel]
|
|
|
);
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model
|
|
|
.findOneAndUpdate(
|
|
|
{
|
|
|
_id: new Types.ObjectId(groupId),
|
|
|
},
|
|
|
{
|
|
|
$pull: {
|
|
|
panels: {
|
|
|
$or: [
|
|
|
{
|
|
|
id: new Types.ObjectId(panelId),
|
|
|
},
|
|
|
{
|
|
|
parentId: new Types.ObjectId(panelId),
|
|
|
},
|
|
|
],
|
|
|
} as any,
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
new: true,
|
|
|
}
|
|
|
)
|
|
|
.exec();
|
|
|
|
|
|
const json = await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
return json;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取群组大厅的会话ID()
|
|
|
*/
|
|
|
async getGroupLobbyConverseId(ctx: TcContext<{ groupId: string }>) {
|
|
|
const groupId = ctx.params.groupId;
|
|
|
const t = ctx.meta.t;
|
|
|
|
|
|
const group = await this.adapter.model.findById(groupId);
|
|
|
if (!group) {
|
|
|
throw new DataNotFoundError(t('群组未找到'));
|
|
|
}
|
|
|
|
|
|
const firstTextPanel = group.panels.find(
|
|
|
(panel) => panel.type === GroupPanelType.TEXT
|
|
|
);
|
|
|
|
|
|
if (!firstTextPanel) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
return firstTextPanel.id;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 创建群组角色
|
|
|
*/
|
|
|
async createGroupRole(
|
|
|
ctx: TcContext<{ groupId: string; roleName: string; permissions: string[] }>
|
|
|
) {
|
|
|
const { groupId, roleName, permissions } = ctx.params;
|
|
|
const { userId, t } = ctx.meta;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.managePanel]
|
|
|
);
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model
|
|
|
.findOneAndUpdate(
|
|
|
{
|
|
|
_id: new Types.ObjectId(groupId),
|
|
|
},
|
|
|
{
|
|
|
$push: {
|
|
|
roles: {
|
|
|
name: roleName,
|
|
|
permissions,
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
new: true,
|
|
|
}
|
|
|
)
|
|
|
.exec();
|
|
|
|
|
|
const json = await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
return json;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 删除群组角色
|
|
|
*/
|
|
|
async deleteGroupRole(ctx: TcContext<{ groupId: string; roleId: string }>) {
|
|
|
const { groupId, roleId } = ctx.params;
|
|
|
const { userId, t } = ctx.meta;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.manageRoles]
|
|
|
);
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model
|
|
|
.findOneAndUpdate(
|
|
|
{
|
|
|
_id: new Types.ObjectId(groupId),
|
|
|
},
|
|
|
{
|
|
|
$pull: {
|
|
|
roles: {
|
|
|
_id: roleId,
|
|
|
}, // 删除角色
|
|
|
'members.$[].roles': roleId, // 删除成员角色
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
new: true,
|
|
|
}
|
|
|
)
|
|
|
.exec();
|
|
|
|
|
|
const json = await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
return json;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 更新群组角色权限
|
|
|
*/
|
|
|
async updateGroupRoleName(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
roleId: string;
|
|
|
roleName: string;
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, roleId, roleName } = ctx.params;
|
|
|
const { userId, t } = ctx.meta;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.managePanel]
|
|
|
);
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model.updateGroupRoleName(
|
|
|
groupId,
|
|
|
roleId,
|
|
|
roleName
|
|
|
);
|
|
|
|
|
|
const json = await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
return json;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 更新群组角色权限
|
|
|
*/
|
|
|
async updateGroupRolePermission(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
roleId: string;
|
|
|
permissions: string[];
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, roleId, permissions } = ctx.params;
|
|
|
const { userId, t } = ctx.meta;
|
|
|
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.managePanel]
|
|
|
);
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model.updateGroupRolePermission(
|
|
|
groupId,
|
|
|
roleId,
|
|
|
permissions
|
|
|
);
|
|
|
|
|
|
const json = await this.notifyGroupInfoUpdate(ctx, group);
|
|
|
return json;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取群组成员权限(对外)
|
|
|
*/
|
|
|
async getPermissions(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
}>
|
|
|
): Promise<string[]> {
|
|
|
const { groupId } = ctx.params;
|
|
|
const userId = ctx.meta.userId;
|
|
|
|
|
|
const permissions = await this.localCall('getUserAllPermissions', {
|
|
|
groupId,
|
|
|
userId,
|
|
|
});
|
|
|
|
|
|
return permissions;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取群组成员权限(对内)
|
|
|
* 带有groupId和userId的缓存
|
|
|
*/
|
|
|
async getUserAllPermissions(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
userId: string;
|
|
|
}>
|
|
|
): Promise<string[]> {
|
|
|
const { groupId, userId } = ctx.params;
|
|
|
const permissions = await this.adapter.model.getGroupUserPermission(
|
|
|
groupId,
|
|
|
userId
|
|
|
);
|
|
|
|
|
|
return permissions;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 禁言群组成员
|
|
|
*/
|
|
|
async muteGroupMember(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
memberId: string;
|
|
|
muteMs: number; // 禁言多少多少毫秒. 精确到ms, 如果小于0则视为解除禁言
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, memberId, muteMs } = ctx.params;
|
|
|
const userId = ctx.meta.userId;
|
|
|
const language = ctx.meta.language;
|
|
|
const isUnmute = muteMs < 0;
|
|
|
|
|
|
const group = await this.adapter.model.updateGroupMemberField(
|
|
|
ctx,
|
|
|
groupId,
|
|
|
memberId,
|
|
|
'muteUntil',
|
|
|
isUnmute ? undefined : new Date(new Date().valueOf() + muteMs),
|
|
|
userId
|
|
|
);
|
|
|
|
|
|
this.notifyGroupInfoUpdate(ctx, group);
|
|
|
|
|
|
const memberInfo = await call(ctx).getUserInfo(memberId);
|
|
|
if (isUnmute) {
|
|
|
await call(ctx).addGroupSystemMessage(
|
|
|
groupId,
|
|
|
`${ctx.meta.user.nickname} 解除了 ${memberInfo.nickname} 的禁言`
|
|
|
);
|
|
|
} else {
|
|
|
await call(ctx).addGroupSystemMessage(
|
|
|
groupId,
|
|
|
`${ctx.meta.user.nickname} 禁言了 ${memberInfo.nickname} ${moment
|
|
|
.duration(muteMs, 'ms')
|
|
|
.locale(language)
|
|
|
.humanize()}`
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async deleteGroupMember(
|
|
|
ctx: TcContext<{
|
|
|
groupId: string;
|
|
|
memberId: string;
|
|
|
}>
|
|
|
) {
|
|
|
const { groupId, memberId } = ctx.params;
|
|
|
const userId = ctx.meta.userId;
|
|
|
const t = ctx.meta.t;
|
|
|
|
|
|
// 检查是否在踢自己
|
|
|
if (String(memberId) === String(userId)) {
|
|
|
throw new Error(t('不允许踢出自己'));
|
|
|
}
|
|
|
|
|
|
// 检查是否有权限
|
|
|
const [hasPermission] = await call(ctx).checkUserPermissions(
|
|
|
groupId,
|
|
|
userId,
|
|
|
[PERMISSION.core.manageUser]
|
|
|
);
|
|
|
if (!hasPermission) {
|
|
|
throw new NoPermissionError(t('没有操作权限'));
|
|
|
}
|
|
|
|
|
|
// 检查是否踢出了不该踢出的人
|
|
|
const groupInfo = await call(ctx).getGroupInfo(groupId);
|
|
|
if (String(memberId) === String(groupInfo.owner)) {
|
|
|
throw new Error(t('不允许踢出群组OP'));
|
|
|
}
|
|
|
|
|
|
const group = await this.adapter.model.findByIdAndUpdate(
|
|
|
groupId,
|
|
|
{
|
|
|
$pull: {
|
|
|
members: {
|
|
|
userId: String(memberId),
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
{ new: true }
|
|
|
);
|
|
|
|
|
|
await this.memberLeaveGroup(ctx, group, memberId);
|
|
|
|
|
|
const memberInfo = await call(ctx).getUserInfo(memberId);
|
|
|
await call(ctx).addGroupSystemMessage(
|
|
|
groupId,
|
|
|
`${ctx.meta.user.nickname} 将 ${memberInfo.nickname} 移出了本群组`
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 退出群组流程
|
|
|
* 用于踢出群组成员和主动退出群组
|
|
|
*
|
|
|
* 先将自己退出房间, 然后再进行房间级别通知
|
|
|
*/
|
|
|
private async memberLeaveGroup(
|
|
|
ctx: TcContext,
|
|
|
group: Group,
|
|
|
memberId: string
|
|
|
) {
|
|
|
const groupId = String(group._id);
|
|
|
|
|
|
await ctx.call('gateway.leaveRoom', {
|
|
|
roomIds: [
|
|
|
groupId,
|
|
|
...group.panels
|
|
|
.filter((p) => p.type === GroupPanelType.TEXT)
|
|
|
.map((p) => p.id),
|
|
|
], // 离开群组和所有面板房间
|
|
|
memberId,
|
|
|
});
|
|
|
await Promise.all([
|
|
|
this.unicastNotify(ctx, memberId, 'remove', { groupId }),
|
|
|
this.notifyGroupInfoUpdate(ctx, group),
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 发送通知群组信息发生变更
|
|
|
*
|
|
|
* 发送通知时会同时清空群组信息缓存
|
|
|
*/
|
|
|
private async notifyGroupInfoUpdate(
|
|
|
ctx: TcContext,
|
|
|
group: Group
|
|
|
): Promise<Group> {
|
|
|
const groupId = String(group._id);
|
|
|
let json = group;
|
|
|
if (_.isPlainObject(group) === false) {
|
|
|
// 当传入的数据为group doc时
|
|
|
json = await this.transformDocuments(ctx, {}, group);
|
|
|
}
|
|
|
|
|
|
this.cleanGroupInfoCache(groupId);
|
|
|
await this.roomcastNotify(ctx, groupId, 'updateInfo', json);
|
|
|
|
|
|
return json;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 清理群组缓存
|
|
|
* @param groupId 群组id
|
|
|
*/
|
|
|
private cleanGroupInfoCache(groupId: string) {
|
|
|
this.cleanActionCache('getGroupInfo', [groupId]);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 清理群组用户缓存
|
|
|
* @param groupId 群组id
|
|
|
* @param userId 用户id
|
|
|
*/
|
|
|
private cleanGroupUserPermission(groupId: string, userId: string) {
|
|
|
this.cleanActionCache('getUserAllPermissions', [groupId, userId]);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 清理群组所有用户缓存
|
|
|
* @param groupId 群组id
|
|
|
*/
|
|
|
private cleanGroupAllUserPermissionCache(groupId: string) {
|
|
|
this.cleanActionCache('getUserAllPermissions', [groupId]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export default GroupService;
|