mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-18 03:31:03 +01:00
Merge branch 'master' of https://github.com/NodeBB/NodeBB
This commit is contained in:
@@ -79,6 +79,12 @@ async function getNodeInfo() {
|
||||
release: os.release(),
|
||||
load: os.loadavg().map(function (load) { return load.toFixed(2); }).join(', '),
|
||||
},
|
||||
nodebb: {
|
||||
isCluster: nconf.get('isCluster'),
|
||||
isPrimary: nconf.get('isPrimary'),
|
||||
runJobs: nconf.get('runJobs'),
|
||||
jobsDisabled: nconf.get('jobsDisabled'),
|
||||
},
|
||||
};
|
||||
data.process.cpuUsage.user /= 1000000;
|
||||
data.process.cpuUsage.user = data.process.cpuUsage.user.toFixed(2);
|
||||
@@ -109,5 +115,5 @@ async function getGitInfo() {
|
||||
getAsync('git rev-parse HEAD'),
|
||||
getAsync('git rev-parse --abbrev-ref HEAD'),
|
||||
]);
|
||||
return { hash: hash, branch: branch };
|
||||
return { hash: hash, hashShort: hash.substr(0, 6), branch: branch };
|
||||
}
|
||||
|
||||
@@ -181,12 +181,17 @@ modsController.postQueue = async function (req, res, next) {
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const postsPerPage = 20;
|
||||
|
||||
const [ids, isAdminOrGlobalMod, moderatedCids] = await Promise.all([
|
||||
const [ids, isAdminOrGlobalMod, moderatedCids, allCategories] = await Promise.all([
|
||||
db.getSortedSetRange('post:queue', 0, -1),
|
||||
user.isAdminOrGlobalMod(req.uid),
|
||||
user.getModeratedCids(req.uid),
|
||||
categories.buildForSelect(req.uid, 'find', ['disabled', 'link', 'slug']),
|
||||
]);
|
||||
|
||||
allCategories.forEach((c) => {
|
||||
c.disabledClass = !isAdminOrGlobalMod && !moderatedCids.includes(String(c.cid));
|
||||
});
|
||||
|
||||
let postData = await getQueuedPosts(ids);
|
||||
postData = postData.filter(p => p && (isAdminOrGlobalMod || moderatedCids.includes(String(p.category.cid))));
|
||||
|
||||
@@ -198,6 +203,7 @@ modsController.postQueue = async function (req, res, next) {
|
||||
res.render('admin/manage/post-queue', {
|
||||
title: '[[pages:post-queue]]',
|
||||
posts: postData,
|
||||
allCategories: allCategories,
|
||||
pagination: pagination.create(page, pageCount),
|
||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]),
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ unreadController.get = async function (req, res, next) {
|
||||
req.query.page = Math.max(1, Math.min(data.pageCount, page));
|
||||
return helpers.redirect(res, '/unread?' + querystring.stringify(req.query));
|
||||
}
|
||||
data.showSelect = isPrivileged;
|
||||
data.showSelect = true;
|
||||
data.showTopicTools = isPrivileged;
|
||||
data.categories = watchedCategories.categories;
|
||||
data.allCategoriesUrl = 'unread' + helpers.buildQueryString('', filter, '');
|
||||
|
||||
@@ -50,10 +50,11 @@ module.exports = function (Messaging) {
|
||||
db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid),
|
||||
]);
|
||||
await Promise.all([
|
||||
Messaging.addSystemMessage('user-join', uid, roomId), // chat owner should also get the user-join system message
|
||||
Messaging.addUsersToRoom(uid, toUids, roomId),
|
||||
Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now),
|
||||
]);
|
||||
// chat owner should also get the user-join system message
|
||||
await Messaging.addSystemMessage('user-join', uid, roomId);
|
||||
|
||||
return roomId;
|
||||
};
|
||||
|
||||
@@ -56,10 +56,6 @@ Tags.parse = async (req, data, meta, link) => {
|
||||
}, {
|
||||
rel: 'manifest',
|
||||
href: nconf.get('relative_path') + '/manifest.json',
|
||||
}, {
|
||||
rel: 'preload',
|
||||
href: nconf.get('relative_path') + '/assets/js-enabled.css?' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''),
|
||||
as: 'style',
|
||||
}];
|
||||
|
||||
if (plugins.hasListeners('filter:search.query')) {
|
||||
|
||||
@@ -147,23 +147,31 @@ module.exports = function (Posts) {
|
||||
socketHelpers.notifyNew(data.uid, 'newPost', result);
|
||||
}
|
||||
|
||||
Posts.editQueuedContent = async function (uid, id, content) {
|
||||
const canEditQueue = await Posts.canEditQueue(uid, id);
|
||||
Posts.editQueuedContent = async function (uid, editData) {
|
||||
const canEditQueue = await Posts.canEditQueue(uid, editData);
|
||||
if (!canEditQueue) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
const data = await getParsedObject(id);
|
||||
const data = await getParsedObject(editData.id);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
data.data.content = content;
|
||||
await db.setObjectField('post:queue:' + id, 'data', JSON.stringify(data.data));
|
||||
if (editData.content !== undefined) {
|
||||
data.data.content = editData.content;
|
||||
}
|
||||
if (editData.title !== undefined) {
|
||||
data.data.title = editData.title;
|
||||
}
|
||||
if (editData.cid !== undefined) {
|
||||
data.data.cid = editData.cid;
|
||||
}
|
||||
await db.setObjectField('post:queue:' + editData.id, 'data', JSON.stringify(data.data));
|
||||
};
|
||||
|
||||
Posts.canEditQueue = async function (uid, id) {
|
||||
Posts.canEditQueue = async function (uid, editData) {
|
||||
const [isAdminOrGlobalMod, data] = await Promise.all([
|
||||
user.isAdminOrGlobalMod(uid),
|
||||
getParsedObject(id),
|
||||
getParsedObject(editData.id),
|
||||
]);
|
||||
if (!data) {
|
||||
return false;
|
||||
@@ -179,6 +187,11 @@ module.exports = function (Posts) {
|
||||
} else if (data.type === 'reply') {
|
||||
cid = await topics.getTopicField(data.data.tid, 'cid');
|
||||
}
|
||||
return await user.isModerator(uid, cid);
|
||||
const isModerator = await user.isModerator(uid, cid);
|
||||
let isModeratorOfTargetCid = true;
|
||||
if (editData.cid) {
|
||||
isModeratorOfTargetCid = await user.isModerator(uid, editData.cid);
|
||||
}
|
||||
return isModerator && isModeratorOfTargetCid;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ Auth.reloadRoutes = async function (params) {
|
||||
loginStrategies.forEach(function (strategy) {
|
||||
if (strategy.url) {
|
||||
router.get(strategy.url, Auth.middleware.applyCSRF, function (req, res, next) {
|
||||
req.session.ssoState = req.csrfToken();
|
||||
req.session.ssoState = req.csrfToken && req.csrfToken();
|
||||
passport.authenticate(strategy.name, {
|
||||
scope: strategy.scope,
|
||||
prompt: strategy.prompt || undefined,
|
||||
|
||||
@@ -150,6 +150,7 @@ async function onMessage(socket, payload) {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
winston.error(err.stack ? err.stack : err.message);
|
||||
callback({ message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ SocketPosts.reject = async function (socket, data) {
|
||||
};
|
||||
|
||||
async function acceptOrReject(method, socket, data) {
|
||||
const canEditQueue = await posts.canEditQueue(socket.uid, data.id);
|
||||
const canEditQueue = await posts.canEditQueue(socket.uid, data);
|
||||
if (!canEditQueue) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
@@ -174,11 +174,14 @@ async function acceptOrReject(method, socket, data) {
|
||||
}
|
||||
|
||||
SocketPosts.editQueuedContent = async function (socket, data) {
|
||||
if (!data || !data.id || !data.content) {
|
||||
if (!data || !data.id || (!data.content && !data.title && !data.cid)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
await posts.editQueuedContent(socket.uid, data.id, data.content);
|
||||
return await plugins.fireHook('filter:parse.post', { postData: data });
|
||||
await posts.editQueuedContent(socket.uid, data);
|
||||
if (data.content) {
|
||||
return await plugins.fireHook('filter:parse.post', { postData: data });
|
||||
}
|
||||
return { postData: data };
|
||||
};
|
||||
|
||||
require('../promisify')(SocketPosts);
|
||||
|
||||
@@ -29,13 +29,14 @@ Digest.execute = async function (payload) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. Sending emails; this may take some time...');
|
||||
await Digest.send({
|
||||
interval: payload.interval,
|
||||
subscribers: subscribers,
|
||||
});
|
||||
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. Sending emails; this may take some time...');
|
||||
winston.info('[user/jobs] Digest (' + payload.interval + ') complete.');
|
||||
} catch (err) {
|
||||
winston.error('[user/jobs] Could not send digests (' + payload.interval + ')', err.stack);
|
||||
winston.error('[user/jobs] Could not send digests (' + payload.interval + ')\n' + err.stack);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -81,7 +82,10 @@ Digest.getSubscribers = async function (interval) {
|
||||
});
|
||||
subUids = await user.bans.filterBanned(subUids);
|
||||
subscribers = subscribers.concat(subUids);
|
||||
}, { interval: 1000 });
|
||||
}, {
|
||||
interval: 1000,
|
||||
batch: 500,
|
||||
});
|
||||
|
||||
const results = await plugins.fireHook('filter:digest.subscribers', {
|
||||
interval: interval,
|
||||
@@ -91,15 +95,13 @@ Digest.getSubscribers = async function (interval) {
|
||||
};
|
||||
|
||||
Digest.send = async function (data) {
|
||||
var emailsSent = 0;
|
||||
let emailsSent = 0;
|
||||
if (!data || !data.subscribers || !data.subscribers.length) {
|
||||
return emailsSent;
|
||||
}
|
||||
const now = new Date();
|
||||
|
||||
const users = await user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline']);
|
||||
|
||||
async.eachLimit(users, 100, async function (userObj) {
|
||||
await async.eachLimit(data.subscribers, 100, async function (uid) {
|
||||
const userObj = await user.getUserFields(uid, ['uid', 'username', 'userslug', 'lastonline']);
|
||||
let [notifications, topicsData] = await Promise.all([
|
||||
user.notifications.getUnreadInterval(userObj.uid, data.interval),
|
||||
getTermTopics(data.interval, userObj.uid, 0, 9),
|
||||
@@ -121,13 +123,14 @@ Digest.send = async function (data) {
|
||||
|
||||
// Fix relative paths in topic data
|
||||
topicsData = topicsData.map(function (topicObj) {
|
||||
const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user;
|
||||
const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser && topicObj.teaser.user ? topicObj.teaser.user : topicObj.user;
|
||||
if (user && user.picture && utils.isRelativeUrl(user.picture)) {
|
||||
user.picture = nconf.get('base_url') + user.picture;
|
||||
}
|
||||
return topicObj;
|
||||
});
|
||||
emailsSent += 1;
|
||||
const now = new Date();
|
||||
try {
|
||||
await emailer.send('digest', userObj.uid, {
|
||||
subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]',
|
||||
@@ -139,15 +142,14 @@ Digest.send = async function (data) {
|
||||
showUnsubscribe: true,
|
||||
});
|
||||
} catch (err) {
|
||||
winston.error('[user/jobs] Could not send digest email', err.stack);
|
||||
winston.error('[user/jobs] Could not send digest email\n' + err.stack);
|
||||
}
|
||||
|
||||
if (data.interval !== 'alltime') {
|
||||
await db.sortedSetAdd('digest:delivery', now.getTime(), userObj.uid);
|
||||
}
|
||||
}, function () {
|
||||
winston.info('[user/jobs] Digest (' + data.interval + ') sending completed. ' + emailsSent + ' emails sent.');
|
||||
});
|
||||
winston.info('[user/jobs] Digest (' + data.interval + ') sending completed. ' + emailsSent + ' emails sent.');
|
||||
};
|
||||
|
||||
Digest.getDeliveryTimes = async (start, stop) => {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<td>[[admin/development/info:host]]</td>
|
||||
<td class="text-center">[[admin/development/info:primary]]</td>
|
||||
<td>[[admin/development/info:pid]]</td>
|
||||
<td>[[admin/development/info:nodejs]]</td>
|
||||
<td>[[admin/development/info:online]]</td>
|
||||
@@ -25,6 +26,10 @@
|
||||
<!-- BEGIN info -->
|
||||
<tr>
|
||||
<td>{info.os.hostname}:{info.process.port}</td>
|
||||
<td class="text-center">
|
||||
{{{if info.nodebb.isPrimary}}}<i class="fa fa-check"></i>{{{else}}}<i class="fa fa-times"></i>{{{end}}} /
|
||||
{{{if info.nodebb.runJobs}}}<i class="fa fa-check"></i>{{{else}}}<i class="fa fa-times"></i>{{{end}}}
|
||||
</td>
|
||||
<td>{info.process.pid}</td>
|
||||
<td>{info.process.version}</td>
|
||||
<td>
|
||||
@@ -32,7 +37,7 @@
|
||||
<span title="[[admin/development/info:guests]]">{info.stats.onlineGuestCount}</span> /
|
||||
<span title="[[admin/development/info:sockets]]">{info.stats.socketCount}</span>
|
||||
</td>
|
||||
<td>{info.git.branch}@<a href="https://github.com/NodeBB/NodeBB/commit/{info.git.hash}" target="_blank">{info.git.hash}</a></td>
|
||||
<td>{info.git.branch}@<a href="https://github.com/NodeBB/NodeBB/commit/{info.git.hash}" target="_blank">{info.git.hashShort}</a></td>
|
||||
<td>{info.process.cpuUsage.user} / {info.process.cpuUsage.system}</td>
|
||||
<td>{info.process.memoryUsage.humanReadable} mb</td>
|
||||
<td>{info.os.load}</td>
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="[[global:search]]" id="category-search">
|
||||
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
<div component="category/no-matches" class="hidden">[[admin/manage/categories:no-matches]]</div>
|
||||
<div class="categories"></div>
|
||||
|
||||
<button data-action="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-3 pull-right">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" id="group-search">
|
||||
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row groups">
|
||||
<div class="col-xs-12">
|
||||
<div>
|
||||
<input id="group-search" type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" />
|
||||
</div>
|
||||
|
||||
<table class="table table-striped groups-list">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[[admin/manage/post-queue:user]]</th>
|
||||
<th>[[admin/manage/post-queue:category]]</th>
|
||||
<th>[[admin/manage/post-queue:title]]</th>
|
||||
<th>[[admin/manage/post-queue:category]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
|
||||
<th>[[admin/manage/post-queue:title]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
|
||||
<th>[[admin/manage/post-queue:content]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
|
||||
<th>[[admin/manage/post-queue:posted]]</th>
|
||||
<th></th>
|
||||
@@ -35,8 +35,8 @@
|
||||
{posts.user.username}
|
||||
<!-- ENDIF posts.user.userslug -->
|
||||
</td>
|
||||
<td class="col-md-2">
|
||||
<a href="{config.relative_path}/category/{posts.category.slug}"><!-- IF posts.categiry.icon --><span class="fa-stack"><i style="color: {posts.category.bgColor};" class="fa fa-circle fa-stack-2x"></i><i style="color: {posts.category.color};" class="fa fa-stack-1x fa-fw {posts.category.icon}"></i></span><!-- ENDIF posts.category.icon --> {posts.category.name}</a>
|
||||
<td class="col-md-2 topic-category" {{{if posts.data.cid}}}data-editable="editable"{{{end}}}">
|
||||
<a href="{config.relative_path}/category/{posts.category.slug}"><!-- IF posts.category.icon --><span class="fa-stack"><i style="color: {posts.category.bgColor};" class="fa fa-circle fa-stack-2x"></i><i style="color: {posts.category.color};" class="fa fa-stack-1x fa-fw {posts.category.icon}"></i></span><!-- ENDIF posts.category.icon --> {posts.category.name}</a>
|
||||
</td>
|
||||
<td class="col-md-2 topic-title">
|
||||
<!-- IF posts.data.tid -->
|
||||
@@ -44,9 +44,14 @@
|
||||
<!-- ENDIF posts.data.tid -->
|
||||
{posts.data.title}
|
||||
</td>
|
||||
{{{if !posts.data.tid}}}
|
||||
<td class="col-md-2 topic-title-editable hidden">
|
||||
<input class="form-control" type="text" value="{posts.data.title}"/>
|
||||
</td>
|
||||
{{{end}}}
|
||||
<td class="col-md-5 post-content">{posts.data.content}</td>
|
||||
<td class="col-md-5 post-content-editable hidden">
|
||||
<textarea>{posts.data.rawContent}</textarea>
|
||||
<textarea class="form-control">{posts.data.rawContent}</textarea>
|
||||
</td>
|
||||
<td class="col-md-1">
|
||||
<span class="timeago" title={posts.data.timestampISO}></span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ul data-cid="{cid}">
|
||||
<!-- BEGIN categories -->
|
||||
<li data-cid="{categories.cid}" <!-- IF categories.disabled -->class="disabled"<!-- ENDIF categories.disabled -->>
|
||||
<li data-cid="{categories.cid}" data-parent-cid="{categories.parentCid}" data-name="{categories.name}" <!-- IF categories.disabled -->class="disabled"<!-- ENDIF categories.disabled -->>
|
||||
<div class="row category-row">
|
||||
<div class="col-md-9">
|
||||
<div class="clearfix">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<a role="menu-item">[[search:no-matches]]</a>
|
||||
</li>
|
||||
<!-- BEGIN categories -->
|
||||
<li role="presentation" class="category" data-cid="{categories.cid}" data-name="{categories.name}">
|
||||
<li role="presentation" class="category {{{if categories.disabledClass}}}disabled{{{end}}}" data-cid="{categories.cid}" data-name="{categories.name}">
|
||||
<a role="menu-item">{categories.level}<span component="category-markup"><!-- IF categories.icon --><span class="fa-stack" style="{function.generateCategoryBackground}"><i style="color: {categories.color};" class="fa fa-stack-1x fa-fw {categories.icon}"></i></span><!-- ENDIF categories.icon --> {categories.name}</span></a>
|
||||
</li>
|
||||
<!-- END categories -->
|
||||
|
||||
Reference in New Issue
Block a user