repo_description: Node.js based forum software built for the modern web
repo_topics: community forum javascript mongodb node nodebb nodejs postgresql redis socket-io websockets
code: 'use strict';
  
  const _ = require('lodash');
  
  const db = require('../database');
  const user = require('../user');
  const categories = require('../categories');
  const messaging = require('../messaging');
  const privileges = require('../privileges');
  const meta = require('../meta');
  const plugins = require('../plugins');
  const utils = require('../utils');
  
  const controllersHelpers = require('../controllers/helpers');
  
  const searchApi = module.exports;
  
  searchApi.categories = async (caller, data) => {
  	// used by categorySearch module
  	let matchedCids = [];
  	const privilege = data.privilege || 'topics:read';
  	data.states = (data.states || ['watching', 'tracking', 'notwatching', 'ignoring']).map(
  		state => categories.watchStates[state]
  	);
  	data.parentCid = String(data.parentCid || 0);
  
  	let cids;
  	if (data.search) {
  		({ cids, matchedCids } = await findMatchedCids(caller.uid, data));
  	} else {
  		cids = await loadCids(caller.uid, data.parentCid);
  		if (!data.hideUncategorized && meta.config.activitypubEnabled) {
  			cids.unshift(-1);
  		}
  		if (data.localOnly) {
  			cids = cids.filter(cid => utils.isNumber(cid));
  		}
  	}
  
  	const visibleCategories = await controllersHelpers.getVisibleCategories({
  		cids, uid: caller.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid,
  	});
  
  	if (Array.isArray(data.selectedCids)) {
  		data.selectedCids = data.selectedCids.map(cid => String(cid));
  	}
  
  	let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid);
  	categoriesData = categoriesData.slice(0, 1000);
  
  	categoriesData.forEach((category) => {
  		category.selected = data.selectedCids ? data.selectedCids.includes(String(category.cid)) : false;
  		if (matchedCids.includes(String(category.cid))) {
  			category.match = true;
  		}
  	});
  	const result = await plugins.hooks.fire('filter:categories.categorySearch', {
  		categories: categoriesData,
  		...data,
  		uid: caller.uid,
  	});
  
  	return { categories: result.categories };
  };
  
  async function findMatchedCids(uid, data) {
  	const result = await categories.search({
  		uid: uid,
  		query: data.search,
  		qs: data.query,
  		paginate: false,
  		localOnly: data.localOnly,
  	});
  
  	let matchedCids = result.categories.map(c => String(c.cid));
  	// no need to filter if all 3 states are used
  	const filterByWatchState = !Object.values(categories.watchStates)
  		.every(state => data.states.includes(state));
  
  	if (filterByWatchState) {
  		const states = await categories.getWatchState(matchedCids, uid);
  		matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index]));
  	}
  
  	const rootCids = _.uniq(_.flatten(
  		await Promise.all(matchedCids.map(categories.getParentCids))
  	));
  	const allChildCids = _.uniq(_.flatten(
  		await Promise.all(matchedCids.map(categories.getChildrenCids))
  	));
  
  	return {
  		cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)),
  		matchedCids: matchedCids,
  	};
  }
  
  async function loadCids(uid, parentCid) {
  	let resultCids = [];
  	async function getCidsRecursive(cids) {
  		const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']);
  		const cidToData = _.zipObject(cids, categoryData);
  		await Promise.all(cids.map(async (cid) => {
  			const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`);
  			if (allChildCids.length) {
  				const childCids = await privileges.categories.filterCids('find', allChildCids, uid);
  				resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage));
  				await getCidsRecursive(childCids);
  			}
  		}));
  	}
  
  	const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`);
  	const rootCids = await privileges.categories.filterCids('find', allRootCids, uid);
  	const pageCids = rootCids.slice(0, meta.config.categoriesPerPage);
  	resultCids = pageCids;
  	await getCidsRecursive(pageCids);
  	return resultCids;
  }
  
  searchApi.roomUsers = async (caller, { query, roomId }) => {
  	const [isAdmin, inRoom, isRoomOwner] = await Promise.all([
  		user.isAdministrator(caller.uid),
  		messaging.isUserInRoom(caller.uid, roomId),
  		messaging.isRoomOwner(caller.uid, roomId),
  	]);
  
  	if (!isAdmin && !inRoom) {
  		throw new Error('[[error:no-privileges]]');
  	}
  
  	const results = await user.search({
  		query,
  		paginate: false,
  		hardCap: -1,
  		uid: caller.uid,
  	});
  
  	const { users } = results;
  	const foundUids = users.map(user => user && user.uid);
  	const isUidInRoom = _.zipObject(
  		foundUids,
  		await messaging.isUsersInRoom(foundUids, roomId)
  	);
  
  	const roomUsers = users.filter(user => isUidInRoom[user.uid]);
  	const isOwners = await messaging.isRoomOwner(roomUsers.map(u => u.uid), roomId);
  
  	roomUsers.forEach((user, index) => {
  		if (user) {
  			user.isOwner = isOwners[index];
  			user.canKick = isRoomOwner && String(user.uid) !== String(caller.uid);
  		}
  	});
  
  	roomUsers.sort((a, b) => {
  		if (a.isOwner && !b.isOwner) {
  			return -1;
  		} else if (!a.isOwner && b.isOwner) {
  			return 1;
  		}
  		return 0;
  	});
  
  	return { users: roomUsers };
  };
  
  searchApi.roomMessages = async (caller, { query, roomId, uid }) => {
  	const [roomData, inRoom] = await Promise.all([
  		messaging.getRoomData(roomId),
  		messaging.isUserInRoom(caller.uid, roomId),
  	]);
  
  	if (!roomData) {
  		throw new Error('[[error:no-room]]');
  	}
  	if (!inRoom) {
  		throw new Error('[[error:no-privileges]]');
  	}
  	const { ids } = await plugins.hooks.fire('filter:messaging.searchMessages', {
  		content: query,
  		roomId: [roomId],
  		uid: [uid],
  		matchWords: 'any',
  		ids: [],
  	});
  
  	let userjoinTimestamp = 0;
  	if (!roomData.public) {
  		userjoinTimestamp = await db.sortedSetScore(`chat:room:${roomId}:uids`, caller.uid);
  	}
  	let messageData = await messaging.getMessagesData(ids, caller.uid, roomId, false);
  	messageData = messageData
  		.map((msg) => {
  			if (msg) {
  				msg.newSet = true;
  			}
  			return msg;
  		})
  		.filter(msg => msg && !msg.deleted && msg.timestamp > userjoinTimestamp);
  
  	return { messages: messageData };
  };
  