mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-31 21:00:24 +01:00
Compare commits
2 Commits
crossposti
...
renovate/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9449f02a7 | ||
|
|
89abdca179 |
@@ -33,7 +33,7 @@
|
||||
"@fontsource/inter": "5.2.8",
|
||||
"@fontsource/poppins": "5.2.7",
|
||||
"@fortawesome/fontawesome-free": "6.7.2",
|
||||
"@isaacs/ttlcache": "2.1.3",
|
||||
"@isaacs/ttlcache": "2.1.4",
|
||||
"@nodebb/spider-detector": "2.0.3",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@textcomplete/contenteditable": "0.1.13",
|
||||
@@ -110,7 +110,7 @@
|
||||
"nodebb-theme-harmony": "2.1.29",
|
||||
"nodebb-theme-lavender": "7.1.19",
|
||||
"nodebb-theme-peace": "2.2.49",
|
||||
"nodebb-theme-persona": "14.1.21",
|
||||
"nodebb-theme-persona": "14.1.20",
|
||||
"nodebb-widget-essentials": "7.0.41",
|
||||
"nodemailer": "7.0.12",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
"profile-page-for": "Profile page for user %1",
|
||||
"user-watched-tags": "User watched tags",
|
||||
"delete-upload-button": "Delete upload button",
|
||||
"group-page-link-for": "Group page link for %1",
|
||||
"show-crossposts": "Show Cross-posts"
|
||||
"group-page-link-for": "Group page link for %1"
|
||||
}
|
||||
@@ -170,8 +170,6 @@
|
||||
"topic-already-deleted": "This topic has already been deleted",
|
||||
"topic-already-restored": "This topic has already been restored",
|
||||
|
||||
"topic-already-crossposted": "This topic has already been cross-posted there.",
|
||||
|
||||
"cant-purge-main-post": "You can't purge the main post, please delete the topic instead",
|
||||
|
||||
"topic-thumbnails-are-disabled": "Topic thumbnails are disabled.",
|
||||
@@ -264,7 +262,6 @@
|
||||
"no-topics-selected": "No topics selected!",
|
||||
"cant-move-to-same-topic": "Can't move post to same topic!",
|
||||
"cant-move-topic-to-same-category": "Can't move topic to the same category!",
|
||||
"cant-move-topic-to-from-remote-categories": "You cannot move topics in or out of remote categories; consider cross-posting instead.",
|
||||
|
||||
"cannot-block-self": "You cannot block yourself!",
|
||||
"cannot-block-privileged": "You cannot block administrators or global moderators",
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
"users": "Users",
|
||||
"topics": "Topics",
|
||||
"posts": "Posts",
|
||||
"crossposts": "Cross-posts",
|
||||
"x-posts": "<span class=\"formatted-number\">%1</span> posts",
|
||||
"x-topics": "<span class=\"formatted-number\">%1</span> topics",
|
||||
"x-reputation": "<span class=\"formatted-number\">%1</span> reputation",
|
||||
|
||||
@@ -116,7 +116,6 @@
|
||||
"thread-tools.lock": "Lock Topic",
|
||||
"thread-tools.unlock": "Unlock Topic",
|
||||
"thread-tools.move": "Move Topic",
|
||||
"thread-tools.crosspost": "Crosspost Topic",
|
||||
"thread-tools.move-posts": "Move Posts",
|
||||
"thread-tools.move-all": "Move All",
|
||||
"thread-tools.change-owner": "Change Owner",
|
||||
@@ -150,7 +149,6 @@
|
||||
|
||||
"load-categories": "Loading Categories",
|
||||
"confirm-move": "Move",
|
||||
"confirm-crosspost": "Cross-post",
|
||||
"confirm-fork": "Fork",
|
||||
|
||||
"bookmark": "Bookmark",
|
||||
@@ -163,7 +161,6 @@
|
||||
"loading-more-posts": "Loading More Posts",
|
||||
"move-topic": "Move Topic",
|
||||
"move-topics": "Move Topics",
|
||||
"crosspost-topic": "Cross-post Topic",
|
||||
"move-post": "Move Post",
|
||||
"post-moved": "Post moved!",
|
||||
"fork-topic": "Fork Topic",
|
||||
@@ -187,10 +184,6 @@
|
||||
"change-owner-instruction": "Click the posts you want to assign to another user",
|
||||
"manage-editors-instruction": "Manage the users who can edit this post below.",
|
||||
|
||||
"crossposts.instructions": "Select one or more categories to cross-post to. Topic(s) will be accessible from the original category and all cross-posted categories.",
|
||||
"crossposts.listing": "This topic has been cross-posted to the following local categories:",
|
||||
"crossposts.none": "This topic has not been cross-posted to any additional categories",
|
||||
|
||||
"composer.title-placeholder": "Enter your topic title here...",
|
||||
"composer.handle-placeholder": "Enter your name/handle here",
|
||||
"composer.hide": "Hide",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
CrosspostObject:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The cross-post ID
|
||||
cid:
|
||||
type: object
|
||||
description: The category id that the topic was cross-posted to
|
||||
additionalProperties:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
tid:
|
||||
type: object
|
||||
description: The topic id that was cross-posted
|
||||
additionalProperties:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
timestamp:
|
||||
type: number
|
||||
uid:
|
||||
type: object
|
||||
description: The user id that initiated the cross-post
|
||||
additionalProperties:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
CrosspostsArray:
|
||||
type: array
|
||||
description: A list of crosspost objects
|
||||
items:
|
||||
$ref: '#/CrosspostObject'
|
||||
@@ -168,8 +168,6 @@ paths:
|
||||
$ref: 'write/topics/tid/bump.yaml'
|
||||
/topics/{tid}/move:
|
||||
$ref: 'write/topics/tid/move.yaml'
|
||||
/topics/{tid}/crossposts:
|
||||
$ref: 'write/topics/tid/crossposts.yaml'
|
||||
/tags/{tag}/follow:
|
||||
$ref: 'write/tags/tag/follow.yaml'
|
||||
/posts/{pid}:
|
||||
|
||||
@@ -86,6 +86,7 @@ put:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
types:
|
||||
type: object
|
||||
@@ -102,6 +103,7 @@ put:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
types:
|
||||
type: object
|
||||
|
||||
@@ -47,6 +47,7 @@ get:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
isPrivate:
|
||||
type: boolean
|
||||
@@ -64,6 +65,7 @@ get:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
types:
|
||||
type: object
|
||||
|
||||
@@ -93,6 +93,7 @@ put:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
groups:
|
||||
type: array
|
||||
@@ -106,6 +107,7 @@ put:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
types:
|
||||
type: object
|
||||
@@ -228,6 +230,7 @@ delete:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
groups:
|
||||
type: array
|
||||
@@ -241,6 +244,7 @@ delete:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
types:
|
||||
type: object
|
||||
|
||||
@@ -71,4 +71,5 @@ get:
|
||||
privileges:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: A set of privileges with either true or false
|
||||
@@ -1,76 +0,0 @@
|
||||
post:
|
||||
tags:
|
||||
- topics
|
||||
summary: crosspost a topic
|
||||
description: This operation crossposts a topic to another category.
|
||||
parameters:
|
||||
- in: path
|
||||
name: tid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid topic id
|
||||
example: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Topic successfully crossposted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
crossposts:
|
||||
$ref: ../../../components/schemas/CrosspostObject.yaml#/CrosspostsArray
|
||||
delete:
|
||||
tags:
|
||||
- topics
|
||||
summary: uncrossposts a topic
|
||||
description: This operation uncrossposts a topic from a category.
|
||||
parameters:
|
||||
- in: path
|
||||
name: tid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid topic id
|
||||
example: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Topic successfully uncrossposted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
crossposts:
|
||||
$ref: ../../../components/schemas/CrosspostObject.yaml#/CrosspostsArray
|
||||
@@ -69,7 +69,6 @@ define('forum/topic', [
|
||||
setupQuickReply();
|
||||
handleBookmark(tid);
|
||||
handleThumbs();
|
||||
addCrosspostsHandler();
|
||||
|
||||
$(window).on('scroll', utils.debounce(updateTopicTitle, 250));
|
||||
|
||||
@@ -407,22 +406,6 @@ define('forum/topic', [
|
||||
});
|
||||
}
|
||||
|
||||
function addCrosspostsHandler() {
|
||||
const anchorEl = document.getElementById('show-crossposts');
|
||||
if (anchorEl) {
|
||||
anchorEl.addEventListener('click', async () => {
|
||||
const { crossposts } = ajaxify.data;
|
||||
const html = await app.parseAndTranslate('modals/crossposts', { crossposts });
|
||||
bootbox.dialog({
|
||||
onEscape: true,
|
||||
backdrop: true,
|
||||
title: '[[global:crossposts]]',
|
||||
message: html,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupQuickReply() {
|
||||
if (config.enableQuickReply || (config.theme && config.theme.enableQuickReply)) {
|
||||
quickreply.init();
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('forum/topic/crosspost', [
|
||||
'categoryFilter', 'alerts', 'hooks', 'api', 'components',
|
||||
], function (categoryFilter, alerts, hooks, api, components) {
|
||||
const Crosspost = {};
|
||||
let modal;
|
||||
let selectedCids;
|
||||
|
||||
Crosspost.init = function (tid) {
|
||||
if (modal) {
|
||||
return;
|
||||
}
|
||||
Crosspost.tid = tid;
|
||||
Crosspost.cid = ajaxify.data.cid;
|
||||
Crosspost.current = ajaxify.data.crossposts;
|
||||
|
||||
showModal();
|
||||
};
|
||||
|
||||
function showModal() {
|
||||
app.parseAndTranslate('modals/crosspost-topic', {
|
||||
selectedCategory: ajaxify.data.crossposts.length ?
|
||||
{
|
||||
icon: 'fa-plus',
|
||||
name: '[[unread:multiple-categories-selected]]',
|
||||
bgColor: '#ddd',
|
||||
} :
|
||||
ajaxify.data.category,
|
||||
}, function (html) {
|
||||
modal = html;
|
||||
$('body').append(modal);
|
||||
|
||||
const dropdownEl = modal.find('[component="category-selector"]');
|
||||
dropdownEl.addClass('dropup');
|
||||
|
||||
categoryFilter.init($('[component="category/dropdown"]'), {
|
||||
onHidden: onCategoriesSelected,
|
||||
hideAll: true,
|
||||
hideUncategorized: true,
|
||||
localOnly: true,
|
||||
selectedCids: Array.from(new Set([ajaxify.data.cid, ...ajaxify.data.crossposts.map(c => c.cid)])),
|
||||
});
|
||||
|
||||
modal.find('#crosspost_thread_commit').on('click', onCommitClicked);
|
||||
modal.find('#crosspost_topic_cancel').on('click', closeCrosspostModal);
|
||||
});
|
||||
}
|
||||
|
||||
function onCategoriesSelected(data) {
|
||||
selectedCids = data.selectedCids.filter(utils.isNumber);
|
||||
if (data.changed) {
|
||||
modal.find('#crosspost_thread_commit').prop('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
function onCommitClicked() {
|
||||
const commitEl = modal.find('#crosspost_thread_commit');
|
||||
|
||||
if (!commitEl.prop('disabled') && selectedCids && selectedCids.length) {
|
||||
commitEl.prop('disabled', true);
|
||||
const data = {
|
||||
tid: Crosspost.tid,
|
||||
cids: selectedCids,
|
||||
};
|
||||
|
||||
// TODO
|
||||
// if (config.undoTimeout > 0) {
|
||||
// return alerts.alert({
|
||||
// alert_id: 'tids_move_' + (Crosspost.tid ? Crosspost.tid.join('-') : 'all'),
|
||||
// title: '[[topic:thread-tools.move]]',
|
||||
// message: message,
|
||||
// type: 'success',
|
||||
// timeout: config.undoTimeout,
|
||||
// timeoutfn: function () {
|
||||
// moveTopics(data);
|
||||
// },
|
||||
// clickfn: function (alert, params) {
|
||||
// delete params.timeoutfn;
|
||||
// alerts.success('[[topic:topic-move-undone]]');
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
crosspost(data);
|
||||
}
|
||||
}
|
||||
|
||||
function crosspost(data) {
|
||||
hooks.fire('action:topic.crosspost', data);
|
||||
|
||||
const cids = data.cids.map((cid) => parseInt(cid, 10));
|
||||
if (!cids.includes(Crosspost.cid)) {
|
||||
cids.unshift(Crosspost.cid);
|
||||
}
|
||||
const current = [Crosspost.cid, ...Crosspost.current.map(x => parseInt(x.cid, 10))];
|
||||
const add = cids.filter(cid => !current.includes(cid));
|
||||
const remove = current.filter(cid => !cids.includes(cid));
|
||||
|
||||
const queries = [
|
||||
...add.map((cid) => { return api.post(`/topics/${data.tid}/crossposts`, { cid }); }),
|
||||
...remove.map((cid) => { return api.del(`/topics/${data.tid}/crossposts`, { cid }); }),
|
||||
];
|
||||
|
||||
Promise.all(queries).then(async () => {
|
||||
const statsEl = components.get('topic/stats');
|
||||
updateSpinner('progress');
|
||||
const { crossposts } = await api.get(`/topics/${data.tid}/crossposts`);
|
||||
ajaxify.data.crossposts = crossposts;
|
||||
const html = await app.parseAndTranslate('partials/topic/stats', ajaxify.data);
|
||||
statsEl.html(html);
|
||||
updateSpinner('success');
|
||||
}).catch((e) => {
|
||||
updateSpinner('error');
|
||||
alerts.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
const spinnerClasses = new Map(Object.entries({
|
||||
'initial': ['d-none'],
|
||||
'progress': ['fa-spinner', 'text-secondary', 'fa-spin'],
|
||||
'error': ['fa-times', 'text-error'],
|
||||
'success': ['fa-check', 'text-success'],
|
||||
}));
|
||||
function updateSpinner(state) {
|
||||
if (modal) {
|
||||
const spinnerEl = document.getElementById('crosspost_topic_spinner');
|
||||
const remove = [
|
||||
...spinnerClasses.get('initial'),
|
||||
...spinnerClasses.get('progress'),
|
||||
...spinnerClasses.get('error'),
|
||||
...spinnerClasses.get('success'),
|
||||
];
|
||||
spinnerEl.classList.remove(...remove);
|
||||
spinnerEl.classList.add(...spinnerClasses.get(state));
|
||||
|
||||
if (state !== 'initial') {
|
||||
setTimeout(() => {
|
||||
updateSpinner('initial');
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeCrosspostModal() {
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
modal = null;
|
||||
}
|
||||
}
|
||||
|
||||
return Crosspost;
|
||||
});
|
||||
@@ -34,7 +34,6 @@ define('forum/topic/move', [
|
||||
categorySelector.init(dropdownEl, {
|
||||
onSelect: onCategorySelected,
|
||||
privilege: 'moderate',
|
||||
localOnly: true,
|
||||
});
|
||||
|
||||
modal.find('#move_thread_commit').on('click', onCommitClicked);
|
||||
|
||||
@@ -116,12 +116,6 @@ define('forum/topic/threadTools', [
|
||||
return false;
|
||||
});
|
||||
|
||||
topicContainer.on('click', '[component="topic/crosspost"]', () => {
|
||||
require(['forum/topic/crosspost'], (crosspost) => {
|
||||
crosspost.init(tid, ajaxify.data.cid);
|
||||
});
|
||||
});
|
||||
|
||||
topicContainer.on('click', '[component="topic/delete/posts"]', function () {
|
||||
require(['forum/topic/delete-posts'], function (deletePosts) {
|
||||
deletePosts.init();
|
||||
|
||||
@@ -76,8 +76,6 @@ define('categorySearch', ['alerts', 'bootstrap', 'api'], function (alerts, boots
|
||||
privilege: options.privilege,
|
||||
states: options.states,
|
||||
showLinks: options.showLinks,
|
||||
localOnly: options.localOnly,
|
||||
hideUncategorized: options.hideUncategorized,
|
||||
}, function (err, { categories }) {
|
||||
if (err) {
|
||||
return alerts.error(err);
|
||||
@@ -95,7 +93,6 @@ define('categorySearch', ['alerts', 'bootstrap', 'api'], function (alerts, boots
|
||||
categoryItems: categories.slice(0, 200),
|
||||
selectedCategory: ajaxify.data.selectedCategory,
|
||||
allCategoriesUrl: ajaxify.data.allCategoriesUrl,
|
||||
hideAll: options.hideAll,
|
||||
}, function (html) {
|
||||
el.find('[component="category/list"]')
|
||||
.html(html.find('[component="category/list"]').html());
|
||||
|
||||
@@ -411,6 +411,7 @@ Actors.assertGroup = async (ids, options = {}) => {
|
||||
db.sortedSetAdd('usersRemote:lastCrawled', groups.map(() => now), groups.map(p => p.id)),
|
||||
db.sortedSetAddBulk(queries.searchAdd),
|
||||
db.setObject('handle:cid', queries.handleAdd),
|
||||
_migratePersonToGroup(categoryObjs),
|
||||
db.setsAdd(masksAdd, 'topics:create'),
|
||||
db.setsRemove(masksRemove, 'topics:create'),
|
||||
]);
|
||||
@@ -418,6 +419,41 @@ Actors.assertGroup = async (ids, options = {}) => {
|
||||
return categoryObjs;
|
||||
};
|
||||
|
||||
async function _migratePersonToGroup(categoryObjs) {
|
||||
// 4.0.0-4.1.x asserted as:Group as users. This moves relevant stuff over and deletes the now-duplicate user.
|
||||
let ids = categoryObjs.map(category => category.cid);
|
||||
const slugs = categoryObjs.map(category => category.slug);
|
||||
const isUser = await db.isObjectFields('handle:uid', slugs);
|
||||
ids = ids.filter((id, idx) => isUser[idx]);
|
||||
if (!ids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(ids.map(async (id) => {
|
||||
const shares = await db.getSortedSetMembers(`uid:${id}:shares`);
|
||||
let cids = await topics.getTopicsFields(shares, ['cid']);
|
||||
cids = cids.map(o => o.cid);
|
||||
await Promise.all(shares.map(async (share, idx) => {
|
||||
const cid = cids[idx];
|
||||
if (cid === -1) {
|
||||
await topics.tools.move(share, {
|
||||
cid: id,
|
||||
uid: 'system',
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const followers = await db.getSortedSetMembersWithScores(`followersRemote:${id}`);
|
||||
await db.sortedSetAdd(
|
||||
`cid:${id}:uid:watch:state`,
|
||||
followers.map(() => categories.watchStates.tracking),
|
||||
followers.map(({ value }) => value),
|
||||
);
|
||||
await user.deleteAccount(id);
|
||||
}));
|
||||
await categories.onTopicsMoved(ids);
|
||||
}
|
||||
|
||||
Actors.getLocalFollowers = async (id) => {
|
||||
// Returns local uids and cids that follow a remote actor (by id)
|
||||
const response = {
|
||||
@@ -445,19 +481,10 @@ Actors.getLocalFollowers = async (id) => {
|
||||
}
|
||||
});
|
||||
} else if (isCategory) {
|
||||
// Internally, users are different, they follow via watch state instead
|
||||
// Possibly refactor to store in followersRemote:${id} too??
|
||||
const members = await db.getSortedSetRangeByScore(`cid:${id}:uid:watch:state`, 0, -1, categories.watchStates.tracking, categories.watchStates.watching);
|
||||
members.forEach((uid) => {
|
||||
response.uids.add(uid);
|
||||
});
|
||||
|
||||
const cids = await db.getSortedSetMembers(`followersRemote:${id}`);
|
||||
cids.forEach((id) => {
|
||||
if (id.startsWith('cid|') && utils.isNumber(id.slice(4))) {
|
||||
response.cids.add(parseInt(id.slice(4), 10));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -388,10 +388,12 @@ inbox.announce = async (req) => {
|
||||
|
||||
// Category sync, remove when cross-posting available
|
||||
const { cids } = await activitypub.actors.getLocalFollowers(actor);
|
||||
const syncedCids = Array.from(cids);
|
||||
let cid = null;
|
||||
if (cids.size > 0) {
|
||||
cid = Array.from(cids)[0];
|
||||
}
|
||||
|
||||
// 1b12 announce
|
||||
let cid = null;
|
||||
const categoryActor = await categories.exists(actor);
|
||||
if (categoryActor) {
|
||||
cid = actor;
|
||||
@@ -446,7 +448,7 @@ inbox.announce = async (req) => {
|
||||
socketHelpers.sendNotificationToPostOwner(pid, actor, 'announce', 'notifications:activitypub.announce');
|
||||
} else { // Remote object
|
||||
// Follower check
|
||||
if (!fromRelay && !cid && !syncedCids.length) {
|
||||
if (!fromRelay && !cid) {
|
||||
const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
|
||||
if (!followers) {
|
||||
winston.verbose(`[activitypub/inbox.announce] Rejecting ${object.id} via ${actor} due to no followers`);
|
||||
@@ -469,12 +471,6 @@ inbox.announce = async (req) => {
|
||||
({ tid } = assertion);
|
||||
await activitypub.notes.updateLocalRecipients(pid, { to, cc });
|
||||
await activitypub.notes.syncUserInboxes(tid);
|
||||
|
||||
if (syncedCids) {
|
||||
await Promise.all(syncedCids.map(async (cid) => {
|
||||
await topics.crossposts.add(tid, cid, 0);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (!cid) { // Topic events from actors followed by users only
|
||||
|
||||
@@ -104,7 +104,6 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
const hasTid = !!tid;
|
||||
|
||||
const cid = hasTid ? await topics.getTopicField(tid, 'cid') : options.cid || -1;
|
||||
let crosspostCid = false;
|
||||
|
||||
if (options.cid && cid === -1) {
|
||||
// Move topic if currently uncategorized
|
||||
@@ -156,10 +155,8 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
}
|
||||
|
||||
// Auto-categorization (takes place only if all other categorization efforts fail)
|
||||
crosspostCid = await assignCategory(mainPost);
|
||||
if (!options.cid) {
|
||||
options.cid = crosspostCid;
|
||||
crosspostCid = false;
|
||||
options.cid = await assignCategory(mainPost);
|
||||
}
|
||||
|
||||
// mainPid ok to leave as-is
|
||||
@@ -268,10 +265,6 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
|
||||
await Notes.syncUserInboxes(tid, uid);
|
||||
|
||||
if (crosspostCid) {
|
||||
await topics.crossposts.add(tid, crosspostCid, 0);
|
||||
}
|
||||
|
||||
if (!hasTid && uid && options.cid) {
|
||||
// New topic, have category announce it
|
||||
await activitypub.out.announce.topic(tid);
|
||||
|
||||
@@ -9,7 +9,6 @@ const messaging = require('../messaging');
|
||||
const privileges = require('../privileges');
|
||||
const meta = require('../meta');
|
||||
const plugins = require('../plugins');
|
||||
const utils = require('../utils');
|
||||
|
||||
const controllersHelpers = require('../controllers/helpers');
|
||||
|
||||
@@ -30,12 +29,9 @@ searchApi.categories = async (caller, data) => {
|
||||
({ cids, matchedCids } = await findMatchedCids(caller.uid, data));
|
||||
} else {
|
||||
cids = await loadCids(caller.uid, data.parentCid);
|
||||
if (!data.hideUncategorized && meta.config.activitypubEnabled) {
|
||||
if (meta.config.activitypubEnabled) {
|
||||
cids.unshift(-1);
|
||||
}
|
||||
if (data.localOnly) {
|
||||
cids = cids.filter(cid => utils.isNumber(cid));
|
||||
}
|
||||
}
|
||||
|
||||
const visibleCategories = await controllersHelpers.getVisibleCategories({
|
||||
@@ -70,7 +66,6 @@ async function findMatchedCids(uid, data) {
|
||||
query: data.search,
|
||||
qs: data.query,
|
||||
paginate: false,
|
||||
localOnly: data.localOnly,
|
||||
});
|
||||
|
||||
let matchedCids = result.categories.map(c => c.cid);
|
||||
|
||||
@@ -6,14 +6,12 @@ const privileges = require('../privileges');
|
||||
const activitypub = require('../activitypub');
|
||||
const plugins = require('../plugins');
|
||||
const db = require('../database');
|
||||
const utils = require('../utils');
|
||||
|
||||
module.exports = function (Categories) {
|
||||
Categories.search = async function (data) {
|
||||
const query = data.query || '';
|
||||
const page = data.page || 1;
|
||||
const uid = data.uid || 0;
|
||||
const localOnly = data.localOnly || false;
|
||||
const paginate = data.hasOwnProperty('paginate') ? data.paginate : true;
|
||||
|
||||
const startTime = process.hrtime();
|
||||
@@ -23,9 +21,6 @@ module.exports = function (Categories) {
|
||||
}
|
||||
|
||||
let cids = await findCids(query, data.hardCap);
|
||||
if (localOnly) {
|
||||
cids = cids.filter(cid => utils.isNumber(cid));
|
||||
}
|
||||
|
||||
const result = await plugins.hooks.fire('filter:categories.search', {
|
||||
data: data,
|
||||
|
||||
@@ -123,9 +123,8 @@ topicsController.get = async function getTopic(req, res, next) {
|
||||
p => parseInt(p.index, 10) === parseInt(Math.max(0, postIndex - 1), 10)
|
||||
);
|
||||
|
||||
const [author, crossposts] = await Promise.all([
|
||||
const [author] = await Promise.all([
|
||||
user.getUserFields(topicData.uid, ['username', 'userslug']),
|
||||
topics.crossposts.get(topicData.tid),
|
||||
buildBreadcrumbs(topicData),
|
||||
addOldCategory(topicData, userPrivileges),
|
||||
addTags(topicData, req, res, currentPage, postAtIndex),
|
||||
@@ -135,7 +134,6 @@ topicsController.get = async function getTopic(req, res, next) {
|
||||
]);
|
||||
|
||||
topicData.author = author;
|
||||
topicData.crossposts = crossposts;
|
||||
topicData.pagination = pagination.create(currentPage, pageCount, req.query);
|
||||
topicData.pagination.rel.forEach((rel) => {
|
||||
rel.href = `${url}/topic/${topicData.slug}${rel.href}`;
|
||||
|
||||
@@ -213,22 +213,3 @@ Topics.move = async (req, res) => {
|
||||
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Topics.getCrossposts = async (req, res) => {
|
||||
const crossposts = await topics.crossposts.get(req.params.tid);
|
||||
helpers.formatApiResponse(200, res, { crossposts });
|
||||
};
|
||||
|
||||
Topics.crosspost = async (req, res) => {
|
||||
const { cid } = req.body;
|
||||
const crossposts = await topics.crossposts.add(req.params.tid, cid, req.uid);
|
||||
|
||||
helpers.formatApiResponse(200, res, { crossposts });
|
||||
};
|
||||
|
||||
Topics.uncrosspost = async (req, res) => {
|
||||
const { cid } = req.body;
|
||||
const crossposts = await topics.crossposts.remove(req.params.tid, cid, req.uid);
|
||||
|
||||
helpers.formatApiResponse(200, res, { crossposts });
|
||||
};
|
||||
|
||||
@@ -54,9 +54,5 @@ module.exports = function () {
|
||||
|
||||
setupApiRoute(router, 'put', '/:tid/move', [...middlewares, middleware.assert.topic], controllers.write.topics.move);
|
||||
|
||||
setupApiRoute(router, 'get', '/:tid/crossposts', [...middlewares, middleware.assert.topic], controllers.write.topics.getCrossposts);
|
||||
setupApiRoute(router, 'post', '/:tid/crossposts', [...middlewares, middleware.assert.topic], controllers.write.topics.crosspost);
|
||||
setupApiRoute(router, 'delete', '/:tid/crossposts', [...middlewares, middleware.assert.topic], controllers.write.topics.uncrosspost);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../database');
|
||||
const topics = require('.');
|
||||
const user = require('../user');
|
||||
const categories = require('../categories');
|
||||
const posts = require('../posts');
|
||||
const activitypub = require('../activitypub');
|
||||
const utils = require('../utils');
|
||||
|
||||
const Crossposts = module.exports;
|
||||
|
||||
Crossposts.get = async function (tid) {
|
||||
const crosspostIds = await db.getSortedSetMembers(`tid:${tid}:crossposts`);
|
||||
let crossposts = await db.getObjects(crosspostIds.map(id => `crosspost:${id}`));
|
||||
const cids = crossposts.reduce((cids, crossposts) => {
|
||||
cids.add(crossposts.cid);
|
||||
return cids;
|
||||
}, new Set());
|
||||
let categoriesData = await categories.getCategoriesFields(
|
||||
Array.from(cids), ['cid', 'name', 'icon', 'bgColor', 'color', 'slug']
|
||||
);
|
||||
categoriesData = categoriesData.reduce((map, category) => {
|
||||
map.set(parseInt(category.cid, 10), category);
|
||||
return map;
|
||||
}, new Map());
|
||||
crossposts = crossposts.map((crosspost, idx) => {
|
||||
crosspost.id = crosspostIds[idx];
|
||||
crosspost.category = categoriesData.get(parseInt(crosspost.cid, 10));
|
||||
return crosspost;
|
||||
});
|
||||
|
||||
return crossposts;
|
||||
};
|
||||
|
||||
Crossposts.add = async function (tid, cid, uid) {
|
||||
// Target cid must exist
|
||||
if (!utils.isNumber(cid)) {
|
||||
await activitypub.actors.assert(cid);
|
||||
}
|
||||
const exists = await categories.exists(cid);
|
||||
if (!exists) {
|
||||
throw new Error('[[error:invalid-cid]]');
|
||||
}
|
||||
|
||||
const crossposts = await Crossposts.get(tid);
|
||||
const crosspostedCids = crossposts.map(crosspost => String(crosspost.cid));
|
||||
const now = Date.now();
|
||||
const crosspostId = utils.generateUUID();
|
||||
if (!crosspostedCids.includes(String(cid))) {
|
||||
const [topicData, pids] = await Promise.all([
|
||||
topics.getTopicFields(tid, ['uid', 'cid', 'timestamp']),
|
||||
topics.getPids(tid),
|
||||
]);
|
||||
let pidTimestamps = await posts.getPostsFields(pids, ['timestamp']);
|
||||
pidTimestamps = pidTimestamps.map(({ timestamp }) => timestamp);
|
||||
|
||||
if (cid === topicData.cid) {
|
||||
throw new Error('[[error:invalid-cid]]');
|
||||
}
|
||||
const zsets = [
|
||||
`cid:${topicData.cid}:tids`,
|
||||
`cid:${topicData.cid}:tids:create`,
|
||||
`cid:${topicData.cid}:tids:lastposttime`,
|
||||
`cid:${topicData.cid}:uid:${topicData.uid}:tids`,
|
||||
`cid:${topicData.cid}:tids:votes`,
|
||||
`cid:${topicData.cid}:tids:posts`,
|
||||
`cid:${topicData.cid}:tids:views`,
|
||||
];
|
||||
const scores = await db.sortedSetsScore(zsets, tid);
|
||||
const bulkAdd = zsets.map((zset, idx) => {
|
||||
return [zset.replace(`cid:${topicData.cid}`, `cid:${cid}`), scores[idx], tid];
|
||||
});
|
||||
await Promise.all([
|
||||
db.sortedSetAddBulk(bulkAdd),
|
||||
db.sortedSetAdd(`cid:${cid}:pids`, pidTimestamps, pids),
|
||||
db.setObject(`crosspost:${crosspostId}`, { uid, tid, cid, timestamp: now }),
|
||||
db.sortedSetAdd(`tid:${tid}:crossposts`, now, crosspostId),
|
||||
uid > 0 ? db.sortedSetAdd(`uid:${uid}:crossposts`, now, crosspostId) : false,
|
||||
]);
|
||||
await categories.onTopicsMoved([cid]);
|
||||
} else {
|
||||
throw new Error('[[error:topic-already-crossposted]]');
|
||||
}
|
||||
|
||||
return [...crossposts, { id: crosspostId, uid, tid, cid, timestamp: now }];
|
||||
};
|
||||
|
||||
Crossposts.remove = async function (tid, cid, uid) {
|
||||
let crossposts = await Crossposts.get(tid);
|
||||
const isPrivileged = await user.isAdminOrGlobalMod(uid);
|
||||
const isMod = await user.isModerator(uid, cid);
|
||||
const crosspostId = crossposts.reduce((id, { id: _id, cid: _cid, uid: _uid }) => {
|
||||
if (String(cid) === String(_cid) && (isPrivileged || isMod || String(uid) === String(_uid))) {
|
||||
id = _id;
|
||||
}
|
||||
|
||||
return id;
|
||||
}, null);
|
||||
if (!crosspostId) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const [author, pids] = await Promise.all([
|
||||
topics.getTopicField(tid, 'uid'),
|
||||
topics.getPids(tid),
|
||||
]);
|
||||
let bulkRemove = [
|
||||
`cid:${cid}:tids`,
|
||||
`cid:${cid}:tids:create`,
|
||||
`cid:${cid}:tids:lastposttime`,
|
||||
`cid:${cid}:uid:${author}:tids`,
|
||||
`cid:${cid}:tids:votes`,
|
||||
`cid:${cid}:tids:posts`,
|
||||
`cid:${cid}:tids:views`,
|
||||
];
|
||||
bulkRemove = bulkRemove.map(zset => [zset, tid]);
|
||||
bulkRemove.push([`cid:${cid}:pids`, pids]);
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetRemoveBulk(bulkRemove),
|
||||
db.delete(`crosspost:${crosspostId}`),
|
||||
db.sortedSetRemove(`tid:${tid}:crossposts`, crosspostId),
|
||||
uid > 0 ? db.sortedSetRemove(`uid:${uid}:crossposts`, crosspostId) : false,
|
||||
]);
|
||||
await categories.onTopicsMoved([cid]);
|
||||
|
||||
crossposts = await Crossposts.get(tid);
|
||||
return crossposts;
|
||||
};
|
||||
|
||||
Crossposts.removeAll = async function (tid) {
|
||||
const crosspostIds = await db.getSortedSetMembers(`tid:${tid}:crossposts`);
|
||||
const crossposts = await db.getObjects(crosspostIds.map(id => `crosspost:${id}`));
|
||||
await Promise.all(crossposts.map(async ({ tid, cid, uid }) => {
|
||||
return Crossposts.remove(tid, cid, uid);
|
||||
}));
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -102,7 +102,6 @@ module.exports = function (Topics) {
|
||||
Topics.deleteTopicTags(tid),
|
||||
Topics.events.purge(tid),
|
||||
Topics.thumbs.deleteAll(tid),
|
||||
Topics.crossposts.removeAll(tid),
|
||||
reduceCounters(tid),
|
||||
]);
|
||||
plugins.hooks.fire('action:topic.purge', { topic: deletedTopic, uid: uid });
|
||||
|
||||
@@ -35,7 +35,6 @@ Topics.thumbs = require('./thumbs');
|
||||
require('./bookmarks')(Topics);
|
||||
require('./merge')(Topics);
|
||||
Topics.events = require('./events');
|
||||
Topics.crossposts = require('./crossposts');
|
||||
|
||||
Topics.exists = async function (tids) {
|
||||
return await db.exists(
|
||||
|
||||
@@ -5,11 +5,9 @@ const _ = require('lodash');
|
||||
const db = require('../database');
|
||||
const topics = require('.');
|
||||
const categories = require('../categories');
|
||||
const posts = require('../posts');
|
||||
const user = require('../user');
|
||||
const plugins = require('../plugins');
|
||||
const privileges = require('../privileges');
|
||||
const activitypub = require('../activitypub');
|
||||
const utils = require('../utils');
|
||||
|
||||
|
||||
@@ -235,7 +233,7 @@ module.exports = function (Topics) {
|
||||
};
|
||||
|
||||
topicTools.move = async function (tid, data) {
|
||||
const cid = parseInt(data.cid, 10);
|
||||
const cid = utils.isNumber(data.cid) ? parseInt(data.cid, 10) : data.cid;
|
||||
const topicData = await Topics.getTopicData(tid);
|
||||
if (!topicData) {
|
||||
throw new Error('[[error:no-topic]]');
|
||||
@@ -243,10 +241,6 @@ module.exports = function (Topics) {
|
||||
if (cid === topicData.cid) {
|
||||
throw new Error('[[error:cant-move-topic-to-same-category]]');
|
||||
}
|
||||
if (!utils.isNumber(cid) || !utils.isNumber(topicData.cid)) {
|
||||
throw new Error('[[error:cant-move-topic-to-from-remote-categories]]');
|
||||
}
|
||||
|
||||
const tags = await Topics.getTopicTags(tid);
|
||||
await db.sortedSetsRemove([
|
||||
`cid:${topicData.cid}:tids`,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<div class="card tool-modal shadow">
|
||||
<h5 class="card-header">
|
||||
[[topic:crosspost-topic]]
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
[[topic:crossposts.instructions]]
|
||||
</p>
|
||||
<!-- IMPORT partials/category/filter-dropdown-right.tpl -->
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<i class="fa me-2" id="crosspost_topic_spinner"></i>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="crosspost_topic_cancel">[[global:buttons.close]]</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="crosspost_thread_commit" disabled>[[topic:confirm-crosspost]]</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="mb-3">
|
||||
{{{ if crossposts.length }}}
|
||||
<p>[[topic:crossposts.listing]]</p>
|
||||
{{{ each crossposts }}}
|
||||
{buildCategoryLabel(./category, "a", "border")}
|
||||
{{{ end }}}
|
||||
{{{ else }}}
|
||||
<p>[[topic:crossposts.none]]</p>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
@@ -16,14 +16,12 @@
|
||||
</div>
|
||||
|
||||
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu ghost-scrollbar" role="menu">
|
||||
{{{ if !hideAll }}}
|
||||
<li role="presentation" class="category" data-cid="all">
|
||||
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" role="menuitem" href="{{{ if allCategoriesUrl }}}{config.relative_path}/{allCategoriesUrl}{{{ else }}}#{{{ end }}}">
|
||||
<div class="flex-grow-1">[[unread:all-categories]]</div>
|
||||
<i component="category/select/icon" class="flex-shrink-0 fa fa-fw fa-check {{{if selectedCategory}}}invisible{{{end}}}"></i>
|
||||
</a>
|
||||
</li>
|
||||
{{{ end }}}
|
||||
{{{each categoryItems}}}
|
||||
<li role="presentation" class="category {{{ if ./disabledClass }}}disabled{{{ end }}}" data-cid="{./cid}" data-parent-cid="{./parentCid}" data-name="{./name}">
|
||||
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2 {{{ if ./disabledClass }}}disabled{{{ end }}}" role="menuitem" href="#">
|
||||
|
||||
@@ -15,11 +15,9 @@
|
||||
<a component="topic/unpin" href="#" class="dropdown-item rounded-1 d-flex align-items-center gap-2 {{{ if !pinned }}}hidden{{{ end }}}" role="menuitem"><i class="fa fa-fw fa-thumb-tack fa-rotate-90 text-secondary"></i> [[topic:thread-tools.unpin]]</a>
|
||||
</li>
|
||||
|
||||
{{{ if isNumber(cid) }}}
|
||||
<li>
|
||||
<a component="topic/move" href="#" class="dropdown-item rounded-1 d-flex align-items-center gap-2" role="menuitem"><i class="fa fa-fw fa-arrows text-secondary"></i> [[topic:thread-tools.move]]</a>
|
||||
</li>
|
||||
{{{ end }}}
|
||||
|
||||
<li>
|
||||
<a component="topic/merge" href="#" class="dropdown-item rounded-1 d-flex align-items-center gap-2" role="menuitem"><i class="fa fa-fw fa-code-fork text-secondary"></i> [[topic:thread-tools.merge]]</a>
|
||||
|
||||
@@ -76,6 +76,105 @@ describe('Actor asserton', () => {
|
||||
assert.strictEqual(assertion.length, 1);
|
||||
assert.strictEqual(assertion[0].cid, actor.id);
|
||||
});
|
||||
|
||||
describe('remote user to remote category migration', () => {
|
||||
it('should not migrate a user to a category if .assert is called', async () => {
|
||||
// ... because the user isn't due for an update and so is filtered out during qualification
|
||||
const { id } = helpers.mocks.person();
|
||||
await activitypub.actors.assert([id]);
|
||||
|
||||
const { actor } = helpers.mocks.group({ id });
|
||||
const assertion = await activitypub.actors.assertGroup([id]);
|
||||
|
||||
assert(assertion.length, 0);
|
||||
|
||||
const exists = await user.exists(id);
|
||||
assert.strictEqual(exists, false);
|
||||
});
|
||||
|
||||
it('should migrate a user to a category if on re-assertion it identifies as an as:Group', async () => {
|
||||
// This is to handle previous behaviour that saved all as:Group actors as NodeBB users.
|
||||
const { id } = helpers.mocks.person();
|
||||
await activitypub.actors.assert([id]);
|
||||
|
||||
helpers.mocks.group({ id });
|
||||
const assertion = await activitypub.actors.assertGroup([id]);
|
||||
|
||||
assert(assertion && Array.isArray(assertion) && assertion.length === 1);
|
||||
|
||||
const exists = await user.exists(id);
|
||||
assert.strictEqual(exists, false);
|
||||
});
|
||||
|
||||
it('should migrate any shares by that user, into topics in the category', async () => {
|
||||
const { id } = helpers.mocks.person();
|
||||
await activitypub.actors.assert([id]);
|
||||
|
||||
// Two shares
|
||||
for (let x = 0; x < 2; x++) {
|
||||
const { id: pid } = helpers.mocks.note();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { tid } = await activitypub.notes.assert(0, pid, { skipChecks: 1 });
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db.sortedSetAdd(`uid:${id}:shares`, Date.now(), tid);
|
||||
}
|
||||
|
||||
helpers.mocks.group({ id });
|
||||
await activitypub.actors.assertGroup([id]);
|
||||
|
||||
const { topic_count, post_count } = await categories.getCategoryData(id);
|
||||
assert.strictEqual(topic_count, 2);
|
||||
assert.strictEqual(post_count, 2);
|
||||
});
|
||||
|
||||
it('should not migrate shares by that user that already belong to a local category', async () => {
|
||||
const { id } = helpers.mocks.person();
|
||||
await activitypub.actors.assert([id]);
|
||||
|
||||
const { cid } = await categories.create({ name: utils.generateUUID() });
|
||||
|
||||
// Two shares, one moved to local cid
|
||||
for (let x = 0; x < 2; x++) {
|
||||
const { id: pid } = helpers.mocks.note();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { tid } = await activitypub.notes.assert(0, pid, { skipChecks: 1 });
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db.sortedSetAdd(`uid:${id}:shares`, Date.now(), tid);
|
||||
|
||||
if (!x) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await topics.tools.move(tid, {
|
||||
cid,
|
||||
uid: 'system',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
helpers.mocks.group({ id });
|
||||
await activitypub.actors.assertGroup([id]);
|
||||
|
||||
const { topic_count, post_count } = await categories.getCategoryData(id);
|
||||
assert.strictEqual(topic_count, 1);
|
||||
assert.strictEqual(post_count, 1);
|
||||
});
|
||||
|
||||
it('should migrate any local followers into category watches', async () => {
|
||||
const { id } = helpers.mocks.person();
|
||||
await activitypub.actors.assert([id]);
|
||||
|
||||
const followerUid = await user.create({ username: utils.generateUUID() });
|
||||
await Promise.all([
|
||||
db.sortedSetAdd(`followingRemote:${followerUid}`, Date.now(), id),
|
||||
db.sortedSetAdd(`followersRemote:${id}`, Date.now(), followerUid),
|
||||
]);
|
||||
|
||||
helpers.mocks.group({ id });
|
||||
await activitypub.actors.assertGroup([id]);
|
||||
|
||||
const states = await categories.getWatchState([id], followerUid);
|
||||
assert.strictEqual(states[0], categories.watchStates.tracking);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('less happy paths', () => {
|
||||
|
||||
@@ -164,7 +164,9 @@ describe('FEPs', () => {
|
||||
});
|
||||
pid = id;
|
||||
({ activity } = await helpers.mocks.create(note));
|
||||
console.log('before inbox create', activitypub._sent);
|
||||
await activitypub.inbox.create({ body: activity });
|
||||
console.log('after inbox create', activitypub._sent);
|
||||
const activities = Array.from(activitypub._sent);
|
||||
|
||||
const test1 = activities.some((activity) => {
|
||||
|
||||
@@ -153,32 +153,6 @@ Helpers.mocks.like = (override = {}) => {
|
||||
return { activity };
|
||||
};
|
||||
|
||||
Helpers.mocks.follow = (override = {}) => {
|
||||
let actor = override.actor;
|
||||
let object = override.object;
|
||||
if (!actor) {
|
||||
({ id: actor } = Helpers.mocks.person());
|
||||
}
|
||||
if (!object) {
|
||||
({ id: object } = Helpers.mocks.person());
|
||||
}
|
||||
delete override.actor;
|
||||
delete override.object;
|
||||
|
||||
const activity = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: `${Helpers.mocks._baseUrl}/follow/${encodeURIComponent(object.id || object)}`,
|
||||
type: 'Follow',
|
||||
to: [activitypub._constants.publicAddress],
|
||||
cc: [`${actor}/followers`],
|
||||
actor,
|
||||
object,
|
||||
...override,
|
||||
};
|
||||
|
||||
return { activity };
|
||||
};
|
||||
|
||||
Helpers.mocks.announce = (override = {}) => {
|
||||
let actor = override.actor;
|
||||
let object = override.object;
|
||||
|
||||
@@ -659,15 +659,10 @@ describe('API', async () => {
|
||||
case 'boolean':
|
||||
assert.strictEqual(typeof response[prop], 'boolean', `"${prop}" was expected to be a boolean, but was ${typeof response[prop]} instead (path: ${method} ${path}, context: ${context})`);
|
||||
break;
|
||||
case 'object': {
|
||||
let valid = ['object'];
|
||||
if (schema[prop].additionalProperties && schema[prop].additionalProperties.oneOf) {
|
||||
valid = schema[prop].additionalProperties.oneOf.map(({ type }) => type);
|
||||
}
|
||||
assert(valid.includes(typeof response[prop]), `"${prop}" was expected to be an object, but was ${typeof response[prop]} instead (path: ${method} ${path}, context: ${context})`);
|
||||
case 'object':
|
||||
assert.strictEqual(typeof response[prop], 'object', `"${prop}" was expected to be an object, but was ${typeof response[prop]} instead (path: ${method} ${path}, context: ${context})`);
|
||||
compare(schema[prop], response[prop], method, path, context ? [context, prop].join('.') : prop);
|
||||
break;
|
||||
}
|
||||
case 'array':
|
||||
assert.strictEqual(Array.isArray(response[prop]), true, `"${prop}" was expected to be an array, but was ${typeof response[prop]} instead (path: ${method} ${path}, context: ${context})`);
|
||||
|
||||
|
||||
@@ -1,559 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const nconf = require('nconf');
|
||||
|
||||
const db = require('../mocks/databasemock');
|
||||
|
||||
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 posts = require('../../src/posts');
|
||||
const privileges = require('../../src/privileges');
|
||||
const activitypub = require('../../src/activitypub');
|
||||
const utils = require('../../src/utils');
|
||||
|
||||
describe('Crossposting (& related logic)', () => {
|
||||
before(async () => {
|
||||
meta.config.activitypubEnabled = 1;
|
||||
await install.giveWorldPrivileges();
|
||||
});
|
||||
|
||||
describe('topic already in multiple categories', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
|
||||
// Add topic to another category's zset
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
await db.sortedSetAdd(`cid:${crosspostCategory.cid}:tids`, topicData.timestamp, tid);
|
||||
});
|
||||
|
||||
it('should contain the topic in both categories when requested', async () => {
|
||||
const tids1 = await categories.getTopicIds({
|
||||
uid,
|
||||
cid: cid1,
|
||||
start: 0,
|
||||
stop: 1,
|
||||
});
|
||||
|
||||
const tids2 = await categories.getTopicIds({
|
||||
uid,
|
||||
cid: cid2,
|
||||
start: 0,
|
||||
stop: 1,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(tids1, tids2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('crosspost', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
});
|
||||
|
||||
it('should successfully crosspost to another cid', async () => {
|
||||
const crossposts = await topics.crossposts.add(tid, cid2, uid);
|
||||
|
||||
assert(Array.isArray(crossposts));
|
||||
assert.strictEqual(crossposts.length, 1);
|
||||
assert.partialDeepStrictEqual(crossposts[0], {
|
||||
uid,
|
||||
tid,
|
||||
cid: cid2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the tid in both categories when requested', async () => {
|
||||
const tids1 = await categories.getTopicIds({
|
||||
uid,
|
||||
cid: cid1,
|
||||
start: 0,
|
||||
stop: 1,
|
||||
});
|
||||
|
||||
const tids2 = await categories.getTopicIds({
|
||||
uid,
|
||||
cid: cid2,
|
||||
start: 0,
|
||||
stop: 1,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(tids1, tids2);
|
||||
});
|
||||
|
||||
it('should throw on cross-posting again when already cross-posted', async () => {
|
||||
await assert.rejects(
|
||||
topics.crossposts.add(tid, cid2, uid),
|
||||
{ message: '[[error:topic-already-crossposted]]' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uncrosspost', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
|
||||
await topics.crossposts.add(tid, cid2, uid);
|
||||
});
|
||||
|
||||
it('should not let another user uncrosspost', async () => {
|
||||
const uid2 = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
assert.rejects(
|
||||
topics.crossposts.remove(tid, cid2, uid2),
|
||||
'[[error:invalid-data]]',
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully uncrosspost from a cid', async () => {
|
||||
const crossposts = await topics.crossposts.remove(tid, cid2, uid);
|
||||
|
||||
assert(Array.isArray(crossposts));
|
||||
assert.strictEqual(crossposts.length, 0);
|
||||
});
|
||||
|
||||
it('should not contain the topic in the category the topic was uncrossposted from', async () => {
|
||||
const tids = await categories.getTopicIds({
|
||||
uid,
|
||||
cid: cid2,
|
||||
start: 0,
|
||||
stop: 1,
|
||||
});
|
||||
|
||||
assert(!tids.includes(tid));
|
||||
});
|
||||
|
||||
it('should throw on uncrossposting if already uncrossposted', async () => {
|
||||
assert.rejects(
|
||||
topics.crossposts.remove(tid, cid2, uid),
|
||||
'[[error:invalid-data]]',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uncrosspost (as administrator)', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
let privUid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
privUid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
await groups.join('administrators', privUid);
|
||||
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
|
||||
await topics.crossposts.add(tid, cid2, uid);
|
||||
});
|
||||
|
||||
it('should successfully uncrosspost from a cid', async () => {
|
||||
const crossposts = await topics.crossposts.remove(tid, cid2, privUid);
|
||||
|
||||
assert(Array.isArray(crossposts));
|
||||
assert.strictEqual(crossposts.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uncrosspost (as global moderator)', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
let privUid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
privUid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
await groups.join('Global Moderators', privUid);
|
||||
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
|
||||
await topics.crossposts.add(tid, cid2, uid);
|
||||
});
|
||||
|
||||
it('should successfully uncrosspost from a cid', async () => {
|
||||
const crossposts = await topics.crossposts.remove(tid, cid2, privUid);
|
||||
|
||||
assert(Array.isArray(crossposts));
|
||||
assert.strictEqual(crossposts.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uncrosspost (as category moderator)', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
let privUid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
privUid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
|
||||
await topics.crossposts.add(tid, cid2, uid);
|
||||
});
|
||||
|
||||
it('should fail to uncrosspost if not mod of passed-in category', async () => {
|
||||
await privileges.categories.give(['moderate'], cid1, [privUid]);
|
||||
assert.rejects(
|
||||
topics.crossposts.remove(tid, cid2, privUid),
|
||||
'[[error:invalid-data]]',
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully uncrosspost from a cid if proper mod', async () => {
|
||||
await privileges.categories.give(['moderate'], cid2, [privUid]);
|
||||
const crossposts = await topics.crossposts.remove(tid, cid2, privUid);
|
||||
|
||||
assert(Array.isArray(crossposts));
|
||||
assert.strictEqual(crossposts.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Deletion', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
|
||||
await topics.crossposts.add(tid, cid2, uid);
|
||||
await topics.delete(tid, uid);
|
||||
});
|
||||
|
||||
it('should maintain crossposts when topic is deleted', async () => {
|
||||
const crossposts = await topics.crossposts.get(tid);
|
||||
assert(Array.isArray(crossposts));
|
||||
assert.strictEqual(crossposts.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Purging', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
|
||||
await topics.crossposts.add(tid, cid2, uid);
|
||||
await topics.purge(tid, uid);
|
||||
});
|
||||
|
||||
it('should remove crossposts when topic is purged', async () => {
|
||||
const crossposts = await topics.crossposts.get(tid);
|
||||
assert(Array.isArray(crossposts));
|
||||
assert.strictEqual(crossposts.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('category sync; integration with', () => {
|
||||
let cid;
|
||||
let remoteCid;
|
||||
let pid;
|
||||
let post;
|
||||
|
||||
const helpers = require('../activitypub/helpers');
|
||||
|
||||
before(async () => {
|
||||
({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
({ id: remoteCid } = helpers.mocks.group());
|
||||
({ id: pid, note: post } = helpers.mocks.note({
|
||||
audience: [remoteCid],
|
||||
}));
|
||||
|
||||
// Mock a group follow/accept
|
||||
const timestamp = Date.now();
|
||||
console.log('saving', remoteCid);
|
||||
await Promise.all([
|
||||
db.sortedSetAdd(`cid:${cid}:following`, timestamp, remoteCid),
|
||||
db.sortedSetAdd(`followersRemote:${remoteCid}`, timestamp, `cid|${cid}`),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should automatically cross-post the topic when the remote category announces', async () => {
|
||||
const { activity: body } = helpers.mocks.announce({
|
||||
actor: remoteCid,
|
||||
object: post,
|
||||
});
|
||||
|
||||
await activitypub.inbox.announce({ body });
|
||||
|
||||
const tid = await posts.getPostField(pid, 'tid');
|
||||
const crossposts = await topics.crossposts.get(tid);
|
||||
|
||||
assert.strictEqual(crossposts.length, 1);
|
||||
assert.partialDeepStrictEqual(crossposts[0], {
|
||||
uid: '0',
|
||||
tid,
|
||||
cid: String(cid),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto-categorization; integration with', () => {
|
||||
let cid;
|
||||
let remoteCid;
|
||||
let pid;
|
||||
let post;
|
||||
|
||||
const helpers = require('../activitypub/helpers');
|
||||
|
||||
before(async () => {
|
||||
const preferredUsername = utils.generateUUID().slice(0, 8);
|
||||
({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
({ id: remoteCid } = helpers.mocks.group({
|
||||
preferredUsername,
|
||||
}));
|
||||
({ id: pid, note: post } = helpers.mocks.note({
|
||||
audience: [remoteCid],
|
||||
tag: [
|
||||
{
|
||||
type: 'Hashtag',
|
||||
name: `#${preferredUsername}`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
await activitypub.rules.add('hashtag', preferredUsername, cid);
|
||||
});
|
||||
|
||||
it('note assertion should automatically cross-post', async () => {
|
||||
await activitypub.notes.assert(0, pid, { skipChecks: true });
|
||||
|
||||
const tid = await posts.getPostField(pid, 'tid');
|
||||
const crossposts = await topics.crossposts.get(tid);
|
||||
assert.strictEqual(crossposts.length, 1);
|
||||
assert.partialDeepStrictEqual(crossposts[0], {
|
||||
uid: '0',
|
||||
tid,
|
||||
cid: String(cid),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActivityPub effects (or lack thereof)', () => {
|
||||
describe('local canonical category', () => {
|
||||
let tid;
|
||||
let cid1;
|
||||
let cid2;
|
||||
let uid;
|
||||
let pid;
|
||||
|
||||
const helpers = require('../activitypub/helpers');
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
||||
cid2 = crosspostCategory.cid;
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
pid = topicData.mainPid,
|
||||
|
||||
// Add some remote followers
|
||||
await Promise.all([cid1, cid2].map(async (cid) => {
|
||||
const {activity} = helpers.mocks.follow({
|
||||
object: {
|
||||
id: `${nconf.get('url')}/category/${cid}`,
|
||||
},
|
||||
});
|
||||
await activitypub.inbox.follow({
|
||||
body: activity,
|
||||
});
|
||||
}));
|
||||
|
||||
activitypub._sent.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
activitypub._sent.clear();
|
||||
});
|
||||
|
||||
it('should not federate out any events on crosspost', async () => {
|
||||
await topics.crossposts.add(tid, cid2, uid);
|
||||
assert.strictEqual(activitypub._sent.size, 0);
|
||||
});
|
||||
|
||||
it('should not federate out anything on uncrosspost', async () => {
|
||||
await topics.crossposts.remove(tid, cid2, uid);
|
||||
assert.strictEqual(activitypub._sent.size, 0);
|
||||
});
|
||||
|
||||
it('should only federate an Announce on a remote reply from the canonical cid', async () => {
|
||||
const { note: object } = helpers.mocks.note({
|
||||
audience: `${nconf.get('url')}/category/${cid1}`,
|
||||
inReplyTo: `${nconf.get('url')}/post/${pid}`,
|
||||
});
|
||||
const { activity } = helpers.mocks.create(object);
|
||||
await activitypub.inbox.create({
|
||||
body: activity,
|
||||
});
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 1);
|
||||
assert.partialDeepStrictEqual(Array.from(activitypub._sent).pop()[1], {
|
||||
type: 'Announce',
|
||||
actor: `${nconf.get('url')}/category/${cid1}`,
|
||||
object: activity,
|
||||
});
|
||||
});
|
||||
|
||||
it('should only federate an Announce on a remote like from the canonical cid', async () => {
|
||||
const { activity: body } = helpers.mocks.like({
|
||||
object: {
|
||||
id: `${nconf.get('url')}/post/${pid}`,
|
||||
},
|
||||
});
|
||||
await activitypub.inbox.like({ body });
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 1);
|
||||
assert.partialDeepStrictEqual(Array.from(activitypub._sent).pop()[1], {
|
||||
type: 'Announce',
|
||||
actor: `${nconf.get('url')}/category/${cid1}`,
|
||||
object: body,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote canonical category', () => {
|
||||
let tid;
|
||||
let cid;
|
||||
let remoteCid;
|
||||
let uid;
|
||||
let pid;
|
||||
|
||||
const helpers = require('../activitypub/helpers');
|
||||
|
||||
before(async () => {
|
||||
({ id: remoteCid } = helpers.mocks.group());
|
||||
await activitypub.actors.assertGroup(remoteCid);
|
||||
({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
|
||||
({ id: pid } = helpers.mocks.note());
|
||||
await activitypub.notes.assert(0, pid, { skipChecks: 1, cid: remoteCid });
|
||||
|
||||
tid = await posts.getPostField(pid, 'tid');
|
||||
|
||||
await topics.crossposts.add(tid, cid, uid);
|
||||
});
|
||||
|
||||
it('should properly address the remote category when federating out a local reply', async () => {
|
||||
const postData = await topics.reply({
|
||||
uid,
|
||||
cid,
|
||||
tid,
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
const mocked = await activitypub.mocks.notes.public(postData);
|
||||
assert(mocked.to.includes(remoteCid));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const db = require('../mocks/databasemock');
|
||||
|
||||
const user = require('../../src/user');
|
||||
const categories = require('../../src/categories');
|
||||
const topics = require('../../src/topics');
|
||||
const utils = require('../../src/utils');
|
||||
|
||||
describe('Topic tools', () => {
|
||||
describe('Topic moving', () => {
|
||||
let cid1;
|
||||
let cid2;
|
||||
let tid;
|
||||
let uid;
|
||||
|
||||
before(async () => {
|
||||
({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
({ cid: cid2 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: cid1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid = topicData.tid;
|
||||
});
|
||||
|
||||
it('should not error when moving a topic from one cid to another', async () => {
|
||||
await topics.tools.move(tid, {
|
||||
cid: cid2,
|
||||
uid,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reflect the topic in the new category', async () => {
|
||||
const tids = await categories.getTopicIds({
|
||||
uid,
|
||||
cid: cid2,
|
||||
start: 0,
|
||||
stop: 1,
|
||||
});
|
||||
|
||||
assert(Array.isArray(tids));
|
||||
assert.deepStrictEqual(tids, [String(tid)]);
|
||||
});
|
||||
|
||||
it('should NOT reflect the topic in the old category', async () => {
|
||||
const tids = await categories.getTopicIds({
|
||||
uid,
|
||||
cid: cid1,
|
||||
start: 0,
|
||||
stop: 1,
|
||||
});
|
||||
|
||||
assert(Array.isArray(tids));
|
||||
assert.deepStrictEqual(tids, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with remote categories', () => {
|
||||
let remoteCid;
|
||||
let localCid;
|
||||
let tid1;
|
||||
let tid2;
|
||||
|
||||
before(async () => {
|
||||
const helpers = require('../activitypub/helpers');
|
||||
({ id: remoteCid } = helpers.mocks.group());
|
||||
({ cid: localCid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
|
||||
({ id: tid1 } = helpers.mocks.note({
|
||||
audience: remoteCid,
|
||||
}));
|
||||
const uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: localCid,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
tid2 = topicData.tid;
|
||||
});
|
||||
|
||||
it('should throw when attempting to move a topic from a remote category', async () => {
|
||||
assert.rejects(
|
||||
topics.tools.move(tid1, {
|
||||
cid: localCid,
|
||||
uid: 'system',
|
||||
}),
|
||||
'[[error:cant-move-topic-to-from-remote-categories]]'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when attempting to move a topic to a remote category', async () => {
|
||||
assert.rejects(
|
||||
topics.tools.move(tid2, {
|
||||
cid: remoteCid,
|
||||
uid: 'system',
|
||||
}),
|
||||
'[[error:cant-move-topic-to-from-remote-categories]]'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user