mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-19 23:10:21 +01:00
Compare commits
30 Commits
v4.1.0
...
normalize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec7552cfb | ||
|
|
6c26d9f4a3 | ||
|
|
e3edfef865 | ||
|
|
deb5ee5e01 | ||
|
|
feb9421507 | ||
|
|
324d232faa | ||
|
|
9549f1fa93 | ||
|
|
15d921f375 | ||
|
|
40a5314e64 | ||
|
|
02e2d4ee7e | ||
|
|
dca3c35d76 | ||
|
|
7ceb6d69ae | ||
|
|
d948334713 | ||
|
|
1d989a0144 | ||
|
|
39ff5dde3f | ||
|
|
e510e82633 | ||
|
|
f671ae2c6f | ||
|
|
8dbd50d452 | ||
|
|
1d4be4752c | ||
|
|
e19109ad2c | ||
|
|
6e872b5fe4 | ||
|
|
73aaa990fb | ||
|
|
98aafaaff8 | ||
|
|
30068245d3 | ||
|
|
ad680d6abe | ||
|
|
4c22af8c43 | ||
|
|
f56838a3f0 | ||
|
|
d8151986a6 | ||
|
|
6106b3c200 | ||
|
|
3292a85820 |
130
CHANGELOG.md
130
CHANGELOG.md
@@ -1,3 +1,133 @@
|
|||||||
|
#### v4.1.0 (2025-02-27)
|
||||||
|
|
||||||
|
##### Chores
|
||||||
|
|
||||||
|
* incrementing version number - v4.0.6 (4a52fb2e)
|
||||||
|
* update changelog for v4.0.6 (78bbea30)
|
||||||
|
* comment out testing helper call (bad0a4c2)
|
||||||
|
* incrementing version number - v4.0.5 (1792a62b)
|
||||||
|
* incrementing version number - v4.0.4 (b1125cce)
|
||||||
|
* incrementing version number - v4.0.3 (2b65c735)
|
||||||
|
* up harmony (ea110a0e)
|
||||||
|
* incrementing version number - v4.0.2 (73fe5fcf)
|
||||||
|
* incrementing version number - v4.0.1 (a461b758)
|
||||||
|
* bump emoji for #13077 as well (ff0de097)
|
||||||
|
* fix ap dev helper (b14494b0)
|
||||||
|
* add helper method to ease in dev (d7d64a14)
|
||||||
|
* add helper method to ease in dev (7d5482b2)
|
||||||
|
* incrementing version number - v4.0.0 (c1eaee45)
|
||||||
|
* **deps:**
|
||||||
|
* update dependency sass-embedded to v1.85.1 (#13208) (3907e6c8)
|
||||||
|
* update postgres docker tag to v17.4 (#13196) (cba2bc5e)
|
||||||
|
* update postgres docker tag to v17.3 (#13162) (47e28a0e)
|
||||||
|
* update dependency sass-embedded to v1.85.0 (#13161) (2258e145)
|
||||||
|
* update commitlint monorepo to v19.7.1 (#13123) (ca6734b3)
|
||||||
|
* update coverallsapp/github-action action to v2.3.6 (#13089) (84b28fae)
|
||||||
|
* update dependency lint-staged to v15.4.3 (#13079) (1d846134)
|
||||||
|
* update dependency mocha to v11.1.0 (#13069) (8e99c97a)
|
||||||
|
* update dependency lint-staged to v15.4.1 (#13060) (153e65bc)
|
||||||
|
* update dependency lint-staged to v15.4.1 (#13060) (37b2b83d)
|
||||||
|
* **i18n:**
|
||||||
|
* fallback strings for new resources: nodebb.category (00253821)
|
||||||
|
* fallback strings for new resources: nodebb.error (589be143)
|
||||||
|
* fallback strings for new resources: nodebb.themes-harmony (25049714)
|
||||||
|
* fallback strings for new resources: nodebb.admin-settings-advanced (ad6b6132)
|
||||||
|
* fallback strings for new resources: nodebb.themes-harmony (fc063bb0)
|
||||||
|
* fallback strings for new resources: nodebb.admin-settings-general (d41109a0)
|
||||||
|
|
||||||
|
##### New Features
|
||||||
|
|
||||||
|
* support remote "Video" type objects in note assertion, #13120 (95f2c4ed)
|
||||||
|
* 1b12 compatibility (7dc1e8ab)
|
||||||
|
* remove activities older than a week (d9e86c7b)
|
||||||
|
* federate out Announce of a tid's mainPid if the tid is moved out of cid -1 (b7f9983a)
|
||||||
|
* syncUserInboxes to take into account remote topic tags, closes #13074 (637addc4)
|
||||||
|
* allow search bar to load remote 7888 Conversations, aka nodebb topics (7687da00)
|
||||||
|
* introduce new 'markdown' post parsing type, closes #13077 (b386e4a6)
|
||||||
|
* #13066, report canonical URL in user agent for outgoing requests (c3e9cb68)
|
||||||
|
* changes to how a topic is presented via ActivityPub; conformance with upcoming changes to 7888 (4fd7a9dc)
|
||||||
|
* changes to how a topic is presented via ActivityPub; conformance with upcoming changes to 7888 (adeaff4b)
|
||||||
|
|
||||||
|
##### Bug Fixes
|
||||||
|
|
||||||
|
* scheduled topics and posts should return 404 on AP request (428300de)
|
||||||
|
* tag handling when remote objects contain tags without leading # symbol (5c3f1cfe)
|
||||||
|
* handle multiple types in remote actor payload (65895651)
|
||||||
|
* missing db (058befb3)
|
||||||
|
* remove handle on category purge (adb430f2)
|
||||||
|
* restrict feps methods to real cids (8b717d54)
|
||||||
|
* restore old behaviour of 1b12 federating both object and activity (f0ee43dc)
|
||||||
|
* send `actor` with 1b12 announce, fixes #13072 again (86b0e591)
|
||||||
|
* isArray check (5f3ed76d)
|
||||||
|
* delete from payload instead of setting null (489c5ce2)
|
||||||
|
* send `actor` with 1b12 announce, fixes #13072 (3cd87f1b)
|
||||||
|
* #13139, payload.version can be null (be1598d1)
|
||||||
|
* tidChanged (bfd4e68b)
|
||||||
|
* bad logic that invisibly broke outgoing user follows completely (334be721)
|
||||||
|
* #13076, allow pulling in of topics by their topic URL fix: reapply fixes that were part of since-reverted 4fd7a9dc59b65e8654d704c493f2254793e8d6a9 (c6e6ab43)
|
||||||
|
* call relativeToAbsolute helper when generating markdown source content in mocks.notes.public/private (02fb99eb)
|
||||||
|
* extend remoteAnchorToLocalProfile ap helper to handle markdown content (db1f8959)
|
||||||
|
* incorrect `posts` url in topic posts collection (812ec73e)
|
||||||
|
* incorrect `posts` url in topic posts collection (b2530e61)
|
||||||
|
* **deps:**
|
||||||
|
* update dependency sass to v1.85.1 (#13209) (386ab89f)
|
||||||
|
* update dependency bcryptjs to v3 (#13160) (6ea65678)
|
||||||
|
* update dependency cron to v4 (#13184) (41eec8d7)
|
||||||
|
* update dependency xregexp to v5.1.2 (#13195) (23621eca)
|
||||||
|
* update dependency ace-builds to v1.39.0 (#13197) (a3f5721a)
|
||||||
|
* update dependency chart.js to v4.4.8 (#13182) (474d267e)
|
||||||
|
* update dependency postcss to v8.5.3 (#13183) (5fc4c806)
|
||||||
|
* update dependency mongodb to v6.13.1 (#13187) (77b0160c)
|
||||||
|
* update dependency nodebb-plugin-web-push to v0.7.3 (#13178) (000ceee4)
|
||||||
|
* update dependency sass to v1.85.0 (#13163) (75a7188a)
|
||||||
|
* update dependency pg to v8.13.3 (#13157) (f3c156e9)
|
||||||
|
* update dependency pg-cursor to v2.12.3 (#13158) (6b8e4b39)
|
||||||
|
* update dependency webpack to v5.98.0 (#13159) (db74c1e8)
|
||||||
|
* update dependency nodebb-widget-essentials to v7.0.33 (#13156) (af7f4242)
|
||||||
|
* update dependency pg-cursor to v2.12.2 (#13150) (b5ce9e14)
|
||||||
|
* update dependency compression to v1.8.0 (#13152) (1e52cf34)
|
||||||
|
* update dependency ace-builds to v1.38.0 (#13151) (db0b816c)
|
||||||
|
* update dependency pg to v8.13.2 (#13149) (bea1367d)
|
||||||
|
* update dependency postcss to v8.5.2 (#13144) (3449e76d)
|
||||||
|
* update dependency benchpressjs to v2.5.3 (#13098) (6688edde)
|
||||||
|
* update dependency esbuild to v0.25.0 (#13141) (d7fdd80c)
|
||||||
|
* update dependency tough-cookie to v5.1.1 (#13140) (33ce7239)
|
||||||
|
* update dependency ioredis to v5.5.0 (#13138) (b337e999)
|
||||||
|
* update dependency sass to v1.84.0 (#13128) (f872a768)
|
||||||
|
* update dependency semver to v7.7.1 (#13122) (5f3c5a55)
|
||||||
|
* update dependency mongodb to v6.13.0 (#13106) (31ff6c2e)
|
||||||
|
* update dependency semver to v7.7.0 (#13099) (a348e808)
|
||||||
|
* update dependency nodemailer to v6.10.0 (#13073) (8ab71e4f)
|
||||||
|
* update dependency nodebb-theme-persona to v14.0.2 (#13064) (8ec3ceae)
|
||||||
|
* update dependency nodebb-theme-harmony to v2.0.3 (#13063) (b98d047a)
|
||||||
|
|
||||||
|
##### Other Changes
|
||||||
|
|
||||||
|
* remove unused db (06b3d9ad)
|
||||||
|
* remove tab (54bc54e1)
|
||||||
|
* fix tab (397d28e3)
|
||||||
|
|
||||||
|
##### Performance Improvements
|
||||||
|
|
||||||
|
* closes #13145, reduce calls in actors.prune (676acb7e)
|
||||||
|
|
||||||
|
##### Refactors
|
||||||
|
|
||||||
|
* remove cid:-1:tids (and variants) from intersection in /world, fixes #13125 (d0561a60)
|
||||||
|
* single remove (0784e11b)
|
||||||
|
* move 1b12 announce logic out of inbox and into separate feps module (9fd6ac6b)
|
||||||
|
* acceptable types in context.js to index.js, allow searching for remote topis by topic url (d644c0f4)
|
||||||
|
* Posts.relativeToAbsolute so that the regexes passed to it no longer need a pre-defined length, it is now calculated from the match result, added new regex for markdown image/anchors (f64e6f0f)
|
||||||
|
|
||||||
|
##### Tests
|
||||||
|
|
||||||
|
* moved AP actor tests to separate actors.js file, added failing test for scheduled topics (01be4d79)
|
||||||
|
* update test to assert the note assertion itself (c6ba56a5)
|
||||||
|
* update bcrypt hash for 3.x (bfffbfbe)
|
||||||
|
* update pwd test for bcrypt3.x (ca0fa1d3)
|
||||||
|
* add sourceContent to spec (d1d55461)
|
||||||
|
* adjust webfinger test for updated 404 status code (59afd193)
|
||||||
|
|
||||||
#### v4.0.6 (2025-02-27)
|
#### v4.0.6 (2025-02-27)
|
||||||
|
|
||||||
##### Chores
|
##### Chores
|
||||||
|
|||||||
@@ -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.0.0",
|
"cron": "4.1.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.13.1",
|
"mongodb": "6.14.0",
|
||||||
"morgan": "1.10.0",
|
"morgan": "1.10.0",
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"multiparty": "4.2.3",
|
"multiparty": "4.2.3",
|
||||||
@@ -103,12 +103,12 @@
|
|||||||
"nodebb-plugin-dbsearch": "6.2.13",
|
"nodebb-plugin-dbsearch": "6.2.13",
|
||||||
"nodebb-plugin-emoji": "6.0.2",
|
"nodebb-plugin-emoji": "6.0.2",
|
||||||
"nodebb-plugin-emoji-android": "4.1.1",
|
"nodebb-plugin-emoji-android": "4.1.1",
|
||||||
"nodebb-plugin-markdown": "13.1.0",
|
"nodebb-plugin-markdown": "13.1.1",
|
||||||
"nodebb-plugin-mentions": "4.7.0",
|
"nodebb-plugin-mentions": "4.7.1",
|
||||||
"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.37",
|
"nodebb-theme-harmony": "2.0.38",
|
||||||
"nodebb-theme-lavender": "7.1.17",
|
"nodebb-theme-lavender": "7.1.17",
|
||||||
"nodebb-theme-peace": "2.2.39",
|
"nodebb-theme-peace": "2.2.39",
|
||||||
"nodebb-theme-persona": "14.0.15",
|
"nodebb-theme-persona": "14.0.15",
|
||||||
@@ -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.11",
|
"terser-webpack-plugin": "5.3.12",
|
||||||
"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.1",
|
"tough-cookie": "5.1.2",
|
||||||
"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",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"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; Xin vui lòng 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; hãy 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": "Cập Nhật Biểu Đồ Thời Gian Thực",
|
"realtime-chart-updates": "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 dùng đã đăng ký",
|
"guest-registered-users": "Khách vs Người Đã Đăng Ký",
|
||||||
"guest": "Khách",
|
"guest": "Khách",
|
||||||
"registered": "Đã đăng ký",
|
"registered": "Đã đăng ký",
|
||||||
|
|
||||||
"user-presence": "Người Dùng Có Mặt",
|
"user-presence": "Người 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": "Tên Đăng Nhập Người Được Mời (nếu đã đăng ký)",
|
"invitations.invitee-username": "Mời Tên Đăng Nhập (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": "Đoạn giới thiệu không tồn tại",
|
"no-teaser": "Xem thử 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": "Danh mục không được chọn.",
|
"category-not-selected": "Chưa chọn danh mục.",
|
||||||
"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": "Xóa hẳn",
|
"purge": "Loại bỏ",
|
||||||
"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,7 +10,8 @@ define('forum/category', [
|
|||||||
'hooks',
|
'hooks',
|
||||||
'alerts',
|
'alerts',
|
||||||
'api',
|
'api',
|
||||||
], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts, api) {
|
'clipboard',
|
||||||
|
], 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) {
|
||||||
@@ -48,6 +49,8 @@ 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,7 +73,9 @@ 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,6 +3,7 @@
|
|||||||
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;
|
||||||
@@ -68,8 +69,10 @@ Feps.announceObject = async function announceObject(id) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const author = await posts.getPostField(id, 'uid');
|
let author = await posts.getPostField(id, 'uid');
|
||||||
if (!author.startsWith(nconf.get('url'))) {
|
if (utils.isNumber(author)) {
|
||||||
|
author = `${nconf.get('url')}/uid/${author}`;
|
||||||
|
} else if (!author.startsWith(nconf.get('url'))) {
|
||||||
followers.unshift(author);
|
followers.unshift(author);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +83,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: id,
|
object: utils.isNumber(id) ? `${nconf.get('url')}/post/${id}` : id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ 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');
|
||||||
@@ -335,7 +336,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: {
|
||||||
@@ -375,6 +376,11 @@ 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,6 +40,95 @@ 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) => {
|
||||||
@@ -154,6 +243,8 @@ 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
|
||||||
@@ -166,25 +257,11 @@ Mocks.post = async (objects) => {
|
|||||||
url,
|
url,
|
||||||
attributedTo: uid,
|
attributedTo: uid,
|
||||||
inReplyTo: toPid,
|
inReplyTo: toPid,
|
||||||
published, updated, name, content, source,
|
published, updated, name, content, sourceContent,
|
||||||
type, to, cc, audience, attachment, tag, image,
|
type, to, cc, audience, attachment, tag, image,
|
||||||
} = object;
|
} = object;
|
||||||
|
|
||||||
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);
|
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') {
|
||||||
@@ -195,59 +272,6 @@ 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 });
|
||||||
@@ -274,6 +298,19 @@ 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,31 +286,30 @@ 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(object.attributedTo, { uids: [...recipients] });
|
roomId = await messaging.newRoom(payload.uid, { 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(object.attributedTo, added, roomId);
|
await messaging.addUsersToRoom(payload.uid, added, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add message to room
|
// Add message to room
|
||||||
const message = await messaging.sendMessage({
|
const message = await messaging.sendMessage({
|
||||||
mid: object.id,
|
...payload,
|
||||||
uid: object.attributedTo,
|
|
||||||
roomId: roomId,
|
|
||||||
content: object.content,
|
|
||||||
toMid: toMid,
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
// ip: caller.ip,
|
roomId: roomId,
|
||||||
|
toMid: toMid,
|
||||||
});
|
});
|
||||||
messaging.notifyUsersInRoom(object.attributedTo, roomId, message);
|
messaging.notifyUsersInRoom(payload.uid, 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(object.id, 'timestamp', timestamp);
|
await messaging.setMessageField(payload.mid, 'timestamp', timestamp);
|
||||||
|
|
||||||
return { roomId };
|
return { roomId };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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');
|
||||||
@@ -161,6 +162,12 @@ 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);
|
||||||
|
|||||||
@@ -275,8 +275,12 @@ async function fireActionHook(hook, hookList, params) {
|
|||||||
winston.warn(`[plugins] Expected method for hook '${hook}' in plugin '${hookObj.id}' not found, skipping.`);
|
winston.warn(`[plugins] Expected method for hook '${hook}' in plugin '${hookObj.id}' not found, skipping.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await hookObj.method(params);
|
await hookObj.method(params);
|
||||||
|
} catch (err) {
|
||||||
|
winston.error(`[plugins] Error in hook ${hookObj.id}@${hookObj.hook} \n${err.stack}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ Plugins.toggleActive = async function (socket, plugin_id) {
|
|||||||
|
|
||||||
Plugins.toggleInstall = async function (socket, data) {
|
Plugins.toggleInstall = async function (socket, data) {
|
||||||
const isInstalled = await plugins.isInstalled(data.id);
|
const isInstalled = await plugins.isInstalled(data.id);
|
||||||
if (nconf.get('acpPluginInstallDisabled') && !isInstalled) {
|
const isStarterPlan = nconf.get('saas_plan') === 'starter';
|
||||||
|
if ((isStarterPlan || nconf.get('acpPluginInstallDisabled')) && !isInstalled) {
|
||||||
throw new Error('[[error:plugin-installation-via-acp-disabled]]');
|
throw new Error('[[error:plugin-installation-via-acp-disabled]]');
|
||||||
}
|
}
|
||||||
postsCache.reset();
|
postsCache.reset();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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) {
|
||||||
@@ -83,6 +84,7 @@ module.exports = function (Topics) {
|
|||||||
}),
|
}),
|
||||||
db.sortedSetsAdd(['topics:votes', `cid:${cid}:tids:votes`], postData.votes, tid),
|
db.sortedSetsAdd(['topics:votes', `cid:${cid}:tids:votes`], postData.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: tid, fromTid: fromTid, uid: uid });
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ module.exports = function (User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteImages(uid) {
|
async function deleteImages(uid) {
|
||||||
|
if (utils.isNumber(uid)) {
|
||||||
const folder = path.join(nconf.get('upload_path'), 'profile', `uid-${uid}`);
|
const folder = path.join(nconf.get('upload_path'), 'profile', `uid-${uid}`);
|
||||||
await rimraf(folder);
|
await rimraf(folder);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,6 +29,16 @@ 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;
|
||||||
|
|||||||
95
test/activitypub/feps.js
Normal file
95
test/activitypub/feps.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
'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,12 +1,42 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const utils = require('../../src/utils');
|
|
||||||
const activitypub = require('../../src/activitypub');
|
const activitypub = require('../../src/activitypub');
|
||||||
|
const utils = require('../../src/utils');
|
||||||
|
const slugify = require('../../src/slugify');
|
||||||
|
|
||||||
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('../../src/database');
|
const db = require('../mocks/databasemock');
|
||||||
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');
|
||||||
|
|||||||
Reference in New Issue
Block a user