You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
844 lines
26 KiB
844 lines
26 KiB
import { Db, FindOneOptions, FilterQuery } from 'mongodb';
|
|
import { escapeRegExp } from '@rocket.chat/string-helpers';
|
|
|
|
import { checkUsernameAvailability } from '../../../app/lib/server/functions';
|
|
import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom';
|
|
import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUserFromRoom';
|
|
import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner';
|
|
import { MessagesRaw } from '../../../app/models/server/raw/Messages';
|
|
import { RoomsRaw } from '../../../app/models/server/raw/Rooms';
|
|
import { SubscriptionsRaw } from '../../../app/models/server/raw/Subscriptions';
|
|
import { TeamRaw } from '../../../app/models/server/raw/Team';
|
|
import { TeamMemberRaw } from '../../../app/models/server/raw/TeamMember';
|
|
import { UsersRaw } from '../../../app/models/server/raw/Users';
|
|
import { IRoom } from '../../../definition/IRoom';
|
|
import {
|
|
IPaginationOptions,
|
|
IQueryOptions,
|
|
IRecordsWithTotal,
|
|
ITeam,
|
|
ITeamMember,
|
|
ITeamStats,
|
|
TEAM_TYPE,
|
|
} from '../../../definition/ITeam';
|
|
import { IUser } from '../../../definition/IUser';
|
|
import { Room } from '../../sdk';
|
|
import {
|
|
IListRoomsFilter,
|
|
ITeamCreateParams,
|
|
ITeamInfo,
|
|
ITeamMemberInfo,
|
|
ITeamMemberParams,
|
|
ITeamService,
|
|
ITeamUpdateData,
|
|
} from '../../sdk/types/ITeamService';
|
|
import { ServiceClass } from '../../sdk/types/ServiceClass';
|
|
import { canAccessRoom } from '../authorization/canAccessRoom';
|
|
import { saveRoomName } from '../../../app/channel-settings/server';
|
|
import { saveRoomType } from '../../../app/channel-settings/server/functions/saveRoomType';
|
|
|
|
export class TeamService extends ServiceClass implements ITeamService {
|
|
protected name = 'team';
|
|
|
|
private TeamModel: TeamRaw;
|
|
|
|
private RoomsModel: RoomsRaw;
|
|
|
|
private SubscriptionsModel: SubscriptionsRaw;
|
|
|
|
private Users: UsersRaw;
|
|
|
|
private TeamMembersModel: TeamMemberRaw;
|
|
|
|
private MessagesModel: MessagesRaw;
|
|
|
|
constructor(db: Db) {
|
|
super();
|
|
|
|
this.RoomsModel = new RoomsRaw(db.collection('rocketchat_room'));
|
|
this.SubscriptionsModel = new SubscriptionsRaw(db.collection('rocketchat_subscription'));
|
|
this.TeamModel = new TeamRaw(db.collection('rocketchat_team'));
|
|
this.TeamMembersModel = new TeamMemberRaw(db.collection('rocketchat_team_member'));
|
|
this.Users = new UsersRaw(db.collection('users'));
|
|
this.MessagesModel = new MessagesRaw(db.collection('rocketchat_message'));
|
|
}
|
|
|
|
async create(uid: string, { team, room = { name: team.name, extraData: {} }, members, owner }: ITeamCreateParams): Promise<ITeam> {
|
|
if (!checkUsernameAvailability(team.name)) {
|
|
throw new Error('team-name-already-exists');
|
|
}
|
|
|
|
const existingRoom = await this.RoomsModel.findOneByName(team.name, { projection: { _id: 1 } });
|
|
if (existingRoom && existingRoom._id !== room.id) {
|
|
throw new Error('room-name-already-exists');
|
|
}
|
|
|
|
const createdBy = await this.Users.findOneById(uid, { projection: { username: 1 } });
|
|
if (!createdBy) {
|
|
throw new Error('invalid-user');
|
|
}
|
|
|
|
// TODO add validations to `data` and `members`
|
|
|
|
const membersResult = await this.Users.findActiveByIds(members, { projection: { username: 1, _id: 0 } }).toArray();
|
|
const memberUsernames = membersResult.map(({ username }) => username);
|
|
|
|
const teamData = {
|
|
...team,
|
|
createdAt: new Date(),
|
|
createdBy,
|
|
_updatedAt: new Date(), // TODO how to avoid having to do this?
|
|
roomId: '', // this will be populated at the end
|
|
};
|
|
|
|
try {
|
|
const result = await this.TeamModel.insertOne(teamData);
|
|
const teamId = result.insertedId;
|
|
// the same uid can be passed at 3 positions: owner, member list or via caller
|
|
// if the owner is present, remove it from the members list
|
|
// if the owner is not present, remove the caller from the members list
|
|
const excludeFromMembers = owner ? [owner] : [uid];
|
|
|
|
// filter empty strings and falsy values from members list
|
|
const membersList: Array<Omit<ITeamMember, '_id'>> = members?.filter(Boolean)
|
|
.filter((memberId) => !excludeFromMembers.includes(memberId))
|
|
.map((memberId) => ({
|
|
teamId,
|
|
userId: memberId,
|
|
createdAt: new Date(),
|
|
createdBy,
|
|
_updatedAt: new Date(), // TODO how to avoid having to do this?
|
|
})) || [];
|
|
|
|
membersList.push({
|
|
teamId,
|
|
userId: owner || uid,
|
|
roles: ['owner'],
|
|
createdAt: new Date(),
|
|
createdBy,
|
|
_updatedAt: new Date(), // TODO how to avoid having to do this?
|
|
});
|
|
|
|
await this.TeamMembersModel.insertMany(membersList);
|
|
|
|
let roomId = room.id;
|
|
if (roomId) {
|
|
await this.RoomsModel.setTeamMainById(roomId, teamId);
|
|
} else {
|
|
const roomType: IRoom['t'] = team.type === TEAM_TYPE.PRIVATE ? 'p' : 'c';
|
|
|
|
const newRoom = {
|
|
...room,
|
|
type: roomType,
|
|
name: team.name,
|
|
members: memberUsernames,
|
|
extraData: {
|
|
...room.extraData,
|
|
teamId,
|
|
teamMain: true,
|
|
},
|
|
};
|
|
|
|
const createdRoom = await Room.create(owner || uid, newRoom);
|
|
roomId = createdRoom._id;
|
|
}
|
|
|
|
await this.TeamModel.updateMainRoomForTeam(teamId, roomId);
|
|
teamData.roomId = roomId;
|
|
|
|
return {
|
|
_id: teamId,
|
|
...teamData,
|
|
};
|
|
} catch (e) {
|
|
throw new Error('error-team-creation');
|
|
}
|
|
}
|
|
|
|
async update(uid: string, teamId: string, updateData: ITeamUpdateData): Promise<void> {
|
|
const team = await this.TeamModel.findOneById(teamId, { projection: { roomId: 1 } });
|
|
if (!team) {
|
|
return;
|
|
}
|
|
|
|
const user = await this.Users.findOneById(uid);
|
|
if (!user) {
|
|
return;
|
|
}
|
|
|
|
const { name, type, updateRoom = true } = updateData;
|
|
|
|
if (updateRoom && name) {
|
|
saveRoomName(team.roomId, name, user);
|
|
}
|
|
|
|
if (updateRoom && typeof type !== 'undefined') {
|
|
saveRoomType(team.roomId, type === TEAM_TYPE.PRIVATE ? 'p' : 'c', user);
|
|
}
|
|
|
|
await this.TeamModel.updateNameAndType(teamId, updateData);
|
|
}
|
|
|
|
async findBySubscribedUserIds(userId: string, callerId?: string): Promise<ITeam[]> {
|
|
const unfilteredTeams = await this.TeamMembersModel.findByUserId(userId, { projection: { teamId: 1, roles: 1 } }).toArray();
|
|
const unfilteredTeamIds = unfilteredTeams.map(({ teamId }) => teamId);
|
|
|
|
let teamIds;
|
|
|
|
if (callerId) {
|
|
const publicTeams = await this.TeamModel.findByIdsAndType(unfilteredTeamIds, TEAM_TYPE.PUBLIC, { projection: { teamId: 1 } }).toArray();
|
|
const publicTeamIds = publicTeams.map(({ _id }) => _id);
|
|
const privateTeamIds = unfilteredTeamIds.filter((teamId) => !publicTeamIds.includes(teamId));
|
|
|
|
const privateTeams = await this.TeamMembersModel.findByUserIdAndTeamIds(callerId, privateTeamIds, { projection: { teamId: 1 } }).toArray();
|
|
const visibleTeamIds = privateTeams.map(({ teamId }) => teamId).concat(publicTeamIds);
|
|
teamIds = unfilteredTeamIds.filter((teamId) => visibleTeamIds.includes(teamId));
|
|
} else {
|
|
teamIds = unfilteredTeamIds;
|
|
}
|
|
|
|
const ownedTeams = unfilteredTeams.filter(({ roles = [] }) => roles.includes('owner')).map(({ teamId }) => teamId);
|
|
|
|
const results = await this.TeamModel.findByIds(teamIds).toArray();
|
|
return results.map((team) => ({
|
|
...team,
|
|
isOwner: ownedTeams.includes(team._id),
|
|
}));
|
|
}
|
|
|
|
async search(userId: string, term: string | RegExp, options?: FindOneOptions<ITeam>): Promise<ITeam[]> {
|
|
if (typeof term === 'string') {
|
|
term = new RegExp(`^${ escapeRegExp(term) }`, 'i');
|
|
}
|
|
|
|
const userTeams = await this.TeamMembersModel.findByUserId(userId, { projection: { teamId: 1 } }).toArray();
|
|
const teamIds = userTeams.map(({ teamId }) => teamId);
|
|
|
|
return this.TeamModel.findByNameAndTeamIds(term, teamIds, options).toArray();
|
|
}
|
|
|
|
async list(uid: string, { offset, count }: IPaginationOptions = { offset: 0, count: 50 }, { sort, query }: IQueryOptions<ITeam> = { sort: {} }): Promise<IRecordsWithTotal<ITeamInfo>> {
|
|
const userTeams = await this.TeamMembersModel.findByUserId(uid, { projection: { teamId: 1 } }).toArray();
|
|
|
|
const teamIds = userTeams.map(({ teamId }) => teamId);
|
|
if (teamIds.length === 0) {
|
|
return {
|
|
total: 0,
|
|
records: [],
|
|
};
|
|
}
|
|
|
|
const cursor = this.TeamModel.findByIds(teamIds, {
|
|
sort,
|
|
limit: count,
|
|
skip: offset,
|
|
}, query);
|
|
|
|
const records = await cursor.toArray();
|
|
const results: ITeamInfo[] = [];
|
|
for await (const record of records) {
|
|
const rooms = this.RoomsModel.findByTeamId(record._id);
|
|
const users = this.TeamMembersModel.findByTeamId(record._id);
|
|
results.push({
|
|
...record,
|
|
rooms: await rooms.count(),
|
|
numberOfUsers: await users.count(),
|
|
});
|
|
}
|
|
|
|
return {
|
|
total: await cursor.count(),
|
|
records: results,
|
|
};
|
|
}
|
|
|
|
async listAll({ offset, count }: IPaginationOptions = { offset: 0, count: 50 }): Promise<IRecordsWithTotal<ITeamInfo>> {
|
|
const cursor = this.TeamModel.find({}, {
|
|
limit: count,
|
|
skip: offset,
|
|
});
|
|
|
|
const records = await cursor.toArray();
|
|
|
|
const results: ITeamInfo[] = [];
|
|
for await (const record of records) {
|
|
const rooms = this.RoomsModel.findByTeamId(record._id);
|
|
const users = this.TeamMembersModel.findByTeamId(record._id);
|
|
results.push({
|
|
...record,
|
|
rooms: await rooms.count(),
|
|
numberOfUsers: await users.count(),
|
|
});
|
|
}
|
|
|
|
return {
|
|
total: await cursor.count(),
|
|
records: results,
|
|
};
|
|
}
|
|
|
|
async listByNames(names: Array<string>, options?: FindOneOptions<ITeam>): Promise<ITeam[]> {
|
|
return this.TeamModel.findByNames(names, options).toArray();
|
|
}
|
|
|
|
async listByIds(ids: Array<string>, options?: FindOneOptions<ITeam>): Promise<ITeam[]> {
|
|
return this.TeamModel.findByIds(ids, options).toArray();
|
|
}
|
|
|
|
async addRooms(uid: string, rooms: Array<string>, teamId: string): Promise<Array<IRoom>> {
|
|
if (!teamId) {
|
|
throw new Error('missing-teamId');
|
|
}
|
|
if (!rooms) {
|
|
throw new Error('missing-rooms');
|
|
}
|
|
if (!uid) {
|
|
throw new Error('missing-userId');
|
|
}
|
|
|
|
const team = await this.TeamModel.findOneById(teamId, { projection: { _id: 1 } });
|
|
if (!team) {
|
|
throw new Error('invalid-team');
|
|
}
|
|
|
|
// at this point, we already checked for the permission
|
|
// so we just need to check if the user can see the room
|
|
const user = await this.Users.findOneById(uid);
|
|
const rids = rooms.filter((rid) => rid && typeof rid === 'string');
|
|
const validRooms = await this.RoomsModel.findManyByRoomIds(rids).toArray();
|
|
if (validRooms.length < rids.length) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
|
|
// validate access for every room first
|
|
for await (const room of validRooms) {
|
|
const canSeeRoom = await canAccessRoom(room, user);
|
|
if (!canSeeRoom) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
}
|
|
|
|
for await (const room of validRooms) {
|
|
if (room.teamId) {
|
|
throw new Error('room-already-on-team');
|
|
}
|
|
|
|
if (!await this.SubscriptionsModel.isUserInRole(uid, 'owner', room._id)) {
|
|
throw new Error('error-no-owner-channel');
|
|
}
|
|
|
|
room.teamId = teamId;
|
|
}
|
|
|
|
this.RoomsModel.setTeamByIds(rids, teamId);
|
|
return validRooms;
|
|
}
|
|
|
|
async removeRoom(uid: string, rid: string, teamId: string, canRemoveAnyRoom = false): Promise<IRoom> {
|
|
if (!teamId) {
|
|
throw new Error('missing-teamId');
|
|
}
|
|
if (!rid) {
|
|
throw new Error('missing-roomId');
|
|
}
|
|
if (!uid) {
|
|
throw new Error('missing-userId');
|
|
}
|
|
|
|
const room = await this.RoomsModel.findOneById(rid);
|
|
if (!room) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
|
|
if (!canRemoveAnyRoom) {
|
|
const user = await this.Users.findOneById(uid);
|
|
const canSeeRoom = await canAccessRoom(room, user);
|
|
if (!canSeeRoom) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
}
|
|
|
|
const team = await this.TeamModel.findOneById(teamId, { projection: { _id: 1 } });
|
|
if (!team) {
|
|
throw new Error('invalid-team');
|
|
}
|
|
|
|
if (room.teamId !== teamId) {
|
|
throw new Error('room-not-on-that-team');
|
|
}
|
|
|
|
delete room.teamId;
|
|
delete room.teamDefault;
|
|
this.RoomsModel.unsetTeamById(room._id);
|
|
return {
|
|
...room,
|
|
};
|
|
}
|
|
|
|
async unsetTeamIdOfRooms(teamId: string): Promise<void> {
|
|
if (!teamId) {
|
|
throw new Error('missing-teamId');
|
|
}
|
|
|
|
await this.RoomsModel.unsetTeamId(teamId);
|
|
}
|
|
|
|
async updateRoom(uid: string, rid: string, isDefault: boolean, canUpdateAnyRoom = false): Promise<IRoom> {
|
|
if (!rid) {
|
|
throw new Error('missing-roomId');
|
|
}
|
|
if (!uid) {
|
|
throw new Error('missing-userId');
|
|
}
|
|
|
|
const room = await this.RoomsModel.findOneById(rid);
|
|
if (!room) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
|
|
const user = await this.Users.findOneById(uid);
|
|
if (!canUpdateAnyRoom) {
|
|
const canSeeRoom = await canAccessRoom(room, user);
|
|
if (!canSeeRoom) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
}
|
|
|
|
if (!room.teamId) {
|
|
throw new Error('room-not-on-team');
|
|
}
|
|
room.teamDefault = isDefault;
|
|
this.RoomsModel.setTeamDefaultById(rid, isDefault);
|
|
|
|
if (room.teamDefault) {
|
|
const teamMembers = await this.members(uid, room.teamId, true, undefined, undefined);
|
|
|
|
teamMembers.records.map((m) => addUserToRoom(room._id, m.user, user));
|
|
}
|
|
|
|
return {
|
|
...room,
|
|
};
|
|
}
|
|
|
|
async listTeamsBySubscriberUserId(uid: string, options?: FindOneOptions<ITeamMember>): Promise<Array<ITeamMember> | null> {
|
|
return this.TeamMembersModel.findByUserId(uid, options).toArray();
|
|
}
|
|
|
|
async listRooms(uid: string, teamId: string, filter: IListRoomsFilter, { offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 }): Promise<IRecordsWithTotal<IRoom>> {
|
|
if (!teamId) {
|
|
throw new Error('missing-teamId');
|
|
}
|
|
const team = await this.TeamModel.findOneById(teamId, { projection: { _id: 1, type: 1 } });
|
|
if (!team) {
|
|
throw new Error('invalid-team');
|
|
}
|
|
|
|
const { getAllRooms, allowPrivateTeam, name, isDefault } = filter;
|
|
|
|
const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(uid, teamId);
|
|
if (team.type === TEAM_TYPE.PRIVATE && !allowPrivateTeam && !isMember) {
|
|
throw new Error('user-not-on-private-team');
|
|
}
|
|
|
|
if (getAllRooms) {
|
|
const teamRoomsCursor = this.RoomsModel.findByTeamIdContainingNameAndDefault(teamId, name, isDefault, undefined, { skip, limit });
|
|
return {
|
|
total: await teamRoomsCursor.count(),
|
|
records: await teamRoomsCursor.toArray(),
|
|
};
|
|
}
|
|
|
|
const user = await this.Users.findOneById(uid, { fields: { __rooms: 1 } });
|
|
const userRooms = user.__rooms;
|
|
const validTeamRoomsCursor = this.RoomsModel.findByTeamIdContainingNameAndDefault(teamId, name, isDefault, userRooms, { skip, limit });
|
|
|
|
return {
|
|
total: await validTeamRoomsCursor.count(),
|
|
records: await validTeamRoomsCursor.toArray(),
|
|
};
|
|
}
|
|
|
|
async listRoomsOfUser(uid: string, teamId: string, userId: string, allowPrivateTeam: boolean, { offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 }): Promise<IRecordsWithTotal<IRoom>> {
|
|
if (!teamId) {
|
|
throw new Error('missing-teamId');
|
|
}
|
|
const team = await this.TeamModel.findOneById(teamId, {});
|
|
if (!team) {
|
|
throw new Error('invalid-team');
|
|
}
|
|
const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(uid, teamId);
|
|
if (team.type === TEAM_TYPE.PRIVATE && !allowPrivateTeam && !isMember) {
|
|
throw new Error('user-not-on-private-team');
|
|
}
|
|
|
|
const teamRooms = await this.RoomsModel.findByTeamId(teamId, { projection: { _id: 1, t: 1 } }).toArray();
|
|
const teamRoomIds = teamRooms.filter((room) => room.t === 'p' || room.t === 'c').map((room) => room._id);
|
|
|
|
const subscriptionsCursor = this.SubscriptionsModel.findByUserIdAndRoomIds(userId, teamRoomIds);
|
|
const subscriptionRoomIds = (await subscriptionsCursor.toArray()).map((subscription) => subscription.rid);
|
|
const availableRoomsCursor = this.RoomsModel.findManyByRoomIds(subscriptionRoomIds, { skip, limit });
|
|
const rooms = await availableRoomsCursor.toArray();
|
|
const roomData = getSubscribedRoomsForUserWithDetails(userId, false, teamRoomIds);
|
|
const records = [];
|
|
|
|
for (const room of rooms) {
|
|
const roomInfo = roomData.find((data) => data.rid === room._id);
|
|
room.isLastOwner = roomInfo.userIsLastOwner;
|
|
records.push(room);
|
|
}
|
|
|
|
return {
|
|
total: await availableRoomsCursor.count(),
|
|
records,
|
|
};
|
|
}
|
|
|
|
async getMatchingTeamRooms(teamId: string, rids: Array<string>): Promise<Array<string>> {
|
|
if (!teamId) {
|
|
throw new Error('missing-teamId');
|
|
}
|
|
|
|
if (!rids) {
|
|
return [];
|
|
}
|
|
|
|
if (!Array.isArray(rids)) {
|
|
throw new Error('invalid-list-of-rooms');
|
|
}
|
|
|
|
const rooms = await this.RoomsModel.findByTeamIdAndRoomsId(teamId, rids, { projection: { _id: 1 } }).toArray();
|
|
return rooms.map(({ _id }: { _id: string}) => _id);
|
|
}
|
|
|
|
async getMembersByTeamIds(teamIds: Array<string>, options: FindOneOptions<ITeamMember>): Promise<Array<ITeamMember>> {
|
|
return this.TeamMembersModel.findByTeamIds(teamIds, options).toArray();
|
|
}
|
|
|
|
async members(uid: string, teamId: string, canSeeAll: boolean, { offset, count }: IPaginationOptions = { offset: 0, count: 50 }, query: FilterQuery<IUser> = {}): Promise<IRecordsWithTotal<ITeamMemberInfo>> {
|
|
const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(uid, teamId);
|
|
if (!isMember && !canSeeAll) {
|
|
return {
|
|
total: 0,
|
|
records: [],
|
|
};
|
|
}
|
|
|
|
const users = await this.Users.find({ ...query }).toArray();
|
|
const userIds = users.map((m) => m._id);
|
|
const cursor = this.TeamMembersModel.findMembersInfoByTeamId(teamId, count, offset, { userId: { $in: userIds } });
|
|
|
|
const records = await cursor.toArray();
|
|
const results: ITeamMemberInfo[] = [];
|
|
for await (const record of records) {
|
|
const user = users.find((u) => u._id === record.userId);
|
|
results.push({
|
|
user: {
|
|
_id: user._id,
|
|
username: user.username,
|
|
name: user.name,
|
|
status: user.status,
|
|
},
|
|
roles: record.roles,
|
|
createdBy: {
|
|
_id: record.createdBy._id,
|
|
username: record.createdBy.username,
|
|
},
|
|
createdAt: record.createdAt,
|
|
});
|
|
}
|
|
|
|
return {
|
|
total: await cursor.count(),
|
|
records: results,
|
|
};
|
|
}
|
|
|
|
async addMembers(uid: string, teamId: string, members: Array<ITeamMemberParams>): Promise<void> {
|
|
const createdBy = await this.Users.findOneById(uid, { projection: { username: 1 } });
|
|
if (!createdBy) {
|
|
throw new Error('invalid-user');
|
|
}
|
|
|
|
const membersList: Array<Omit<ITeamMember, '_id'>> = members?.map((member) => ({
|
|
teamId,
|
|
userId: member.userId,
|
|
roles: member.roles || [],
|
|
createdAt: new Date(),
|
|
createdBy,
|
|
_updatedAt: new Date(), // TODO how to avoid having to do this?
|
|
})) || [];
|
|
|
|
await this.TeamMembersModel.insertMany(membersList);
|
|
await this.addMembersToDefaultRooms(createdBy, teamId, membersList);
|
|
}
|
|
|
|
async updateMember(teamId: string, member: ITeamMemberParams): Promise<void> {
|
|
if (!member.userId) {
|
|
throw new Error('invalid-user');
|
|
}
|
|
|
|
const memberUpdate: Partial<ITeamMember> = {
|
|
roles: member.roles ? member.roles : [],
|
|
_updatedAt: new Date(),
|
|
};
|
|
|
|
await this.TeamMembersModel.updateOneByUserIdAndTeamId(member.userId, teamId, memberUpdate);
|
|
}
|
|
|
|
async removeMember(teamId: string, userId: string): Promise<void> {
|
|
await this.TeamMembersModel.deleteByUserIdAndTeamId(userId, teamId);
|
|
}
|
|
|
|
async removeMembers(uid: string, teamId: string, members: Array<ITeamMemberParams>): Promise<boolean> {
|
|
const team = await this.TeamModel.findOneById(teamId, { projection: { _id: 1, roomId: 1 } });
|
|
if (!team) {
|
|
throw new Error('team-does-not-exist');
|
|
}
|
|
|
|
const membersIds = members.map((m) => m.userId);
|
|
const usersToRemove = await this.Users.findByIds(membersIds, { projection: { _id: 1, username: 1 } }).toArray();
|
|
const byUser = await this.Users.findOneById(uid, { projection: { _id: 1, username: 1 } });
|
|
|
|
for await (const member of members) {
|
|
if (!member.userId) {
|
|
throw new Error('invalid-user');
|
|
}
|
|
|
|
const existingMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(member.userId, team._id);
|
|
if (!existingMember) {
|
|
throw new Error('member-does-not-exist');
|
|
}
|
|
|
|
if (existingMember.roles?.includes('owner')) {
|
|
const owners = this.TeamMembersModel.findByTeamIdAndRole(team._id, 'owner');
|
|
const totalOwners = await owners.count();
|
|
if (totalOwners === 1) {
|
|
throw new Error('last-owner-can-not-be-removed');
|
|
}
|
|
}
|
|
|
|
this.TeamMembersModel.removeById(existingMember._id);
|
|
const removedUser = usersToRemove.find((u) => u._id === existingMember.userId);
|
|
removeUserFromRoom(team.roomId, removedUser, { byUser: uid !== member.userId ? byUser : undefined });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async insertMemberOnTeams(userId: string, teamIds: Array<string>): Promise<void> {
|
|
const inviter = { _id: 'rocket.cat', username: 'rocket.cat' };
|
|
|
|
await Promise.all(teamIds.map(async (teamId) => {
|
|
const team = await this.TeamModel.findOneById(teamId);
|
|
const user = await this.Users.findOneById(userId);
|
|
|
|
if (!team || !user) {
|
|
return;
|
|
}
|
|
|
|
await addUserToRoom(team.roomId, user, inviter, false);
|
|
}));
|
|
}
|
|
|
|
async removeMemberFromTeams(userId: string, teamIds: Array<string>): Promise<void> {
|
|
await Promise.all(teamIds.map(async (teamId) => {
|
|
const team = await this.TeamModel.findOneById(teamId);
|
|
const user = await this.Users.findOneById(userId);
|
|
|
|
if (!team || !user) {
|
|
return;
|
|
}
|
|
|
|
await removeUserFromRoom(team.roomId, user);
|
|
}));
|
|
}
|
|
|
|
async removeAllMembersFromTeam(teamId: string): Promise<void> {
|
|
const team = await this.TeamModel.findOneById(teamId);
|
|
|
|
if (!team) {
|
|
return;
|
|
}
|
|
|
|
await this.TeamMembersModel.deleteByTeamId(team._id);
|
|
}
|
|
|
|
async addMember(inviter: IUser, userId: string, teamId: string): Promise<boolean> {
|
|
const isAlreadyAMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } });
|
|
|
|
if (isAlreadyAMember) {
|
|
return false;
|
|
}
|
|
|
|
let inviterData = {} as Pick<IUser, '_id' | 'username'>;
|
|
if (inviter) {
|
|
inviterData = { _id: inviter._id, username: inviter.username };
|
|
}
|
|
|
|
const member = (await this.TeamMembersModel.createOneByTeamIdAndUserId(teamId, userId, inviterData)).ops[0];
|
|
await this.addMembersToDefaultRooms(inviter, teamId, [member]);
|
|
|
|
return true;
|
|
}
|
|
|
|
async getOneById(teamId: string, options?: FindOneOptions<ITeam>): Promise<ITeam | undefined> {
|
|
return this.TeamModel.findOneById(teamId, options);
|
|
}
|
|
|
|
async getOneByName(teamName: string | RegExp, options?: FindOneOptions<ITeam>): Promise<ITeam | null> {
|
|
return this.TeamModel.findOneByName(teamName, options);
|
|
}
|
|
|
|
async getOneByMainRoomId(roomId: string): Promise<ITeam | null> {
|
|
return this.TeamModel.findOneByMainRoomId(roomId, { projection: { _id: 1 } });
|
|
}
|
|
|
|
async getOneByRoomId(roomId: string): Promise<ITeam | undefined> {
|
|
const room = await this.RoomsModel.findOneById(roomId);
|
|
|
|
if (!room) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
|
|
if (!room.teamId) {
|
|
throw new Error('room-not-on-team');
|
|
}
|
|
|
|
return this.TeamModel.findOneById(room.teamId);
|
|
}
|
|
|
|
async addRolesToMember(teamId: string, userId: string, roles: Array<string>): Promise<boolean> {
|
|
const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } });
|
|
|
|
if (!isMember) {
|
|
// TODO should this throw an error instead?
|
|
return false;
|
|
}
|
|
|
|
return !!await this.TeamMembersModel.updateRolesByTeamIdAndUserId(teamId, userId, roles);
|
|
}
|
|
|
|
async removeRolesFromMember(teamId: string, userId: string, roles: Array<string>): Promise<boolean> {
|
|
const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } });
|
|
|
|
if (!isMember) {
|
|
// TODO should this throw an error instead?
|
|
return false;
|
|
}
|
|
|
|
return !!await this.TeamMembersModel.removeRolesByTeamIdAndUserId(teamId, userId, roles);
|
|
}
|
|
|
|
async getInfoByName(teamName: string): Promise<Partial<ITeam> | undefined> {
|
|
return this.TeamModel.findOne({
|
|
name: teamName,
|
|
}, { projection: { usernames: 0 } });
|
|
}
|
|
|
|
async getInfoById(teamId: string): Promise<Partial<ITeam> | undefined> {
|
|
return this.TeamModel.findOne({
|
|
_id: teamId,
|
|
}, { projection: { usernames: 0 } });
|
|
}
|
|
|
|
async addMembersToDefaultRooms(inviter: IUser, teamId: string, members: Array<Partial<ITeamMember>>): Promise<void> {
|
|
const defaultRooms = await this.RoomsModel.findDefaultRoomsForTeam(teamId).toArray();
|
|
const users = await this.Users.findActiveByIds(members.map((member) => member.userId)).toArray();
|
|
|
|
defaultRooms.map(async (room) => {
|
|
// at this point, users are already part of the team so we won't check for membership
|
|
for await (const user of users) {
|
|
// add each user to the default room
|
|
addUserToRoom(room._id, user, inviter, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
async deleteById(teamId: string): Promise<boolean> {
|
|
return !!await this.TeamModel.deleteOneById(teamId);
|
|
}
|
|
|
|
async deleteByName(teamName: string): Promise<boolean> {
|
|
return !!await this.TeamModel.deleteOneByName(teamName);
|
|
}
|
|
|
|
async getAllPublicTeams(options: FindOneOptions<ITeam>): Promise<Array<ITeam>> {
|
|
return this.TeamModel.findByType(TEAM_TYPE.PUBLIC, options).toArray();
|
|
}
|
|
|
|
async getStatistics(): Promise<ITeamStats> {
|
|
const stats = {} as ITeamStats;
|
|
const teams = this.TeamModel.find({});
|
|
const teamsArray = await teams.toArray();
|
|
|
|
stats.totalTeams = await teams.count();
|
|
stats.teamStats = [];
|
|
|
|
for await (const team of teamsArray) {
|
|
// exclude the main room from the stats
|
|
const teamRooms = await this.RoomsModel.find({ teamId: team._id, teamMain: { $exists: false } }).toArray();
|
|
const roomIds = teamRooms.map((r) => r._id);
|
|
const [totalMessagesInTeam, defaultRooms, totalMembers] = await Promise.all([
|
|
this.MessagesModel.find({ rid: { $in: roomIds } }).count(),
|
|
this.RoomsModel.findDefaultRoomsForTeam(team._id).count(),
|
|
this.TeamMembersModel.findByTeamId(team._id).count(),
|
|
]);
|
|
|
|
const teamData = {
|
|
teamId: team._id,
|
|
mainRoom: team.roomId,
|
|
totalRooms: teamRooms.length,
|
|
totalMessages: totalMessagesInTeam,
|
|
totalPublicRooms: teamRooms.filter((r) => r.t === 'c').length,
|
|
totalPrivateRooms: teamRooms.filter((r) => r.t !== 'c').length,
|
|
totalDefaultRooms: defaultRooms,
|
|
totalMembers,
|
|
};
|
|
|
|
stats.teamStats.push(teamData);
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
async autocomplete(uid: string, name: string): Promise<IRoom[]> {
|
|
const nameRegex = new RegExp(`^${ escapeRegExp(name).trim() }`, 'i');
|
|
|
|
const subscriptions = await this.SubscriptionsModel.find({ 'u._id': uid }, { projection: { rid: 1 } }).toArray();
|
|
const subscriptionIds = subscriptions.map(({ rid }) => rid);
|
|
|
|
const rooms = await this.RoomsModel.find({
|
|
teamMain: true,
|
|
$and: [{
|
|
$or: [{
|
|
name: nameRegex,
|
|
}, {
|
|
fname: nameRegex,
|
|
}],
|
|
}, {
|
|
$or: [{
|
|
t: 'c',
|
|
}, {
|
|
_id: { $in: subscriptionIds },
|
|
}],
|
|
}],
|
|
}, {
|
|
fields: {
|
|
_id: 1,
|
|
fname: 1,
|
|
teamId: 1,
|
|
name: 1,
|
|
t: 1,
|
|
avatarETag: 1,
|
|
},
|
|
limit: 10,
|
|
sort: {
|
|
name: 1,
|
|
fname: 1,
|
|
},
|
|
}).toArray();
|
|
|
|
return rooms;
|
|
}
|
|
}
|
|
|