NodeBB/NodeBB

View on GitHub
src/user/notifications.js

Summary

Maintainability
A
25 mins
Test Coverage

'use strict';

const winston = require('winston');
const _ = require('lodash');

const db = require('../database');
const meta = require('../meta');
const notifications = require('../notifications');
const privileges = require('../privileges');
const plugins = require('../plugins');
const utils = require('../utils');

const UserNotifications = module.exports;

UserNotifications.get = async function (uid) {
    if (parseInt(uid, 10) <= 0) {
        return { read: [], unread: [] };
    }

    let unread = await getNotificationsFromSet(`uid:${uid}:notifications:unread`, uid, 0, 49);
    unread = unread.filter(Boolean);
    let read = [];
    if (unread.length < 50) {
        read = await getNotificationsFromSet(`uid:${uid}:notifications:read`, uid, 0, 49 - unread.length);
    }

    return await plugins.hooks.fire('filter:user.notifications.get', {
        uid,
        read: read.filter(Boolean),
        unread: unread,
    });
};

async function filterNotifications(nids, filter) {
    if (!filter) {
        return nids;
    }
    const keys = nids.map(nid => `notifications:${nid}`);
    const notifications = await db.getObjectsFields(keys, ['nid', 'type']);
    return notifications.filter(n => n && n.nid && n.type === filter).map(n => n.nid);
}

UserNotifications.getAll = async function (uid, filter) {
    let nids = await db.getSortedSetRevRange([
        `uid:${uid}:notifications:unread`,
        `uid:${uid}:notifications:read`,
    ], 0, -1);
    nids = _.uniq(nids);
    const exists = await db.isSortedSetMembers('notifications', nids);
    const deleteNids = [];

    nids = nids.filter((nid, index) => {
        if (!nid || !exists[index]) {
            deleteNids.push(nid);
        }
        return nid && exists[index];
    });

    await deleteUserNids(deleteNids, uid);
    return await filterNotifications(nids, filter);
};

async function deleteUserNids(nids, uid) {
    await db.sortedSetRemove([
        `uid:${uid}:notifications:read`,
        `uid:${uid}:notifications:unread`,
    ], nids);
}

async function getNotificationsFromSet(set, uid, start, stop) {
    const nids = await db.getSortedSetRevRange(set, start, stop);
    return await UserNotifications.getNotifications(nids, uid);
}

UserNotifications.getNotifications = async function (nids, uid) {
    if (!Array.isArray(nids) || !nids.length) {
        return [];
    }

    const [notifObjs, hasRead] = await Promise.all([
        notifications.getMultiple(nids),
        db.isSortedSetMembers(`uid:${uid}:notifications:read`, nids),
    ]);

    const deletedNids = [];
    let notificationData = notifObjs.filter((notification, index) => {
        if (!notification || !notification.nid) {
            deletedNids.push(nids[index]);
        }
        if (notification) {
            notification.read = hasRead[index];
            notification.readClass = !notification.read ? 'unread' : '';
        }

        return notification;
    });

    await deleteUserNids(deletedNids, uid);
    notificationData = await notifications.merge(notificationData);
    const result = await plugins.hooks.fire('filter:user.notifications.getNotifications', {
        uid: uid,
        notifications: notificationData,
    });
    return result && result.notifications;
};

UserNotifications.getUnreadInterval = async function (uid, interval) {
    const dayInMs = 1000 * 60 * 60 * 24;
    const times = {
        day: dayInMs,
        week: 7 * dayInMs,
        month: 30 * dayInMs,
    };
    if (!times[interval]) {
        return [];
    }
    const min = Date.now() - times[interval];
    const nids = await db.getSortedSetRevRangeByScore(`uid:${uid}:notifications:unread`, 0, 20, '+inf', min);
    return await UserNotifications.getNotifications(nids, uid);
};

UserNotifications.getDailyUnread = async function (uid) {
    return await UserNotifications.getUnreadInterval(uid, 'day');
};

UserNotifications.getUnreadCount = async function (uid) {
    if (parseInt(uid, 10) <= 0) {
        return 0;
    }
    let nids = await db.getSortedSetRevRange(`uid:${uid}:notifications:unread`, 0, 99);
    nids = await notifications.filterExists(nids);
    const keys = nids.map(nid => `notifications:${nid}`);
    const notifData = await db.getObjectsFields(keys, ['mergeId']);
    const mergeIds = notifData.map(n => n.mergeId);

    // Collapse any notifications with identical mergeIds
    let count = mergeIds.reduce((count, mergeId, idx, arr) => {
        // A missing (null) mergeId means that notification is counted separately.
        if (mergeId === null || idx === arr.indexOf(mergeId)) {
            count += 1;
        }

        return count;
    }, 0);

    ({ count } = await plugins.hooks.fire('filter:user.notifications.getCount', { uid, count }));
    return count;
};

UserNotifications.getUnreadByField = async function (uid, field, values) {
    const nids = await db.getSortedSetRevRange(`uid:${uid}:notifications:unread`, 0, 99);
    if (!nids.length) {
        return [];
    }
    const keys = nids.map(nid => `notifications:${nid}`);
    const notifData = await db.getObjectsFields(keys, ['nid', field]);
    const valuesSet = new Set(values.map(value => String(value)));
    return notifData.filter(n => n && n[field] && valuesSet.has(String(n[field]))).map(n => n.nid);
};

UserNotifications.deleteAll = async function (uid) {
    if (parseInt(uid, 10) <= 0) {
        return;
    }
    await db.deleteAll([
        `uid:${uid}:notifications:unread`,
        `uid:${uid}:notifications:read`,
    ]);
};

UserNotifications.sendTopicNotificationToFollowers = async function (uid, topicData, postData) {
    try {
        let followers = await db.getSortedSetRange(`followers:${uid}`, 0, -1);
        followers = await privileges.categories.filterUids('read', topicData.cid, followers);
        if (!followers.length) {
            return;
        }
        let { title } = topicData;
        if (title) {
            title = utils.decodeHTMLEntities(title);
            title = title.replace(/,/g, '\\,');
        }

        const notifObj = await notifications.create({
            type: 'new-topic',
            bodyShort: `[[notifications:user_posted_topic, ${postData.user.username}, ${title}]]`,
            bodyLong: postData.content,
            pid: postData.pid,
            path: `/post/${postData.pid}`,
            nid: `tid:${postData.tid}:uid:${uid}`,
            tid: postData.tid,
            from: uid,
        });

        await notifications.push(notifObj, followers);
    } catch (err) {
        winston.error(err.stack);
    }
};

UserNotifications.sendWelcomeNotification = async function (uid) {
    if (!meta.config.welcomeNotification) {
        return;
    }

    const path = meta.config.welcomeLink ? meta.config.welcomeLink : '#';
    const notifObj = await notifications.create({
        bodyShort: meta.config.welcomeNotification,
        path: path,
        nid: `welcome_${uid}`,
        from: meta.config.welcomeUid ? meta.config.welcomeUid : null,
    });

    await notifications.push(notifObj, [uid]);
};

UserNotifications.sendNameChangeNotification = async function (uid, username) {
    const notifObj = await notifications.create({
        bodyShort: `[[user:username_taken_workaround, ${username}]]`,
        image: 'brand:logo',
        nid: `username_taken:${uid}`,
        datetime: Date.now(),
    });

    await notifications.push(notifObj, uid);
};

UserNotifications.pushCount = async function (uid) {
    const websockets = require('../socket.io');
    const count = await UserNotifications.getUnreadCount(uid);
    websockets.in(`uid_${uid}`).emit('event:notifications.updateCount', count);
};