mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-06-23 10:30:32 +02:00
Merge commit '61e5293a76aafe9e09f3c665ac9f514a94b4769d' into v3.x
This commit is contained in:
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,3 +1,73 @@
|
||||
#### v3.8.2 (2024-05-29)
|
||||
|
||||
##### Chores
|
||||
|
||||
* up composer (83facb7d)
|
||||
* up harmony (17ea61a0)
|
||||
* incrementing version number - v3.8.1 (527326f7)
|
||||
* update changelog for v3.8.1 (5ef3e0f3)
|
||||
* incrementing version number - v3.8.0 (e228a6eb)
|
||||
* incrementing version number - v3.7.5 (6882894d)
|
||||
* incrementing version number - v3.7.4 (6678744c)
|
||||
* incrementing version number - v3.7.3 (2d62b6f6)
|
||||
* incrementing version number - v3.7.2 (cc257e7e)
|
||||
* incrementing version number - v3.7.1 (712365a5)
|
||||
* incrementing version number - v3.7.0 (9a6153d7)
|
||||
* incrementing version number - v3.6.7 (86a17e38)
|
||||
* incrementing version number - v3.6.6 (6604bf37)
|
||||
* incrementing version number - v3.6.5 (6c653625)
|
||||
* incrementing version number - v3.6.4 (83d131b4)
|
||||
* incrementing version number - v3.6.3 (fc7d2bfd)
|
||||
* incrementing version number - v3.6.2 (0f577a57)
|
||||
* incrementing version number - v3.6.1 (f1a69468)
|
||||
* incrementing version number - v3.6.0 (4cdf85f8)
|
||||
* incrementing version number - v3.5.3 (ed0e8783)
|
||||
* incrementing version number - v3.5.2 (52fbb2da)
|
||||
* incrementing version number - v3.5.1 (4c543488)
|
||||
* incrementing version number - v3.5.0 (d06fb4f0)
|
||||
* incrementing version number - v3.4.3 (5c984250)
|
||||
* incrementing version number - v3.4.2 (3f0dac38)
|
||||
* incrementing version number - v3.4.1 (01e69574)
|
||||
* incrementing version number - v3.4.0 (fd9247c5)
|
||||
* incrementing version number - v3.3.9 (5805e770)
|
||||
* incrementing version number - v3.3.8 (a5603565)
|
||||
* incrementing version number - v3.3.7 (b26f1744)
|
||||
* incrementing version number - v3.3.6 (7fb38792)
|
||||
* incrementing version number - v3.3.4 (a67f84ea)
|
||||
* incrementing version number - v3.3.3 (f94d239b)
|
||||
* incrementing version number - v3.3.2 (ec9dac97)
|
||||
* incrementing version number - v3.3.1 (151cc68f)
|
||||
* incrementing version number - v3.3.0 (fc1ad70f)
|
||||
* incrementing version number - v3.2.3 (b06d3e63)
|
||||
* incrementing version number - v3.2.2 (758ecfcd)
|
||||
* incrementing version number - v3.2.1 (20145074)
|
||||
* incrementing version number - v3.2.0 (9ecac38e)
|
||||
* incrementing version number - v3.1.7 (0b4e81ab)
|
||||
* incrementing version number - v3.1.6 (b3a3b130)
|
||||
* incrementing version number - v3.1.5 (ec19343a)
|
||||
* incrementing version number - v3.1.4 (2452783c)
|
||||
* incrementing version number - v3.1.3 (3b4e9d3f)
|
||||
* incrementing version number - v3.1.2 (40fa3489)
|
||||
* incrementing version number - v3.1.1 (40250733)
|
||||
* incrementing version number - v3.1.0 (0cb386bd)
|
||||
* incrementing version number - v3.0.1 (26f6ea49)
|
||||
* incrementing version number - v3.0.0 (224e08cd)
|
||||
|
||||
##### New Features
|
||||
|
||||
* show ignored/watched topics in topic list, closes #10974 (29dbe92d)
|
||||
* convert "All Votes Are Public" toggle to vote visibility (e0515080)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* wrong var for ignored (7969e62d)
|
||||
* reduce docker image size again and speed up build (56ef2bdd)
|
||||
* update thumb count when removing thumbs (6214336c)
|
||||
|
||||
##### Refactors
|
||||
|
||||
* render (2c0f8c91)
|
||||
|
||||
#### v3.8.1 (2024-05-15)
|
||||
|
||||
##### Chores
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@fontsource/poppins": "5.0.14",
|
||||
"@fortawesome/fontawesome-free": "6.5.2",
|
||||
"@isaacs/ttlcache": "1.4.1",
|
||||
"@nodebb/spider-detector": "2.0.3",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"ace-builds": "1.33.2",
|
||||
"archiver": "7.0.1",
|
||||
@@ -93,9 +94,9 @@
|
||||
"mousetrap": "1.6.5",
|
||||
"multiparty": "4.2.3",
|
||||
"nconf": "0.12.1",
|
||||
"nodebb-plugin-2factor": "7.5.1",
|
||||
"nodebb-plugin-2factor": "7.5.3",
|
||||
"nodebb-plugin-composer-default": "10.2.36",
|
||||
"nodebb-plugin-dbsearch": "6.2.3",
|
||||
"nodebb-plugin-dbsearch": "6.2.5",
|
||||
"nodebb-plugin-emoji": "5.1.15",
|
||||
"nodebb-plugin-emoji-android": "4.0.0",
|
||||
"nodebb-plugin-markdown": "12.2.6",
|
||||
@@ -103,10 +104,10 @@
|
||||
"nodebb-plugin-ntfy": "1.7.4",
|
||||
"nodebb-plugin-spam-be-gone": "2.2.2",
|
||||
"nodebb-rewards-essentials": "1.0.0",
|
||||
"nodebb-theme-harmony": "1.2.57",
|
||||
"nodebb-theme-harmony": "1.2.63",
|
||||
"nodebb-theme-lavender": "7.1.8",
|
||||
"nodebb-theme-peace": "2.2.5",
|
||||
"nodebb-theme-persona": "13.3.20",
|
||||
"nodebb-theme-peace": "2.2.6",
|
||||
"nodebb-theme-persona": "13.3.24",
|
||||
"nodebb-widget-essentials": "7.0.16",
|
||||
"nodemailer": "6.9.13",
|
||||
"nprogress": "0.2.0",
|
||||
@@ -134,7 +135,6 @@
|
||||
"@socket.io/redis-adapter": "8.3.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"spdx-license-list": "6.9.0",
|
||||
"spider-detector": "2.0.1",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"textcomplete": "0.18.2",
|
||||
"textcomplete.contenteditable": "0.1.1",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"ip": "IP <strong>%1</strong>",
|
||||
"nodes-responded": "%1 nodes responded within %2ms!",
|
||||
"host": "host",
|
||||
"primary": "primary / run jobs",
|
||||
"primary": "primary / jobs",
|
||||
"pid": "pid",
|
||||
"nodejs": "nodejs",
|
||||
"online": "online",
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
"registered": "Registered",
|
||||
"sockets": "Sockets",
|
||||
"connection-count": "Connection Count",
|
||||
"guests": "Guests",
|
||||
|
||||
"info": "Info"
|
||||
|
||||
@@ -38,5 +38,6 @@
|
||||
"remove-selected": "Remove Selected",
|
||||
"remove-selected-confirm": "Do you want to remove %1 selected posts?",
|
||||
"bulk-accept-success": "%1 posts accepted",
|
||||
"bulk-reject-success": "%1 posts rejected"
|
||||
"bulk-reject-success": "%1 posts rejected",
|
||||
"links-in-this-post": "Links in this post"
|
||||
}
|
||||
@@ -331,6 +331,12 @@ UserObjectFull:
|
||||
example:
|
||||
- administrators
|
||||
- Staff
|
||||
iconBackgrounds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: A valid CSS colour code
|
||||
example: '#fff'
|
||||
muted:
|
||||
type: boolean
|
||||
description: Whether or not the user has been muted.
|
||||
|
||||
@@ -168,6 +168,8 @@ paths:
|
||||
$ref: 'read/admin/upload/file.yaml'
|
||||
/api/admin/uploadDefaultAvatar:
|
||||
$ref: 'read/admin/uploadDefaultAvatar.yaml'
|
||||
/api/admin/config:
|
||||
$ref: 'read/admin/config.yaml'
|
||||
/api/config:
|
||||
$ref: 'read/config.yaml'
|
||||
/api/users:
|
||||
|
||||
174
public/openapi/read/admin/config.yaml
Normal file
174
public/openapi/read/admin/config.yaml
Normal file
@@ -0,0 +1,174 @@
|
||||
get:
|
||||
tags:
|
||||
- admin
|
||||
summary: Get forum settings and admin only settings
|
||||
description: This route retrieves forum settings and user-specific settings for client-side and admin-side options on the forum.
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
relative_path:
|
||||
type: string
|
||||
upload_url:
|
||||
type: string
|
||||
assetBaseUrl:
|
||||
type: string
|
||||
asset_base_url:
|
||||
type: string
|
||||
siteTitle:
|
||||
type: string
|
||||
browserTitle:
|
||||
type: string
|
||||
titleLayout:
|
||||
type: string
|
||||
showSiteTitle:
|
||||
type: boolean
|
||||
maintenanceMode:
|
||||
type: boolean
|
||||
postQueue:
|
||||
type: number
|
||||
minimumTitleLength:
|
||||
type: number
|
||||
maximumTitleLength:
|
||||
type: number
|
||||
minimumPostLength:
|
||||
type: number
|
||||
maximumPostLength:
|
||||
type: number
|
||||
minimumTagsPerTopic:
|
||||
type: number
|
||||
maximumTagsPerTopic:
|
||||
type: number
|
||||
minimumTagLength:
|
||||
type: number
|
||||
undoTimeout:
|
||||
type: number
|
||||
maximumTagLength:
|
||||
type: number
|
||||
useOutgoingLinksPage:
|
||||
type: boolean
|
||||
allowGuestHandles:
|
||||
type: boolean
|
||||
allowTopicsThumbnail:
|
||||
type: boolean
|
||||
usePagination:
|
||||
type: boolean
|
||||
disableChat:
|
||||
type: boolean
|
||||
disableChatMessageEditing:
|
||||
type: boolean
|
||||
maximumChatMessageLength:
|
||||
type: number
|
||||
socketioTransports:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
socketioOrigins:
|
||||
type: string
|
||||
websocketAddress:
|
||||
type: string
|
||||
maxReconnectionAttempts:
|
||||
type: number
|
||||
reconnectionDelay:
|
||||
type: number
|
||||
topicsPerPage:
|
||||
type: number
|
||||
postsPerPage:
|
||||
type: number
|
||||
maximumFileSize:
|
||||
type: number
|
||||
theme:id:
|
||||
type: string
|
||||
theme:src:
|
||||
type: string
|
||||
defaultLang:
|
||||
type: string
|
||||
userLang:
|
||||
type: string
|
||||
loggedIn:
|
||||
type: boolean
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
cache-buster:
|
||||
type: string
|
||||
topicPostSort:
|
||||
type: string
|
||||
categoryTopicSort:
|
||||
type: string
|
||||
csrf_token:
|
||||
type: string
|
||||
searchEnabled:
|
||||
type: boolean
|
||||
searchDefaultInQuick:
|
||||
type: string
|
||||
disableCustomUserSkins:
|
||||
type: boolean
|
||||
bootswatchSkin:
|
||||
type: string
|
||||
defaultBootswatchSkin:
|
||||
type: string
|
||||
composer:showHelpTab:
|
||||
type: boolean
|
||||
enablePostHistory:
|
||||
type: boolean
|
||||
timeagoCutoff:
|
||||
type: number
|
||||
timeagoCodes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
cookies:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
dismiss:
|
||||
type: string
|
||||
link:
|
||||
type: string
|
||||
link_url:
|
||||
type: string
|
||||
thumbs:
|
||||
type: object
|
||||
properties:
|
||||
size:
|
||||
type: number
|
||||
acpLang:
|
||||
type: string
|
||||
openOutgoingLinksInNewTab:
|
||||
type: boolean
|
||||
topicSearchEnabled:
|
||||
type: boolean
|
||||
hideSubCategories:
|
||||
type: boolean
|
||||
hideCategoryLastPost:
|
||||
type: boolean
|
||||
enableQuickReply:
|
||||
type: boolean
|
||||
emailPrompt:
|
||||
type: number
|
||||
useragent:
|
||||
type: object
|
||||
properties:
|
||||
isSafari:
|
||||
type: boolean
|
||||
composer-default:
|
||||
type: object
|
||||
fontawesome:
|
||||
type: object
|
||||
properties:
|
||||
pro:
|
||||
type: boolean
|
||||
styles:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
@@ -116,6 +116,8 @@ get:
|
||||
type: number
|
||||
socketCount:
|
||||
type: number
|
||||
connectionCount:
|
||||
type: number
|
||||
users:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -152,129 +152,13 @@ get:
|
||||
type: boolean
|
||||
enableQuickReply:
|
||||
type: boolean
|
||||
iconBackgrounds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: A valid CSS colour code
|
||||
example: '#fff'
|
||||
emailPrompt:
|
||||
type: number
|
||||
useragent:
|
||||
type: object
|
||||
properties:
|
||||
isYaBrowser:
|
||||
type: boolean
|
||||
isAuthoritative:
|
||||
type: boolean
|
||||
isMobile:
|
||||
type: boolean
|
||||
isMobileNative:
|
||||
type: boolean
|
||||
isTablet:
|
||||
type: boolean
|
||||
isiPad:
|
||||
type: boolean
|
||||
isiPod:
|
||||
type: boolean
|
||||
isiPhone:
|
||||
type: boolean
|
||||
isiPhoneNative:
|
||||
type: boolean
|
||||
isAndroid:
|
||||
type: boolean
|
||||
isAndroidNative:
|
||||
type: boolean
|
||||
isBlackberry:
|
||||
type: boolean
|
||||
isOpera:
|
||||
type: boolean
|
||||
isIE:
|
||||
type: boolean
|
||||
isEdge:
|
||||
type: boolean
|
||||
isIECompatibilityMode:
|
||||
type: boolean
|
||||
isSafari:
|
||||
type: boolean
|
||||
isFirefox:
|
||||
type: boolean
|
||||
isWebkit:
|
||||
type: boolean
|
||||
isChrome:
|
||||
type: boolean
|
||||
isKonqueror:
|
||||
type: boolean
|
||||
isOmniWeb:
|
||||
type: boolean
|
||||
isSeaMonkey:
|
||||
type: boolean
|
||||
isFlock:
|
||||
type: boolean
|
||||
isAmaya:
|
||||
type: boolean
|
||||
isPhantomJS:
|
||||
type: boolean
|
||||
isEpiphany:
|
||||
type: boolean
|
||||
isDesktop:
|
||||
type: boolean
|
||||
isWindows:
|
||||
type: boolean
|
||||
isLinux:
|
||||
type: boolean
|
||||
isLinux64:
|
||||
type: boolean
|
||||
isMac:
|
||||
type: boolean
|
||||
isChromeOS:
|
||||
type: boolean
|
||||
isBada:
|
||||
type: boolean
|
||||
isSamsung:
|
||||
type: boolean
|
||||
isRaspberry:
|
||||
type: boolean
|
||||
isBot:
|
||||
type: boolean
|
||||
isCurl:
|
||||
type: boolean
|
||||
isAndroidTablet:
|
||||
type: boolean
|
||||
isWinJs:
|
||||
type: boolean
|
||||
isKindleFire:
|
||||
type: boolean
|
||||
isSilk:
|
||||
type: boolean
|
||||
isCaptive:
|
||||
type: boolean
|
||||
isSmartTV:
|
||||
type: boolean
|
||||
isUC:
|
||||
type: boolean
|
||||
isFacebook:
|
||||
type: boolean
|
||||
isAlamoFire:
|
||||
type: boolean
|
||||
isElectron:
|
||||
type: boolean
|
||||
silkAccelerated:
|
||||
type: boolean
|
||||
browser:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
os:
|
||||
type: string
|
||||
platform:
|
||||
type: string
|
||||
geoIp:
|
||||
type: object
|
||||
source:
|
||||
type: string
|
||||
isWechat:
|
||||
type: boolean
|
||||
composer-default:
|
||||
type: object
|
||||
fontawesome:
|
||||
|
||||
@@ -382,6 +382,8 @@ get:
|
||||
type: number
|
||||
downvote:disabled:
|
||||
type: number
|
||||
voteVisibility:
|
||||
type: string
|
||||
feeds:disableRSS:
|
||||
type: number
|
||||
signatures:hideDuplicates:
|
||||
|
||||
@@ -180,6 +180,10 @@ paths:
|
||||
$ref: 'write/posts/pid/move.yaml'
|
||||
/posts/{pid}/vote:
|
||||
$ref: 'write/posts/pid/vote.yaml'
|
||||
/posts/{pid}/voters:
|
||||
$ref: 'write/posts/pid/voters.yaml'
|
||||
/posts/{pid}/upvoters:
|
||||
$ref: 'write/posts/pid/upvoters.yaml'
|
||||
/posts/{pid}/bookmark:
|
||||
$ref: 'write/posts/pid/bookmark.yaml'
|
||||
/posts/{pid}/diffs:
|
||||
|
||||
33
public/openapi/write/posts/pid/upvoters.yaml
Normal file
33
public/openapi/write/posts/pid/upvoters.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
get:
|
||||
tags:
|
||||
- posts
|
||||
summary: get upvoter usernames of a post
|
||||
description: This is used for getting a list of upvoter usernames for the vote tooltip
|
||||
parameters:
|
||||
- in: path
|
||||
name: pid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid post id
|
||||
example: 2
|
||||
responses:
|
||||
'200':
|
||||
description: Usernames of upvoters of post
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
otherCount:
|
||||
type: number
|
||||
usernames:
|
||||
type: array
|
||||
cutoff:
|
||||
type: number
|
||||
|
||||
37
public/openapi/write/posts/pid/voters.yaml
Normal file
37
public/openapi/write/posts/pid/voters.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
get:
|
||||
tags:
|
||||
- posts
|
||||
summary: get voters of a post
|
||||
description: This returns the upvoters and downvoters of a post if the user has permission to view them
|
||||
parameters:
|
||||
- in: path
|
||||
name: pid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid post id
|
||||
example: 2
|
||||
responses:
|
||||
'200':
|
||||
description: Data about upvoters and downvoters of the post
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
upvoteCount:
|
||||
type: number
|
||||
downvoteCount:
|
||||
type: number
|
||||
showDownvotes:
|
||||
type: boolean
|
||||
upvoters:
|
||||
type: array
|
||||
downvoters:
|
||||
type: array
|
||||
|
||||
@@ -20,3 +20,20 @@ html[data-dir="rtl"] {
|
||||
}
|
||||
}
|
||||
/*rtl:end:ignore*/
|
||||
|
||||
[component="post/content"], [component="chat/message/body"], [component="composer"] .preview {
|
||||
h1 { font-size: calc(1.15rem + 1vw); }
|
||||
h2 { font-size: calc(1.1rem + 0.8vw); }
|
||||
h3 { font-size: calc(1.075rem + 0.6vw); }
|
||||
h4 { font-size: calc(1.05rem + 0.3vw); }
|
||||
h5 { font-size: 1.125rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
@include media-breakpoint-up(xl) {
|
||||
h1 { font-size: 1.75rem; }
|
||||
h2 { font-size: 1.5rem; }
|
||||
h3 { font-size: 1.375rem; }
|
||||
h4 { font-size: 1.250rem; }
|
||||
h5 { font-size: 1.125rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ ajaxify.widgets = { render: render };
|
||||
}
|
||||
ajaxify.go = function (url, callback, quiet) {
|
||||
// Automatically reconnect to socket and re-ajaxify on success
|
||||
if (!socket.connected) {
|
||||
if (!socket.connected && parseInt(app.user.uid, 10) >= 0) {
|
||||
app.reconnect();
|
||||
|
||||
if (ajaxify.reconnectAction) {
|
||||
|
||||
@@ -37,7 +37,7 @@ define('forum/chats/recent', ['alerts', 'api', 'chat'], function (alerts, api, c
|
||||
recentChats.attr('loading', 1);
|
||||
api.get(`/chats`, {
|
||||
uid: ajaxify.data.uid,
|
||||
after: recentChats.attr('data-nextstart'),
|
||||
start: recentChats.attr('data-nextstart'),
|
||||
}).then(({ rooms, nextStart }) => {
|
||||
if (rooms.length) {
|
||||
onRecentChatsLoaded({ rooms, nextStart }, function () {
|
||||
|
||||
@@ -29,8 +29,23 @@ define('forum/post-queue', [
|
||||
});
|
||||
|
||||
$('[component="post/content"] img:not(.not-responsive)').addClass('img-fluid');
|
||||
showLinksInPosts();
|
||||
};
|
||||
|
||||
function showLinksInPosts() {
|
||||
$('.posts-list [data-id]').each((idx, el) => {
|
||||
const $el = $(el);
|
||||
const linkContainer = $el.find('[component="post-queue/link-container"]');
|
||||
const linkList = linkContainer.find('[component="post-queue/link-container/list"]');
|
||||
const linksInPost = $el.find('.post-content a');
|
||||
linksInPost.each((idx, link) => {
|
||||
const href = $(link).attr('href');
|
||||
linkList.append(`<li><a href="${href}">${href}</a></li>`);
|
||||
});
|
||||
linkContainer.toggleClass('hidden', !linksInPost.length);
|
||||
});
|
||||
}
|
||||
|
||||
function confirmReject(msg) {
|
||||
return new Promise((resolve) => {
|
||||
bootbox.confirm(msg, resolve);
|
||||
|
||||
@@ -297,7 +297,7 @@ define('forum/topic', [
|
||||
destroyed = true;
|
||||
}
|
||||
$(window).one('action:ajaxify.start', destroyTooltip);
|
||||
$('[component="topic"]').on('mouseenter', '[component="post"] a, [component="topic/event"] a', async function () {
|
||||
$('[component="topic"]').on('mouseenter', '[component="post/parent"] a, [component="post/content"] a, [component="topic/event"] a', async function () {
|
||||
const link = $(this);
|
||||
destroyed = false;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ define('forum/topic/postTools', [
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
$('[component="topic"]').on('show.bs.dropdown', '.moderator-tools', function () {
|
||||
$('[component="topic"]').on('show.bs.dropdown', '[component="post/tools"]', function () {
|
||||
const $this = $(this);
|
||||
const dropdownMenu = $this.find('.dropdown-menu');
|
||||
const { top } = this.getBoundingClientRect();
|
||||
@@ -45,6 +45,10 @@ define('forum/topic/postTools', [
|
||||
if (dropdownMenu.attr('data-loaded')) {
|
||||
return;
|
||||
}
|
||||
dropdownMenu.html(helpers.generatePlaceholderWave([
|
||||
3, 5, 9, 7, 10, 'divider', 10,
|
||||
]));
|
||||
|
||||
const postEl = $this.parents('[data-pid]');
|
||||
const pid = postEl.attr('data-pid');
|
||||
const index = parseInt(postEl.attr('data-index'), 10);
|
||||
|
||||
@@ -8,21 +8,22 @@ define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts', 'api'], f
|
||||
const post = button.closest('[data-pid]');
|
||||
const pid = post.data('pid');
|
||||
const open = button.find('[component="post/replies/open"]');
|
||||
const loading = button.find('[component="post/replies/loading"]');
|
||||
const close = button.find('[component="post/replies/close"]');
|
||||
|
||||
if (open.is(':not(.hidden)') && loading.is('.hidden')) {
|
||||
open.addClass('hidden');
|
||||
loading.removeClass('hidden');
|
||||
if (open.attr('loading') !== '1' && open.attr('loaded') !== '1') {
|
||||
open.attr('loading', '1')
|
||||
.removeClass('fa-chevron-down')
|
||||
.addClass('fa-spin fa-spinner');
|
||||
|
||||
api.get(`/posts/${pid}/replies`, {}, function (err, { replies }) {
|
||||
const postData = replies;
|
||||
loading.addClass('hidden');
|
||||
open.removeAttr('loading')
|
||||
.attr('loaded', '1')
|
||||
.removeClass('fa-spin fa-spinner')
|
||||
.addClass('fa-chevron-up');
|
||||
if (err) {
|
||||
open.removeClass('hidden');
|
||||
return alerts.error(err);
|
||||
}
|
||||
|
||||
close.removeClass('hidden');
|
||||
postData.forEach((post, index) => {
|
||||
if (post) {
|
||||
post.index = index;
|
||||
@@ -50,10 +51,11 @@ define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts', 'api'], f
|
||||
hooks.fire('action:posts.loaded', { posts: postData });
|
||||
});
|
||||
});
|
||||
} else if (close.is(':not(.hidden)')) {
|
||||
close.addClass('hidden');
|
||||
open.removeClass('hidden');
|
||||
loading.addClass('hidden');
|
||||
} else if (open.attr('loaded') === '1') {
|
||||
open.removeAttr('loaded')
|
||||
.removeAttr('loading')
|
||||
.removeClass('fa-spin fa-spinner fa-chevron-up')
|
||||
.addClass('fa-chevron-down');
|
||||
post.find('[component="post/replies"]').slideUp('fast', function () {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@ define('forum/topic/threadTools', [
|
||||
'bootbox',
|
||||
'alerts',
|
||||
'bootstrap',
|
||||
], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts, bootstrap) {
|
||||
'helpers',
|
||||
], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts, bootstrap, helpers) {
|
||||
const ThreadTools = {};
|
||||
|
||||
ThreadTools.init = function (tid, topicContainer) {
|
||||
@@ -211,6 +212,7 @@ define('forum/topic/threadTools', [
|
||||
if (dropdownMenu.attr('data-loaded')) {
|
||||
return;
|
||||
}
|
||||
dropdownMenu.html(helpers.generatePlaceholderWave([8, 8, 8]));
|
||||
const data = await socket.emit('topics.loadTopicTools', { tid: ajaxify.data.tid, cid: ajaxify.data.cid });
|
||||
const html = await app.parseAndTranslate('partials/topic/topic-menu-list', data);
|
||||
$(dropdownMenu).attr('data-loaded', 'true').html(html);
|
||||
|
||||
@@ -9,10 +9,19 @@ define('forum/topic/votes', [
|
||||
|
||||
Votes.addVoteHandler = function () {
|
||||
_showTooltip = {};
|
||||
components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', loadDataAndCreateTooltip);
|
||||
components.get('topic').on('mouseleave', '[data-pid] [component="post/vote-count"]', destroyTooltip);
|
||||
if (canSeeVotes()) {
|
||||
components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', loadDataAndCreateTooltip);
|
||||
components.get('topic').on('mouseleave', '[data-pid] [component="post/vote-count"]', destroyTooltip);
|
||||
}
|
||||
};
|
||||
|
||||
function canSeeVotes() {
|
||||
const { voteVisibility, privileges } = ajaxify.data;
|
||||
return privileges.isAdminOrMod ||
|
||||
voteVisibility === 'all' ||
|
||||
(voteVisibility === 'loggedin' && config.loggedIn);
|
||||
}
|
||||
|
||||
function destroyTooltip() {
|
||||
const $this = $(this);
|
||||
const pid = $this.parents('[data-pid]').attr('data-pid');
|
||||
@@ -35,15 +44,12 @@ define('forum/topic/votes', [
|
||||
$this.attr('title', '');
|
||||
}
|
||||
|
||||
socket.emit('posts.getUpvoters', [pid], function (err, data) {
|
||||
api.get(`/posts/${pid}/upvoters`, {}, function (err, data) {
|
||||
if (err) {
|
||||
if (err.message === '[[error:no-privileges]]') {
|
||||
return;
|
||||
}
|
||||
return alerts.error(err);
|
||||
}
|
||||
if (_showTooltip[pid] && data.length) {
|
||||
createTooltip($this, data[0]);
|
||||
if (_showTooltip[pid] && data) {
|
||||
createTooltip($this, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -101,13 +107,11 @@ define('forum/topic/votes', [
|
||||
};
|
||||
|
||||
Votes.showVotes = function (pid) {
|
||||
socket.emit('posts.getVoters', { pid: pid }, function (err, data) {
|
||||
if (!canSeeVotes()) {
|
||||
return;
|
||||
}
|
||||
api.get(`/posts/${pid}/voters`, {}, function (err, data) {
|
||||
if (err) {
|
||||
if (err.message === '[[error:no-privileges]]') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show error if it's an unexpected error.
|
||||
return alerts.error(err);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ define('accounts/picture', [
|
||||
icon: { text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor'] },
|
||||
defaultAvatar: ajaxify.data.defaultAvatar,
|
||||
allowProfileImageUploads: ajaxify.data.allowProfileImageUploads,
|
||||
iconBackgrounds: config.iconBackgrounds,
|
||||
iconBackgrounds: ajaxify.data.iconBackgrounds,
|
||||
user: {
|
||||
uid: ajaxify.data.uid,
|
||||
username: ajaxify.data.username,
|
||||
|
||||
@@ -25,12 +25,14 @@ module.exports = function (utils, Benchpress, relative_path) {
|
||||
userAgentIcons,
|
||||
buildAvatar,
|
||||
increment,
|
||||
generateWroteReplied,
|
||||
generateRepliedTo,
|
||||
generateWrote,
|
||||
isoTimeToLocaleString,
|
||||
shouldHideReplyContainer,
|
||||
humanReadableNumber,
|
||||
formattedNumber,
|
||||
generatePlaceholderWave,
|
||||
register,
|
||||
__escape: identity,
|
||||
};
|
||||
@@ -295,34 +297,24 @@ module.exports = function (utils, Benchpress, relative_path) {
|
||||
if (!userObj) {
|
||||
userObj = this;
|
||||
}
|
||||
|
||||
classNames = classNames || '';
|
||||
const attributes = new Map([
|
||||
['alt', userObj.username],
|
||||
['title', userObj.username],
|
||||
['data-uid', userObj.uid],
|
||||
['loading', 'lazy'],
|
||||
['aria-label', `[[aria:user-avatar-for, ${userObj.username}]]`],
|
||||
['class', `avatar ${classNames}${rounded ? ' avatar-rounded' : ''}`],
|
||||
]);
|
||||
const styles = [`--avatar-size: ${size};`];
|
||||
const attr2String = attributes => Array.from(attributes).reduce((output, [prop, value]) => {
|
||||
output += ` ${prop}="${value}"`;
|
||||
return output;
|
||||
}, '');
|
||||
classNames = classNames || '';
|
||||
|
||||
attributes.set('class', `avatar ${classNames}${rounded ? ' avatar-rounded' : ''}`);
|
||||
|
||||
let output = '';
|
||||
|
||||
if (userObj.picture) {
|
||||
attributes.set('component', component || 'avatar/picture');
|
||||
output += '<img ' + attr2String(attributes) + ' src="' + userObj.picture + '" style="' + styles.join(' ') + '" onError="this.remove();" itemprop="image" />';
|
||||
output += `<img${attr2String(attributes)} alt="${userObj.username}" loading="lazy" component="${component || 'avatar/picture'}" src="${userObj.picture}" style="${styles.join(' ')}" onError="this.remove()" itemprop="image" />`;
|
||||
}
|
||||
|
||||
attributes.set('component', component || 'avatar/icon');
|
||||
styles.push('background-color: ' + userObj['icon:bgColor'] + ';');
|
||||
output += '<span ' + attr2String(attributes) + ' style="' + styles.join(' ') + '">' + userObj['icon:text'] + '</span>';
|
||||
|
||||
output += `<span${attr2String(attributes)} component="${component || 'avatar/icon'}" style="${styles.join(' ')} background-color: ${userObj['icon:bgColor']}">${userObj['icon:text']}</span>`;
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -330,6 +322,13 @@ module.exports = function (utils, Benchpress, relative_path) {
|
||||
return String(value + parseInt(inc, 10));
|
||||
}
|
||||
|
||||
function generateWroteReplied(post, timeagoCutoff) {
|
||||
if (post.toPid) {
|
||||
return generateRepliedTo(post, timeagoCutoff);
|
||||
}
|
||||
return generateWrote(post, timeagoCutoff);
|
||||
}
|
||||
|
||||
function generateRepliedTo(post, timeagoCutoff) {
|
||||
const displayname = post.parent && post.parent.displayname ?
|
||||
post.parent.displayname : '[[global:guest]]';
|
||||
@@ -367,6 +366,21 @@ module.exports = function (utils, Benchpress, relative_path) {
|
||||
return utils.addCommas(number);
|
||||
}
|
||||
|
||||
function generatePlaceholderWave(items) {
|
||||
const html = items.map((i) => {
|
||||
if (i === 'divider') {
|
||||
return '<li class="dropdown-divider"></li>';
|
||||
}
|
||||
return `
|
||||
<li class="dropdown-item placeholder-wave">
|
||||
<div class="placeholder" style="width: 20px;"></div>
|
||||
<div class="placeholder col-${i}"></div>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function register() {
|
||||
Object.keys(helpers).forEach(function (helperName) {
|
||||
Benchpress.registerHelper(helperName, helpers[helperName]);
|
||||
|
||||
@@ -183,7 +183,10 @@ define('navigator', [
|
||||
async function updateThumbTimestampToIndex(thumb, index) {
|
||||
const el = thumb.find('.thumb-timestamp');
|
||||
if (el.length) {
|
||||
const timestamp = await getPostTimestampByIndex(index);
|
||||
const postAtIndex = ajaxify.data.posts.find(
|
||||
p => parseInt(p.index, 10) === Math.max(0, parseInt(index, 10) - 1)
|
||||
);
|
||||
const timestamp = postAtIndex ? postAtIndex.timestamp : await getPostTimestampByIndex(index);
|
||||
el.attr('title', utils.toISOString(timestamp)).timeago();
|
||||
}
|
||||
}
|
||||
@@ -450,7 +453,6 @@ define('navigator', [
|
||||
}
|
||||
count = value;
|
||||
navigator.updateTextAndProgressBar();
|
||||
setThumbToIndex(index);
|
||||
toggle(count > 0);
|
||||
};
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ define('quickreply', [
|
||||
const textEl = components.get('topic/quickreply/text');
|
||||
composer.newReply({
|
||||
tid: ajaxify.data.tid,
|
||||
title: ajaxify.data.title,
|
||||
title: ajaxify.data.titleRaw,
|
||||
body: textEl.val(),
|
||||
});
|
||||
textEl.val('');
|
||||
|
||||
@@ -16,6 +16,7 @@ app = window.app || {};
|
||||
reconnectionAttempts: config.maxReconnectionAttempts,
|
||||
reconnectionDelay: config.reconnectionDelay,
|
||||
transports: config.socketioTransports,
|
||||
autoConnect: false,
|
||||
path: config.relative_path + '/socket.io',
|
||||
query: {
|
||||
_csrf: config.csrf_token,
|
||||
@@ -48,11 +49,12 @@ app = window.app || {};
|
||||
hooks = _hooks;
|
||||
if (parseInt(app.user.uid, 10) >= 0) {
|
||||
addHandlers();
|
||||
socket.connect();
|
||||
}
|
||||
});
|
||||
|
||||
window.app.reconnect = () => {
|
||||
if (socket.connected) {
|
||||
if (socket.connected || parseInt(app.user.uid, 10) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
const validator = require('validator');
|
||||
const _ = require('lodash');
|
||||
|
||||
const db = require('../database');
|
||||
const utils = require('../utils');
|
||||
const user = require('../user');
|
||||
const posts = require('../posts');
|
||||
const postsCache = require('../posts/cache');
|
||||
const topics = require('../topics');
|
||||
const groups = require('../groups');
|
||||
const plugins = require('../plugins');
|
||||
@@ -224,7 +226,7 @@ postsAPI.purge = async function (caller, data) {
|
||||
if (!canPurge) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
require('../posts/cache').del(data.pid);
|
||||
postsCache.del(data.pid);
|
||||
await posts.purge(data.pid, caller.uid);
|
||||
|
||||
websockets.in(`topic_${postData.tid}`).emit('event:post_purged', postData);
|
||||
@@ -306,6 +308,95 @@ postsAPI.unvote = async function (caller, data) {
|
||||
return await apiHelpers.postCommand(caller, 'unvote', 'voted', '', data);
|
||||
};
|
||||
|
||||
postsAPI.getVoters = async function (caller, data) {
|
||||
if (!data || !data.pid) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const { pid } = data;
|
||||
const cid = await posts.getCidByPid(pid);
|
||||
if (!await canSeeVotes(caller.uid, cid)) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
const showDownvotes = !meta.config['downvote:disabled'];
|
||||
const [upvoteUids, downvoteUids] = await Promise.all([
|
||||
db.getSetMembers(`pid:${data.pid}:upvote`),
|
||||
showDownvotes ? db.getSetMembers(`pid:${data.pid}:downvote`) : [],
|
||||
]);
|
||||
|
||||
const [upvoters, downvoters] = await Promise.all([
|
||||
user.getUsersFields(upvoteUids, ['username', 'userslug', 'picture']),
|
||||
user.getUsersFields(downvoteUids, ['username', 'userslug', 'picture']),
|
||||
]);
|
||||
|
||||
return {
|
||||
upvoteCount: upvoters.length,
|
||||
downvoteCount: downvoters.length,
|
||||
showDownvotes: showDownvotes,
|
||||
upvoters: upvoters,
|
||||
downvoters: downvoters,
|
||||
};
|
||||
};
|
||||
|
||||
postsAPI.getUpvoters = async function (caller, data) {
|
||||
if (!data.pid) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const { pid } = data;
|
||||
const cid = await posts.getCidByPid(pid);
|
||||
if (!await canSeeVotes(caller.uid, cid)) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
let upvotedUids = (await posts.getUpvotedUidsByPids([pid]))[0];
|
||||
const cutoff = 6;
|
||||
if (!upvotedUids.length) {
|
||||
return {
|
||||
otherCount: 0,
|
||||
usernames: [],
|
||||
cutoff,
|
||||
};
|
||||
}
|
||||
let otherCount = 0;
|
||||
if (upvotedUids.length > cutoff) {
|
||||
otherCount = upvotedUids.length - (cutoff - 1);
|
||||
upvotedUids = upvotedUids.slice(0, cutoff - 1);
|
||||
}
|
||||
|
||||
const usernames = await user.getUsernamesByUids(upvotedUids);
|
||||
return {
|
||||
otherCount,
|
||||
usernames,
|
||||
cutoff,
|
||||
};
|
||||
};
|
||||
|
||||
async function canSeeVotes(uid, cids) {
|
||||
const isArray = Array.isArray(cids);
|
||||
if (!isArray) {
|
||||
cids = [cids];
|
||||
}
|
||||
const uniqCids = _.uniq(cids);
|
||||
const [canRead, isAdmin, isMod] = await Promise.all([
|
||||
privileges.categories.isUserAllowedTo(
|
||||
'topics:read', uniqCids, uid
|
||||
),
|
||||
privileges.users.isAdministrator(uid),
|
||||
privileges.users.isModerator(uid, cids),
|
||||
]);
|
||||
const cidToAllowed = _.zipObject(uniqCids, canRead);
|
||||
const checks = cids.map(
|
||||
(cid, index) => isAdmin || isMod[index] ||
|
||||
(
|
||||
cidToAllowed[cid] &&
|
||||
(
|
||||
meta.config.voteVisibility === 'all' ||
|
||||
(meta.config.voteVisibility === 'loggedin' && parseInt(uid, 10) > 0)
|
||||
)
|
||||
)
|
||||
);
|
||||
return isArray ? checks : checks[0];
|
||||
}
|
||||
|
||||
postsAPI.bookmark = async function (caller, data) {
|
||||
return await apiHelpers.postCommand(caller, 'bookmark', 'bookmarked', '', data);
|
||||
};
|
||||
|
||||
@@ -656,7 +656,7 @@ usersAPI.changePicture = async (caller, data) => {
|
||||
picture = returnData && returnData.picture;
|
||||
}
|
||||
|
||||
const validBackgrounds = await user.getIconBackgrounds(caller.uid);
|
||||
const validBackgrounds = await user.getIconBackgrounds();
|
||||
if (!validBackgrounds.includes(data.bgColor)) {
|
||||
data.bgColor = validBackgrounds[0];
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ blocksController.getBlocks = async function (req, res) {
|
||||
const resultsPerPage = 50;
|
||||
const start = Math.max(0, page - 1) * resultsPerPage;
|
||||
const stop = start + resultsPerPage - 1;
|
||||
const payload = res.locals.userData;
|
||||
const { uid, username, userslug, blocksCount } = payload;
|
||||
|
||||
const { uid, username, userslug, blocksCount } = await user.getUserFields(res.locals.uid, ['uid', 'username', 'userslug', 'blocksCount']);
|
||||
const payload = {};
|
||||
const uids = await user.blocks.list(uid);
|
||||
const data = await plugins.hooks.fire('filter:user.getBlocks', {
|
||||
uids: uids,
|
||||
|
||||
@@ -9,7 +9,8 @@ const meta = require('../../meta');
|
||||
const categoriesController = module.exports;
|
||||
|
||||
categoriesController.get = async function (req, res) {
|
||||
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
|
||||
const payload = res.locals.userData;
|
||||
const { username, userslug } = payload;
|
||||
const [states, allCategoriesData] = await Promise.all([
|
||||
user.getCategoryWatchState(res.locals.uid),
|
||||
categories.buildForSelect(res.locals.uid, 'find', ['descriptionParsed', 'depth', 'slug']),
|
||||
@@ -31,7 +32,6 @@ categoriesController.get = async function (req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
const payload = {};
|
||||
payload.categories = categoriesData;
|
||||
payload.title = `[[pages:account/watched-categories, ${username}]]`;
|
||||
payload.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
const db = require('../../database');
|
||||
const meta = require('../../meta');
|
||||
const user = require('../../user');
|
||||
const helpers = require('../helpers');
|
||||
|
||||
const consentController = module.exports;
|
||||
@@ -11,11 +10,10 @@ consentController.get = async function (req, res, next) {
|
||||
if (!meta.config.gdpr_enabled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
|
||||
const payload = res.locals.userData;
|
||||
const { username, userslug } = payload;
|
||||
const consented = await db.getObjectField(`user:${res.locals.uid}`, 'gdpr_consent');
|
||||
|
||||
const payload = {};
|
||||
payload.gdpr_consent = parseInt(consented, 10) === 1;
|
||||
payload.digest = {
|
||||
frequency: meta.config.dailyDigestFreq || 'off',
|
||||
|
||||
@@ -6,13 +6,16 @@ const helpers = require('../helpers');
|
||||
const groups = require('../../groups');
|
||||
const privileges = require('../../privileges');
|
||||
const plugins = require('../../plugins');
|
||||
const accountHelpers = require('./helpers');
|
||||
const file = require('../../file');
|
||||
|
||||
const editController = module.exports;
|
||||
|
||||
editController.get = async function (req, res) {
|
||||
const [{
|
||||
editController.get = async function (req, res, next) {
|
||||
const { userData } = res.locals;
|
||||
if (!userData) {
|
||||
return next();
|
||||
}
|
||||
const {
|
||||
username,
|
||||
userslug,
|
||||
isSelf,
|
||||
@@ -20,36 +23,36 @@ editController.get = async function (req, res) {
|
||||
groups: _groups,
|
||||
groupTitleArray,
|
||||
allowMultipleBadges,
|
||||
}, canUseSignature, canManageUsers] = await Promise.all([
|
||||
accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query),
|
||||
} = userData;
|
||||
|
||||
const [canUseSignature, canManageUsers] = await Promise.all([
|
||||
privileges.global.can('signature', req.uid),
|
||||
privileges.admin.can('admin:users', req.uid),
|
||||
]);
|
||||
|
||||
const payload = {};
|
||||
payload.maximumSignatureLength = meta.config.maximumSignatureLength;
|
||||
payload.maximumAboutMeLength = meta.config.maximumAboutMeLength;
|
||||
payload.maximumProfileImageSize = meta.config.maximumProfileImageSize;
|
||||
payload.allowMultipleBadges = meta.config.allowMultipleBadges === 1;
|
||||
payload.allowAccountDelete = meta.config.allowAccountDelete === 1;
|
||||
payload.allowWebsite = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:website'];
|
||||
payload.allowAboutMe = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:aboutme'];
|
||||
payload.allowSignature = canUseSignature && (!isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:signature']);
|
||||
payload.profileImageDimension = meta.config.profileImageDimension;
|
||||
payload.defaultAvatar = user.getDefaultAvatar();
|
||||
userData.maximumSignatureLength = meta.config.maximumSignatureLength;
|
||||
userData.maximumAboutMeLength = meta.config.maximumAboutMeLength;
|
||||
userData.maximumProfileImageSize = meta.config.maximumProfileImageSize;
|
||||
userData.allowMultipleBadges = meta.config.allowMultipleBadges === 1;
|
||||
userData.allowAccountDelete = meta.config.allowAccountDelete === 1;
|
||||
userData.allowWebsite = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:website'];
|
||||
userData.allowAboutMe = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:aboutme'];
|
||||
userData.allowSignature = canUseSignature && (!isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:signature']);
|
||||
userData.profileImageDimension = meta.config.profileImageDimension;
|
||||
userData.defaultAvatar = user.getDefaultAvatar();
|
||||
|
||||
payload.groups = _groups.filter(g => g && g.userTitleEnabled && !groups.isPrivilegeGroup(g.name) && g.name !== 'registered-users');
|
||||
userData.groups = _groups.filter(g => g && g.userTitleEnabled && !groups.isPrivilegeGroup(g.name) && g.name !== 'registered-users');
|
||||
|
||||
if (req.uid === res.locals.uid || canManageUsers) {
|
||||
const { associations } = await plugins.hooks.fire('filter:auth.list', { uid: res.locals.uid, associations: [] });
|
||||
payload.sso = associations;
|
||||
userData.sso = associations;
|
||||
}
|
||||
|
||||
if (!allowMultipleBadges) {
|
||||
payload.groupTitle = groupTitleArray[0];
|
||||
userData.groupTitle = groupTitleArray[0];
|
||||
}
|
||||
|
||||
payload.groups.sort((a, b) => {
|
||||
userData.groups.sort((a, b) => {
|
||||
const i1 = groupTitleArray.indexOf(a.name);
|
||||
const i2 = groupTitleArray.indexOf(b.name);
|
||||
if (i1 === -1) {
|
||||
@@ -59,14 +62,14 @@ editController.get = async function (req, res) {
|
||||
}
|
||||
return i1 - i2;
|
||||
});
|
||||
payload.groups.forEach((group) => {
|
||||
userData.groups.forEach((group) => {
|
||||
group.userTitle = group.userTitle || group.displayName;
|
||||
group.selected = groupTitleArray.includes(group.name);
|
||||
});
|
||||
payload.groupSelectSize = Math.min(10, Math.max(5, payload.groups.length + 1));
|
||||
userData.groupSelectSize = Math.min(10, Math.max(5, userData.groups.length + 1));
|
||||
|
||||
payload.title = `[[pages:account/edit, ${username}]]`;
|
||||
payload.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
userData.title = `[[pages:account/edit, ${username}]]`;
|
||||
userData.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
{
|
||||
text: username,
|
||||
url: `/user/${userslug}`,
|
||||
@@ -75,9 +78,9 @@ editController.get = async function (req, res) {
|
||||
text: '[[user:edit]]',
|
||||
},
|
||||
]);
|
||||
payload.editButtons = [];
|
||||
userData.editButtons = [];
|
||||
|
||||
res.render('account/edit', payload);
|
||||
res.render('account/edit', userData);
|
||||
};
|
||||
|
||||
editController.password = async function (req, res, next) {
|
||||
@@ -102,6 +105,7 @@ editController.email = async function (req, res, next) {
|
||||
};
|
||||
|
||||
async function renderRoute(name, req, res) {
|
||||
const { userData } = res.locals;
|
||||
const [isAdmin, { username, userslug }, hasPassword] = await Promise.all([
|
||||
privileges.admin.can('admin:users', req.uid),
|
||||
user.getUserFields(res.locals.uid, ['username', 'userslug']),
|
||||
@@ -112,14 +116,14 @@ async function renderRoute(name, req, res) {
|
||||
return helpers.notAllowed(req, res);
|
||||
}
|
||||
|
||||
const payload = { hasPassword };
|
||||
userData.hasPassword = hasPassword;
|
||||
if (name === 'password') {
|
||||
payload.minimumPasswordLength = meta.config.minimumPasswordLength;
|
||||
payload.minimumPasswordStrength = meta.config.minimumPasswordStrength;
|
||||
userData.minimumPasswordLength = meta.config.minimumPasswordLength;
|
||||
userData.minimumPasswordStrength = meta.config.minimumPasswordStrength;
|
||||
}
|
||||
|
||||
payload.title = `[[pages:account/edit/${name}, ${username}]]`;
|
||||
payload.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
userData.title = `[[pages:account/edit/${name}, ${username}]]`;
|
||||
userData.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
{
|
||||
text: username,
|
||||
url: `/user/${userslug}`,
|
||||
@@ -133,7 +137,7 @@ async function renderRoute(name, req, res) {
|
||||
},
|
||||
]);
|
||||
|
||||
res.render(`account/edit/${name}`, payload);
|
||||
res.render(`account/edit/${name}`, userData);
|
||||
}
|
||||
|
||||
editController.uploadPicture = async function (req, res, next) {
|
||||
|
||||
@@ -14,19 +14,20 @@ followController.getFollowers = async function (req, res, next) {
|
||||
await getFollow('account/followers', 'followers', req, res, next);
|
||||
};
|
||||
|
||||
async function getFollow(tpl, name, req, res) {
|
||||
async function getFollow(tpl, name, req, res, next) {
|
||||
const { userData: payload } = res.locals;
|
||||
if (!payload) {
|
||||
return next();
|
||||
}
|
||||
const {
|
||||
username, userslug, followerCount, followingCount,
|
||||
} = await user.getUserFields(res.locals.uid, [
|
||||
'username', 'userslug', 'followerCount', 'followingCount',
|
||||
]);
|
||||
} = payload;
|
||||
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const resultsPerPage = 50;
|
||||
const start = Math.max(0, page - 1) * resultsPerPage;
|
||||
const stop = start + resultsPerPage - 1;
|
||||
|
||||
const payload = {};
|
||||
payload.title = `[[pages:${tpl}, ${username}]]`;
|
||||
|
||||
const method = name === 'following' ? 'getFollowing' : 'getFollowers';
|
||||
|
||||
@@ -9,7 +9,7 @@ const groupsController = module.exports;
|
||||
groupsController.get = async function (req, res) {
|
||||
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
|
||||
|
||||
const payload = {};
|
||||
const payload = res.locals.userData;
|
||||
|
||||
let groupsData = await groups.getUserGroups([res.locals.uid]);
|
||||
groupsData = groupsData[0];
|
||||
|
||||
@@ -32,11 +32,7 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
|
||||
await parseAboutMe(results.userData);
|
||||
|
||||
let { userData } = results;
|
||||
const { userSettings } = results;
|
||||
const { isAdmin } = results;
|
||||
const { isGlobalModerator } = results;
|
||||
const { isModerator } = results;
|
||||
const { canViewInfo } = results;
|
||||
const { userSettings, isAdmin, isGlobalModerator, isModerator, canViewInfo } = results;
|
||||
const isSelf = parseInt(callerUID, 10) === parseInt(userData.uid, 10);
|
||||
|
||||
if (meta.config['reputation:disabled']) {
|
||||
@@ -84,6 +80,7 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
|
||||
userData.isFollowing = results.isFollowing;
|
||||
userData.canChat = results.canChat;
|
||||
userData.hasPrivateChat = results.hasPrivateChat;
|
||||
userData.iconBackgrounds = results.iconBackgrounds;
|
||||
userData.showHidden = results.canEdit; // remove in v1.19.0
|
||||
userData.allowProfilePicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:profile-picture'];
|
||||
userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture'];
|
||||
@@ -142,12 +139,18 @@ function escape(value) {
|
||||
}
|
||||
|
||||
async function getAllData(uid, callerUID) {
|
||||
// loading these before caches them, so the big promiseParallel doesn't make extra db calls
|
||||
const [[isTargetAdmin, isCallerAdmin], isGlobalModerator] = await Promise.all([
|
||||
user.isAdministrator([uid, callerUID]),
|
||||
user.isGlobalModerator(callerUID),
|
||||
]);
|
||||
|
||||
return await utils.promiseParallel({
|
||||
userData: user.getUserData(uid),
|
||||
isTargetAdmin: user.isAdministrator(uid),
|
||||
isTargetAdmin: isTargetAdmin,
|
||||
userSettings: user.getSettings(uid),
|
||||
isAdmin: user.isAdministrator(callerUID),
|
||||
isGlobalModerator: user.isGlobalModerator(callerUID),
|
||||
isAdmin: isCallerAdmin,
|
||||
isGlobalModerator: isGlobalModerator,
|
||||
isModerator: user.isModeratorOfAnyCategory(callerUID),
|
||||
isFollowing: user.isFollowing(callerUID, uid),
|
||||
ips: user.getIPs(uid, 4),
|
||||
@@ -160,6 +163,7 @@ async function getAllData(uid, callerUID) {
|
||||
canViewInfo: privileges.global.can('view:users:info', callerUID),
|
||||
canChat: canChat(callerUID, uid),
|
||||
hasPrivateChat: messaging.hasPrivateChat(callerUID, uid),
|
||||
iconBackgrounds: user.getIconBackgrounds(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,8 +184,8 @@ async function getCounts(userData, callerUID) {
|
||||
const cids = await categories.getCidsByPrivilege('categories:cid', callerUID, 'topics:read');
|
||||
const promises = {
|
||||
posts: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:pids`)),
|
||||
best: Promise.all(cids.map(async c => db.sortedSetCount(`cid:${c}:uid:${uid}:pids:votes`, 1, '+inf'))),
|
||||
controversial: Promise.all(cids.map(async c => db.sortedSetCount(`cid:${c}:uid:${uid}:pids:votes`, '-inf', -1))),
|
||||
best: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:pids:votes`), 1, '+inf'),
|
||||
controversial: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:pids:votes`), '-inf', -1),
|
||||
topics: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:tids`)),
|
||||
};
|
||||
if (userData.isAdmin || userData.isSelf) {
|
||||
@@ -196,8 +200,6 @@ async function getCounts(userData, callerUID) {
|
||||
promises.blocks = user.getUserField(userData.uid, 'blocksCount');
|
||||
}
|
||||
const counts = await utils.promiseParallel(promises);
|
||||
counts.best = counts.best.reduce((sum, count) => sum + count, 0);
|
||||
counts.controversial = counts.controversial.reduce((sum, count) => sum + count, 0);
|
||||
counts.categoriesWatched = counts.categoriesWatched && counts.categoriesWatched.length;
|
||||
counts.groups = userData.groups.length;
|
||||
counts.following = userData.followingCount;
|
||||
|
||||
@@ -13,19 +13,17 @@ infoController.get = async function (req, res) {
|
||||
const start = (page - 1) * itemsPerPage;
|
||||
const stop = start + itemsPerPage - 1;
|
||||
|
||||
const [{ username, userslug }, isPrivileged] = await Promise.all([
|
||||
user.getUserFields(res.locals.uid, ['username', 'userslug']),
|
||||
const payload = res.locals.userData;
|
||||
const { username, userslug } = payload;
|
||||
const [isPrivileged, history, sessions, usernames, emails] = await Promise.all([
|
||||
user.isPrivileged(req.uid),
|
||||
]);
|
||||
const [history, sessions, usernames, emails, notes] = await Promise.all([
|
||||
user.getModerationHistory(res.locals.uid),
|
||||
user.auth.getSessions(res.locals.uid, req.sessionID),
|
||||
user.getHistory(`user:${res.locals.uid}:usernames`),
|
||||
user.getHistory(`user:${res.locals.uid}:emails`),
|
||||
getNotes({ uid: res.locals.uid, isPrivileged }, start, stop),
|
||||
]);
|
||||
|
||||
const payload = {};
|
||||
const notes = await getNotes({ uid: res.locals.uid, isPrivileged }, start, stop);
|
||||
|
||||
payload.history = history;
|
||||
payload.sessions = sessions;
|
||||
|
||||
@@ -177,10 +177,9 @@ async function getPostsFromUserSet(template, req, res) {
|
||||
const data = templateToData[template];
|
||||
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
||||
|
||||
const [{ username, userslug }, settings] = await Promise.all([
|
||||
user.getUserFields(res.locals.uid, ['username', 'userslug']),
|
||||
user.getSettings(req.uid),
|
||||
]);
|
||||
const payload = res.locals.userData;
|
||||
const { username, userslug } = payload;
|
||||
const settings = await user.getSettings(req.uid);
|
||||
|
||||
const itemsPerPage = data.type === 'topics' ? settings.topicsPerPage : settings.postsPerPage;
|
||||
const start = (page - 1) * itemsPerPage;
|
||||
@@ -207,7 +206,6 @@ async function getPostsFromUserSet(template, req, res) {
|
||||
}
|
||||
const { itemCount, itemData } = result;
|
||||
|
||||
const payload = {};
|
||||
payload[data.type] = itemData[data.type];
|
||||
payload.nextStart = itemData.nextStart;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const nconf = require('nconf');
|
||||
const _ = require('lodash');
|
||||
|
||||
const db = require('../../database');
|
||||
@@ -9,24 +8,13 @@ const posts = require('../../posts');
|
||||
const categories = require('../../categories');
|
||||
const plugins = require('../../plugins');
|
||||
const privileges = require('../../privileges');
|
||||
const accountHelpers = require('./helpers');
|
||||
const helpers = require('../helpers');
|
||||
const utils = require('../../utils');
|
||||
|
||||
const profileController = module.exports;
|
||||
|
||||
profileController.get = async function (req, res, next) {
|
||||
const lowercaseSlug = req.params.userslug.toLowerCase();
|
||||
|
||||
if (req.params.userslug !== lowercaseSlug) {
|
||||
if (res.locals.isAPI) {
|
||||
req.params.userslug = lowercaseSlug;
|
||||
} else {
|
||||
return res.redirect(`${nconf.get('relative_path')}/user/${lowercaseSlug}`);
|
||||
}
|
||||
}
|
||||
|
||||
const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query);
|
||||
const { userData } = res.locals;
|
||||
if (!userData) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ const helpers = require('../helpers');
|
||||
const sessionController = module.exports;
|
||||
|
||||
sessionController.get = async function (req, res) {
|
||||
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
|
||||
const payload = res.locals.userData;
|
||||
const { username, userslug } = payload;
|
||||
|
||||
const payload = {
|
||||
sessions: await user.auth.getSessions(res.locals.uid, req.sessionID),
|
||||
title: '[[pages:account/sessions]]',
|
||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: username, url: `/user/${userslug}` }, { text: '[[pages:account/sessions]]' }]),
|
||||
};
|
||||
payload.sessions = await user.auth.getSessions(res.locals.uid, req.sessionID);
|
||||
payload.title = '[[pages:account/sessions]]';
|
||||
payload.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
{ text: username, url: `/user/${userslug}` },
|
||||
{ text: '[[pages:account/sessions]]' },
|
||||
]);
|
||||
|
||||
res.render('account/sessions', payload);
|
||||
};
|
||||
|
||||
@@ -13,13 +13,12 @@ const plugins = require('../../plugins');
|
||||
const notifications = require('../../notifications');
|
||||
const db = require('../../database');
|
||||
const helpers = require('../helpers');
|
||||
const accountHelpers = require('./helpers');
|
||||
const slugify = require('../../slugify');
|
||||
|
||||
const settingsController = module.exports;
|
||||
|
||||
settingsController.get = async function (req, res, next) {
|
||||
const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query);
|
||||
const { userData } = res.locals;
|
||||
if (!userData) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../../database');
|
||||
const user = require('../../user');
|
||||
const helpers = require('../helpers');
|
||||
|
||||
const tagsController = module.exports;
|
||||
@@ -10,10 +9,10 @@ tagsController.get = async function (req, res) {
|
||||
if (req.uid !== res.locals.uid) {
|
||||
return helpers.notAllowed(req, res);
|
||||
}
|
||||
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
|
||||
const payload = res.locals.userData;
|
||||
const { username, userslug } = payload;
|
||||
const tagData = await db.getSortedSetRange(`uid:${res.locals.uid}:followed_tags`, 0, -1);
|
||||
|
||||
const payload = {};
|
||||
payload.tags = tagData;
|
||||
payload.title = `[[pages:account/watched-tags, ${username}]]`;
|
||||
payload.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
|
||||
@@ -6,14 +6,14 @@ const nconf = require('nconf');
|
||||
|
||||
const db = require('../../database');
|
||||
const helpers = require('../helpers');
|
||||
const user = require('../../user');
|
||||
const meta = require('../../meta');
|
||||
const pagination = require('../../pagination');
|
||||
|
||||
const uploadsController = module.exports;
|
||||
|
||||
uploadsController.get = async function (req, res) {
|
||||
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
|
||||
const payload = res.locals.userData;
|
||||
const { username, userslug } = payload;
|
||||
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
||||
const itemsPerPage = 25;
|
||||
const start = (page - 1) * itemsPerPage;
|
||||
@@ -23,7 +23,6 @@ uploadsController.get = async function (req, res) {
|
||||
db.getSortedSetRevRange(`uid:${res.locals.uid}:uploads`, start, stop),
|
||||
]);
|
||||
|
||||
const payload = {};
|
||||
payload.uploads = uploadNames.map(uploadName => ({
|
||||
name: uploadName,
|
||||
url: path.resolve(nconf.get('upload_url'), uploadName),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const privileges = require('../privileges');
|
||||
const plugins = require('../plugins');
|
||||
const helpers = require('./helpers');
|
||||
const apiController = require('./api');
|
||||
|
||||
const adminController = {
|
||||
dashboard: require('./admin/dashboard'),
|
||||
@@ -55,4 +57,15 @@ adminController.routeIndex = async (req, res) => {
|
||||
return helpers.notAllowed(req, res);
|
||||
};
|
||||
|
||||
adminController.loadConfig = async function (req) {
|
||||
const config = await apiController.loadConfig(req);
|
||||
await plugins.hooks.fire('filter:config.get.admin', config);
|
||||
return config;
|
||||
};
|
||||
|
||||
adminController.getConfig = async (req, res) => {
|
||||
const config = await adminController.loadConfig(req);
|
||||
res.json(config);
|
||||
};
|
||||
|
||||
module.exports = adminController;
|
||||
|
||||
@@ -6,7 +6,7 @@ const utils = require('../../utils');
|
||||
const plugins = require('../../plugins');
|
||||
|
||||
cacheController.get = async function (req, res) {
|
||||
const postCache = require('../../posts/cache');
|
||||
const postCache = require('../../posts/cache').getOrCreate();
|
||||
const groupCache = require('../../groups').cache;
|
||||
const { objectCache } = require('../../database');
|
||||
const localCache = require('../../cache');
|
||||
@@ -46,7 +46,7 @@ cacheController.get = async function (req, res) {
|
||||
|
||||
cacheController.dump = async function (req, res, next) {
|
||||
let caches = {
|
||||
post: require('../../posts/cache'),
|
||||
post: require('../../posts/cache').getOrCreate(),
|
||||
object: require('../../database').objectCache,
|
||||
group: require('../../groups').cache,
|
||||
local: require('../../cache'),
|
||||
|
||||
@@ -88,9 +88,10 @@ apiController.loadConfig = async function (req) {
|
||||
thumbs: {
|
||||
size: meta.config.topicThumbSize,
|
||||
},
|
||||
iconBackgrounds: await user.getIconBackgrounds(req.uid),
|
||||
emailPrompt: meta.config.emailPrompt,
|
||||
useragent: req.useragent,
|
||||
useragent: {
|
||||
isSafari: req.useragent.isSafari,
|
||||
},
|
||||
fontawesome: {
|
||||
pro: fontawesome_pro,
|
||||
styles: fontawesome_styles,
|
||||
|
||||
@@ -342,7 +342,7 @@ authenticationController.doLogin = async function (req, uid) {
|
||||
await authenticationController.onSuccessfulLogin(req, uid);
|
||||
};
|
||||
|
||||
authenticationController.onSuccessfulLogin = async function (req, uid) {
|
||||
authenticationController.onSuccessfulLogin = async function (req, uid, trackSession = true) {
|
||||
/*
|
||||
* Older code required that this method be called from within the SSO plugin.
|
||||
* That behaviour is no longer required, onSuccessfulLogin is now automatically
|
||||
@@ -380,7 +380,7 @@ authenticationController.onSuccessfulLogin = async function (req, uid) {
|
||||
new Promise((resolve) => {
|
||||
req.session.save(resolve);
|
||||
}),
|
||||
user.auth.addSession(uid, req.sessionID, uuid),
|
||||
trackSession ? user.auth.addSession(uid, req.sessionID) : undefined,
|
||||
user.updateLastOnlineTime(uid),
|
||||
user.onUserOnline(uid, Date.now()),
|
||||
analytics.increment('logins'),
|
||||
|
||||
@@ -31,15 +31,17 @@ topicsController.get = async function getTopic(req, res, next) {
|
||||
return next();
|
||||
}
|
||||
let postIndex = parseInt(req.params.post_index, 10) || 1;
|
||||
const topicData = await topics.getTopicData(tid);
|
||||
if (!topicData) {
|
||||
return next();
|
||||
}
|
||||
const [
|
||||
userPrivileges,
|
||||
settings,
|
||||
topicData,
|
||||
rssToken,
|
||||
] = await Promise.all([
|
||||
privileges.topics.get(tid, req.uid),
|
||||
user.getSettings(req.uid),
|
||||
topics.getTopicData(tid),
|
||||
user.auth.getFeedToken(req.uid),
|
||||
]);
|
||||
|
||||
@@ -47,7 +49,6 @@ topicsController.get = async function getTopic(req, res, next) {
|
||||
const pageCount = Math.max(1, Math.ceil((topicData && topicData.postcount) / settings.postsPerPage));
|
||||
const invalidPagination = (settings.usePagination && (currentPage < 1 || currentPage > pageCount));
|
||||
if (
|
||||
!topicData ||
|
||||
userPrivileges.disabled ||
|
||||
invalidPagination ||
|
||||
(topicData.scheduled && !userPrivileges.view_scheduled)
|
||||
@@ -96,6 +97,7 @@ topicsController.get = async function getTopic(req, res, next) {
|
||||
topicData.topicStaleDays = meta.config.topicStaleDays;
|
||||
topicData['reputation:disabled'] = meta.config['reputation:disabled'];
|
||||
topicData['downvote:disabled'] = meta.config['downvote:disabled'];
|
||||
topicData.voteVisibility = meta.config.voteVisibility;
|
||||
topicData['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0;
|
||||
topicData['signatures:hideDuplicates'] = meta.config['signatures:hideDuplicates'];
|
||||
topicData.bookmarkThreshold = meta.config.bookmarkThreshold;
|
||||
@@ -379,16 +381,14 @@ topicsController.pagination = async function (req, res, next) {
|
||||
if (!utils.isNumber(tid)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [userPrivileges, settings, topic] = await Promise.all([
|
||||
privileges.topics.get(tid, req.uid),
|
||||
user.getSettings(req.uid),
|
||||
topics.getTopicData(tid),
|
||||
]);
|
||||
|
||||
const topic = await topics.getTopicData(tid);
|
||||
if (!topic) {
|
||||
return next();
|
||||
}
|
||||
const [userPrivileges, settings] = await Promise.all([
|
||||
privileges.topics.get(tid, req.uid),
|
||||
user.getSettings(req.uid),
|
||||
]);
|
||||
|
||||
if (!userPrivileges.read || !privileges.topics.canViewDeletedScheduled(topic, userPrivileges)) {
|
||||
return helpers.notAllowed(req, res);
|
||||
|
||||
@@ -10,7 +10,7 @@ Chats.list = async (req, res) => {
|
||||
let { page, perPage, start, uid } = req.query;
|
||||
([page, perPage, start, uid] = [page, perPage, start, uid].map(value => isFinite(value) && parseInt(value, 10)));
|
||||
page = page || 1;
|
||||
perPage = perPage || 20;
|
||||
perPage = Math.min(100, perPage || 20);
|
||||
|
||||
// start supercedes page
|
||||
if (start) {
|
||||
|
||||
@@ -131,6 +131,16 @@ Posts.unvote = async (req, res) => {
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Posts.getVoters = async (req, res) => {
|
||||
const data = await api.posts.getVoters(req, { pid: req.params.pid });
|
||||
helpers.formatApiResponse(200, res, data);
|
||||
};
|
||||
|
||||
Posts.getUpvoters = async (req, res) => {
|
||||
const data = await api.posts.getUpvoters(req, { pid: req.params.pid });
|
||||
helpers.formatApiResponse(200, res, data);
|
||||
};
|
||||
|
||||
Posts.bookmark = async (req, res) => {
|
||||
const data = await mock(req);
|
||||
await api.posts.bookmark(req, data);
|
||||
|
||||
@@ -137,11 +137,11 @@ mongoModule.info = async function (db) {
|
||||
listCollections = listCollections.map(collectionInfo => ({
|
||||
name: collectionInfo.ns,
|
||||
count: collectionInfo.count,
|
||||
size: collectionInfo.size,
|
||||
avgObjSize: collectionInfo.avgObjSize,
|
||||
storageSize: collectionInfo.storageSize,
|
||||
totalIndexSize: collectionInfo.totalIndexSize,
|
||||
indexSizes: collectionInfo.indexSizes,
|
||||
size: collectionInfo.storageStats && collectionInfo.storageStats.size,
|
||||
avgObjSize: collectionInfo.storageStats && collectionInfo.storageStats.avgObjSize,
|
||||
storageSize: collectionInfo.storageStats && collectionInfo.storageStats.storageSize,
|
||||
totalIndexSize: collectionInfo.storageStats && collectionInfo.storageStats.totalIndexSize,
|
||||
indexSizes: collectionInfo.storageStats && collectionInfo.storageStats.indexSizes,
|
||||
}));
|
||||
|
||||
stats.mem = serverStatus.mem || { resident: 0, virtual: 0 };
|
||||
@@ -169,11 +169,14 @@ mongoModule.info = async function (db) {
|
||||
|
||||
async function getCollectionStats(db) {
|
||||
const items = await db.listCollections().toArray();
|
||||
return await Promise.all(
|
||||
items.map(collection => db.collection(collection.name).aggregate([
|
||||
{ $collStats: { latencyStats: {}, storageStats: {}, count: {} } },
|
||||
]))
|
||||
const cols = await Promise.all(
|
||||
items.map(
|
||||
collection => db.collection(collection.name).aggregate([
|
||||
{ $collStats: { latencyStats: {}, storageStats: {}, count: {} } },
|
||||
]).toArray()
|
||||
)
|
||||
);
|
||||
return cols.map(col => col[0]);
|
||||
}
|
||||
|
||||
mongoModule.close = async function () {
|
||||
|
||||
@@ -17,11 +17,14 @@ module.exports = function (module) {
|
||||
}
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
if (!key.length) {
|
||||
return [];
|
||||
}
|
||||
const data = await module.client.collection('objects').find({
|
||||
_key: { $in: key },
|
||||
}, { _id: 0, _key: 1 }).toArray();
|
||||
|
||||
const map = {};
|
||||
const map = Object.create(null);
|
||||
data.forEach((item) => {
|
||||
map[item._key] = true;
|
||||
});
|
||||
|
||||
@@ -157,33 +157,39 @@ module.exports = function (module) {
|
||||
query.score.$lte = max;
|
||||
}
|
||||
|
||||
const count = await module.client.collection('objects').countDocuments(query);
|
||||
return count || 0;
|
||||
return await module.client.collection('objects').countDocuments(query);
|
||||
};
|
||||
|
||||
module.sortedSetCard = async function (key) {
|
||||
if (!key) {
|
||||
return 0;
|
||||
}
|
||||
const count = await module.client.collection('objects').countDocuments({ _key: key });
|
||||
return parseInt(count, 10) || 0;
|
||||
return await module.client.collection('objects').countDocuments({ _key: key });
|
||||
};
|
||||
|
||||
module.sortedSetsCard = async function (keys) {
|
||||
if (!Array.isArray(keys) || !keys.length) {
|
||||
return [];
|
||||
}
|
||||
const promises = keys.map(k => module.sortedSetCard(k));
|
||||
return await Promise.all(promises);
|
||||
return await Promise.all(keys.map(module.sortedSetCard));
|
||||
};
|
||||
|
||||
module.sortedSetsCardSum = async function (keys) {
|
||||
if (!keys || (Array.isArray(keys) && !keys.length)) {
|
||||
module.sortedSetsCardSum = async function (keys, min = '-inf', max = '+inf') {
|
||||
const isArray = Array.isArray(keys);
|
||||
if (!keys || (isArray && !keys.length)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const count = await module.client.collection('objects').countDocuments({ _key: Array.isArray(keys) ? { $in: keys } : keys });
|
||||
return parseInt(count, 10) || 0;
|
||||
const query = { _key: isArray ? { $in: keys } : keys };
|
||||
if (min !== '-inf') {
|
||||
query.score = { $gte: min };
|
||||
}
|
||||
if (max !== '+inf') {
|
||||
query.score = query.score || {};
|
||||
query.score.$lte = max;
|
||||
}
|
||||
|
||||
return await module.client.collection('objects').countDocuments(query);
|
||||
};
|
||||
|
||||
module.sortedSetRank = async function (key, value) {
|
||||
|
||||
@@ -16,38 +16,59 @@ module.exports = function (module) {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Redis/Mongo consider empty zsets as non-existent, match that behaviour
|
||||
const type = await module.type(key);
|
||||
if (type === 'zset') {
|
||||
if (Array.isArray(key)) {
|
||||
const members = await Promise.all(key.map(key => module.getSortedSetRange(key, 0, 0)));
|
||||
return members.map(member => member.length > 0);
|
||||
}
|
||||
const members = await module.getSortedSetRange(key, 0, 0);
|
||||
return members.length > 0;
|
||||
const isArray = Array.isArray(key);
|
||||
if (isArray && !key.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
async function checkIfzSetsExist(keys) {
|
||||
const members = await Promise.all(
|
||||
keys.map(key => module.getSortedSetRange(key, 0, 0))
|
||||
);
|
||||
return members.map(member => member.length > 0);
|
||||
}
|
||||
|
||||
async function checkIfKeysExist(keys) {
|
||||
const res = await module.pool.query({
|
||||
name: 'existsArray',
|
||||
text: `
|
||||
SELECT o."_key" k
|
||||
FROM "legacy_object_live" o
|
||||
WHERE o."_key" = ANY($1::TEXT[])`,
|
||||
values: [key],
|
||||
values: [keys],
|
||||
});
|
||||
return key.map(k => res.rows.some(r => r.k === k));
|
||||
return keys.map(k => res.rows.some(r => r.k === k));
|
||||
}
|
||||
|
||||
// Redis/Mongo consider empty zsets as non-existent, match that behaviour
|
||||
if (isArray) {
|
||||
const types = await Promise.all(key.map(module.type));
|
||||
const zsetKeys = key.filter((_key, i) => types[i] === 'zset');
|
||||
const otherKeys = key.filter((_key, i) => types[i] !== 'zset');
|
||||
const [zsetExits, otherExists] = await Promise.all([
|
||||
checkIfzSetsExist(zsetKeys),
|
||||
checkIfKeysExist(otherKeys),
|
||||
]);
|
||||
const existsMap = Object.create(null);
|
||||
zsetKeys.forEach((k, i) => { existsMap[k] = zsetExits[i]; });
|
||||
otherKeys.forEach((k, i) => { existsMap[k] = otherExists[i]; });
|
||||
return key.map(k => existsMap[k]);
|
||||
}
|
||||
const type = await module.type(key);
|
||||
if (type === 'zset') {
|
||||
const members = await module.getSortedSetRange(key, 0, 0);
|
||||
return members.length > 0;
|
||||
}
|
||||
const res = await module.pool.query({
|
||||
name: 'exists',
|
||||
text: `
|
||||
SELECT EXISTS(SELECT *
|
||||
FROM "legacy_object_live"
|
||||
WHERE "_key" = $1::TEXT
|
||||
LIMIT 1) e`,
|
||||
WHERE "_key" = $1::TEXT
|
||||
LIMIT 1) e`,
|
||||
values: [key],
|
||||
});
|
||||
|
||||
return res.rows[0].e;
|
||||
};
|
||||
|
||||
|
||||
@@ -221,16 +221,42 @@ SELECT o."_key" k,
|
||||
return keys.map(k => parseInt((res.rows.find(r => r.k === k) || { c: 0 }).c, 10));
|
||||
};
|
||||
|
||||
module.sortedSetsCardSum = async function (keys) {
|
||||
module.sortedSetsCardSum = async function (keys, min = '-inf', max = '+inf') {
|
||||
if (!keys || (Array.isArray(keys) && !keys.length)) {
|
||||
return 0;
|
||||
}
|
||||
if (!Array.isArray(keys)) {
|
||||
keys = [keys];
|
||||
}
|
||||
const counts = await module.sortedSetsCard(keys);
|
||||
const sum = counts.reduce((acc, val) => acc + val, 0);
|
||||
return sum;
|
||||
let counts = [];
|
||||
if (min !== '-inf' || max !== '+inf') {
|
||||
if (min === '-inf') {
|
||||
min = null;
|
||||
}
|
||||
if (max === '+inf') {
|
||||
max = null;
|
||||
}
|
||||
|
||||
const res = await module.pool.query({
|
||||
name: 'sortedSetsCardSum',
|
||||
text: `
|
||||
SELECT o."_key" k,
|
||||
COUNT(*) c
|
||||
FROM "legacy_object_live" o
|
||||
INNER JOIN "legacy_zset" z
|
||||
ON o."_key" = z."_key"
|
||||
AND o."type" = z."type"
|
||||
WHERE o."_key" = ANY($1::TEXT[])
|
||||
AND (z."score" >= $2::NUMERIC OR $2::NUMERIC IS NULL)
|
||||
AND (z."score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)
|
||||
GROUP BY o."_key"`,
|
||||
values: [keys, min, max],
|
||||
});
|
||||
counts = keys.map(k => parseInt((res.rows.find(r => r.k === k) || { c: 0 }).c, 10));
|
||||
} else {
|
||||
counts = await module.sortedSetsCard(keys);
|
||||
}
|
||||
return counts.reduce((acc, val) => acc + val, 0);
|
||||
};
|
||||
|
||||
module.sortedSetRank = async function (key, value) {
|
||||
|
||||
@@ -14,6 +14,9 @@ module.exports = function (module) {
|
||||
|
||||
module.exists = async function (key) {
|
||||
if (Array.isArray(key)) {
|
||||
if (!key.length) {
|
||||
return [];
|
||||
}
|
||||
const batch = module.client.batch();
|
||||
key.forEach(key => batch.exists(key));
|
||||
const data = await helpers.execBatch(batch);
|
||||
|
||||
@@ -116,16 +116,21 @@ module.exports = function (module) {
|
||||
return await helpers.execBatch(batch);
|
||||
};
|
||||
|
||||
module.sortedSetsCardSum = async function (keys) {
|
||||
module.sortedSetsCardSum = async function (keys, min = '-inf', max = '+inf') {
|
||||
if (!keys || (Array.isArray(keys) && !keys.length)) {
|
||||
return 0;
|
||||
}
|
||||
if (!Array.isArray(keys)) {
|
||||
keys = [keys];
|
||||
}
|
||||
const counts = await module.sortedSetsCard(keys);
|
||||
const sum = counts.reduce((acc, val) => acc + val, 0);
|
||||
return sum;
|
||||
const batch = module.client.batch();
|
||||
if (min !== '-inf' || max !== '+inf') {
|
||||
keys.forEach(k => batch.zcount(String(k), min, max));
|
||||
} else {
|
||||
keys.forEach(k => batch.zcard(String(k)));
|
||||
}
|
||||
const counts = await helpers.execBatch(batch);
|
||||
return counts.reduce((acc, val) => acc + val, 0);
|
||||
};
|
||||
|
||||
module.sortedSetRank = async function (key, value) {
|
||||
|
||||
@@ -417,7 +417,8 @@ async function checkReputation(uid) {
|
||||
}
|
||||
|
||||
Messaging.hasPrivateChat = async (uid, withUid) => {
|
||||
if (parseInt(uid, 10) === parseInt(withUid, 10)) {
|
||||
if (parseInt(uid, 10) === parseInt(withUid, 10) ||
|
||||
parseInt(uid, 10) <= 0 || parseInt(withUid, 10) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,20 +24,26 @@ Meta.templates = require('./templates');
|
||||
Meta.blacklist = require('./blacklist');
|
||||
Meta.languages = require('./languages');
|
||||
|
||||
const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
|
||||
/* Assorted */
|
||||
Meta.userOrGroupExists = async function (slug) {
|
||||
if (!slug) {
|
||||
const isArray = Array.isArray(slug);
|
||||
if ((isArray && slug.some(slug => !slug)) || (!isArray && !slug)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
slug = slugify(slug);
|
||||
|
||||
slug = isArray ? slug.map(s => slugify(s, false)) : slugify(slug);
|
||||
|
||||
const [userExists, groupExists] = await Promise.all([
|
||||
user.existsBySlug(slug),
|
||||
groups.existsBySlug(slug),
|
||||
]);
|
||||
return userExists || groupExists;
|
||||
|
||||
return isArray ?
|
||||
slug.map((s, i) => userExists[i] || groupExists[i]) :
|
||||
(userExists || groupExists);
|
||||
};
|
||||
|
||||
if (nconf.get('isPrimary')) {
|
||||
|
||||
124
src/meta/tags.js
124
src/meta/tags.js
@@ -14,8 +14,10 @@ const relative_path = nconf.get('relative_path');
|
||||
const upload_url = nconf.get('upload_url');
|
||||
|
||||
Tags.parse = async (req, data, meta, link) => {
|
||||
const isAPI = req.res && req.res.locals && req.res.locals.isAPI;
|
||||
|
||||
// Meta tags
|
||||
const defaultTags = [{
|
||||
const defaultTags = isAPI ? [] : [{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1.0',
|
||||
}, {
|
||||
@@ -40,14 +42,14 @@ Tags.parse = async (req, data, meta, link) => {
|
||||
content: Meta.config.themeColor || '#ffffff',
|
||||
}];
|
||||
|
||||
if (Meta.config.keywords) {
|
||||
if (Meta.config.keywords && !isAPI) {
|
||||
defaultTags.push({
|
||||
name: 'keywords',
|
||||
content: Meta.config.keywords,
|
||||
});
|
||||
}
|
||||
|
||||
if (Meta.config['brand:logo']) {
|
||||
if (Meta.config['brand:logo'] && !isAPI) {
|
||||
defaultTags.push({
|
||||
name: 'msapplication-square150x150logo',
|
||||
content: Meta.config['brand:logo'],
|
||||
@@ -59,7 +61,7 @@ Tags.parse = async (req, data, meta, link) => {
|
||||
const cacheBuster = `${Meta.config['cache-buster'] ? `?${Meta.config['cache-buster']}` : ''}`;
|
||||
|
||||
// Link Tags
|
||||
const defaultLinks = [{
|
||||
const defaultLinks = isAPI ? [] : [{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: `${faviconPath}${cacheBuster}`,
|
||||
@@ -69,7 +71,7 @@ Tags.parse = async (req, data, meta, link) => {
|
||||
crossorigin: `use-credentials`,
|
||||
}];
|
||||
|
||||
if (plugins.hooks.hasListeners('filter:search.query')) {
|
||||
if (plugins.hooks.hasListeners('filter:search.query') && !isAPI) {
|
||||
defaultLinks.push({
|
||||
rel: 'search',
|
||||
type: 'application/opensearchdescription+xml',
|
||||
@@ -78,7 +80,59 @@ Tags.parse = async (req, data, meta, link) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Touch icons for mobile-devices
|
||||
if (!isAPI) {
|
||||
addTouchIcons(defaultLinks);
|
||||
}
|
||||
|
||||
const results = await utils.promiseParallel({
|
||||
tags: plugins.hooks.fire('filter:meta.getMetaTags', { req: req, data: data, tags: defaultTags }),
|
||||
links: plugins.hooks.fire('filter:meta.getLinkTags', { req: req, data: data, links: defaultLinks }),
|
||||
});
|
||||
|
||||
meta = results.tags.tags.concat(meta || []).map((tag) => {
|
||||
if (!tag || typeof tag.content !== 'string') {
|
||||
winston.warn('Invalid meta tag. ', tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
if (!tag.noEscape) {
|
||||
const attributes = Object.keys(tag);
|
||||
attributes.forEach((attr) => {
|
||||
tag[attr] = utils.escapeHTML(String(tag[attr]));
|
||||
});
|
||||
}
|
||||
|
||||
return tag;
|
||||
});
|
||||
|
||||
await addSiteOGImage(meta);
|
||||
|
||||
addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
|
||||
const ogUrl = url + (req.originalUrl !== '/' ? stripRelativePath(req.originalUrl) : '');
|
||||
addIfNotExists(meta, 'property', 'og:url', ogUrl);
|
||||
addIfNotExists(meta, 'name', 'description', Meta.config.description);
|
||||
addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
|
||||
|
||||
link = results.links.links.concat(link || []);
|
||||
if (isAPI) {
|
||||
const whitelist = ['canonical', 'alternate', 'up'];
|
||||
link = link.filter(link => whitelist.some(val => val === link.rel));
|
||||
}
|
||||
link = link.map((tag) => {
|
||||
if (!tag.noEscape) {
|
||||
const attributes = Object.keys(tag);
|
||||
attributes.forEach((attr) => {
|
||||
tag[attr] = utils.escapeHTML(String(tag[attr]));
|
||||
});
|
||||
}
|
||||
|
||||
return tag;
|
||||
});
|
||||
|
||||
return { meta, link };
|
||||
};
|
||||
|
||||
function addTouchIcons(defaultLinks) {
|
||||
if (Meta.config['brand:touchIcon']) {
|
||||
defaultLinks.push({
|
||||
rel: 'apple-touch-icon',
|
||||
@@ -142,64 +196,16 @@ Tags.parse = async (req, data, meta, link) => {
|
||||
href: `${relative_path}/assets/images/touch/512.png`,
|
||||
});
|
||||
}
|
||||
|
||||
const results = await utils.promiseParallel({
|
||||
tags: plugins.hooks.fire('filter:meta.getMetaTags', { req: req, data: data, tags: defaultTags }),
|
||||
links: plugins.hooks.fire('filter:meta.getLinkTags', { req: req, data: data, links: defaultLinks }),
|
||||
});
|
||||
|
||||
meta = results.tags.tags.concat(meta || []).map((tag) => {
|
||||
if (!tag || typeof tag.content !== 'string') {
|
||||
winston.warn('Invalid meta tag. ', tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
if (!tag.noEscape) {
|
||||
const attributes = Object.keys(tag);
|
||||
attributes.forEach((attr) => {
|
||||
tag[attr] = utils.escapeHTML(String(tag[attr]));
|
||||
});
|
||||
}
|
||||
|
||||
return tag;
|
||||
});
|
||||
|
||||
await addSiteOGImage(meta);
|
||||
|
||||
addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
|
||||
const ogUrl = url + (req.originalUrl !== '/' ? stripRelativePath(req.originalUrl) : '');
|
||||
addIfNotExists(meta, 'property', 'og:url', ogUrl);
|
||||
addIfNotExists(meta, 'name', 'description', Meta.config.description);
|
||||
addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
|
||||
|
||||
link = results.links.links.concat(link || []).map((tag) => {
|
||||
if (!tag.noEscape) {
|
||||
const attributes = Object.keys(tag);
|
||||
attributes.forEach((attr) => {
|
||||
tag[attr] = utils.escapeHTML(String(tag[attr]));
|
||||
});
|
||||
}
|
||||
|
||||
return tag;
|
||||
});
|
||||
|
||||
return { meta, link };
|
||||
};
|
||||
}
|
||||
|
||||
function addIfNotExists(meta, keyName, tagName, value) {
|
||||
let exists = false;
|
||||
meta.forEach((tag) => {
|
||||
if (tag[keyName] === tagName) {
|
||||
exists = true;
|
||||
}
|
||||
});
|
||||
const exists = meta.some(tag => tag[keyName] === tagName);
|
||||
|
||||
if (!exists && value) {
|
||||
const data = {
|
||||
meta.push({
|
||||
content: utils.escapeHTML(String(value)),
|
||||
};
|
||||
data[keyName] = tagName;
|
||||
meta.push(data);
|
||||
[keyName]: tagName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ async function compile() {
|
||||
let files = await plugins.getActive();
|
||||
files = await getTemplateDirs(files);
|
||||
files = await getTemplateFiles(files);
|
||||
|
||||
const minify = process.env.NODE_ENV !== 'development';
|
||||
await Promise.all(Object.keys(files).map(async (name) => {
|
||||
const filePath = files[name];
|
||||
let imported = await fs.promises.readFile(filePath, 'utf8');
|
||||
@@ -122,6 +122,11 @@ async function compile() {
|
||||
|
||||
await mkdirp(path.join(viewsPath, path.dirname(name)));
|
||||
|
||||
// remove empty lines and whitespace
|
||||
if (minify) {
|
||||
imported = imported.split('\n').map(line => line.trim()).filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(path.join(viewsPath, name), imported);
|
||||
const compiled = await Benchpress.precompile(imported, { filename: name });
|
||||
await fs.promises.writeFile(path.join(viewsPath, name.replace(/\.tpl$/, '.js')), compiled);
|
||||
|
||||
@@ -10,7 +10,7 @@ const privileges = require('../privileges');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
const controllers = {
|
||||
api: require('../controllers/api'),
|
||||
admin: require('../controllers/admin'),
|
||||
helpers: require('../controllers/helpers'),
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ middleware.buildHeader = helpers.try(async (req, res, next) => {
|
||||
await require('./index').applyCSRFasync(req, res);
|
||||
}
|
||||
|
||||
res.locals.config = await controllers.api.loadConfig(req);
|
||||
res.locals.config = await controllers.admin.loadConfig(req);
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ module.exports = function (middleware) {
|
||||
}
|
||||
|
||||
try {
|
||||
await renderMethod(template, { ...res.locals.templateValues, ...options }, fn);
|
||||
await renderMethod(template, options, fn);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
@@ -130,7 +130,7 @@ module.exports = function (middleware) {
|
||||
return await user.getUserData(req.uid);
|
||||
}
|
||||
return {
|
||||
uid: 0,
|
||||
uid: req.uid === -1 ? -1 : 0,
|
||||
username: '[[global:guest]]',
|
||||
picture: user.getDefaultAvatar(),
|
||||
'icon:text': '?',
|
||||
@@ -184,7 +184,7 @@ module.exports = function (middleware) {
|
||||
timeagoCode: languages.userTimeagoCode(res.locals.config.userLang),
|
||||
browserTitle: translator.translate(controllersHelpers.buildTitle(title)),
|
||||
navigation: navigation.get(req.uid),
|
||||
roomIds: db.getSortedSetRevRange(`uid:${req.uid}:chat:rooms`, 0, 0),
|
||||
roomIds: req.uid > 0 ? db.getSortedSetRevRange(`uid:${req.uid}:chat:rooms`, 0, 0) : [],
|
||||
});
|
||||
|
||||
const unreadData = {
|
||||
|
||||
@@ -5,12 +5,12 @@ const meta = require('../meta');
|
||||
const helpers = require('./helpers');
|
||||
const user = require('../user');
|
||||
|
||||
const cache = cacheCreate({
|
||||
ttl: meta.config.uploadRateLimitCooldown * 1000,
|
||||
});
|
||||
let cache;
|
||||
|
||||
exports.clearCache = function () {
|
||||
cache.clear();
|
||||
if (cache) {
|
||||
cache.clear();
|
||||
}
|
||||
};
|
||||
|
||||
exports.ratelimit = helpers.try(async (req, res, next) => {
|
||||
@@ -18,7 +18,11 @@ exports.ratelimit = helpers.try(async (req, res, next) => {
|
||||
if (!meta.config.uploadRateLimitThreshold || (uid && await user.isAdminOrGlobalMod(uid))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!cache) {
|
||||
cache = cacheCreate({
|
||||
ttl: meta.config.uploadRateLimitCooldown * 1000,
|
||||
});
|
||||
}
|
||||
const count = (cache.get(`${req.ip}:uploaded_file_count`) || 0) + req.files.files.length;
|
||||
if (count > meta.config.uploadRateLimitThreshold) {
|
||||
return next(new Error(['[[error:upload-ratelimit-reached]]']));
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports = function (middleware) {
|
||||
async function finishLogin(req, user) {
|
||||
const loginAsync = util.promisify(req.login).bind(req);
|
||||
await loginAsync(user, { keepSessionInfo: true });
|
||||
await controllers.authentication.onSuccessfulLogin(req, user.uid);
|
||||
await controllers.authentication.onSuccessfulLogin(req, user.uid, false);
|
||||
req.uid = parseInt(user.uid, 10);
|
||||
req.loggedIn = req.uid > 0;
|
||||
return true;
|
||||
@@ -248,7 +248,21 @@ module.exports = function (middleware) {
|
||||
};
|
||||
|
||||
middleware.buildAccountData = async (req, res, next) => {
|
||||
res.locals.templateValues = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query);
|
||||
// use lowercase slug on api routes, or direct to the user/<lowercaseslug>
|
||||
const lowercaseSlug = req.params.userslug.toLowerCase();
|
||||
if (req.params.userslug !== lowercaseSlug) {
|
||||
if (res.locals.isAPI) {
|
||||
req.params.userslug = lowercaseSlug;
|
||||
} else {
|
||||
const newPath = req.path.replace(new RegExp(`/${req.params.userslug}`), () => `/${lowercaseSlug}`);
|
||||
return res.redirect(`${nconf.get('relative_path')}${newPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.locals.userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query);
|
||||
if (!res.locals.userData) {
|
||||
return next('route');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const winston = require('winston');
|
||||
const plugins = require('.');
|
||||
const utils = require('../utils');
|
||||
const als = require('../als');
|
||||
|
||||
const Hooks = module.exports;
|
||||
|
||||
@@ -185,7 +186,6 @@ Hooks.fire = async function (hook, params) {
|
||||
}
|
||||
let deleteCaller = false;
|
||||
if (params && typeof params === 'object' && !Array.isArray(params) && !params.hasOwnProperty('caller')) {
|
||||
const als = require('../als');
|
||||
params.caller = als.getStore();
|
||||
deleteCaller = true;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const cacheCreate = require('../cache/lru');
|
||||
const meta = require('../meta');
|
||||
let cache = null;
|
||||
|
||||
module.exports = cacheCreate({
|
||||
name: 'post',
|
||||
maxSize: meta.config.postCacheSize,
|
||||
sizeCalculation: function (n) { return n.length || 1; },
|
||||
ttl: 0,
|
||||
enabled: global.env === 'production',
|
||||
});
|
||||
exports.getOrCreate = function () {
|
||||
if (!cache) {
|
||||
const cacheCreate = require('../cache/lru');
|
||||
const meta = require('../meta');
|
||||
cache = cacheCreate({
|
||||
name: 'post',
|
||||
maxSize: meta.config.postCacheSize,
|
||||
sizeCalculation: function (n) { return n.length || 1; },
|
||||
ttl: 0,
|
||||
enabled: global.env === 'production',
|
||||
});
|
||||
}
|
||||
|
||||
return cache;
|
||||
};
|
||||
|
||||
exports.del = function (pid) {
|
||||
if (cache) {
|
||||
cache.del(pid);
|
||||
}
|
||||
};
|
||||
|
||||
exports.reset = function () {
|
||||
if (cache) {
|
||||
cache.reset();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ const meta = require('../meta');
|
||||
const plugins = require('../plugins');
|
||||
const translator = require('../translator');
|
||||
const utils = require('../utils');
|
||||
const postCache = require('./cache');
|
||||
|
||||
let sanitizeConfig = {
|
||||
allowedTags: sanitize.defaults.allowedTags.concat([
|
||||
@@ -52,7 +53,7 @@ module.exports = function (Posts) {
|
||||
return postData;
|
||||
}
|
||||
postData.content = String(postData.content || '');
|
||||
const cache = require('./cache');
|
||||
const cache = postCache.getOrCreate();
|
||||
const pid = String(postData.pid);
|
||||
const cachedContent = cache.get(pid);
|
||||
if (postData.pid && cachedContent !== undefined) {
|
||||
|
||||
@@ -163,7 +163,7 @@ module.exports = function (Posts) {
|
||||
filePaths = await _filterValidPaths(filePaths); // Only process files that exist and are within uploads directory
|
||||
|
||||
const now = Date.now();
|
||||
const scores = filePaths.map(() => now);
|
||||
const scores = filePaths.map((p, i) => now + i);
|
||||
const bulkAdd = filePaths.map(path => [`upload:${md5(path)}:pids`, now, pid]);
|
||||
await Promise.all([
|
||||
db.sortedSetAdd(`post:${pid}:uploads`, scores, filePaths),
|
||||
|
||||
@@ -17,7 +17,7 @@ module.exports = function (Posts) {
|
||||
const [userData, userSettings, signatureUids] = await Promise.all([
|
||||
getUserData(uids, uid),
|
||||
user.getMultipleUserSettings(uids),
|
||||
privileges.global.filterUids('signature', uids),
|
||||
meta.config.disableSignatures ? [] : privileges.categories.filterUids('signature', 0, uids),
|
||||
]);
|
||||
const uidsSignatureSet = new Set(signatureUids.map(uid => parseInt(uid, 10)));
|
||||
const groupsMap = await getGroupsMap(userData);
|
||||
|
||||
@@ -37,7 +37,7 @@ module.exports = function (theModule, ignoreKeys) {
|
||||
}
|
||||
|
||||
function wrapCallback(origFn, callbackFn) {
|
||||
return async function wrapperCallback(...args) {
|
||||
return function wrapperCallback(...args) {
|
||||
if (args.length && typeof args[args.length - 1] === 'function') {
|
||||
const cb = args.pop();
|
||||
args.push((err, res) => (res !== undefined ? cb(err, res) : cb(err)));
|
||||
|
||||
@@ -61,6 +61,7 @@ module.exports = function (app, name, middleware, controllers) {
|
||||
|
||||
|
||||
function apiRoutes(router, name, middleware, controllers) {
|
||||
router.get(`/api/${name}/config`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.getConfig));
|
||||
router.get(`/api/${name}/users/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.users.getCSV));
|
||||
router.get(`/api/${name}/groups/:groupname/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.groups.getCSV));
|
||||
router.get(`/api/${name}/analytics`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.dashboard.getAnalytics));
|
||||
|
||||
@@ -6,7 +6,7 @@ const uploadsController = require('../controllers/uploads');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
module.exports = function (app, middleware, controllers) {
|
||||
const middlewares = [middleware.authenticateRequest];
|
||||
const middlewares = [middleware.autoLocale, middleware.authenticateRequest];
|
||||
const router = express.Router();
|
||||
app.use('/api', router);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ helpers.setupPageRoute = function (...args) {
|
||||
}
|
||||
|
||||
middlewares = [
|
||||
middleware.autoLocale,
|
||||
middleware.applyBlacklist,
|
||||
middleware.authenticateRequest,
|
||||
middleware.redirectToHomeIfBanned,
|
||||
@@ -44,7 +45,7 @@ helpers.setupAdminPageRoute = function (...args) {
|
||||
if (args.length === 5) {
|
||||
winston.warn(`[helpers.setupAdminPageRoute(${name})] passing \`middleware\` as the third param is deprecated, it can now be safely removed`);
|
||||
}
|
||||
router.get(name, middleware.admin.buildHeader, middlewares, helpers.tryRoute(controller));
|
||||
router.get(name, middleware.autoLocale, middleware.admin.buildHeader, middlewares, helpers.tryRoute(controller));
|
||||
router.get(`/api${name}`, middlewares, helpers.tryRoute(controller));
|
||||
};
|
||||
|
||||
@@ -55,6 +56,7 @@ helpers.setupApiRoute = function (...args) {
|
||||
const controller = args[args.length - 1];
|
||||
|
||||
middlewares = [
|
||||
middleware.autoLocale,
|
||||
middleware.applyBlacklist,
|
||||
middleware.authenticateRequest,
|
||||
middleware.maintenanceMode,
|
||||
|
||||
@@ -26,6 +26,8 @@ module.exports = function () {
|
||||
|
||||
setupApiRoute(router, 'put', '/:pid/vote', [...middlewares, middleware.checkRequired.bind(null, ['delta'])], controllers.write.posts.vote);
|
||||
setupApiRoute(router, 'delete', '/:pid/vote', middlewares, controllers.write.posts.unvote);
|
||||
setupApiRoute(router, 'get', '/:pid/voters', [middleware.assert.post], controllers.write.posts.getVoters);
|
||||
setupApiRoute(router, 'get', '/:pid/upvoters', [middleware.assert.post], controllers.write.posts.getUpvoters);
|
||||
|
||||
setupApiRoute(router, 'put', '/:pid/bookmark', middlewares, controllers.write.posts.bookmark);
|
||||
setupApiRoute(router, 'delete', '/:pid/bookmark', middlewares, controllers.write.posts.unbookmark);
|
||||
|
||||
@@ -7,7 +7,7 @@ const plugins = require('../../plugins');
|
||||
|
||||
SocketCache.clear = async function (socket, data) {
|
||||
let caches = {
|
||||
post: require('../../posts/cache'),
|
||||
post: require('../../posts/cache').getOrCreate(),
|
||||
object: db.objectCache,
|
||||
group: require('../../groups').cache,
|
||||
local: require('../../cache'),
|
||||
@@ -21,7 +21,7 @@ SocketCache.clear = async function (socket, data) {
|
||||
|
||||
SocketCache.toggle = async function (socket, data) {
|
||||
let caches = {
|
||||
post: require('../../posts/cache'),
|
||||
post: require('../../posts/cache').getOrCreate(),
|
||||
object: db.objectCache,
|
||||
group: require('../../groups').cache,
|
||||
local: require('../../cache'),
|
||||
|
||||
@@ -5,12 +5,13 @@ const nconf = require('nconf');
|
||||
const plugins = require('../../plugins');
|
||||
const events = require('../../events');
|
||||
const db = require('../../database');
|
||||
const postsCache = require('../../posts/cache');
|
||||
const { pluginNamePattern } = require('../../constants');
|
||||
|
||||
const Plugins = module.exports;
|
||||
|
||||
Plugins.toggleActive = async function (socket, plugin_id) {
|
||||
require('../../posts/cache').reset();
|
||||
postsCache.reset();
|
||||
const data = await plugins.toggleActive(plugin_id);
|
||||
await events.log({
|
||||
type: `plugin-${data.active ? 'activate' : 'deactivate'}`,
|
||||
@@ -21,7 +22,7 @@ Plugins.toggleActive = async function (socket, plugin_id) {
|
||||
};
|
||||
|
||||
Plugins.toggleInstall = async function (socket, data) {
|
||||
require('../../posts/cache').reset();
|
||||
postsCache.reset();
|
||||
await plugins.checkWhitelist(data.id, data.version);
|
||||
const pluginData = await plugins.toggleInstall(data.id, data.version);
|
||||
await events.log({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const topics = require('../../topics');
|
||||
const io = require('..');
|
||||
const webserver = require('../../webserver');
|
||||
|
||||
const totals = {};
|
||||
|
||||
@@ -94,6 +95,7 @@ SocketRooms.getLocalStats = function () {
|
||||
onlineGuestCount: 0,
|
||||
onlineRegisteredCount: 0,
|
||||
socketCount: 0,
|
||||
connectionCount: webserver.getConnectionCount(),
|
||||
users: {
|
||||
categories: 0,
|
||||
recent: 0,
|
||||
|
||||
@@ -14,6 +14,8 @@ const logger = require('../logger');
|
||||
const plugins = require('../plugins');
|
||||
const ratelimit = require('../middleware/ratelimit');
|
||||
const blacklist = require('../meta/blacklist');
|
||||
const als = require('../als');
|
||||
const apiHelpers = require('../api/helpers');
|
||||
|
||||
const Namespaces = Object.create(null);
|
||||
|
||||
@@ -88,8 +90,7 @@ function onConnection(socket) {
|
||||
onConnect(socket);
|
||||
socket.onAny((event, ...args) => {
|
||||
const payload = { event: event, ...deserializePayload(args) };
|
||||
const als = require('../als');
|
||||
const apiHelpers = require('../api/helpers');
|
||||
|
||||
als.run({
|
||||
uid: socket.uid,
|
||||
req: apiHelpers.buildReqObject(socket, payload),
|
||||
@@ -131,10 +132,10 @@ async function onConnect(socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket.uid) {
|
||||
if (socket.uid > 0) {
|
||||
socket.join(`uid_${socket.uid}`);
|
||||
socket.join('online_users');
|
||||
} else {
|
||||
} else if (socket.uid === 0) {
|
||||
socket.join('online_guests');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,105 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const db = require('../../database');
|
||||
const user = require('../../user');
|
||||
const posts = require('../../posts');
|
||||
const privileges = require('../../privileges');
|
||||
const meta = require('../../meta');
|
||||
const api = require('../../api');
|
||||
const sockets = require('../index');
|
||||
|
||||
module.exports = function (SocketPosts) {
|
||||
SocketPosts.getVoters = async function (socket, data) {
|
||||
if (!data || !data.pid) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const cid = await posts.getCidByPid(data.pid);
|
||||
if (!await canSeeVotes(socket.uid, cid)) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
const showDownvotes = !meta.config['downvote:disabled'];
|
||||
const [upvoteUids, downvoteUids] = await Promise.all([
|
||||
db.getSetMembers(`pid:${data.pid}:upvote`),
|
||||
showDownvotes ? db.getSetMembers(`pid:${data.pid}:downvote`) : [],
|
||||
]);
|
||||
|
||||
const [upvoters, downvoters] = await Promise.all([
|
||||
user.getUsersFields(upvoteUids, ['username', 'userslug', 'picture']),
|
||||
user.getUsersFields(downvoteUids, ['username', 'userslug', 'picture']),
|
||||
]);
|
||||
|
||||
return {
|
||||
upvoteCount: upvoters.length,
|
||||
downvoteCount: downvoters.length,
|
||||
showDownvotes: showDownvotes,
|
||||
upvoters: upvoters,
|
||||
downvoters: downvoters,
|
||||
};
|
||||
sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/voters');
|
||||
return await api.posts.getVoters(socket, { pid: data.pid });
|
||||
};
|
||||
|
||||
SocketPosts.getUpvoters = async function (socket, pids) {
|
||||
if (!Array.isArray(pids)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const cids = await posts.getCidsByPids(pids);
|
||||
if ((await canSeeVotes(socket.uid, cids)).includes(false)) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const data = await posts.getUpvotedUidsByPids(pids);
|
||||
if (!data.length) {
|
||||
return [];
|
||||
}
|
||||
const cutoff = 6;
|
||||
const sliced = data.map((uids) => {
|
||||
let otherCount = 0;
|
||||
if (uids.length > cutoff) {
|
||||
otherCount = uids.length - (cutoff - 1);
|
||||
uids = uids.slice(0, cutoff - 1);
|
||||
}
|
||||
return {
|
||||
otherCount,
|
||||
uids,
|
||||
};
|
||||
});
|
||||
|
||||
const uniqUids = _.uniq(_.flatten(sliced.map(d => d.uids)));
|
||||
const usernameMap = _.zipObject(uniqUids, await user.getUsernamesByUids(uniqUids));
|
||||
const result = sliced.map(
|
||||
data => ({
|
||||
otherCount: data.otherCount,
|
||||
cutoff: cutoff,
|
||||
usernames: data.uids.map(uid => usernameMap[uid]),
|
||||
})
|
||||
);
|
||||
return result;
|
||||
sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/upvoters');
|
||||
return await api.posts.getUpvoters(socket, { pid: pids[0] });
|
||||
};
|
||||
|
||||
async function canSeeVotes(uid, cids) {
|
||||
const isArray = Array.isArray(cids);
|
||||
if (!isArray) {
|
||||
cids = [cids];
|
||||
}
|
||||
const uniqCids = _.uniq(cids);
|
||||
const [canRead, isAdmin, isMod] = await Promise.all([
|
||||
privileges.categories.isUserAllowedTo(
|
||||
'topics:read', uniqCids, uid
|
||||
),
|
||||
privileges.users.isAdministrator(uid),
|
||||
privileges.users.isModerator(uid, cids),
|
||||
]);
|
||||
const cidToAllowed = _.zipObject(uniqCids, canRead);
|
||||
const checks = cids.map(
|
||||
(cid, index) => isAdmin || isMod[index] ||
|
||||
(
|
||||
cidToAllowed[cid] &&
|
||||
(
|
||||
meta.config.voteVisibility === 'all' ||
|
||||
(meta.config.voteVisibility === 'loggedin' && parseInt(uid, 10) > 0)
|
||||
)
|
||||
)
|
||||
);
|
||||
return isArray ? checks : checks[0];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,10 +111,8 @@ function renderTimeago(event) {
|
||||
}
|
||||
|
||||
Events.get = async (tid, uid, reverse = false) => {
|
||||
const topics = require('.');
|
||||
|
||||
if (!await topics.exists(tid)) {
|
||||
throw new Error('[[error:no-topic]]');
|
||||
if (!tid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let eventIds = await db.getSortedSetRangeWithScores(`topic:${tid}:events`, 0, -1);
|
||||
@@ -122,7 +120,11 @@ Events.get = async (tid, uid, reverse = false) => {
|
||||
const timestamps = eventIds.map(obj => obj.score);
|
||||
eventIds = eventIds.map(obj => obj.value);
|
||||
let events = await db.getObjects(keys);
|
||||
events = await modifyEvent({ tid, uid, eventIds, timestamps, events });
|
||||
events.forEach((e, idx) => {
|
||||
e.timestamp = timestamps[idx];
|
||||
});
|
||||
await addEventsFromPostQueue(tid, uid, events);
|
||||
events = await modifyEvent({ uid, events });
|
||||
if (reverse) {
|
||||
events.reverse();
|
||||
}
|
||||
@@ -146,8 +148,7 @@ async function getCategoryInfo(cids) {
|
||||
return _.zipObject(uniqCids, catData);
|
||||
}
|
||||
|
||||
async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
|
||||
// Add posts from post queue
|
||||
async function addEventsFromPostQueue(tid, uid, events) {
|
||||
const isPrivileged = await user.isPrivileged(uid);
|
||||
if (isPrivileged) {
|
||||
const queuedPosts = await posts.getQueuedPosts({ tid }, { metadata: false });
|
||||
@@ -157,11 +158,10 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
|
||||
timestamp: item.data.timestamp || Date.now(),
|
||||
uid: item.data.uid,
|
||||
})));
|
||||
queuedPosts.forEach((item) => {
|
||||
timestamps.push(item.data.timestamp || Date.now());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function modifyEvent({ uid, events }) {
|
||||
const [users, fromCategories, userSettings] = await Promise.all([
|
||||
getUserInfo(events.map(event => event.uid).filter(Boolean)),
|
||||
getCategoryInfo(events.map(event => event.fromCid).filter(Boolean)),
|
||||
@@ -185,10 +185,8 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
|
||||
events = events.filter(event => Events._types.hasOwnProperty(event.type));
|
||||
|
||||
// Add user & metadata
|
||||
events.forEach((event, idx) => {
|
||||
event.id = parseInt(eventIds[idx], 10);
|
||||
event.timestamp = timestamps[idx];
|
||||
event.timestampISO = new Date(timestamps[idx]).toISOString();
|
||||
events.forEach((event) => {
|
||||
event.timestampISO = utils.toISOString(event.timestamp);
|
||||
if (event.hasOwnProperty('uid')) {
|
||||
event.user = users.get(event.uid === 'system' ? 'system' : parseInt(event.uid, 10));
|
||||
}
|
||||
@@ -223,16 +221,15 @@ Events.log = async (tid, payload) => {
|
||||
}
|
||||
|
||||
const eventId = await db.incrObjectField('global', 'nextTopicEventId');
|
||||
payload.id = eventId;
|
||||
|
||||
await Promise.all([
|
||||
db.setObject(`topicEvent:${eventId}`, payload),
|
||||
db.sortedSetAdd(`topic:${tid}:events`, timestamp, eventId),
|
||||
]);
|
||||
|
||||
payload.timestamp = timestamp;
|
||||
let events = await modifyEvent({
|
||||
uid: payload.uid,
|
||||
eventIds: [eventId],
|
||||
timestamps: [timestamp],
|
||||
events: [payload],
|
||||
});
|
||||
|
||||
|
||||
@@ -6,29 +6,36 @@ const _ = require('lodash');
|
||||
const db = require('../database');
|
||||
const user = require('../user');
|
||||
const privileges = require('../privileges');
|
||||
const search = require('../search');
|
||||
const plugins = require('../plugins');
|
||||
|
||||
module.exports = function (Topics) {
|
||||
Topics.getSuggestedTopics = async function (tid, uid, start, stop, cutoff = 0) {
|
||||
let tids;
|
||||
tid = parseInt(tid, 10);
|
||||
if (!tid) {
|
||||
return [];
|
||||
}
|
||||
tid = String(tid);
|
||||
cutoff = cutoff === 0 ? cutoff : (cutoff * 2592000000);
|
||||
const { cid, title, tags } = await Topics.getTopicFields(tid, [
|
||||
'cid', 'title', 'tags',
|
||||
]);
|
||||
|
||||
const [tagTids, searchTids] = await Promise.all([
|
||||
getTidsWithSameTags(tid, cutoff),
|
||||
getSearchTids(tid, uid, cutoff),
|
||||
getTidsWithSameTags(tid, tags.map(t => t.value), cutoff),
|
||||
getSearchTids(tid, title, cid, cutoff),
|
||||
]);
|
||||
|
||||
tids = _.uniq(tagTids.concat(searchTids));
|
||||
|
||||
let categoryTids = [];
|
||||
if (stop !== -1 && tids.length < stop - start + 1) {
|
||||
categoryTids = await getCategoryTids(tid, cutoff);
|
||||
categoryTids = await getCategoryTids(tid, cid, cutoff);
|
||||
}
|
||||
tids = _.shuffle(_.uniq(tids.concat(categoryTids)));
|
||||
tids = await privileges.topics.filterTids('topics:read', tids, uid);
|
||||
|
||||
let topicData = await Topics.getTopicsByTids(tids, uid);
|
||||
topicData = topicData.filter(topic => topic && topic.tid !== tid);
|
||||
topicData = topicData.filter(topic => topic && String(topic.tid) !== tid);
|
||||
topicData = await user.blocks.filter(uid, topicData);
|
||||
topicData = topicData.slice(start, stop !== -1 ? stop + 1 : undefined)
|
||||
.sort((t1, t2) => t2.timestamp - t1.timestamp);
|
||||
@@ -36,36 +43,37 @@ module.exports = function (Topics) {
|
||||
return topicData;
|
||||
};
|
||||
|
||||
async function getTidsWithSameTags(tid, cutoff) {
|
||||
const tags = await Topics.getTopicTags(tid);
|
||||
async function getTidsWithSameTags(tid, tags, cutoff) {
|
||||
let tids = cutoff === 0 ?
|
||||
await db.getSortedSetRevRange(tags.map(tag => `tag:${tag}:topics`), 0, -1) :
|
||||
await db.getSortedSetRevRangeByScore(tags.map(tag => `tag:${tag}:topics`), 0, -1, '+inf', Date.now() - cutoff);
|
||||
tids = tids.filter(_tid => _tid !== tid); // remove self
|
||||
return _.shuffle(_.uniq(tids)).slice(0, 10).map(Number);
|
||||
return _.shuffle(_.uniq(tids)).slice(0, 10);
|
||||
}
|
||||
|
||||
async function getSearchTids(tid, uid, cutoff) {
|
||||
const topicData = await Topics.getTopicFields(tid, ['title', 'cid']);
|
||||
const data = await search.search({
|
||||
query: topicData.title,
|
||||
searchIn: 'titles',
|
||||
async function getSearchTids(tid, title, cid, cutoff) {
|
||||
let { ids: tids } = await plugins.hooks.fire('filter:search.query', {
|
||||
index: 'topic',
|
||||
content: title,
|
||||
matchWords: 'any',
|
||||
categories: [topicData.cid],
|
||||
uid: uid,
|
||||
returnIds: true,
|
||||
timeRange: cutoff !== 0 ? cutoff / 1000 : 0,
|
||||
timeFilter: 'newer',
|
||||
cid: [cid],
|
||||
limit: 20,
|
||||
ids: [],
|
||||
});
|
||||
data.tids = data.tids.filter(_tid => _tid !== tid); // remove self
|
||||
return _.shuffle(data.tids).slice(0, 10).map(Number);
|
||||
tids = tids.filter(_tid => String(_tid) !== tid); // remove self
|
||||
if (cutoff) {
|
||||
const topicData = await Topics.getTopicsFields(tids, ['tid', 'timestamp']);
|
||||
const now = Date.now();
|
||||
tids = topicData.filter(t => t && t.timestamp > now - cutoff).map(t => t.tid);
|
||||
}
|
||||
|
||||
return _.shuffle(tids).slice(0, 10).map(String);
|
||||
}
|
||||
|
||||
async function getCategoryTids(tid, cutoff) {
|
||||
const cid = await Topics.getTopicField(tid, 'cid');
|
||||
async function getCategoryTids(tid, cid, cutoff) {
|
||||
const tids = cutoff === 0 ?
|
||||
await db.getSortedSetRevRange(`cid:${cid}:tids:lastposttime`, 0, 9) :
|
||||
await db.getSortedSetRevRangeByScore(`cid:${cid}:tids:lastposttime`, 0, 10, '+inf', Date.now() - cutoff);
|
||||
return _.shuffle(tids.map(Number).filter(_tid => _tid !== tid));
|
||||
return _.shuffle(tids.filter(_tid => _tid !== tid));
|
||||
}
|
||||
};
|
||||
|
||||
21
src/upgrades/3.8.3/remove-session-uuid.js
Normal file
21
src/upgrades/3.8.3/remove-session-uuid.js
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
const db = require('../../database');
|
||||
const batch = require('../../batch');
|
||||
|
||||
module.exports = {
|
||||
name: 'Remove uid:<uid>:sessionUUID:sessionId object',
|
||||
timestamp: Date.UTC(2024, 5, 26),
|
||||
method: async function () {
|
||||
const { progress } = this;
|
||||
|
||||
await batch.processSortedSet('users:joindate', async (uids) => {
|
||||
progress.incr(uids.length);
|
||||
await db.deleteAll(uids.map(uid => `uid:${uid}:sessionUUID:sessionId`));
|
||||
}, {
|
||||
batch: 500,
|
||||
progress: progress,
|
||||
});
|
||||
},
|
||||
};
|
||||
38
src/upgrades/3.8.3/topic-event-ids.js
Normal file
38
src/upgrades/3.8.3/topic-event-ids.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
|
||||
'use strict';
|
||||
|
||||
const db = require('../../database');
|
||||
const batch = require('../../batch');
|
||||
|
||||
module.exports = {
|
||||
name: 'Add id field to all topic events',
|
||||
timestamp: Date.UTC(2024, 5, 24),
|
||||
method: async function () {
|
||||
const { progress } = this;
|
||||
|
||||
let nextId = await db.getObjectField('global', 'nextTopicEventId');
|
||||
nextId = parseInt(nextId, 10) || 0;
|
||||
const ids = [];
|
||||
for (let i = 1; i < nextId; i++) {
|
||||
ids.push(i);
|
||||
}
|
||||
await batch.processArray(ids, async (eids) => {
|
||||
const eventData = await db.getObjects(eids.map(eid => `topicEvent:${eid}`));
|
||||
const bulkSet = [];
|
||||
eventData.forEach((event, idx) => {
|
||||
if (event && event.type) {
|
||||
const id = eids[idx];
|
||||
bulkSet.push(
|
||||
[`topicEvent:${id}`, { id: id }]
|
||||
);
|
||||
}
|
||||
});
|
||||
await db.setObjectBulk(bulkSet);
|
||||
progress.incr(eids.length);
|
||||
}, {
|
||||
batch: 500,
|
||||
progress,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const winston = require('winston');
|
||||
const validator = require('validator');
|
||||
const _ = require('lodash');
|
||||
const db = require('../database');
|
||||
@@ -77,56 +76,53 @@ module.exports = function (User) {
|
||||
};
|
||||
|
||||
async function cleanExpiredSessions(uid) {
|
||||
const uuidMapping = await db.getObject(`uid:${uid}:sessionUUID:sessionId`);
|
||||
if (!uuidMapping) {
|
||||
return;
|
||||
const sids = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1);
|
||||
if (!sids.length) {
|
||||
return [];
|
||||
}
|
||||
const expiredUUIDs = [];
|
||||
|
||||
const expiredSids = [];
|
||||
await Promise.all(Object.keys(uuidMapping).map(async (uuid) => {
|
||||
const sid = uuidMapping[uuid];
|
||||
const activeSids = [];
|
||||
await Promise.all(sids.map(async (sid) => {
|
||||
const sessionObj = await db.sessionStoreGet(sid);
|
||||
const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') ||
|
||||
!sessionObj.passport.hasOwnProperty('user') ||
|
||||
parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10);
|
||||
if (expired) {
|
||||
expiredUUIDs.push(uuid);
|
||||
expiredSids.push(sid);
|
||||
} else {
|
||||
activeSids.push(sid);
|
||||
}
|
||||
}));
|
||||
await db.deleteObjectFields(`uid:${uid}:sessionUUID:sessionId`, expiredUUIDs);
|
||||
|
||||
await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids);
|
||||
return activeSids;
|
||||
}
|
||||
|
||||
User.auth.addSession = async function (uid, sessionId, uuid) {
|
||||
User.auth.addSession = async function (uid, sessionId) {
|
||||
if (!(parseInt(uid, 10) > 0)) {
|
||||
return;
|
||||
}
|
||||
await cleanExpiredSessions(uid);
|
||||
await Promise.all([
|
||||
db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId),
|
||||
db.setObjectField(`uid:${uid}:sessionUUID:sessionId`, uuid, sessionId),
|
||||
]);
|
||||
await revokeSessionsAboveThreshold(uid, meta.config.maxUserSessions);
|
||||
|
||||
const activeSids = await cleanExpiredSessions(uid);
|
||||
await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId);
|
||||
await revokeSessionsAboveThreshold(activeSids.push(sessionId), uid);
|
||||
};
|
||||
|
||||
async function revokeSessionsAboveThreshold(uid, maxUserSessions) {
|
||||
const activeSessions = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1);
|
||||
if (activeSessions.length > maxUserSessions) {
|
||||
const sessionsToRevoke = activeSessions.slice(0, activeSessions.length - maxUserSessions);
|
||||
await Promise.all(sessionsToRevoke.map(sessionId => User.auth.revokeSession(sessionId, uid)));
|
||||
async function revokeSessionsAboveThreshold(activeSids, uid) {
|
||||
if (meta.config.maxUserSessions > 0 && activeSids.length > meta.config.maxUserSessions) {
|
||||
const sessionsToRevoke = activeSids.slice(0, activeSids.length - meta.config.maxUserSessions);
|
||||
await User.auth.revokeSession(sessionsToRevoke, uid);
|
||||
}
|
||||
}
|
||||
|
||||
User.auth.revokeSession = async function (sessionId, uid) {
|
||||
winston.verbose(`[user.auth] Revoking session ${sessionId} for user ${uid}`);
|
||||
const sessionObj = await db.sessionStoreGet(sessionId);
|
||||
if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) {
|
||||
await db.deleteObjectField(`uid:${uid}:sessionUUID:sessionId`, sessionObj.meta.uuid);
|
||||
}
|
||||
User.auth.revokeSession = async function (sessionIds, uid) {
|
||||
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds];
|
||||
const destroySids = sids => Promise.all(sids.map(db.sessionStoreDestroy));
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetRemove(`uid:${uid}:sessions`, sessionId),
|
||||
db.sessionStoreDestroy(sessionId),
|
||||
db.sortedSetRemove(`uid:${uid}:sessions`, sessionIds),
|
||||
destroySids(sessionIds),
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -137,7 +133,7 @@ module.exports = function (User) {
|
||||
uids.forEach((uid, index) => {
|
||||
const ids = sids[index].filter(id => id !== except);
|
||||
if (ids.length) {
|
||||
promises.push(ids.map(s => User.auth.revokeSession(s, uid)));
|
||||
promises.push(User.auth.revokeSession(ids, uid));
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
@@ -146,11 +142,10 @@ module.exports = function (User) {
|
||||
User.auth.deleteAllSessions = async function () {
|
||||
await batch.processSortedSet('users:joindate', async (uids) => {
|
||||
const sessionKeys = uids.map(uid => `uid:${uid}:sessions`);
|
||||
const sessionUUIDKeys = uids.map(uid => `uid:${uid}:sessionUUID:sessionId`);
|
||||
const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1));
|
||||
|
||||
await Promise.all([
|
||||
db.deleteAll(sessionKeys.concat(sessionUUIDKeys)),
|
||||
db.deleteAll(sessionKeys),
|
||||
...sids.map(sid => db.sessionStoreDestroy(sid)),
|
||||
]);
|
||||
}, { batch: 1000 });
|
||||
|
||||
@@ -42,7 +42,7 @@ module.exports = function (User) {
|
||||
await db.sortedSetAdd('users:banned', now, uid);
|
||||
await db.sortedSetAdd(`uid:${uid}:bans:timestamp`, now, banKey);
|
||||
await db.setObject(banKey, banData);
|
||||
await User.setUserField(uid, 'banned:expire', banData.expire);
|
||||
await User.setUserFields(uid, { banned: 1, 'banned:expire': banData.expire });
|
||||
if (until > now) {
|
||||
await db.sortedSetAdd('users:banned:expire', until, uid);
|
||||
} else {
|
||||
@@ -69,7 +69,7 @@ module.exports = function (User) {
|
||||
uids = isArray ? uids : [uids];
|
||||
const userData = await User.getUsersFields(uids, ['email:confirmed']);
|
||||
|
||||
await db.setObject(uids.map(uid => `user:${uid}`), { 'banned:expire': 0 });
|
||||
await db.setObject(uids.map(uid => `user:${uid}`), { banned: 0, 'banned:expire': 0 });
|
||||
const now = Date.now();
|
||||
const unbanDataArray = [];
|
||||
/* eslint-disable no-await-in-loop */
|
||||
@@ -124,16 +124,15 @@ module.exports = function (User) {
|
||||
|
||||
User.bans.unbanIfExpired = async function (uids) {
|
||||
// loading user data will unban if it has expired -barisu
|
||||
const userData = await User.getUsersFields(uids, ['banned:expire']);
|
||||
const userData = await User.getUsersFields(uids, ['banned', 'banned:expire']);
|
||||
return User.bans.calcExpiredFromUserData(userData);
|
||||
};
|
||||
|
||||
User.bans.calcExpiredFromUserData = async function (userData) {
|
||||
User.bans.calcExpiredFromUserData = function (userData) {
|
||||
const isArray = Array.isArray(userData);
|
||||
userData = isArray ? userData : [userData];
|
||||
const banned = await groups.isMembers(userData.map(u => u.uid), groups.BANNED_USERS);
|
||||
userData = userData.map((userData, index) => ({
|
||||
banned: banned[index],
|
||||
userData = userData.map(userData => ({
|
||||
banned: !!(userData && userData.banned),
|
||||
'banned:expire': userData && userData['banned:expire'],
|
||||
banExpired: userData && userData['banned:expire'] <= Date.now() && userData['banned:expire'] !== 0,
|
||||
}));
|
||||
|
||||
@@ -44,6 +44,8 @@ module.exports = function (User) {
|
||||
'email:confirmed': 0,
|
||||
};
|
||||
|
||||
let iconBackgrounds;
|
||||
|
||||
User.getUsersFields = async function (uids, fields) {
|
||||
if (!Array.isArray(uids) || !uids.length) {
|
||||
return [];
|
||||
@@ -120,7 +122,9 @@ module.exports = function (User) {
|
||||
user.username = (user.hasOwnProperty('oldUid') && parseInt(user.oldUid, 10)) ? '[[global:former-user]]' : '[[global:guest]]';
|
||||
user.displayname = user.username;
|
||||
}
|
||||
|
||||
if (uid === -1) { // if loading spider set uid to -1 otherwise spiders have uid = 0 like guests
|
||||
user.uid = -1;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
return users;
|
||||
@@ -185,8 +189,12 @@ module.exports = function (User) {
|
||||
['showfullname']
|
||||
));
|
||||
}
|
||||
if (!iconBackgrounds) {
|
||||
iconBackgrounds = await User.getIconBackgrounds();
|
||||
}
|
||||
|
||||
await Promise.all(users.map(async (user) => {
|
||||
const unbanUids = [];
|
||||
users.forEach((user) => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
@@ -202,7 +210,7 @@ module.exports = function (User) {
|
||||
user.email = validator.escape(user.email ? user.email.toString() : '');
|
||||
}
|
||||
|
||||
if (!parseInt(user.uid, 10)) {
|
||||
if (!user.uid) {
|
||||
for (const [key, value] of Object.entries(User.guestData)) {
|
||||
user[key] = value;
|
||||
}
|
||||
@@ -232,15 +240,12 @@ module.exports = function (User) {
|
||||
}
|
||||
|
||||
// User Icons
|
||||
if (requestedFields.includes('picture') && user.username && parseInt(user.uid, 10) && !meta.config.defaultAvatar) {
|
||||
const iconBackgrounds = await User.getIconBackgrounds(user.uid);
|
||||
let bgColor = await User.getUserField(user.uid, 'icon:bgColor');
|
||||
if (!iconBackgrounds.includes(bgColor)) {
|
||||
bgColor = Array.prototype.reduce.call(user.username, (cur, next) => cur + next.charCodeAt(), 0);
|
||||
bgColor = iconBackgrounds[bgColor % iconBackgrounds.length];
|
||||
if (requestedFields.includes('picture') && user.username && user.uid && !meta.config.defaultAvatar) {
|
||||
if (!iconBackgrounds.includes(user['icon:bgColor'])) {
|
||||
const nameAsIndex = Array.from(user.username).reduce((cur, next) => cur + next.charCodeAt(), 0);
|
||||
user['icon:bgColor'] = iconBackgrounds[nameAsIndex % iconBackgrounds.length];
|
||||
}
|
||||
user['icon:text'] = (user.username[0] || '').toUpperCase();
|
||||
user['icon:bgColor'] = bgColor;
|
||||
}
|
||||
|
||||
if (user.hasOwnProperty('joindate')) {
|
||||
@@ -251,22 +256,25 @@ module.exports = function (User) {
|
||||
user.lastonlineISO = utils.toISOString(user.lastonline) || user.joindateISO;
|
||||
}
|
||||
|
||||
if (user.hasOwnProperty('mutedUntil')) {
|
||||
user.muted = user.mutedUntil > Date.now();
|
||||
}
|
||||
|
||||
if (user.hasOwnProperty('banned') || user.hasOwnProperty('banned:expire')) {
|
||||
const result = await User.bans.calcExpiredFromUserData(user);
|
||||
const result = User.bans.calcExpiredFromUserData(user);
|
||||
user.banned = result.banned;
|
||||
const unban = result.banned && result.banExpired;
|
||||
user.banned_until = unban ? 0 : user['banned:expire'];
|
||||
user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned';
|
||||
if (unban) {
|
||||
await User.bans.unban(user.uid, '[[user:info.ban-expired]]');
|
||||
unbanUids.push(user.uid);
|
||||
user.banned = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (user.hasOwnProperty('mutedUntil')) {
|
||||
user.muted = user.mutedUntil > Date.now();
|
||||
}
|
||||
}));
|
||||
});
|
||||
if (unbanUids.length) {
|
||||
await User.bans.unban(unbanUids, '[[user:info.ban-expired]]');
|
||||
}
|
||||
|
||||
return await plugins.hooks.fire('filter:users.get', users);
|
||||
}
|
||||
@@ -311,14 +319,20 @@ module.exports = function (User) {
|
||||
}
|
||||
}
|
||||
|
||||
User.getIconBackgrounds = async (uid = 0) => {
|
||||
let iconBackgrounds = [
|
||||
|
||||
User.getIconBackgrounds = async () => {
|
||||
if (iconBackgrounds) {
|
||||
return iconBackgrounds;
|
||||
}
|
||||
|
||||
const _iconBackgrounds = [
|
||||
'#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3',
|
||||
'#009688', '#1b5e20', '#33691e', '#827717', '#e65100', '#ff5722',
|
||||
'#795548', '#607d8b',
|
||||
];
|
||||
|
||||
({ iconBackgrounds } = await plugins.hooks.fire('filter:user.iconBackgrounds', { uid, iconBackgrounds }));
|
||||
const data = await plugins.hooks.fire('filter:user.iconBackgrounds', { iconBackgrounds: _iconBackgrounds });
|
||||
iconBackgrounds = data.iconBackgrounds;
|
||||
return iconBackgrounds;
|
||||
};
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ module.exports = function (User) {
|
||||
`uid:${uid}:chat:rooms:read`,
|
||||
`uid:${uid}:upvote`, `uid:${uid}:downvote`,
|
||||
`uid:${uid}:flag:pids`,
|
||||
`uid:${uid}:sessions`, `uid:${uid}:sessionUUID:sessionId`,
|
||||
`uid:${uid}:sessions`,
|
||||
`invitation:uid:${uid}`,
|
||||
];
|
||||
|
||||
@@ -209,18 +209,22 @@ module.exports = function (User) {
|
||||
]);
|
||||
|
||||
async function updateCount(uids, name, fieldName) {
|
||||
await async.each(uids, async (uid) => {
|
||||
let count = await db.sortedSetCard(name + uid);
|
||||
count = parseInt(count, 10) || 0;
|
||||
await db.setObjectField(`user:${uid}`, fieldName, count);
|
||||
await batch.processArray(uids, async (uids) => {
|
||||
const counts = await db.sortedSetsCard(uids.map(uid => name + uid));
|
||||
const bulkSet = counts.map(
|
||||
(count, index) => ([`user:${uids[index]}`, { [fieldName]: count || 0 }])
|
||||
);
|
||||
await db.setObjectBulk(bulkSet);
|
||||
}, {
|
||||
batch: 500,
|
||||
});
|
||||
}
|
||||
|
||||
const followingSets = followers.map(uid => `following:${uid}`);
|
||||
const followerSets = following.map(uid => `followers:${uid}`);
|
||||
|
||||
await db.sortedSetsRemove(followerSets.concat(followingSets), uid);
|
||||
await Promise.all([
|
||||
db.sortedSetsRemove(followerSets.concat(followingSets), uid),
|
||||
updateCount(following, 'followers:', 'followerCount'),
|
||||
updateCount(followers, 'following:', 'followingCount'),
|
||||
]);
|
||||
|
||||
@@ -50,8 +50,12 @@ User.exists = async function (uids) {
|
||||
};
|
||||
|
||||
User.existsBySlug = async function (userslug) {
|
||||
const exists = await User.getUidByUserslug(userslug);
|
||||
return !!exists;
|
||||
if (Array.isArray(userslug)) {
|
||||
const uids = await User.getUidsByUserslugs(userslug);
|
||||
return uids.map(uid => !!uid);
|
||||
}
|
||||
const uid = await User.getUidByUserslug(userslug);
|
||||
return !!uid;
|
||||
};
|
||||
|
||||
User.getUidsFromSet = async function (set, start, stop) {
|
||||
@@ -112,6 +116,10 @@ User.getUidByUserslug = async function (userslug) {
|
||||
return await db.sortedSetScore('userslug:uid', userslug);
|
||||
};
|
||||
|
||||
User.getUidsByUserslugs = async function (userslugs) {
|
||||
return await db.sortedSetScores('userslug:uid', userslugs);
|
||||
};
|
||||
|
||||
User.getUsernamesByUids = async function (uids) {
|
||||
const users = await User.getUsersFields(uids, ['username']);
|
||||
return users.map(user => user.username);
|
||||
|
||||
@@ -6,51 +6,52 @@
|
||||
|
||||
<div class="card-body">
|
||||
<span>[[admin/development/info:nodes-responded, {nodeCount}, {timeout}]]</span>
|
||||
|
||||
<table class="table table-sm text-sm">
|
||||
<thead>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="fw-bold">[[admin/development/info:host]]</td>
|
||||
<td class="fw-bold text-center">[[admin/development/info:primary]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:nodejs]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:online]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:git]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:cpu-usage]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:process-memory]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:system-memory]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:load]]</td>
|
||||
<td class="fw-bold text-end">[[admin/development/info:uptime]]</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-xs">
|
||||
{{{ each info }}}
|
||||
<tr>
|
||||
<td class="fw-bold">[[admin/development/info:host]]</td>
|
||||
<td class="fw-bold text-center">[[admin/development/info:primary]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:nodejs]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:online]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:git]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:cpu-usage]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:process-memory]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:system-memory]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:load]]</td>
|
||||
<td class="fw-bold">[[admin/development/info:uptime]]</td>
|
||||
<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.version}</td>
|
||||
<td>
|
||||
<span title="[[admin/development/info:registered]]">{info.stats.onlineRegisteredCount}</span> /
|
||||
<span title="[[admin/development/info:guests]]">{info.stats.onlineGuestCount}</span> /
|
||||
<span title="[[admin/development/info:sockets]]">{info.stats.socketCount}</span> /
|
||||
<span title="[[admin/development/info:connection-count]]">{info.stats.connectionCount}</span>
|
||||
</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}%</td>
|
||||
<td>
|
||||
<span title="[[admin/development/info:used-memory-process]]">{info.process.memoryUsage.humanReadable} gb</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="[[admin/development/info:used-memory-os]]">{info.os.usedmem} gb</span> /
|
||||
<span title="[[admin/development/info:total-memory-os]]">{info.os.totalmem} gb</span>
|
||||
</td>
|
||||
<td>{info.os.load}</td>
|
||||
<td class="text-end">{info.process.uptimeHumanReadable}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-xs">
|
||||
{{{ each 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.version}</td>
|
||||
<td>
|
||||
<span title="[[admin/development/info:registered]]">{info.stats.onlineRegisteredCount}</span> /
|
||||
<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.hashShort}</a></td>
|
||||
<td>{info.process.cpuUsage}%</td>
|
||||
<td>
|
||||
<span title="[[admin/development/info:used-memory-process]]">{info.process.memoryUsage.humanReadable} gb</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="[[admin/development/info:used-memory-os]]">{info.os.usedmem} gb</span> /
|
||||
<span title="[[admin/development/info:total-memory-os]]">{info.os.totalmem} gb</span>
|
||||
</td>
|
||||
<td>{info.os.load}</td>
|
||||
<td>{info.process.uptimeHumanReadable}</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -121,15 +121,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3 pointer" data-container-html='<div class="card"><h5 class="card-header">\{{title}}</h5><div class="card-body">\{{body}}</div></div>'>
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<div class="card-header d-flex justify-content-between text-nowrap flex-wrap align-items-center">
|
||||
[[admin/extend/widgets:container.card-header]]
|
||||
<div class="d-flex gap-1 color-selector">
|
||||
<button data-class="text-bg-primary" class="btn btn-sm btn-primary"</button>
|
||||
<button data-class="" class="btn btn-sm btn-secondary"</button>
|
||||
<button data-class="text-bg-success" class="btn btn-sm btn-success"</button>
|
||||
<button data-class="text-bg-info" class="btn btn-sm btn-info"</button>
|
||||
<button data-class="text-bg-warning" class="btn btn-sm btn-warning"</button>
|
||||
<button data-class="text-bg-danger" class="btn btn-sm btn-danger"</button>
|
||||
<div class="d-flex gap-1 color-selector" style="height: 18px;">
|
||||
<button data-class="text-bg-primary" class="btn btn-sm btn-primary"></button>
|
||||
<button data-class="" class="btn btn-sm btn-secondary"></button>
|
||||
<button data-class="text-bg-success" class="btn btn-sm btn-success"></button>
|
||||
<button data-class="text-bg-info" class="btn btn-sm btn-info"></button>
|
||||
<button data-class="text-bg-warning" class="btn btn-sm btn-warning"></button>
|
||||
<button data-class="text-bg-danger" class="btn btn-sm btn-danger"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -138,9 +138,9 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info pointer" data-container-html='<div class="alert alert-info">\{{body}}</div>'>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex justify-content-between text-nowrap flex-wrap align-items-center">
|
||||
[[admin/extend/widgets:container.alert]]
|
||||
<div class="d-flex gap-1 color-selector">
|
||||
<div class="d-flex gap-1 color-selector" style="height: 18px;">
|
||||
<button data-class="alert-success" class="btn btn-sm btn-success"></button>
|
||||
<button data-class="alert-info" class="btn btn-sm btn-info"></button>
|
||||
<button data-class="alert-warning" class="btn btn-sm btn-warning"></button>
|
||||
|
||||
@@ -18,7 +18,7 @@ const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const useragent = require('express-useragent');
|
||||
const favicon = require('serve-favicon');
|
||||
const detector = require('spider-detector');
|
||||
const detector = require('@nodebb/spider-detector');
|
||||
const helmet = require('helmet');
|
||||
|
||||
const Benchpress = require('benchpressjs');
|
||||
@@ -76,6 +76,10 @@ exports.destroy = function (callback) {
|
||||
}
|
||||
};
|
||||
|
||||
exports.getConnectionCount = function () {
|
||||
return Object.keys(connections).length;
|
||||
};
|
||||
|
||||
exports.listen = async function () {
|
||||
emailer.registerApp(app);
|
||||
setupExpressApp(app);
|
||||
@@ -182,7 +186,6 @@ function setupExpressApp(app) {
|
||||
req: apiHelpers.buildReqObject(req),
|
||||
}, next);
|
||||
});
|
||||
app.use(middleware.autoLocale); // must be added after auth middlewares are added
|
||||
|
||||
const toobusy = require('toobusy-js');
|
||||
toobusy.maxLag(meta.config.eventLoopLagThreshold);
|
||||
|
||||
@@ -562,8 +562,10 @@ describe('API', async () => {
|
||||
const reloginPaths = ['GET /api/user/{userslug}/edit/email', 'PUT /users/{uid}/password', 'DELETE /users/{uid}/sessions/{uuid}'];
|
||||
if (reloginPaths.includes(`${method.toUpperCase()} ${path}`)) {
|
||||
({ jar } = await helpers.loginUser('admin', '123456'));
|
||||
const sessionUUIDs = await db.getObject('uid:1:sessionUUID:sessionId');
|
||||
mocks.delete['/users/{uid}/sessions/{uuid}'][1].example = Object.keys(sessionUUIDs).pop();
|
||||
const sessionIds = await db.getSortedSetRange('uid:1:sessions', 0, -1);
|
||||
const sessObj = await db.sessionStoreGet(sessionIds[0]);
|
||||
const { uuid } = sessObj.meta;
|
||||
mocks.delete['/users/{uid}/sessions/{uuid}'][1].example = uuid;
|
||||
|
||||
// Retrieve CSRF token using cookie, to test Write API
|
||||
csrfToken = await helpers.getCsrfToken(jar);
|
||||
|
||||
@@ -195,7 +195,7 @@ describe('authentication', () => {
|
||||
});
|
||||
assert(body);
|
||||
assert.equal(body.username, username);
|
||||
const sessions = await db.getObject(`uid:${uid}:sessionUUID:sessionId`);
|
||||
const sessions = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1);
|
||||
assert(sessions);
|
||||
assert(Object.keys(sessions).length > 0);
|
||||
});
|
||||
|
||||
@@ -64,12 +64,15 @@ describe('Key methods', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should work for an array of keys', (done) => {
|
||||
db.exists(['testKey', 'doesnotexist'], (err, exists) => {
|
||||
assert.ifError(err);
|
||||
assert.deepStrictEqual(exists, [true, false]);
|
||||
done();
|
||||
});
|
||||
it('should work for an array of keys', async () => {
|
||||
assert.deepStrictEqual(
|
||||
await db.exists(['testKey', 'doesnotexist']),
|
||||
[true, false]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
await db.exists([]),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
describe('scan', () => {
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const db = require('../mocks/databasemock');
|
||||
|
||||
describe('Sorted Set methods', () => {
|
||||
before((done) => {
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('sortedSetTest1', [1.1, 1.2, 1.3], ['value1', 'value2', 'value3'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('sortedSetTest2', [1, 4], ['value1', 'value4'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('sortedSetTest3', [2, 4], ['value2', 'value4'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('sortedSetTest4', [1, 1, 2, 3, 5], ['b', 'a', 'd', 'e', 'c'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('sortedSetLex', [0, 0, 0, 0], ['a', 'b', 'c', 'd'], next);
|
||||
},
|
||||
], done);
|
||||
before(async () => {
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('sortedSetTest1', [1.1, 1.2, 1.3], ['value1', 'value2', 'value3']),
|
||||
db.sortedSetAdd('sortedSetTest2', [1, 4], ['value1', 'value4']),
|
||||
db.sortedSetAdd('sortedSetTest3', [2, 4], ['value2', 'value4']),
|
||||
db.sortedSetAdd('sortedSetTest4', [1, 1, 2, 3, 5], ['b', 'a', 'd', 'e', 'c']),
|
||||
db.sortedSetAdd('sortedSetLex', [0, 0, 0, 0], ['a', 'b', 'c', 'd']),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('sortedSetScan', () => {
|
||||
@@ -617,6 +605,23 @@ describe('Sorted Set methods', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with min/max', async () => {
|
||||
let count = await db.sortedSetsCardSum([
|
||||
'sortedSetTest1', 'sortedSetTest2', 'sortedSetTest3',
|
||||
], '-inf', 2);
|
||||
assert.strictEqual(count, 5);
|
||||
|
||||
count = await db.sortedSetsCardSum([
|
||||
'sortedSetTest1', 'sortedSetTest2', 'sortedSetTest3',
|
||||
], 2, '+inf');
|
||||
assert.strictEqual(count, 3);
|
||||
|
||||
count = await db.sortedSetsCardSum([
|
||||
'sortedSetTest1', 'sortedSetTest2', 'sortedSetTest3',
|
||||
], '-inf', '+inf');
|
||||
assert.strictEqual(count, 7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortedSetRank()', () => {
|
||||
@@ -1225,11 +1230,11 @@ describe('Sorted Set methods', () => {
|
||||
});
|
||||
|
||||
describe('sortedSetsRemove()', () => {
|
||||
before((done) => {
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetAdd, 'sorted4', [1, 2], ['value1', 'value2']),
|
||||
async.apply(db.sortedSetAdd, 'sorted5', [1, 2], ['value1', 'value3']),
|
||||
], done);
|
||||
before(async () => {
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('sorted4', [1, 2], ['value1', 'value2']),
|
||||
db.sortedSetAdd('sorted5', [1, 2], ['value1', 'value3']),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove element from multiple sorted sets', (done) => {
|
||||
@@ -1278,15 +1283,11 @@ describe('Sorted Set methods', () => {
|
||||
});
|
||||
|
||||
describe('getSortedSetIntersect', () => {
|
||||
before((done) => {
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('interSet1', [1, 2, 3], ['value1', 'value2', 'value3'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('interSet2', [4, 5, 6], ['value2', 'value3', 'value5'], next);
|
||||
},
|
||||
], done);
|
||||
before(async () => {
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('interSet1', [1, 2, 3], ['value1', 'value2', 'value3']),
|
||||
db.sortedSetAdd('interSet2', [4, 5, 6], ['value2', 'value3', 'value5']),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the intersection of two sets', (done) => {
|
||||
@@ -1446,21 +1447,13 @@ describe('Sorted Set methods', () => {
|
||||
});
|
||||
|
||||
describe('sortedSetIntersectCard', () => {
|
||||
before((done) => {
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('interCard1', [0, 0, 0], ['value1', 'value2', 'value3'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('interCard2', [0, 0, 0], ['value2', 'value3', 'value4'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('interCard3', [0, 0, 0], ['value3', 'value4', 'value5'], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('interCard4', [0, 0, 0], ['value4', 'value5', 'value6'], next);
|
||||
},
|
||||
], done);
|
||||
before(async () => {
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('interCard1', [0, 0, 0], ['value1', 'value2', 'value3']),
|
||||
db.sortedSetAdd('interCard2', [0, 0, 0], ['value2', 'value3', 'value4']),
|
||||
db.sortedSetAdd('interCard3', [0, 0, 0], ['value3', 'value4', 'value5']),
|
||||
db.sortedSetAdd('interCard4', [0, 0, 0], ['value4', 'value5', 'value6']),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return # of elements in intersection', (done) => {
|
||||
|
||||
@@ -194,7 +194,7 @@ async function setupMockDefaults() {
|
||||
meta.config.autoDetectLang = 0;
|
||||
|
||||
require('../../src/groups').cache.reset();
|
||||
require('../../src/posts/cache').reset();
|
||||
require('../../src/posts/cache').getOrCreate().reset();
|
||||
require('../../src/cache').reset();
|
||||
require('../../src/middleware/uploads').clearCache();
|
||||
// privileges must be given after cache reset
|
||||
|
||||
@@ -184,8 +184,8 @@ describe('Post\'s', () => {
|
||||
it('should get upvoters', (done) => {
|
||||
socketPosts.getUpvoters({ uid: globalModUid }, [postData.pid], (err, data) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(data[0].otherCount, 0);
|
||||
assert.equal(data[0].usernames, 'upvoter');
|
||||
assert.equal(data.otherCount, 0);
|
||||
assert.equal(data.usernames, 'upvoter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -740,7 +740,7 @@ describe('socket.io', () => {
|
||||
|
||||
it('should toggle caches', async () => {
|
||||
const caches = {
|
||||
post: require('../src/posts/cache'),
|
||||
post: require('../src/posts/cache').getOrCreate(),
|
||||
object: require('../src/database').objectCache,
|
||||
group: require('../src/groups').cache,
|
||||
local: require('../src/cache'),
|
||||
|
||||
36
test/user.js
36
test/user.js
@@ -607,7 +607,7 @@ describe('User', () => {
|
||||
|
||||
it('should return an icon text and valid background if username and picture is explicitly requested', async () => {
|
||||
const payload = await User.getUserFields(testUid, ['username', 'picture']);
|
||||
const validBackgrounds = await User.getIconBackgrounds(testUid);
|
||||
const validBackgrounds = await User.getIconBackgrounds();
|
||||
assert.strictEqual(payload['icon:text'], userData.username.slice(0, 1).toUpperCase());
|
||||
assert(payload['icon:bgColor']);
|
||||
assert(validBackgrounds.includes(payload['icon:bgColor']));
|
||||
@@ -616,7 +616,7 @@ describe('User', () => {
|
||||
it('should return a valid background, even if an invalid background colour is set', async () => {
|
||||
await User.setUserField(testUid, 'icon:bgColor', 'teal');
|
||||
const payload = await User.getUserFields(testUid, ['username', 'picture']);
|
||||
const validBackgrounds = await User.getIconBackgrounds(testUid);
|
||||
const validBackgrounds = await User.getIconBackgrounds();
|
||||
|
||||
assert(payload['icon:bgColor']);
|
||||
assert(validBackgrounds.includes(payload['icon:bgColor']));
|
||||
@@ -1492,28 +1492,18 @@ describe('User', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if user/group exists', (done) => {
|
||||
meta.userOrGroupExists('registered-users', (err, exists) => {
|
||||
assert.ifError(err);
|
||||
assert(exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should return true/false if user/group exists or not', async () => {
|
||||
assert.strictEqual(await meta.userOrGroupExists('registered-users'), true);
|
||||
assert.strictEqual(await meta.userOrGroupExists('John Smith'), true);
|
||||
assert.strictEqual(await meta.userOrGroupExists('doesnot exist'), false);
|
||||
assert.deepStrictEqual(await meta.userOrGroupExists(['doesnot exist', 'nope not here']), [false, false]);
|
||||
assert.deepStrictEqual(await meta.userOrGroupExists(['doesnot exist', 'John Smith']), [false, true]);
|
||||
assert.deepStrictEqual(await meta.userOrGroupExists(['administrators', 'John Smith']), [true, true]);
|
||||
|
||||
it('should return true if user/group exists', (done) => {
|
||||
meta.userOrGroupExists('John Smith', (err, exists) => {
|
||||
assert.ifError(err);
|
||||
assert(exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if user/group does not exists', (done) => {
|
||||
meta.userOrGroupExists('doesnot exist', (err, exists) => {
|
||||
assert.ifError(err);
|
||||
assert(!exists);
|
||||
done();
|
||||
});
|
||||
await assert.rejects(
|
||||
meta.userOrGroupExists(['', undefined]),
|
||||
{ message: '[[error:invalid-data]]' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete user', async () => {
|
||||
|
||||
Reference in New Issue
Block a user