'use strict'; const _ = require('lodash'); const db = require('../database'); const websockets = require('./index'); const user = require('../user'); const posts = require('../posts'); const topics = require('../topics'); const categories = require('../categories'); const privileges = require('../privileges'); const notifications = require('../notifications'); const plugins = require('../plugins'); const utils = require('../utils'); const batch = require('../batch'); const apiHelpers = require('../api/helpers'); const SocketHelpers = module.exports; SocketHelpers.setDefaultPostData = function (data, socket) { websockets.warnDeprecated(socket, 'apiHelpers.setDefaultPostData'); apiHelpers.setDefaultPostData(socket, data); }; SocketHelpers.notifyNew = async function (uid, type, result) { let uids = await user.getUidsFromSet('users:online', 0, -1); uids = uids.filter(toUid => parseInt(toUid, 10) !== uid); await batch.processArray(uids, async (uids) => { await notifyUids(uid, uids, type, result); }, { interval: 1000, }); }; async function notifyUids(uid, uids, type, result) { const post = result.posts[0]; const { tid } = post.topic; const { cid } = post.topic; uids = await privileges.topics.filterUids('topics:read', tid, uids); const watchStateUids = uids; const watchStates = await getWatchStates(watchStateUids, tid, cid); const categoryWatchStates = _.zipObject(watchStateUids, watchStates.categoryWatchStates); const topicFollowState = _.zipObject(watchStateUids, watchStates.topicFollowed); uids = filterTidCidIgnorers(watchStateUids, watchStates); uids = await user.blocks.filterUids(uid, uids); uids = await user.blocks.filterUids(post.topic.uid, uids); const data = await plugins.hooks.fire('filter:sockets.sendNewPostToUids', { uidsTo: uids, uidFrom: uid, type: type }); post.ip = undefined; data.uidsTo.forEach((toUid) => { post.categoryWatchState = categoryWatchStates[toUid]; post.topic.isFollowing = topicFollowState[toUid]; websockets.in(`uid_${toUid}`).emit('event:new_post', result); if (result.topic && type === 'newTopic') { websockets.in(`uid_${toUid}`).emit('event:new_topic', result.topic); } }); } async function getWatchStates(uids, tid, cid) { return await utils.promiseParallel({ topicFollowed: db.isSetMembers(`tid:${tid}:followers`, uids), topicIgnored: db.isSetMembers(`tid:${tid}:ignorers`, uids), categoryWatchStates: categories.getUidsWatchStates(cid, uids), }); } function filterTidCidIgnorers(uids, watchStates) { return uids.filter((uid, index) => watchStates.topicFollowed[index] || (!watchStates.topicIgnored[index] && watchStates.categoryWatchStates[index] !== categories.watchStates.ignoring)); } SocketHelpers.sendNotificationToPostOwner = async function (pid, fromuid, command, notification) { if (!pid || !fromuid || !notification) { return; } fromuid = parseInt(fromuid, 10); const postData = await posts.getPostFields(pid, ['tid', 'uid', 'content']); const [canRead, isIgnoring] = await Promise.all([ privileges.posts.can('topics:read', pid, postData.uid), topics.isIgnoring([postData.tid], postData.uid), ]); if (!canRead || isIgnoring[0] || !postData.uid || fromuid === postData.uid) { return; } const [username, topicTitle, postObj] = await Promise.all([ user.getUserField(fromuid, 'username'), topics.getTopicField(postData.tid, 'title'), posts.parsePost(postData), ]); const title = utils.decodeHTMLEntities(topicTitle); const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); const notifObj = await notifications.create({ type: command, bodyShort: `[[${notification}, ${username}, ${titleEscaped}]]`, bodyLong: postObj.content, pid: pid, tid: postData.tid, path: `/post/${pid}`, nid: `${command}:post:${pid}:uid:${fromuid}`, from: fromuid, mergeId: `${notification}|${pid}`, topicTitle: topicTitle, }); notifications.push(notifObj, [postData.uid]); }; SocketHelpers.sendNotificationToTopicOwner = async function (tid, fromuid, command, notification) { if (!tid || !fromuid || !notification) { return; } fromuid = parseInt(fromuid, 10); const [username, topicData] = await Promise.all([ user.getUserField(fromuid, 'username'), topics.getTopicFields(tid, ['uid', 'slug', 'title']), ]); if (fromuid === topicData.uid) { return; } const ownerUid = topicData.uid; const title = utils.decodeHTMLEntities(topicData.title); const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); const notifObj = await notifications.create({ bodyShort: `[[${notification}, ${username}, ${titleEscaped}]]`, path: `/topic/${topicData.slug}`, nid: `${command}:tid:${tid}:uid:${fromuid}`, from: fromuid, }); if (ownerUid) { notifications.push(notifObj, [ownerUid]); } }; SocketHelpers.upvote = async function (data, notification) { if (!data || !data.post || !data.post.uid || !data.post.votes || !data.post.pid || !data.fromuid) { return; } const { votes } = data.post; const touid = data.post.uid; const { fromuid } = data; const { pid } = data.post; const shouldNotify = { all: function () { return votes > 0; }, first: function () { return votes === 1; }, everyTen: function () { return votes > 0 && votes % 10 === 0; }, threshold: function () { return [1, 5, 10, 25].includes(votes) || (votes >= 50 && votes % 50 === 0); }, logarithmic: function () { return votes > 1 && Math.log10(votes) % 1 === 0; }, disabled: function () { return false; }, }; const settings = await user.getSettings(touid); const should = shouldNotify[settings.upvoteNotifFreq] || shouldNotify.all; if (should()) { SocketHelpers.sendNotificationToPostOwner(pid, fromuid, 'upvote', notification); } }; SocketHelpers.rescindUpvoteNotification = async function (pid, fromuid) { await notifications.rescind(`upvote:post:${pid}:uid:${fromuid}`); const uid = await posts.getPostField(pid, 'uid'); const count = await user.notifications.getUnreadCount(uid); websockets.in(`uid_${uid}`).emit('event:notifications.updateCount', count); }; SocketHelpers.emitToUids = async function (event, data, uids) { uids.forEach(toUid => websockets.in(`uid_${toUid}`).emit(event, data)); }; require('../promisify')(SocketHelpers);