EE Team Mentions (#21418)

Co-authored-by: Diego Sampaio <chinello@gmail.com>
pull/21423/head
pierre-lehnen-rc 4 years ago committed by GitHub
parent aa96a20018
commit b50785ba1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      app/lib/server/lib/notifyUsersOnMessage.js
  2. 23
      app/lib/server/lib/sendNotificationsOnMessage.js
  3. 19
      app/mentions/lib/MentionsParser.js
  4. 34
      app/mentions/server/server.js
  5. 4
      app/models/server/raw/TeamMember.ts
  6. 4
      definition/IMessage.ts
  7. 17
      ee/app/teams-mention/server/EEMentionQueries.js
  8. 4
      ee/app/teams-mention/server/EESpotlight.js
  9. 9
      ee/app/teams-mention/server/index.js
  10. 45
      ee/app/teams-mention/server/index.ts
  11. 1
      server/sdk/types/ITeamService.ts
  12. 4
      server/services/team/service.ts

@ -23,7 +23,9 @@ export function messageContainsHighlight(message, highlights) {
});
}
export function getMentions({ mentions, u: { _id: senderId } }) {
export function getMentions(message) {
const { mentions, u: { _id: senderId } } = message;
if (!mentions) {
return {
toAll: false,
@ -34,10 +36,20 @@ export function getMentions({ mentions, u: { _id: senderId } }) {
const toAll = mentions.some(({ _id }) => _id === 'all');
const toHere = mentions.some(({ _id }) => _id === 'here');
const mentionIds = mentions
const userMentions = mentions.filter((mention) => !mention.type || mention.type === 'user');
const otherMentions = mentions.filter((mention) => mention?.type !== 'user');
const filteredMentions = userMentions
.filter(({ _id }) => _id !== senderId && !['all', 'here'].includes(_id))
.map(({ _id }) => _id);
const mentionIds = callbacks.run('beforeGetMentions', filteredMentions, {
userMentions,
otherMentions,
message,
});
return {
toAll,
toHere,

@ -12,6 +12,7 @@ import { getPushData, shouldNotifyMobile } from '../functions/notifications/mobi
import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop';
import { notifyAudioUser, shouldNotifyAudio } from '../functions/notifications/audio';
import { Notification } from '../../../notification-queue/server/NotificationQueue';
import { getMentions } from './notifyUsersOnMessage';
let TroubleshootDisableNotifications;
@ -233,10 +234,24 @@ export async function sendMessageNotifications(message, room, usersInThread = []
return message;
}
const mentionIds = (message.mentions || []).map(({ _id }) => _id).concat(usersInThread); // add users in thread to mentions array because they follow the same rules
const mentionIdsWithoutGroups = mentionIds.filter((_id) => _id !== 'all' && _id !== 'here');
const hasMentionToAll = mentionIds.includes('all');
const hasMentionToHere = mentionIds.includes('here');
const {
toAll: hasMentionToAll,
toHere: hasMentionToHere,
mentionIds,
} = getMentions(message);
const mentionIdsWithoutGroups = [...mentionIds];
// getMentions removes `all` and `here` from mentionIds so we need to add them back for compatibility
if (hasMentionToAll) {
mentionIds.push('all');
}
if (hasMentionToHere) {
mentionIds.push('here');
}
// add users in thread to mentions array because they follow the same rules
mentionIds.push(...usersInThread);
let notificationMessage = callbacks.run('beforeSendMessageNotifications', message.msg);
if (mentionIds.length > 0 && settings.get('UI_Use_Real_Name')) {

@ -66,18 +66,27 @@ export class MentionsParser {
return this.userTemplate({ prefix, className, mention, label: mention, type: 'group' });
}
const filterUser = ({ username, type }) => (!type || type === 'user') && username === mention;
const filterTeam = ({ name, type }) => type === 'team' && name === mention;
const [mentionObj] = (mentions || []).filter((m) => filterUser(m) || filterTeam(m));
const label = temp
? mention && escapeHTML(mention)
: (mentions || [])
.filter(({ username }) => username === mention)
.map(({ name, username }) => (this.useRealName ? name : username))
.map((label) => label && escapeHTML(label))[0];
: mentionObj && escapeHTML(mentionObj.type === 'team' || this.useRealName ? mentionObj.name : mentionObj.username);
if (!label) {
return match;
}
return this.userTemplate({ prefix, className, mention, label, title: this.useRealName ? mention : label });
return this.userTemplate({
prefix,
className,
mention,
label,
type: mentionObj?.type === 'team' ? 'team' : 'username',
title: this.useRealName ? mention : label,
});
})
replaceChannels = (msg, { temp, channels }) => msg

@ -1,6 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import MentionsServer from './Mentions';
import { settings } from '../../settings';
@ -8,13 +7,38 @@ import { callbacks } from '../../callbacks';
import { Users, Subscriptions, Rooms } from '../../models';
import { api } from '../../../server/sdk/api';
export class MentionQueries {
getUsers(usernames) {
const users = Meteor.users.find({ username: { $in: [...new Set(usernames)] } }, { fields: { _id: true, username: true, name: 1 } }).fetch();
return users.map((user) => ({
...user,
type: 'user',
}));
}
getUser(userId) {
return Users.findOneById(userId);
}
getTotalChannelMembers(rid) {
return Subscriptions.findByRoomId(rid).count();
}
getChannels(channels) {
return Rooms.find({ name: { $in: [...new Set(channels)] }, t: { $in: ['c', 'p'] } }, { fields: { _id: 1, name: 1 } }).fetch();
}
}
const queries = new MentionQueries();
const mention = new MentionsServer({
pattern: () => settings.get('UTF8_Names_Validation'),
messageMaxAll: () => settings.get('Message_MaxAll'),
getUsers: (usernames) => Meteor.users.find({ username: { $in: _.unique(usernames) } }, { fields: { _id: true, username: true, name: 1 } }).fetch(),
getUser: (userId) => Users.findOneById(userId),
getTotalChannelMembers: (rid) => Subscriptions.findByRoomId(rid).count(),
getChannels: (channels) => Rooms.find({ name: { $in: _.unique(channels) }, t: { $in: ['c', 'p'] } }, { fields: { _id: 1, name: 1 } }).fetch(),
getUsers: (usernames) => queries.getUsers(usernames),
getUser: (userId) => queries.getUser(userId),
getTotalChannelMembers: (rid) => queries.getTotalChannelMembers(rid),
getChannels: (channels) => queries.getChannels(channels),
onMaxRoomMembersExceeded({ sender, rid }) {
// Get the language of the user for the error notification.
const { language } = this.getUser(sender._id);

@ -32,6 +32,10 @@ export class TeamMemberRaw extends BaseRaw<T> {
return this.col.find({ teamId }, options);
}
findByTeamIds(teamIds: Array<string>, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({ teamId: { $in: teamIds } }, options);
}
findByTeamIdAndRole(teamId: string, role?: string, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({ teamId, roles: role }, options);
}

@ -2,13 +2,17 @@ import { IRocketChatRecord } from './IRocketChatRecord';
import { IUser } from './IUser';
import { ChannelName, RoomID } from './IRoom';
type MentionType = 'user' | 'team';
export interface IMessage extends IRocketChatRecord {
rid: RoomID;
msg: string;
ts: Date;
mentions?: {
_id: string;
type: MentionType;
name?: string;
username?: string;
}[];
channels?: Array<ChannelName>;
u: Pick<IUser, '_id' | 'username' | 'name'>;

@ -0,0 +1,17 @@
import { Team } from '../../../../server/sdk';
export const MentionQueriesEnterprise = {
getUsers(sup, usernames) {
const uniqueUsernames = [...new Set(usernames)];
const teams = Promise.await(Team.listByNames(uniqueUsernames, { projection: { name: 1 } }));
if (!teams?.length) {
return sup(usernames);
}
return teams.map((team) => ({
...team,
type: 'team',
})).concat(sup(usernames));
},
};

@ -25,8 +25,6 @@ export const SpotlightEnterprise = {
},
_performExtraUserSearches(_, userId, searchParams) {
if (this._searchTeams(userId, searchParams)) {
return searchParams.users;
}
return this._searchTeams(userId, searchParams);
},
};

@ -1,9 +0,0 @@
import { onLicense } from '../../license/server';
import { overwriteClassOnLicense } from '../../license/server/license';
import { SpotlightEnterprise } from './EESpotlight';
import { Spotlight } from '../../../../server/lib/spotlight';
onLicense('teams-mention', () => {
// Override spotlight with EE version
overwriteClassOnLicense('teams-mention', Spotlight, SpotlightEnterprise);
});

@ -0,0 +1,45 @@
import { Promise } from 'meteor/promise';
import { onLicense } from '../../license/server';
import { overwriteClassOnLicense } from '../../license/server/license';
import { SpotlightEnterprise } from './EESpotlight';
import { Spotlight } from '../../../../server/lib/spotlight';
import { MentionQueries } from '../../../../app/mentions/server/server';
import { callbacks } from '../../../../app/callbacks/server';
import { MentionQueriesEnterprise } from './EEMentionQueries';
import { Team } from '../../../../server/sdk';
import { ITeamMember } from '../../../../definition/ITeam';
import { IMessage } from '../../../../definition/IMessage';
interface IExtraDataForNotification {
userMentions: any[];
otherMentions: any[];
message: IMessage;
}
onLicense('teams-mention', () => {
// Override spotlight with EE version
overwriteClassOnLicense('teams-mention', Spotlight, SpotlightEnterprise);
overwriteClassOnLicense('teams-mention', MentionQueries, MentionQueriesEnterprise);
callbacks.add('beforeGetMentions', (mentionIds: Array<string>, extra: IExtraDataForNotification) => {
const { otherMentions } = extra;
const teamIds = otherMentions
.filter(({ type }) => type === 'team')
.map(({ _id }) => _id);
if (!teamIds.length) {
return mentionIds;
}
const members: ITeamMember[] = Promise.await(Team.getMembersByTeamIds(teamIds, { projection: { userId: 1 } }));
mentionIds.push(...new Set(
members
.map(({ userId }: { userId: string }) => userId)
.filter((userId: string) => !mentionIds.includes(userId)),
));
return mentionIds;
});
});

@ -74,4 +74,5 @@ export interface ITeamService {
getMatchingTeamRooms(teamId: string, rids: Array<string>): Promise<Array<string>>;
autocomplete(uid: string, name: string): Promise<Array<IRoom>>;
getAllPublicTeams(options: FindOneOptions<ITeam>): Promise<Array<ITeam>>;
getMembersByTeamIds(teamIds: Array<string>, options: FindOneOptions<ITeamMember>): Promise<Array<ITeamMember>>;
}

@ -471,6 +471,10 @@ export class TeamService extends ServiceClass implements ITeamService {
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 }: IQueryOptions<ITeam>): Promise<IRecordsWithTotal<ITeamMemberInfo>> {
const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(uid, teamId);
if (!isMember && !canSeeAll) {

Loading…
Cancel
Save