Compare commits

...

23 Commits

Author SHA1 Message Date
Julian Lam
4ec7552cfb refactor: move all input note normalization into helper method, have assertPrivate mock a message object (with said normalization) before sending message 2025-03-04 14:11:41 -05:00
Julian Lam
6c26d9f4a3 feat: add line to description exposing a category's handle if accessible by fediverse pseudo-user, closes #13126 2025-03-03 15:03:21 -05:00
Julian Lam
e3edfef865 feat: call announceObject on topic fork, #13215 2025-03-03 11:54:26 -05:00
Julian Lam
deb5ee5e01 fix: improper cc and object fields in announceObject 2025-03-03 11:54:26 -05:00
Julian Lam
feb9421507 test: add failing test for #13215 2025-03-03 11:54:26 -05:00
Barış Soner Uşaklı
324d232faa Merge branch 'master' into develop 2025-03-03 09:24:16 -05:00
Barış Soner Uşaklı
15d921f375 Merge branch 'master' into develop 2025-03-03 09:15:52 -05:00
Misty Release Bot
02e2d4ee7e Latest translations and fallbacks 2025-03-03 09:20:18 +00:00
Julian Lam
dca3c35d76 fix: move AP send logging earlier 2025-03-02 22:58:05 -05:00
Julian Lam
7ceb6d69ae test: adjust test runner detection in AP code 2025-03-02 21:58:46 -05:00
Julian Lam
d948334713 fix: #13224, handle note attributedTo when it is of type object 2025-03-02 21:35:54 -05:00
Barış Soner Uşaklı
1d989a0144 Merge branch 'master' into develop 2025-02-28 20:52:08 -05:00
Julian Lam
e510e82633 test: new test file for feps 2025-02-28 14:46:12 -05:00
Barış Soner Uşaklı
f671ae2c6f Merge branch 'master' into develop 2025-02-28 14:37:16 -05:00
renovate[bot]
e19109ad2c fix(deps): update dependency tough-cookie to v5.1.2 (#13217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 14:08:08 -05:00
Julian Lam
6e872b5fe4 test: log outgoing AP messages for local test runner 2025-02-28 13:56:40 -05:00
Julian Lam
73aaa990fb fix: allow actor assertion of loopback actors depending on ACP setting 2025-02-28 13:56:40 -05:00
Julian Lam
98aafaaff8 test: allow ap/notes tests to be run in isolation 2025-02-28 13:56:40 -05:00
renovate[bot]
ad680d6abe fix(deps): update dependency mongodb to v6.14.0 (#13214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 10:52:17 -05:00
renovate[bot]
4c22af8c43 fix(deps): update dependency terser-webpack-plugin to v5.3.12 (#13213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 10:40:46 -05:00
renovate[bot]
f56838a3f0 fix(deps): update dependency cron to v4.1.0 (#13200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 10:03:18 -05:00
Barış Soner Uşaklı
d8151986a6 Merge branch 'master' into develop 2025-02-28 09:51:52 -05:00
Misty Release Bot
6106b3c200 Latest translations and fallbacks 2025-02-28 09:20:14 +00:00
18 changed files with 298 additions and 102 deletions

View File

@@ -63,7 +63,7 @@
"connect-pg-simple": "10.0.0",
"connect-redis": "8.0.1",
"cookie-parser": "1.4.7",
"cron": "4.0.0",
"cron": "4.1.0",
"cropperjs": "1.6.2",
"csrf-sync": "4.0.3",
"daemon": "1.1.0",
@@ -93,7 +93,7 @@
"lru-cache": "10.4.3",
"mime": "3.0.0",
"mkdirp": "3.0.1",
"mongodb": "6.13.1",
"mongodb": "6.14.0",
"morgan": "1.10.0",
"mousetrap": "1.6.5",
"multiparty": "4.2.3",
@@ -108,7 +108,7 @@
"nodebb-plugin-spam-be-gone": "2.3.1",
"nodebb-plugin-web-push": "0.7.3",
"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-peace": "2.2.39",
"nodebb-theme-persona": "14.0.15",
@@ -140,13 +140,13 @@
"@socket.io/redis-adapter": "8.3.0",
"sortablejs": "1.15.6",
"spdx-license-list": "6.9.0",
"terser-webpack-plugin": "5.3.11",
"terser-webpack-plugin": "5.3.12",
"textcomplete": "0.18.2",
"textcomplete.contenteditable": "0.1.1",
"timeago": "1.6.7",
"tinycon": "0.6.8",
"toobusy-js": "0.5.1",
"tough-cookie": "5.1.1",
"tough-cookie": "5.1.2",
"validator": "13.12.0",
"webpack": "5.98.0",
"webpack-merge": "6.0.1",

View File

@@ -3,6 +3,7 @@
"subcategories": "Subcategories",
"uncategorized": "Uncategorized",
"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",
"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?",

View File

@@ -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-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!",
"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",
"notices": "Thông báo",
@@ -49,7 +49,7 @@
"maintenance-mode": "Chế Độ Bảo Trì",
"maintenance-mode-title": "Bấm vào đây để thiết lập chế độ bảo trì cho NodeBB",
"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.users": "Người Dùng",
@@ -57,11 +57,11 @@
"active-users.total": "Tổng",
"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",
"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",
"reading-posts": "Đọc bài viết",
"browsing-topics": "Lướt xem chủ đề",
@@ -69,7 +69,7 @@
"unread": "Chưa đọc",
"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-registered": "Đã Đăng Ký Xem Trang",

View File

@@ -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.inviter-username": "Tên Đăng Nhập Người 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?"
}

View File

@@ -62,7 +62,7 @@
"no-post": "Bài đăng 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-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-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.",
@@ -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ự.",
"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ự.",
"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-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",

View File

@@ -23,7 +23,7 @@
"delete": "Xóa",
"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?",
"purge": "Xóa hẳn",
"purge": "Loại bỏ",
"restore": "Khôi phục",
"move": "Di chuyển",
"change-owner": "Đổi Chủ Sở Hữu",

View File

@@ -10,7 +10,8 @@ define('forum/category', [
'hooks',
'alerts',
'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 = {};
$(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:category.loaded', { cid: ajaxify.data.cid });
};

View File

@@ -73,7 +73,9 @@ Actors.assert = async (ids, options = {}) => {
}
// Filter out loopback uris
ids = ids.filter(uri => uri !== 'loopback' && new URL(uri).host !== nconf.get('url_parsed').host);
if (!meta.config.activitypubAllowLoopback) {
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)
if (!options.update) {

View File

@@ -3,6 +3,7 @@
const nconf = require('nconf');
const posts = require('../posts');
const utils = require('../utils');
const activitypub = module.parent.exports;
const Feps = module.exports;
@@ -68,8 +69,10 @@ Feps.announceObject = async function announceObject(id) {
return;
}
const author = await posts.getPostField(id, 'uid');
if (!author.startsWith(nconf.get('url'))) {
let author = await posts.getPostField(id, 'uid');
if (utils.isNumber(author)) {
author = `${nconf.get('url')}/uid/${author}`;
} else if (!author.startsWith(nconf.get('url'))) {
followers.unshift(author);
}
@@ -80,6 +83,6 @@ Feps.announceObject = async function announceObject(id) {
actor: `${nconf.get('url')}/category/${cid}`,
to: [`${nconf.get('url')}/category/${cid}/followers`],
cc: [author, activitypub._constants.publicAddress],
object: id,
object: utils.isNumber(id) ? `${nconf.get('url')}/post/${id}` : id,
});
};

View File

@@ -48,6 +48,7 @@ ActivityPub._constants = Object.freeze({
},
});
ActivityPub._cache = requestCache;
ActivityPub._sent = new Map(); // used only in local tests
ActivityPub.helpers = require('./helpers');
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) {
const keyData = await ActivityPub.getPrivateKey(type, id);
const headers = await ActivityPub.sign(keyData, uri, payload);
ActivityPub.helpers.log(`[activitypub/send] ${uri}`);
try {
const { response, body } = await request.post(uri, {
headers: {
@@ -375,6 +376,11 @@ ActivityPub.send = async (type, id, targets, payload) => {
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)) {
targets = [targets];
}

View File

@@ -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) => {
// Should only ever be called by activitypub.actors.assert
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) => {
object = await Mocks._normalize(object);
if (
!activitypub._constants.acceptedPostTypes.includes(object.type) ||
!activitypub.helpers.isUri(object.id) // sanity-check the id
@@ -166,25 +257,11 @@ Mocks.post = async (objects) => {
url,
attributedTo: uid,
inReplyTo: toPid,
published, updated, name, content, source,
published, updated, name, content, sourceContent,
type, to, cc, audience, attachment, tag, image,
} = 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);
if (resolved.type === 'post') {
@@ -195,59 +272,6 @@ Mocks.post = async (objects) => {
let edited = new Date(updated);
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') {
attachment = attachment || [];
attachment.push({ url });
@@ -274,6 +298,19 @@ Mocks.post = async (objects) => {
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.user = async (uid) => {

View File

@@ -286,31 +286,30 @@ Notes.assertPrivate = async (object) => {
timestamp = Date.now();
}
const payload = await activitypub.mocks.message(object);
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
const added = Array.from(recipients).filter(uid => !participantUids.includes(uid));
const assertion = await activitypub.actors.assert(added);
if (assertion) {
await messaging.addUsersToRoom(object.attributedTo, added, roomId);
await messaging.addUsersToRoom(payload.uid, added, roomId);
}
// Add message to room
const message = await messaging.sendMessage({
mid: object.id,
uid: object.attributedTo,
roomId: roomId,
content: object.content,
toMid: toMid,
...payload,
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
await messaging.setMessageField(object.id, 'timestamp', timestamp);
await messaging.setMessageField(payload.mid, 'timestamp', timestamp);
return { roomId };
};

View File

@@ -10,6 +10,7 @@ const privileges = require('../privileges');
const user = require('../user');
const categories = require('../categories');
const meta = require('../meta');
const activitypub = require('../activitypub');
const pagination = require('../pagination');
const helpers = require('./helpers');
const utils = require('../utils');
@@ -161,6 +162,12 @@ categoryController.get = async function (req, res, next) {
if (meta.config.activitypubEnabled) {
// Include link header for richer parsing
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);

View File

@@ -7,6 +7,7 @@ const categories = require('../categories');
const privileges = require('../privileges');
const plugins = require('../plugins');
const meta = require('../meta');
const activitypub = require('../activitypub');
module.exports = function (Topics) {
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),
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 });

View File

@@ -21,6 +21,7 @@ const activitypub = require('../src/activitypub');
describe('ActivityPub integration', () => {
before(async () => {
meta.config.activitypubEnabled = 1;
meta.config.activitypubAllowLoopback = 1;
await install.giveWorldPrivileges();
});
@@ -28,6 +29,16 @@ describe('ActivityPub integration', () => {
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', () => {
before(async () => {
delete meta.config.activitypubEnabled;

95
test/activitypub/feps.js Normal file
View 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}`);
});
});
});
});

View File

@@ -1,12 +1,42 @@
'use strict';
const utils = require('../../src/utils');
const activitypub = require('../../src/activitypub');
const utils = require('../../src/utils');
const slugify = require('../../src/slugify');
const Helpers = module.exports;
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 = {}) => {
const baseUrl = 'https://example.org';
const uuid = utils.generateUUID();

View File

@@ -3,7 +3,7 @@
const assert = require('assert');
const nconf = require('nconf');
const db = require('../../src/database');
const db = require('../mocks/databasemock');
const meta = require('../../src/meta');
const install = require('../../src/install');
const user = require('../../src/user');