mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-19 23:10:21 +01:00
Compare commits
19 Commits
normalize-
...
v4.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f875a83423 | ||
|
|
fc9cc8d6d7 | ||
|
|
de502cd2ee | ||
|
|
1ca7b7ecce | ||
|
|
c4e3139599 | ||
|
|
e775564fc1 | ||
|
|
76896859fa | ||
|
|
6d74ee2f59 | ||
|
|
810e8dbbbf | ||
|
|
1e6c6f4e44 | ||
|
|
6b9f166cb8 | ||
|
|
b517f05e90 | ||
|
|
0427971879 | ||
|
|
bef1792086 | ||
|
|
c83f91bd12 | ||
|
|
84d3fe7969 | ||
|
|
bb13ea3013 | ||
|
|
48f0f47a2e | ||
|
|
34414f168a |
@@ -147,6 +147,7 @@
|
|||||||
"username:disableEdit": 0,
|
"username:disableEdit": 0,
|
||||||
"email:disableEdit": 0,
|
"email:disableEdit": 0,
|
||||||
"email:smtpTransport:pool": 0,
|
"email:smtpTransport:pool": 0,
|
||||||
|
"email:smtpTransport:allow-self-signed": 0,
|
||||||
"hideFullname": 0,
|
"hideFullname": 0,
|
||||||
"hideEmail": 0,
|
"hideEmail": 0,
|
||||||
"showFullnameAsDisplayName": 0,
|
"showFullnameAsDisplayName": 0,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "nodebb",
|
"name": "nodebb",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"description": "NodeBB Forum",
|
"description": "NodeBB Forum",
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"homepage": "https://www.nodebb.org",
|
"homepage": "https://www.nodebb.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"connect-pg-simple": "10.0.0",
|
"connect-pg-simple": "10.0.0",
|
||||||
"connect-redis": "8.0.1",
|
"connect-redis": "8.0.1",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cron": "4.1.0",
|
"cron": "4.0.0",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.2",
|
||||||
"csrf-sync": "4.0.3",
|
"csrf-sync": "4.0.3",
|
||||||
"daemon": "1.1.0",
|
"daemon": "1.1.0",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"lru-cache": "10.4.3",
|
"lru-cache": "10.4.3",
|
||||||
"mime": "3.0.0",
|
"mime": "3.0.0",
|
||||||
"mkdirp": "3.0.1",
|
"mkdirp": "3.0.1",
|
||||||
"mongodb": "6.14.0",
|
"mongodb": "6.13.1",
|
||||||
"morgan": "1.10.0",
|
"morgan": "1.10.0",
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"multiparty": "4.2.3",
|
"multiparty": "4.2.3",
|
||||||
@@ -108,10 +108,10 @@
|
|||||||
"nodebb-plugin-spam-be-gone": "2.3.1",
|
"nodebb-plugin-spam-be-gone": "2.3.1",
|
||||||
"nodebb-plugin-web-push": "0.7.3",
|
"nodebb-plugin-web-push": "0.7.3",
|
||||||
"nodebb-rewards-essentials": "1.0.1",
|
"nodebb-rewards-essentials": "1.0.1",
|
||||||
"nodebb-theme-harmony": "2.0.38",
|
"nodebb-theme-harmony": "2.0.39",
|
||||||
"nodebb-theme-lavender": "7.1.17",
|
"nodebb-theme-lavender": "7.1.18",
|
||||||
"nodebb-theme-peace": "2.2.39",
|
"nodebb-theme-peace": "2.2.39",
|
||||||
"nodebb-theme-persona": "14.0.15",
|
"nodebb-theme-persona": "14.0.16",
|
||||||
"nodebb-widget-essentials": "7.0.35",
|
"nodebb-widget-essentials": "7.0.35",
|
||||||
"nodemailer": "6.10.0",
|
"nodemailer": "6.10.0",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
@@ -140,13 +140,13 @@
|
|||||||
"@socket.io/redis-adapter": "8.3.0",
|
"@socket.io/redis-adapter": "8.3.0",
|
||||||
"sortablejs": "1.15.6",
|
"sortablejs": "1.15.6",
|
||||||
"spdx-license-list": "6.9.0",
|
"spdx-license-list": "6.9.0",
|
||||||
"terser-webpack-plugin": "5.3.12",
|
"terser-webpack-plugin": "5.3.11",
|
||||||
"textcomplete": "0.18.2",
|
"textcomplete": "0.18.2",
|
||||||
"textcomplete.contenteditable": "0.1.1",
|
"textcomplete.contenteditable": "0.1.1",
|
||||||
"timeago": "1.6.7",
|
"timeago": "1.6.7",
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"toobusy-js": "0.5.1",
|
"toobusy-js": "0.5.1",
|
||||||
"tough-cookie": "5.1.2",
|
"tough-cookie": "5.1.1",
|
||||||
"validator": "13.12.0",
|
"validator": "13.12.0",
|
||||||
"webpack": "5.98.0",
|
"webpack": "5.98.0",
|
||||||
"webpack-merge": "6.0.1",
|
"webpack-merge": "6.0.1",
|
||||||
@@ -200,4 +200,4 @@
|
|||||||
"url": "https://github.com/barisusakli"
|
"url": "https://github.com/barisusakli"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -94,6 +94,7 @@
|
|||||||
"federation.followers-handle": "Handle",
|
"federation.followers-handle": "Handle",
|
||||||
"federation.followers-id": "ID",
|
"federation.followers-id": "ID",
|
||||||
"federation.followers-none": "No followers.",
|
"federation.followers-none": "No followers.",
|
||||||
|
"federation.followers-autofill": "Autofill",
|
||||||
|
|
||||||
"alert.created": "Created",
|
"alert.created": "Created",
|
||||||
"alert.create-success": "Category successfully created!",
|
"alert.create-success": "Category successfully created!",
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"smtp-transport.password": "Password",
|
"smtp-transport.password": "Password",
|
||||||
"smtp-transport.pool": "Enable pooled connections",
|
"smtp-transport.pool": "Enable pooled connections",
|
||||||
"smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.",
|
"smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.",
|
||||||
|
"smtp-transport.allow-self-signed": "Allow self-signed certificates",
|
||||||
|
"smtp-transport.allow-self-signed-help": "Enabling this setting will allow you to use self-signed or invalid TLS certificates.",
|
||||||
|
|
||||||
"template": "Edit Email Template",
|
"template": "Edit Email Template",
|
||||||
"template.select": "Select Email Template",
|
"template.select": "Select Email Template",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"subcategories": "Subcategories",
|
"subcategories": "Subcategories",
|
||||||
"uncategorized": "Uncategorized",
|
"uncategorized": "Uncategorized",
|
||||||
"uncategorized.description": "Topics that do not strictly fit in with any existing categories",
|
"uncategorized.description": "Topics that do not strictly fit in with any existing categories",
|
||||||
"handle.description": "This category can be followed from the open social web via the handle %1",
|
|
||||||
"new-topic-button": "New Topic",
|
"new-topic-button": "New Topic",
|
||||||
"guest-login-post": "Log in to post",
|
"guest-login-post": "Log in to post",
|
||||||
"no-topics": "<strong>There are no topics in this category.</strong><br />Why don't you try posting one?",
|
"no-topics": "<strong>There are no topics in this category.</strong><br />Why don't you try posting one?",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"prerelease-upgrade-available": "Đây là phiên bản trước khi phát hành đã lỗi thời của NodeBB. Một phiên bản mới (v%1) đã được phát hành. Cân nhắc <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">nâng cấp NodeBB của bạn</a>.",
|
"prerelease-upgrade-available": "Đây là phiên bản trước khi phát hành đã lỗi thời của NodeBB. Một phiên bản mới (v%1) đã được phát hành. Cân nhắc <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">nâng cấp NodeBB của bạn</a>.",
|
||||||
"prerelease-warning": "Đây là một phiên bản NodeBB <strong>trước khi phát hành</strong>. Lỗi ngoài ý muốn có thể xảy ra. <i class=\"fa fa-exclamation-triangle\"></i>",
|
"prerelease-warning": "Đây là một phiên bản NodeBB <strong>trước khi phát hành</strong>. Lỗi ngoài ý muốn có thể xảy ra. <i class=\"fa fa-exclamation-triangle\"></i>",
|
||||||
"fallback-emailer-not-found": "Không tìm thấy trình gửi email dự phòng!",
|
"fallback-emailer-not-found": "Không tìm thấy trình gửi email dự phòng!",
|
||||||
"running-in-development": "Diễn đàn đang chạy ở chế độ phát triển. Diễn đàn có thể mở ra các lỗ hổng tiềm ẩn; hãy liên hệ với quản trị hệ thống của bạn",
|
"running-in-development": "Diễn đàn đang chạy ở chế độ phát triển. Diễn đàn có thể mở ra các lỗ hổng tiềm ẩn; Xin vui lòng liên hệ với quản trị hệ thống của bạn",
|
||||||
"latest-lookup-failed": "Không tìm được phiên bản mới nhất hiện có của NodeBB",
|
"latest-lookup-failed": "Không tìm được phiên bản mới nhất hiện có của NodeBB",
|
||||||
|
|
||||||
"notices": "Thông báo",
|
"notices": "Thông báo",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"maintenance-mode": "Chế Độ Bảo Trì",
|
"maintenance-mode": "Chế Độ Bảo Trì",
|
||||||
"maintenance-mode-title": "Bấm vào đây để thiết lập chế độ bảo trì cho NodeBB",
|
"maintenance-mode-title": "Bấm vào đây để thiết lập chế độ bảo trì cho NodeBB",
|
||||||
"dark-mode": "Chế Độ Tối",
|
"dark-mode": "Chế Độ Tối",
|
||||||
"realtime-chart-updates": "Biểu Đồ Thời Gian Thực",
|
"realtime-chart-updates": "Cập Nhật Biểu Đồ Thời Gian Thực",
|
||||||
|
|
||||||
"active-users": "Người Dùng Hoạt Động",
|
"active-users": "Người Dùng Hoạt Động",
|
||||||
"active-users.users": "Người Dùng",
|
"active-users.users": "Người Dùng",
|
||||||
@@ -57,11 +57,11 @@
|
|||||||
"active-users.total": "Tổng",
|
"active-users.total": "Tổng",
|
||||||
"active-users.connections": "Kết nối",
|
"active-users.connections": "Kết nối",
|
||||||
|
|
||||||
"guest-registered-users": "Khách vs Người Đã Đăng Ký",
|
"guest-registered-users": "Khách vs Người dùng đã đăng ký",
|
||||||
"guest": "Khách",
|
"guest": "Khách",
|
||||||
"registered": "Đã đăng ký",
|
"registered": "Đã đăng ký",
|
||||||
|
|
||||||
"user-presence": "Người Có Mặt",
|
"user-presence": "Người Dùng Có Mặt",
|
||||||
"on-categories": "Trên danh sách danh mục",
|
"on-categories": "Trên danh sách danh mục",
|
||||||
"reading-posts": "Đọc bài viết",
|
"reading-posts": "Đọc bài viết",
|
||||||
"browsing-topics": "Lướt xem chủ đề",
|
"browsing-topics": "Lướt xem chủ đề",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"unread": "Chưa đọc",
|
"unread": "Chưa đọc",
|
||||||
|
|
||||||
"high-presence-topics": "Chủ Đề Hiện Diện Cao",
|
"high-presence-topics": "Chủ Đề Hiện Diện Cao",
|
||||||
"popular-searches": "Tìm Kiếm Phổ Biến",
|
"popular-searches": "Tìm kiếm Phổ biến",
|
||||||
|
|
||||||
"graphs.page-views": "Xem Trang",
|
"graphs.page-views": "Xem Trang",
|
||||||
"graphs.page-views-registered": "Đã Đăng Ký Xem Trang",
|
"graphs.page-views-registered": "Đã Đăng Ký Xem Trang",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"invitations.description": "Dưới đây là danh sách hoàn tất các lời mời đã gửi. Bấm ctrl-f để tìm kiếm trong danh sách bằng email hoặc tên đăng nhập. <br><br>Tên đăng nhập sẽ hiển thị bên phải email cho những người dùng đã đổi lời mời của họ.",
|
"invitations.description": "Dưới đây là danh sách hoàn tất các lời mời đã gửi. Bấm ctrl-f để tìm kiếm trong danh sách bằng email hoặc tên đăng nhập. <br><br>Tên đăng nhập sẽ hiển thị bên phải email cho những người dùng đã đổi lời mời của họ.",
|
||||||
"invitations.inviter-username": "Tên Đăng Nhập Người Mời",
|
"invitations.inviter-username": "Tên Đăng Nhập Người Mời",
|
||||||
"invitations.invitee-email": "Email của người được mời",
|
"invitations.invitee-email": "Email của người được mời",
|
||||||
"invitations.invitee-username": "Mời Tên Đăng Nhập (nếu đã đăng ký)",
|
"invitations.invitee-username": "Tên Đăng Nhập Người Được Mời (nếu đã đăng ký)",
|
||||||
|
|
||||||
"invitations.confirm-delete": "Bạn có chắc chắn muốn xóa lời mời này không?"
|
"invitations.confirm-delete": "Bạn có chắc chắn muốn xóa lời mời này không?"
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
"no-post": "Bài đăng không tồn tại",
|
"no-post": "Bài đăng không tồn tại",
|
||||||
"no-group": "Nhóm không tồn tại",
|
"no-group": "Nhóm không tồn tại",
|
||||||
"no-user": "Người dùng không tồn tại",
|
"no-user": "Người dùng không tồn tại",
|
||||||
"no-teaser": "Xem thử không tồn tại",
|
"no-teaser": "Đoạn giới thiệu không tồn tại",
|
||||||
"no-flag": "Cờ không tồn tại",
|
"no-flag": "Cờ không tồn tại",
|
||||||
"no-chat-room": "Phòng trò chuyện không tồn tại",
|
"no-chat-room": "Phòng trò chuyện không tồn tại",
|
||||||
"no-privileges": "Bạn không đủ đặc quyền cho hành động này.",
|
"no-privileges": "Bạn không đủ đặc quyền cho hành động này.",
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"content-too-long": "Hãy nhập một bài đăng ngắn hơn. Bài đăng không thể dài hơn %1 ký tự.",
|
"content-too-long": "Hãy nhập một bài đăng ngắn hơn. Bài đăng không thể dài hơn %1 ký tự.",
|
||||||
"title-too-short": "Hãy nhập tiêu đề dài hơn. Tiêu đề nên có ít nhất %1 ký tự.",
|
"title-too-short": "Hãy nhập tiêu đề dài hơn. Tiêu đề nên có ít nhất %1 ký tự.",
|
||||||
"title-too-long": "Hãy nhập tiêu đề ngắn hơn. Tiêu đề không thể dài hơn %1 ký tự.",
|
"title-too-long": "Hãy nhập tiêu đề ngắn hơn. Tiêu đề không thể dài hơn %1 ký tự.",
|
||||||
"category-not-selected": "Chưa chọn danh mục.",
|
"category-not-selected": "Danh mục không được chọn.",
|
||||||
"too-many-posts": "Bạn chỉ có đăng một bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.",
|
"too-many-posts": "Bạn chỉ có đăng một bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.",
|
||||||
"too-many-posts-newbie": "Là người mới, bạn chỉ có thể đăng %1 giây một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại",
|
"too-many-posts-newbie": "Là người mới, bạn chỉ có thể đăng %1 giây một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại",
|
||||||
"too-many-posts-newbie-minutes": "Là người mới, bạn chỉ được đăng bài %1 phút một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại",
|
"too-many-posts-newbie-minutes": "Là người mới, bạn chỉ được đăng bài %1 phút một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"delete": "Xóa",
|
"delete": "Xóa",
|
||||||
"delete-event": "Xóa Sự Kiện",
|
"delete-event": "Xóa Sự Kiện",
|
||||||
"delete-event-confirm": "Bạn có chắc muốn xóa sự kiện này không?",
|
"delete-event-confirm": "Bạn có chắc muốn xóa sự kiện này không?",
|
||||||
"purge": "Loại bỏ",
|
"purge": "Xóa hẳn",
|
||||||
"restore": "Khôi phục",
|
"restore": "Khôi phục",
|
||||||
"move": "Di chuyển",
|
"move": "Di chuyển",
|
||||||
"change-owner": "Đổi Chủ Sở Hữu",
|
"change-owner": "Đổi Chủ Sở Hữu",
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ define('forum/category', [
|
|||||||
'hooks',
|
'hooks',
|
||||||
'alerts',
|
'alerts',
|
||||||
'api',
|
'api',
|
||||||
'clipboard',
|
], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts, api) {
|
||||||
], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts, api, clipboard) {
|
|
||||||
const Category = {};
|
const Category = {};
|
||||||
|
|
||||||
$(window).on('action:ajaxify.start', function (ev, data) {
|
$(window).on('action:ajaxify.start', function (ev, data) {
|
||||||
@@ -49,8 +48,6 @@ define('forum/category', [
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
new clipboard('[data-clipboard-text]');
|
|
||||||
|
|
||||||
hooks.fire('action:topics.loaded', { topics: ajaxify.data.topics });
|
hooks.fire('action:topics.loaded', { topics: ajaxify.data.topics });
|
||||||
hooks.fire('action:category.loaded', { cid: ajaxify.data.cid });
|
hooks.fire('action:category.loaded', { cid: ajaxify.data.cid });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ Actors.assert = async (ids, options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter out loopback uris
|
// Filter out loopback uris
|
||||||
if (!meta.config.activitypubAllowLoopback) {
|
ids = ids.filter(uri => uri !== 'loopback' && new URL(uri).host !== nconf.get('url_parsed').host);
|
||||||
ids = ids.filter(uri => uri !== 'loopback' && new URL(uri).host !== nconf.get('url_parsed').host);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only assert those who haven't been seen recently (configurable), unless update flag passed in (force refresh)
|
// Only assert those who haven't been seen recently (configurable), unless update flag passed in (force refresh)
|
||||||
if (!options.update) {
|
if (!options.update) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
|
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
const utils = require('../utils');
|
|
||||||
|
|
||||||
const activitypub = module.parent.exports;
|
const activitypub = module.parent.exports;
|
||||||
const Feps = module.exports;
|
const Feps = module.exports;
|
||||||
@@ -69,10 +68,8 @@ Feps.announceObject = async function announceObject(id) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let author = await posts.getPostField(id, 'uid');
|
const author = await posts.getPostField(id, 'uid');
|
||||||
if (utils.isNumber(author)) {
|
if (!author.startsWith(nconf.get('url'))) {
|
||||||
author = `${nconf.get('url')}/uid/${author}`;
|
|
||||||
} else if (!author.startsWith(nconf.get('url'))) {
|
|
||||||
followers.unshift(author);
|
followers.unshift(author);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +80,6 @@ Feps.announceObject = async function announceObject(id) {
|
|||||||
actor: `${nconf.get('url')}/category/${cid}`,
|
actor: `${nconf.get('url')}/category/${cid}`,
|
||||||
to: [`${nconf.get('url')}/category/${cid}/followers`],
|
to: [`${nconf.get('url')}/category/${cid}/followers`],
|
||||||
cc: [author, activitypub._constants.publicAddress],
|
cc: [author, activitypub._constants.publicAddress],
|
||||||
object: utils.isNumber(id) ? `${nconf.get('url')}/post/${id}` : id,
|
object: id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ inbox.announce = async (req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
({ tid } = assertion);
|
({ tid } = assertion);
|
||||||
await topics.updateLastPostTime(tid, timestamp);
|
|
||||||
await activitypub.notes.updateLocalRecipients(pid, { to, cc });
|
await activitypub.notes.updateLocalRecipients(pid, { to, cc });
|
||||||
await activitypub.notes.syncUserInboxes(tid);
|
await activitypub.notes.syncUserInboxes(tid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ ActivityPub._constants = Object.freeze({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
ActivityPub._cache = requestCache;
|
ActivityPub._cache = requestCache;
|
||||||
ActivityPub._sent = new Map(); // used only in local tests
|
|
||||||
|
|
||||||
ActivityPub.helpers = require('./helpers');
|
ActivityPub.helpers = require('./helpers');
|
||||||
ActivityPub.inbox = require('./inbox');
|
ActivityPub.inbox = require('./inbox');
|
||||||
@@ -336,7 +335,7 @@ pubsub.on(`activitypub-retry-queue:lruCache:del`, (keys) => {
|
|||||||
async function sendMessage(uri, id, type, payload, attempts = 1) {
|
async function sendMessage(uri, id, type, payload, attempts = 1) {
|
||||||
const keyData = await ActivityPub.getPrivateKey(type, id);
|
const keyData = await ActivityPub.getPrivateKey(type, id);
|
||||||
const headers = await ActivityPub.sign(keyData, uri, payload);
|
const headers = await ActivityPub.sign(keyData, uri, payload);
|
||||||
|
ActivityPub.helpers.log(`[activitypub/send] ${uri}`);
|
||||||
try {
|
try {
|
||||||
const { response, body } = await request.post(uri, {
|
const { response, body } = await request.post(uri, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -376,11 +375,6 @@ ActivityPub.send = async (type, id, targets, payload) => {
|
|||||||
return ActivityPub.helpers.log('[activitypub/send] Federation not enabled; not sending.');
|
return ActivityPub.helpers.log('[activitypub/send] Federation not enabled; not sending.');
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityPub.helpers.log(`[activitypub/send] ${payload.id}`);
|
|
||||||
if (process.env.hasOwnProperty('CI')) {
|
|
||||||
ActivityPub._sent.set(payload.id, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(targets)) {
|
if (!Array.isArray(targets)) {
|
||||||
targets = [targets];
|
targets = [targets];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,95 +40,6 @@ const sanitizeConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocks._normalize = async (object) => {
|
|
||||||
// Normalized incoming AP objects into expected types for easier mocking
|
|
||||||
let { attributedTo, url, image, content, source } = object;
|
|
||||||
|
|
||||||
switch (true) { // non-string attributedTo handling
|
|
||||||
case Array.isArray(attributedTo): {
|
|
||||||
attributedTo = attributedTo.reduce((valid, cur) => {
|
|
||||||
if (typeof cur === 'string') {
|
|
||||||
valid.push(cur);
|
|
||||||
} else if (typeof cur === 'object') {
|
|
||||||
if (cur.type === 'Person' && cur.id) {
|
|
||||||
valid.push(cur.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid;
|
|
||||||
}, []);
|
|
||||||
attributedTo = attributedTo.shift(); // take first valid uid
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case typeof attributedTo === 'object' && attributedTo.hasOwnProperty('id'): {
|
|
||||||
attributedTo = attributedTo.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sourceContent = source && source.mediaType === 'text/markdown' ? source.content : undefined;
|
|
||||||
if (sourceContent) {
|
|
||||||
content = null;
|
|
||||||
sourceContent = await activitypub.helpers.remoteAnchorToLocalProfile(sourceContent, true);
|
|
||||||
} else if (content && content.length) {
|
|
||||||
content = sanitize(content, sanitizeConfig);
|
|
||||||
content = await activitypub.helpers.remoteAnchorToLocalProfile(content);
|
|
||||||
} else {
|
|
||||||
content = '<em>This post did not contain any content.</em>';
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case image && image.hasOwnProperty('url') && !!image.url: {
|
|
||||||
image = image.url;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case image && typeof image === 'string': {
|
|
||||||
// no change
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
image = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (image) {
|
|
||||||
const parsed = new URL(image);
|
|
||||||
if (!mime.getType(parsed.pathname).startsWith('image/')) {
|
|
||||||
activitypub.helpers.log(`[activitypub/mocks.post] Received image not identified as image due to MIME type: ${image}`);
|
|
||||||
image = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) { // Handle url array
|
|
||||||
if (Array.isArray(url)) {
|
|
||||||
url = url.reduce((valid, cur) => {
|
|
||||||
if (typeof cur === 'string') {
|
|
||||||
valid.push(cur);
|
|
||||||
} else if (typeof cur === 'object') {
|
|
||||||
if (cur.type === 'Link' && cur.href) {
|
|
||||||
if (!cur.mediaType || (cur.mediaType && cur.mediaType === 'text/html')) {
|
|
||||||
valid.push(cur.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid;
|
|
||||||
}, []);
|
|
||||||
url = url.shift(); // take first valid url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...object,
|
|
||||||
attributedTo,
|
|
||||||
content,
|
|
||||||
sourceContent,
|
|
||||||
image,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Mocks.profile = async (actors, hostMap) => {
|
Mocks.profile = async (actors, hostMap) => {
|
||||||
// Should only ever be called by activitypub.actors.assert
|
// Should only ever be called by activitypub.actors.assert
|
||||||
const profiles = await Promise.all(actors.map(async (actor) => {
|
const profiles = await Promise.all(actors.map(async (actor) => {
|
||||||
@@ -243,8 +154,6 @@ Mocks.post = async (objects) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const posts = await Promise.all(objects.map(async (object) => {
|
const posts = await Promise.all(objects.map(async (object) => {
|
||||||
object = await Mocks._normalize(object);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!activitypub._constants.acceptedPostTypes.includes(object.type) ||
|
!activitypub._constants.acceptedPostTypes.includes(object.type) ||
|
||||||
!activitypub.helpers.isUri(object.id) // sanity-check the id
|
!activitypub.helpers.isUri(object.id) // sanity-check the id
|
||||||
@@ -257,11 +166,25 @@ Mocks.post = async (objects) => {
|
|||||||
url,
|
url,
|
||||||
attributedTo: uid,
|
attributedTo: uid,
|
||||||
inReplyTo: toPid,
|
inReplyTo: toPid,
|
||||||
published, updated, name, content, sourceContent,
|
published, updated, name, content, source,
|
||||||
type, to, cc, audience, attachment, tag, image,
|
type, to, cc, audience, attachment, tag, image,
|
||||||
} = object;
|
} = object;
|
||||||
|
|
||||||
await activitypub.actors.assert(uid);
|
if (Array.isArray(uid)) { // Handle array attributedTo
|
||||||
|
uid = uid.reduce((valid, cur) => {
|
||||||
|
if (typeof cur === 'string') {
|
||||||
|
valid.push(cur);
|
||||||
|
} else if (typeof cur === 'object') {
|
||||||
|
if (cur.type === 'Person' && cur.id) {
|
||||||
|
valid.push(cur.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}, []);
|
||||||
|
uid = uid.shift(); // take first valid uid
|
||||||
|
await activitypub.actors.assert(uid);
|
||||||
|
}
|
||||||
|
|
||||||
const resolved = await activitypub.helpers.resolveLocalId(toPid);
|
const resolved = await activitypub.helpers.resolveLocalId(toPid);
|
||||||
if (resolved.type === 'post') {
|
if (resolved.type === 'post') {
|
||||||
@@ -272,6 +195,59 @@ Mocks.post = async (objects) => {
|
|||||||
let edited = new Date(updated);
|
let edited = new Date(updated);
|
||||||
edited = Number.isNaN(edited.valueOf()) ? undefined : edited;
|
edited = Number.isNaN(edited.valueOf()) ? undefined : edited;
|
||||||
|
|
||||||
|
let sourceContent = source && source.mediaType === 'text/markdown' ? source.content : undefined;
|
||||||
|
if (sourceContent) {
|
||||||
|
content = null;
|
||||||
|
sourceContent = await activitypub.helpers.remoteAnchorToLocalProfile(sourceContent, true);
|
||||||
|
} else if (content && content.length) {
|
||||||
|
content = sanitize(content, sanitizeConfig);
|
||||||
|
content = await activitypub.helpers.remoteAnchorToLocalProfile(content);
|
||||||
|
} else {
|
||||||
|
content = '<em>This post did not contain any content.</em>';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case image && image.hasOwnProperty('url') && !!image.url: {
|
||||||
|
image = image.url;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case image && typeof image === 'string': {
|
||||||
|
// no change
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
image = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (image) {
|
||||||
|
const parsed = new URL(image);
|
||||||
|
if (!mime.getType(parsed.pathname).startsWith('image/')) {
|
||||||
|
activitypub.helpers.log(`[activitypub/mocks.post] Received image not identified as image due to MIME type: ${image}`);
|
||||||
|
image = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url) { // Handle url array
|
||||||
|
if (Array.isArray(url)) {
|
||||||
|
url = url.reduce((valid, cur) => {
|
||||||
|
if (typeof cur === 'string') {
|
||||||
|
valid.push(cur);
|
||||||
|
} else if (typeof cur === 'object') {
|
||||||
|
if (cur.type === 'Link' && cur.href) {
|
||||||
|
if (!cur.mediaType || (cur.mediaType && cur.mediaType === 'text/html')) {
|
||||||
|
valid.push(cur.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}, []);
|
||||||
|
url = url.shift(); // take first valid url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'Video') {
|
if (type === 'Video') {
|
||||||
attachment = attachment || [];
|
attachment = attachment || [];
|
||||||
attachment.push({ url });
|
attachment.push({ url });
|
||||||
@@ -298,19 +274,6 @@ Mocks.post = async (objects) => {
|
|||||||
return single ? posts.pop() : posts;
|
return single ? posts.pop() : posts;
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocks.message = async (object) => {
|
|
||||||
object = await Mocks._normalize(object);
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
mid: object.id,
|
|
||||||
uid: object.attributedTo,
|
|
||||||
content: object.content,
|
|
||||||
// ip: caller.ip,
|
|
||||||
};
|
|
||||||
|
|
||||||
return message;
|
|
||||||
};
|
|
||||||
|
|
||||||
Mocks.actors = {};
|
Mocks.actors = {};
|
||||||
|
|
||||||
Mocks.actors.user = async (uid) => {
|
Mocks.actors.user = async (uid) => {
|
||||||
|
|||||||
@@ -286,30 +286,31 @@ Notes.assertPrivate = async (object) => {
|
|||||||
timestamp = Date.now();
|
timestamp = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await activitypub.mocks.message(object);
|
|
||||||
|
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
roomId = await messaging.newRoom(payload.uid, { uids: [...recipients] });
|
roomId = await messaging.newRoom(object.attributedTo, { uids: [...recipients] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any new members to the chat
|
// Add any new members to the chat
|
||||||
const added = Array.from(recipients).filter(uid => !participantUids.includes(uid));
|
const added = Array.from(recipients).filter(uid => !participantUids.includes(uid));
|
||||||
const assertion = await activitypub.actors.assert(added);
|
const assertion = await activitypub.actors.assert(added);
|
||||||
if (assertion) {
|
if (assertion) {
|
||||||
await messaging.addUsersToRoom(payload.uid, added, roomId);
|
await messaging.addUsersToRoom(object.attributedTo, added, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add message to room
|
// Add message to room
|
||||||
const message = await messaging.sendMessage({
|
const message = await messaging.sendMessage({
|
||||||
...payload,
|
mid: object.id,
|
||||||
timestamp: Date.now(),
|
uid: object.attributedTo,
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
|
content: object.content,
|
||||||
toMid: toMid,
|
toMid: toMid,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
// ip: caller.ip,
|
||||||
});
|
});
|
||||||
messaging.notifyUsersInRoom(payload.uid, roomId, message);
|
messaging.notifyUsersInRoom(object.attributedTo, roomId, message);
|
||||||
|
|
||||||
// Set real timestamp back so that the message shows even though it predates room joining
|
// Set real timestamp back so that the message shows even though it predates room joining
|
||||||
await messaging.setMessageField(payload.mid, 'timestamp', timestamp);
|
await messaging.setMessageField(object.id, 'timestamp', timestamp);
|
||||||
|
|
||||||
return { roomId };
|
return { roomId };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const winston = require('winston');
|
||||||
|
const sanitizeHtml = require('sanitize-html');
|
||||||
|
|
||||||
const meta = require('../../meta');
|
const meta = require('../../meta');
|
||||||
const posts = require('../../posts');
|
const posts = require('../../posts');
|
||||||
@@ -22,9 +24,15 @@ uploadsController.get = async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
const itemsPerPage = 20;
|
const itemsPerPage = 20;
|
||||||
const page = parseInt(req.query.page, 10) || 1;
|
const page = parseInt(req.query.page, 10) || 1;
|
||||||
|
let files = [];
|
||||||
|
try {
|
||||||
|
await checkSymLinks(req.query.dir);
|
||||||
|
files = await getFilesInFolder(currentFolder);
|
||||||
|
} catch (err) {
|
||||||
|
winston.error(err.stack);
|
||||||
|
return next(new Error('[[error:invalid-path]]'));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let files = await fs.promises.readdir(currentFolder);
|
|
||||||
files = files.filter(filename => filename !== '.gitignore');
|
|
||||||
const itemCount = files.length;
|
const itemCount = files.length;
|
||||||
const start = Math.max(0, (page - 1) * itemsPerPage);
|
const start = Math.max(0, (page - 1) * itemsPerPage);
|
||||||
const stop = start + itemsPerPage;
|
const stop = start + itemsPerPage;
|
||||||
@@ -64,6 +72,34 @@ uploadsController.get = async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function checkSymLinks(folder) {
|
||||||
|
let dir = path.normalize(folder || '');
|
||||||
|
while (dir.length && dir !== '.') {
|
||||||
|
const nextPath = path.join(nconf.get('upload_path'), dir);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const stat = await fs.promises.lstat(nextPath);
|
||||||
|
if (stat.isSymbolicLink()) {
|
||||||
|
throw new Error('[[invalid-path]]');
|
||||||
|
}
|
||||||
|
const newDir = path.dirname(dir);
|
||||||
|
if (newDir === dir) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dir = newDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFilesInFolder(folder) {
|
||||||
|
const dirents = await fs.promises.readdir(folder, { withFileTypes: true });
|
||||||
|
const files = [];
|
||||||
|
for await (const dirent of dirents) {
|
||||||
|
if (!dirent.isSymbolicLink() && dirent.name !== '.gitignore') {
|
||||||
|
files.push(dirent.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
function buildBreadcrumbs(currentFolder) {
|
function buildBreadcrumbs(currentFolder) {
|
||||||
const crumbs = [];
|
const crumbs = [];
|
||||||
const parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep);
|
const parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep);
|
||||||
@@ -94,14 +130,14 @@ async function getFileData(currentDir, file) {
|
|||||||
const stat = await fs.promises.stat(pathToFile);
|
const stat = await fs.promises.stat(pathToFile);
|
||||||
let filesInDir = [];
|
let filesInDir = [];
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
filesInDir = await fs.promises.readdir(pathToFile);
|
filesInDir = await getFilesInFolder(pathToFile);
|
||||||
}
|
}
|
||||||
const url = `${nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '')}/${file}`;
|
const url = `${nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '')}/${file}`;
|
||||||
return {
|
return {
|
||||||
name: file,
|
name: file,
|
||||||
path: pathToFile.replace(path.join(nconf.get('upload_path'), '/'), ''),
|
path: pathToFile.replace(path.join(nconf.get('upload_path'), '/'), ''),
|
||||||
url: url,
|
url: url,
|
||||||
fileCount: Math.max(0, filesInDir.length - 1), // ignore .gitignore
|
fileCount: filesInDir.length,
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
sizeHumanReadable: `${(stat.size / 1024).toFixed(1)}KiB`,
|
sizeHumanReadable: `${(stat.size / 1024).toFixed(1)}KiB`,
|
||||||
isDirectory: stat.isDirectory(),
|
isDirectory: stat.isDirectory(),
|
||||||
@@ -121,11 +157,50 @@ uploadsController.uploadCategoryPicture = async function (req, res, next) {
|
|||||||
return next(new Error('[[error:invalid-json]]'));
|
return next(new Error('[[error:invalid-json]]'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uploadedFile.path.endsWith('.svg')) {
|
||||||
|
await sanitizeSvg(uploadedFile.path);
|
||||||
|
}
|
||||||
|
|
||||||
await validateUpload(uploadedFile, allowedImageTypes);
|
await validateUpload(uploadedFile, allowedImageTypes);
|
||||||
const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`;
|
const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`;
|
||||||
await uploadImage(filename, 'category', uploadedFile, req, res, next);
|
await uploadImage(filename, 'category', uploadedFile, req, res, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function sanitizeSvg(filePath) {
|
||||||
|
const dirty = await fs.promises.readFile(filePath, 'utf8');
|
||||||
|
const clean = sanitizeHtml(dirty, {
|
||||||
|
allowedTags: [
|
||||||
|
'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop',
|
||||||
|
'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect',
|
||||||
|
'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern',
|
||||||
|
'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode',
|
||||||
|
],
|
||||||
|
allowedAttributes: {
|
||||||
|
'*': [
|
||||||
|
// Geometry
|
||||||
|
'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
|
||||||
|
'width', 'height', 'd', 'points', 'viewBox', 'transform',
|
||||||
|
|
||||||
|
// Presentation
|
||||||
|
'fill', 'stroke', 'stroke-width', 'opacity',
|
||||||
|
'stop-color', 'stop-opacity', 'offset', 'style', 'class',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
'text-anchor', 'font-size', 'font-family',
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform',
|
||||||
|
'xmlns', 'preserveAspectRatio',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
parser: {
|
||||||
|
lowerCaseTags: false,
|
||||||
|
lowerCaseAttributeNames: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fs.promises.writeFile(filePath, clean);
|
||||||
|
}
|
||||||
|
|
||||||
uploadsController.uploadFavicon = async function (req, res, next) {
|
uploadsController.uploadFavicon = async function (req, res, next) {
|
||||||
const uploadedFile = req.files.files[0];
|
const uploadedFile = req.files.files[0];
|
||||||
const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon'];
|
const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon'];
|
||||||
@@ -197,6 +272,9 @@ uploadsController.uploadFile = async function (req, res, next) {
|
|||||||
return next(new Error('[[error:invalid-json]]'));
|
return next(new Error('[[error:invalid-json]]'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await file.exists(path.join(nconf.get('upload_path'), params.folder))) {
|
||||||
|
return next(new Error('[[error:invalid-path]]'));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path);
|
const data = await file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path);
|
||||||
res.json([{ url: data.url }]);
|
res.json([{ url: data.url }]);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const privileges = require('../privileges');
|
|||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const categories = require('../categories');
|
const categories = require('../categories');
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const activitypub = require('../activitypub');
|
|
||||||
const pagination = require('../pagination');
|
const pagination = require('../pagination');
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
@@ -162,12 +161,6 @@ categoryController.get = async function (req, res, next) {
|
|||||||
if (meta.config.activitypubEnabled) {
|
if (meta.config.activitypubEnabled) {
|
||||||
// Include link header for richer parsing
|
// Include link header for richer parsing
|
||||||
res.set('Link', `<${nconf.get('url')}/actegory/${cid}>; rel="alternate"; type="application/activity+json"`);
|
res.set('Link', `<${nconf.get('url')}/actegory/${cid}>; rel="alternate"; type="application/activity+json"`);
|
||||||
|
|
||||||
// Category accessible
|
|
||||||
const remoteOk = await privileges.categories.can('read', cid, activitypub._constants.uid);
|
|
||||||
if (remoteOk) {
|
|
||||||
categoryData.handleFull = `${categoryData.handle}@${nconf.get('url_parsed').host}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('category', categoryData);
|
res.render('category', categoryData);
|
||||||
|
|||||||
@@ -153,7 +153,11 @@ Emailer.setupFallbackTransport = (config) => {
|
|||||||
} else {
|
} else {
|
||||||
smtpOptions.service = String(config['email:smtpTransport:service']);
|
smtpOptions.service = String(config['email:smtpTransport:service']);
|
||||||
}
|
}
|
||||||
|
if (config['email:smtpTransport:allow-self-signed']) {
|
||||||
|
smtpOptions.tls = {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
Emailer.transports.smtp = nodemailer.createTransport(smtpOptions);
|
Emailer.transports.smtp = nodemailer.createTransport(smtpOptions);
|
||||||
Emailer.fallbackTransport = Emailer.transports.smtp;
|
Emailer.fallbackTransport = Emailer.transports.smtp;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ module.exports = function (Messaging) {
|
|||||||
uids = [uids];
|
uids = [uids];
|
||||||
}
|
}
|
||||||
uids = uids.filter(uid => parseInt(uid, 10) > 0);
|
uids = uids.filter(uid => parseInt(uid, 10) > 0);
|
||||||
if (!uids.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uids.forEach((uid) => {
|
uids.forEach((uid) => {
|
||||||
io.in(`uid_${uid}`).emit('event:unread.updateChatCount', data);
|
io.in(`uid_${uid}`).emit('event:unread.updateChatCount', data);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ Configs.setMultiple = async function (data) {
|
|||||||
await processConfig(data);
|
await processConfig(data);
|
||||||
data = serialize(data);
|
data = serialize(data);
|
||||||
await db.setObject('config', data);
|
await db.setObject('config', data);
|
||||||
|
await updateNavItems(data);
|
||||||
updateConfig(deserialize(data));
|
updateConfig(deserialize(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,6 +229,13 @@ async function getLogoSize(data) {
|
|||||||
data['brand:emailLogo:width'] = size.width;
|
data['brand:emailLogo:width'] = size.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateNavItems(data) {
|
||||||
|
if (data.hasOwnProperty('activitypubEnabled')) {
|
||||||
|
const navAdmin = require('../navigation/admin');
|
||||||
|
await navAdmin.update('/world', { enabled: data.activitypubEnabled ? 'on' : '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateConfig(config) {
|
function updateConfig(config) {
|
||||||
updateLocalConfig(config);
|
updateLocalConfig(config);
|
||||||
pubsub.publish('config:update', config);
|
pubsub.publish('config:update', config);
|
||||||
|
|||||||
@@ -85,6 +85,19 @@ admin.get = async function () {
|
|||||||
return cache.map(item => ({ ...item }));
|
return cache.map(item => ({ ...item }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
admin.update = async function (route, data) {
|
||||||
|
const ids = await db.getSortedSetRange('navigation:enabled', 0, -1);
|
||||||
|
const navItems = await db.getObjects(ids.map(id => `navigation:enabled:${id}`));
|
||||||
|
const matchedRoutes = navItems.filter(item => item && item.route === route);
|
||||||
|
if (matchedRoutes.length) {
|
||||||
|
await db.setObjectBulk(
|
||||||
|
matchedRoutes.map(item => [`navigation:enabled:${item.order}`, data])
|
||||||
|
);
|
||||||
|
cache = null;
|
||||||
|
pubsub.publish('admin:navigation:save');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function getAvailable() {
|
async function getAvailable() {
|
||||||
const core = require('../../install/data/navigation.json').map((item) => {
|
const core = require('../../install/data/navigation.json').map((item) => {
|
||||||
item.core = true;
|
item.core = true;
|
||||||
|
|||||||
@@ -6,20 +6,23 @@ const user = require('../user');
|
|||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const topics = require('../topics');
|
const topics = require('../topics');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
|
const messaging = require('../messaging');
|
||||||
|
|
||||||
const SocketMeta = module.exports;
|
const SocketMeta = module.exports;
|
||||||
SocketMeta.rooms = {};
|
SocketMeta.rooms = {};
|
||||||
|
|
||||||
SocketMeta.reconnected = function (socket, data, callback) {
|
SocketMeta.reconnected = async function (socket) {
|
||||||
callback = callback || function () {};
|
if (socket.uid > 0) {
|
||||||
if (socket.uid) {
|
await Promise.all([
|
||||||
topics.pushUnreadCount(socket.uid);
|
topics.pushUnreadCount(socket.uid),
|
||||||
user.notifications.pushCount(socket.uid);
|
user.notifications.pushCount(socket.uid),
|
||||||
|
messaging.pushUnreadCount(socket.uid),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
callback(null, {
|
return {
|
||||||
'cache-buster': meta.config['cache-buster'],
|
'cache-buster': meta.config['cache-buster'],
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Rooms */
|
/* Rooms */
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const categories = require('../categories');
|
|||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const activitypub = require('../activitypub');
|
|
||||||
|
|
||||||
module.exports = function (Topics) {
|
module.exports = function (Topics) {
|
||||||
Topics.createTopicFromPosts = async function (uid, title, pids, fromTid, cid) {
|
Topics.createTopicFromPosts = async function (uid, title, pids, fromTid, cid) {
|
||||||
@@ -39,25 +38,29 @@ module.exports = function (Topics) {
|
|||||||
cid = await posts.getCidByPid(mainPid);
|
cid = await posts.getCidByPid(mainPid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [postData, isAdminOrMod] = await Promise.all([
|
const [mainPost, isAdminOrMod] = await Promise.all([
|
||||||
posts.getPostData(mainPid),
|
posts.getPostData(mainPid),
|
||||||
privileges.categories.isAdminOrMod(cid, uid),
|
privileges.categories.isAdminOrMod(cid, uid),
|
||||||
]);
|
]);
|
||||||
|
let lastPost = mainPost;
|
||||||
|
if (pids.length > 1) {
|
||||||
|
lastPost = await posts.getPostData(pids[pids.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAdminOrMod) {
|
if (!isAdminOrMod) {
|
||||||
throw new Error('[[error:no-privileges]]');
|
throw new Error('[[error:no-privileges]]');
|
||||||
}
|
}
|
||||||
|
const now = Date.now();
|
||||||
const scheduled = postData.timestamp > Date.now();
|
const scheduled = mainPost.timestamp > now;
|
||||||
const params = {
|
const params = {
|
||||||
uid: postData.uid,
|
uid: mainPost.uid,
|
||||||
title: title,
|
title: title,
|
||||||
cid: cid,
|
cid: cid,
|
||||||
timestamp: scheduled && postData.timestamp,
|
timestamp: mainPost.timestamp,
|
||||||
};
|
};
|
||||||
const result = await plugins.hooks.fire('filter:topic.fork', {
|
const result = await plugins.hooks.fire('filter:topic.fork', {
|
||||||
params: params,
|
params: params,
|
||||||
tid: postData.tid,
|
tid: mainPost.tid,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tid = await Topics.create(result.params);
|
const tid = await Topics.create(result.params);
|
||||||
@@ -72,22 +75,21 @@ module.exports = function (Topics) {
|
|||||||
await Topics.movePostToTopic(uid, pid, tid, scheduled);
|
await Topics.movePostToTopic(uid, pid, tid, scheduled);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Topics.updateLastPostTime(tid, scheduled ? (postData.timestamp + 1) : Date.now());
|
await Topics.updateLastPostTime(tid, scheduled ? (mainPost.timestamp + 1) : lastPost.timestamp);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Topics.setTopicFields(tid, {
|
Topics.setTopicFields(tid, {
|
||||||
upvotes: postData.upvotes,
|
upvotes: mainPost.upvotes,
|
||||||
downvotes: postData.downvotes,
|
downvotes: mainPost.downvotes,
|
||||||
forkedFromTid: fromTid,
|
forkedFromTid: fromTid,
|
||||||
forkerUid: uid,
|
forkerUid: uid,
|
||||||
forkTimestamp: Date.now(),
|
forkTimestamp: now,
|
||||||
}),
|
}),
|
||||||
db.sortedSetsAdd(['topics:votes', `cid:${cid}:tids:votes`], postData.votes, tid),
|
db.sortedSetsAdd(['topics:votes', `cid:${cid}:tids:votes`], mainPost.votes, tid),
|
||||||
Topics.events.log(fromTid, { type: 'fork', uid, href: `/topic/${tid}` }),
|
Topics.events.log(fromTid, { type: 'fork', uid, href: `/topic/${tid}` }),
|
||||||
activitypub.feps.announceObject(pids[0]),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
plugins.hooks.fire('action:topic.fork', { tid: tid, fromTid: fromTid, uid: uid });
|
plugins.hooks.fire('action:topic.fork', { tid, fromTid, uid });
|
||||||
|
|
||||||
return await Topics.getTopicData(tid);
|
return await Topics.getTopicData(tid);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -124,23 +124,22 @@ UserEmail.sendValidationEmail = async function (uid, options) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirm_code = utils.generateUUID();
|
|
||||||
const confirm_link = `${nconf.get('url')}/confirm/${confirm_code}`;
|
|
||||||
|
|
||||||
const { emailConfirmInterval, emailConfirmExpiry } = meta.config;
|
|
||||||
|
|
||||||
// If no email passed in (default), retrieve email from uid
|
// If no email passed in (default), retrieve email from uid
|
||||||
if (!options.email || !options.email.length) {
|
if (!options.email || !options.email.length) {
|
||||||
options.email = await user.getUserField(uid, 'email');
|
options.email = await user.getUserField(uid, 'email');
|
||||||
}
|
}
|
||||||
if (!options.email) {
|
if (!options.email) {
|
||||||
|
winston.warn(`[user/email] No email found for uid ${uid}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { emailConfirmInterval, emailConfirmExpiry } = meta.config;
|
||||||
if (!options.force && !await UserEmail.canSendValidation(uid, options.email)) {
|
if (!options.force && !await UserEmail.canSendValidation(uid, options.email)) {
|
||||||
throw new Error(`[[error:confirm-email-already-sent, ${emailConfirmInterval}]]`);
|
throw new Error(`[[error:confirm-email-already-sent, ${emailConfirmInterval}]]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirm_code = utils.generateUUID();
|
||||||
|
const confirm_link = `${nconf.get('url')}/confirm/${confirm_code}`;
|
||||||
const username = await user.getUserField(uid, 'username');
|
const username = await user.getUserField(uid, 'username');
|
||||||
const data = await plugins.hooks.fire('filter:user.verify', {
|
const data = await plugins.hooks.fire('filter:user.verify', {
|
||||||
uid,
|
uid,
|
||||||
|
|||||||
@@ -14,85 +14,85 @@
|
|||||||
<a class="btn btn-primary" href="{config.relative_path}/admin/settings/activitypub">[[admin/manage/categories:federation.disabled-cta]]</a>
|
<a class="btn btn-primary" href="{config.relative_path}/admin/settings/activitypub">[[admin/manage/categories:federation.disabled-cta]]</a>
|
||||||
</div>
|
</div>
|
||||||
{{{ else }}}
|
{{{ else }}}
|
||||||
<div class="acp-page-container">
|
<div class="row settings m-0">
|
||||||
<div class="row settings m-0">
|
<div class="col-12 px-0 mb-4" tabindex="0">
|
||||||
<div class="col-12 col-md-8 px-0 mb-4" tabindex="0">
|
<div id="site-settings" class="mb-4">
|
||||||
<div id="site-settings" class="mb-4">
|
<form role="form">
|
||||||
<form role="form">
|
<h5 class="fw-bold settings-header">[[admin/manage/categories:federation.syncing-header]]</h5>
|
||||||
<h5 class="fw-bold settings-header">[[admin/manage/categories:federation.syncing-header]]</h5>
|
<p>[[admin/manage/categories:federation.syncing-intro]]</p>
|
||||||
<p>[[admin/manage/categories:federation.syncing-intro]]</p>
|
<p class="form-text">[[admin/manage/categories:federation.syncing-caveat]]</p>
|
||||||
<p class="form-text">[[admin/manage/categories:federation.syncing-caveat]]</p>
|
|
||||||
|
|
||||||
{{{ if !following.length }}}
|
{{{ if !following.length }}}
|
||||||
<div class="alert alert-info">[[admin/manage/categories:federation.syncing-none]]</div>
|
<div class="alert alert-info">[[admin/manage/categories:federation.syncing-none]]</div>
|
||||||
{{{ else }}}
|
{{{ else }}}
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>[[admin/manage/categories:federation.syncing-actorUri]]</th>
|
<th>[[admin/manage/categories:federation.syncing-actorUri]]</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{{ each following }}}
|
{{{ each following }}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<pre class="mb-0 mt-1">{./id}</pre>
|
<pre class="mb-0 mt-1">{./id}</pre>
|
||||||
{{{ if !./approved }}}
|
{{{ if !./approved }}}
|
||||||
<span class="form-text text-warning">Pending</span>
|
<span class="form-text text-warning">Pending</span>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" data-action="unfollow" data-actor="{./id}" class="btn btn-sm btn-danger">[[admin/manage/categories:federation.syncing-unfollow]]</button>
|
<button type="button" data-action="unfollow" data-actor="{./id}" class="btn btn-sm btn-danger">[[admin/manage/categories:federation.syncing-unfollow]]</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="syncing-add">[[admin/manage/categories:federation.syncing-add]]</label>
|
<label class="form-label" for="syncing-add">[[admin/manage/categories:federation.syncing-add]]</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="syncing-add" type="url" class="form-control" />
|
<input id="syncing-add" type="url" class="form-control" />
|
||||||
<button data-action="follow" type="button" class="btn btn-primary">[[admin/manage/categories:federation.syncing-follow]]</button>
|
<button data-action="follow" type="button" class="btn btn-primary">[[admin/manage/categories:federation.syncing-follow]]</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p>[[admin/manage/categories:federation.followers]]</p>
|
<p>[[admin/manage/categories:federation.followers]]</p>
|
||||||
<table class="table small">
|
<table class="table small">
|
||||||
<tr>
|
<tr>
|
||||||
<th>[[admin/manage/categories:federation.followers-handle]]</th>
|
<th>[[admin/manage/categories:federation.followers-handle]]</th>
|
||||||
<th>[[admin/manage/categories:federation.followers-id]]</th>
|
<th>[[admin/manage/categories:federation.followers-id]]</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{{ if !followers.length}}}
|
{{{ if !followers.length}}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center border-0" colspan="2">
|
<td class="text-center border-0" colspan="2">
|
||||||
<em>[[admin/manage/categories:federation.followers-none]]</em>
|
<em>[[admin/manage/categories:federation.followers-none]]</em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
{{{ each followers }}}
|
{{{ each followers }}}
|
||||||
<tr data-uid="{./uid}">
|
<tr data-uid="{./uid}">
|
||||||
<td>
|
<td class="w-100 text-truncate" style="max-width: 1px;">
|
||||||
{buildAvatar(followers, "24px", true)}
|
{buildAvatar(followers, "24px", true)}
|
||||||
{./userslug}
|
{./userslug}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="w-0">
|
||||||
<code>{./uid}</code>
|
<div class="d-flex gap-2 flex-nowrap align-items-center">
|
||||||
<button type="button" class="btn btn-link" data-action="autofill">
|
<button type="button" class="btn btn-ghost btn-sm border" data-action="autofill" title="[[admin/manage/categories:federation.followers-autofill]]">
|
||||||
<i class="fa fa-exchange-alt"></i>
|
<i class="fa fa-exchange-alt text-primary"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
<code>{./uid}</code>
|
||||||
</tr>
|
</div>
|
||||||
{{{ end }}}
|
</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
{{{ end }}}
|
||||||
</form>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -116,6 +116,11 @@
|
|||||||
<label for="email:smtpTransport:pool" class="form-check-label">[[admin/settings/email:smtp-transport.pool]]</label>
|
<label for="email:smtpTransport:pool" class="form-check-label">[[admin/settings/email:smtp-transport.pool]]</label>
|
||||||
<p class="form-text">[[admin/settings/email:smtp-transport.pool-help]]</p>
|
<p class="form-text">[[admin/settings/email:smtp-transport.pool-help]]</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="email:smtpTransport:allow-self-signed" data-field="email:smtpTransport:allow-self-signed" name="email:smtpTransport:allow-self-signed" />
|
||||||
|
<label for="email:smtpTransport:allow-self-signed" class="form-check-label">[[admin/settings/email:smtp-transport.allow-self-signed]]</label>
|
||||||
|
<p class="form-text">[[admin/settings/email:smtp-transport.allow-self-signed-help]]</p>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="email:smtpTransport:service">[[admin/settings/email:smtp-transport.service]]</label>
|
<label class="form-label" for="email:smtpTransport:service">[[admin/settings/email:smtp-transport.service]]</label>
|
||||||
<select class="form-select" id="email:smtpTransport:service" data-field="email:smtpTransport:service">
|
<select class="form-select" id="email:smtpTransport:service" data-field="email:smtpTransport:service">
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const activitypub = require('../src/activitypub');
|
|||||||
describe('ActivityPub integration', () => {
|
describe('ActivityPub integration', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
meta.config.activitypubEnabled = 1;
|
meta.config.activitypubEnabled = 1;
|
||||||
meta.config.activitypubAllowLoopback = 1;
|
|
||||||
await install.giveWorldPrivileges();
|
await install.giveWorldPrivileges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,16 +28,6 @@ describe('ActivityPub integration', () => {
|
|||||||
delete meta.config.activitypubEnabled;
|
delete meta.config.activitypubEnabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Outgoing AP logging for test runner', () => {
|
|
||||||
it('should log an entry in ActivityPub._sent when .send is called', async () => {
|
|
||||||
const uuid = utils.generateUUID();
|
|
||||||
const uid = await user.create({ username: uuid });
|
|
||||||
await activitypub.send('uid', 0, [`https://localhost/uid/${uid}`], { id: `${nconf.get('url')}/activity/${uuid}`, foo: 'bar' });
|
|
||||||
|
|
||||||
assert(activitypub._sent.has(`${nconf.get('url')}/activity/${uuid}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Master toggle', () => {
|
describe('Master toggle', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
delete meta.config.activitypubEnabled;
|
delete meta.config.activitypubEnabled;
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const assert = require('assert');
|
|
||||||
const nconf = require('nconf');
|
|
||||||
|
|
||||||
const db = require('../mocks/databasemock');
|
|
||||||
const activitypub = require('../../src/activitypub');
|
|
||||||
const utils = require('../../src/utils');
|
|
||||||
const meta = require('../../src/meta');
|
|
||||||
const install = require('../../src/install');
|
|
||||||
const user = require('../../src/user');
|
|
||||||
const groups = require('../../src/groups');
|
|
||||||
const categories = require('../../src/categories');
|
|
||||||
const topics = require('../../src/topics');
|
|
||||||
const api = require('../../src/api');
|
|
||||||
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
|
|
||||||
describe('FEPs', () => {
|
|
||||||
before(async () => {
|
|
||||||
meta.config.activitypubEnabled = 1;
|
|
||||||
await install.giveWorldPrivileges();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.only('1b12', () => {
|
|
||||||
describe('announceObject()', () => {
|
|
||||||
let cid;
|
|
||||||
let uid;
|
|
||||||
let adminUid;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
const name = utils.generateUUID();
|
|
||||||
const description = utils.generateUUID();
|
|
||||||
({ cid } = await categories.create({ name, description }));
|
|
||||||
|
|
||||||
adminUid = await user.create({ username: utils.generateUUID() });
|
|
||||||
await groups.join('administrators', adminUid);
|
|
||||||
uid = await user.create({ username: utils.generateUUID() });
|
|
||||||
|
|
||||||
const { id: followerId, actor } = helpers.mocks.actor();
|
|
||||||
activitypub._cache.set(`0;${followerId}`, actor);
|
|
||||||
user.setCategoryWatchState(followerId, [cid], categories.watchStates.tracking);
|
|
||||||
|
|
||||||
activitypub._sent.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
activitypub._sent.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called when a topic is moved from uncategorized to another category', async () => {
|
|
||||||
const { topicData } = await topics.post({
|
|
||||||
uid,
|
|
||||||
cid: -1,
|
|
||||||
title: utils.generateUUID(),
|
|
||||||
content: utils.generateUUID(),
|
|
||||||
});
|
|
||||||
|
|
||||||
assert(topicData);
|
|
||||||
|
|
||||||
await api.topics.move({ uid: adminUid }, {
|
|
||||||
tid: topicData.tid,
|
|
||||||
cid,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.strictEqual(activitypub._sent.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be called for a newly forked topic', async () => {
|
|
||||||
const { topicData } = await topics.post({
|
|
||||||
uid,
|
|
||||||
cid: -1,
|
|
||||||
title: utils.generateUUID(),
|
|
||||||
content: utils.generateUUID(),
|
|
||||||
});
|
|
||||||
const { tid } = topicData;
|
|
||||||
const [{ pid: reply1Pid }, { pid: reply2Pid }] = await Promise.all([
|
|
||||||
topics.reply({ uid, tid, content: utils.generateUUID() }),
|
|
||||||
topics.reply({ uid, tid, content: utils.generateUUID() }),
|
|
||||||
]);
|
|
||||||
const forked = await topics.createTopicFromPosts(
|
|
||||||
adminUid, utils.generateUUID(), [reply1Pid, reply2Pid], tid, cid
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(activitypub._sent.size, 1);
|
|
||||||
|
|
||||||
const key = Array.from(activitypub._sent.keys())[0];
|
|
||||||
const activity = activitypub._sent.get(key);
|
|
||||||
|
|
||||||
assert(activity);
|
|
||||||
assert.strictEqual(activity.object, `${nconf.get('url')}/post/${reply1Pid}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,42 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const activitypub = require('../../src/activitypub');
|
|
||||||
const utils = require('../../src/utils');
|
const utils = require('../../src/utils');
|
||||||
const slugify = require('../../src/slugify');
|
const activitypub = require('../../src/activitypub');
|
||||||
|
|
||||||
const Helpers = module.exports;
|
const Helpers = module.exports;
|
||||||
|
|
||||||
Helpers.mocks = {};
|
Helpers.mocks = {};
|
||||||
|
|
||||||
Helpers.mocks.actor = () => {
|
|
||||||
const baseUrl = 'https://example.org';
|
|
||||||
const uuid = utils.generateUUID();
|
|
||||||
const id = `${baseUrl}/${uuid}`;
|
|
||||||
|
|
||||||
const actor = {
|
|
||||||
'@context': [
|
|
||||||
'https://www.w3.org/ns/activitystreams',
|
|
||||||
'https://w3id.org/security/v1',
|
|
||||||
],
|
|
||||||
id: `${id}`,
|
|
||||||
url: `${id}`,
|
|
||||||
inbox: `${id}/inbox`,
|
|
||||||
outbox: `${id}/outbox`,
|
|
||||||
|
|
||||||
type: 'Person',
|
|
||||||
name: slugify(uuid),
|
|
||||||
preferredUsername: uuid,
|
|
||||||
|
|
||||||
publicKey: {
|
|
||||||
id: `${id}#key`,
|
|
||||||
owner: `${id}`,
|
|
||||||
publicKeyPem: 'todo',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return { id, actor };
|
|
||||||
};
|
|
||||||
|
|
||||||
Helpers.mocks.note = (override = {}) => {
|
Helpers.mocks.note = (override = {}) => {
|
||||||
const baseUrl = 'https://example.org';
|
const baseUrl = 'https://example.org';
|
||||||
const uuid = utils.generateUUID();
|
const uuid = utils.generateUUID();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
|
|
||||||
const db = require('../mocks/databasemock');
|
const db = require('../../src/database');
|
||||||
const meta = require('../../src/meta');
|
const meta = require('../../src/meta');
|
||||||
const install = require('../../src/install');
|
const install = require('../../src/install');
|
||||||
const user = require('../../src/user');
|
const user = require('../../src/user');
|
||||||
|
|||||||
24
test/navigation.js
Normal file
24
test/navigation.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const db = require('./mocks/databasemock');
|
||||||
|
const meta = require('../src/meta');
|
||||||
|
const navAdmin = require('../src/navigation/admin');
|
||||||
|
|
||||||
|
describe('Navigation', () => {
|
||||||
|
before(async () => {
|
||||||
|
const data = require('../install/data/navigation.json');
|
||||||
|
await navAdmin.save(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle /world route when ap is toggled', async () => {
|
||||||
|
let nav = await navAdmin.get();
|
||||||
|
let world = nav.find(item => item.route === '/world');
|
||||||
|
assert.strictEqual(!!world.enabled, true);
|
||||||
|
await meta.configs.setMultiple({ activitypubEnabled: 0 });
|
||||||
|
nav = await navAdmin.get();
|
||||||
|
world = nav.find(item => item.route === '/world');
|
||||||
|
assert.strictEqual(!!world.enabled, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -269,12 +269,9 @@ describe('socket.io', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should push unread notifications on reconnect', (done) => {
|
it('should push unread notifications/chats on reconnect', async () => {
|
||||||
const socketMeta = require('../src/socket.io/meta');
|
const socketMeta = require('../src/socket.io/meta');
|
||||||
socketMeta.reconnected({ uid: 1 }, {}, (err) => {
|
await socketMeta.reconnected({ uid: 1 }, {});
|
||||||
assert.ifError(err);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -400,6 +400,17 @@ describe('Upload Controllers', () => {
|
|||||||
assert.strictEqual(body.error, '[[error:invalid-path]]');
|
assert.strictEqual(body.error, '[[error:invalid-path]]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail to upload regular file if directory does not exist', async () => {
|
||||||
|
const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/upload/file`, path.join(__dirname, '../test/files/test.png'), {
|
||||||
|
params: JSON.stringify({
|
||||||
|
folder: 'does-not-exist',
|
||||||
|
}),
|
||||||
|
}, jar, csrf_token);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 500);
|
||||||
|
assert.strictEqual(body.error, '[[error:invalid-path]]');
|
||||||
|
});
|
||||||
|
|
||||||
describe('ACP uploads screen', () => {
|
describe('ACP uploads screen', () => {
|
||||||
it('should create a folder', async () => {
|
it('should create a folder', async () => {
|
||||||
const { response } = await helpers.createFolder('', 'myfolder', jar, csrf_token);
|
const { response } = await helpers.createFolder('', 'myfolder', jar, csrf_token);
|
||||||
|
|||||||
Reference in New Issue
Block a user