import s from 'underscore.string'; Meteor.methods({ messageSearch(text, rid, limit) { check(text, String); check(rid, Match.Maybe(String)); check(limit, Match.Optional(Number)); // TODO: Evaluate why we are returning `users` and `channels`, as the only thing that gets set is the `messages`. const result = { message: { docs:[] } }; const currentUserId = Meteor.userId(); if (!currentUserId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'messageSearch' }); } // Don't process anything else if the user can't access the room if (rid) { if (!Meteor.call('canAccessRoom', rid, currentUserId)) { return result; } } else if (RocketChat.settings.get('Search.defaultProvider.GlobalSearchEnabled') !== true) { return result; } const user = Meteor.user(); const currentUserName = user.username; const currentUserTimezoneOffset = user.utcOffset; const query = {}; const options = { sort: { ts: -1 }, limit: limit || 20 }; // I would place these methods at the bottom of the file for clarity but travis doesn't appreciate that. // (no-use-before-define) function filterStarred() { query['starred._id'] = currentUserId; return ''; } function filterUrl() { query['urls.0'] = { $exists: true }; return ''; } function filterPinned() { query.pinned = true; return ''; } function filterLocation() { query.location = { $exist: true }; return ''; } function filterBeforeDate(_, day, month, year) { month--; const beforeDate = new Date(year, month, day); beforeDate.setHours(beforeDate.getUTCHours() + beforeDate.getTimezoneOffset()/60 + currentUserTimezoneOffset); query.ts = { $lte: beforeDate }; return ''; } function filterAfterDate(_, day, month, year) { month--; day++; const afterDate = new Date(year, month, day); afterDate.setUTCHours(afterDate.getUTCHours() + afterDate.getTimezoneOffset()/60 + currentUserTimezoneOffset); if (query.ts) { query.ts.$gte = afterDate; } else { query.ts = { $gte: afterDate }; } return ''; } function filterOnDate(_, day, month, year) { month--; const date = new Date(year, month, day); date.setUTCHours(date.getUTCHours() + date.getTimezoneOffset()/60 + currentUserTimezoneOffset); const dayAfter = new Date(date); dayAfter.setDate(dayAfter.getDate() + 1); delete query.ts; query.ts = { $gte: date, $lt: dayAfter }; return ''; } function filterLabel(_, tag) { query['attachments.0.labels'] = new RegExp(s.escapeRegExp(tag), 'i'); return ''; } function sortByTimestamp(_, direction) { if (direction.startsWith('asc')) { options.sort.ts = 1; } else if (direction.startsWith('desc')) { options.sort.ts = -1; } return ''; } /* text = 'from:rodrigo mention:gabriel chat' */ // Query for senders const from = []; text = text.replace(/from:([a-z0-9.-_]+)/ig, function(match, username) { if (username === 'me' && !from.includes(currentUserName)) { username = currentUserName; } from.push(username); return ''; }); if (from.length > 0) { query['u.username'] = { $regex: from.join('|'), $options: 'i' }; } // Query for senders const mention = []; text = text.replace(/mention:([a-z0-9.-_]+)/ig, function(match, username) { mention.push(username); return ''; }); if (mention.length > 0) { query['mentions.username'] = { $regex: mention.join('|'), $options: 'i' }; } // Filter on messages that are starred by the current user. text = text.replace(/has:star/g, filterStarred); // Filter on messages that have an url. text = text.replace(/has:url|has:link/g, filterUrl); // Filter on pinned messages. text = text.replace(/is:pinned|has:pin/g, filterPinned); // Filter on messages which have a location attached. text = text.replace(/has:location|has:map/g, filterLocation); // Filter image tags text = text.replace(/label:(\w+)/g, filterLabel); // Filtering before/after/on a date // matches dd-MM-yyyy, dd/MM/yyyy, dd-MM-yyyy, prefixed by before:, after: and on: respectively. // Example: before:15/09/2016 after: 10-08-2016 // if "on:" is set, "before:" and "after:" are ignored. text = text.replace(/before:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterBeforeDate); text = text.replace(/after:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterAfterDate); text = text.replace(/on:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterOnDate); // Sort order text = text.replace(/(?:order|sort):(asc|ascend|ascending|desc|descend|descending)/g, sortByTimestamp); // Query in message text text = text.trim().replace(/\s\s/g, ' '); if (text !== '') { if (/^\/.+\/[imxs]*$/.test(text)) { const r = text.split('/'); query.msg = { $regex: r[1], $options: r[2] }; } else if (RocketChat.settings.get('Message_AlwaysSearchRegExp')) { query.msg = { $regex: text, $options: 'i' }; } else { query.$text = { $search: text }; options.fields = { score: { $meta: 'textScore' } }; } } if (Object.keys(query).length > 0) { query.t = { $ne: 'rm' //hide removed messages (useful when searching for user messages) }; query._hidden = { $ne: true // don't return _hidden messages }; if (rid) { query.rid = rid; } else { query.rid = { $in: RocketChat.models.Subscriptions.findByUserId(user._id) .fetch() .map(subscription => subscription.rid) }; } if (!RocketChat.settings.get('Message_ShowEditedStatus')) { options.fields = { 'editedAt': 0 }; } result.message.docs = RocketChat.models.Messages.find(query, options).fetch(); } return result; } });