mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-19 11:01:11 +01:00
feat: API v3 calls to crosspost and uncrosspost a topic to and from a category
This commit is contained in:
@@ -170,6 +170,8 @@
|
|||||||
"topic-already-deleted": "This topic has already been deleted",
|
"topic-already-deleted": "This topic has already been deleted",
|
||||||
"topic-already-restored": "This topic has already been restored",
|
"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",
|
"cant-purge-main-post": "You can't purge the main post, please delete the topic instead",
|
||||||
|
|
||||||
"topic-thumbnails-are-disabled": "Topic thumbnails are disabled.",
|
"topic-thumbnails-are-disabled": "Topic thumbnails are disabled.",
|
||||||
|
|||||||
34
public/openapi/components/schemas/CrosspostObject.yaml
Normal file
34
public/openapi/components/schemas/CrosspostObject.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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,6 +168,8 @@ paths:
|
|||||||
$ref: 'write/topics/tid/bump.yaml'
|
$ref: 'write/topics/tid/bump.yaml'
|
||||||
/topics/{tid}/move:
|
/topics/{tid}/move:
|
||||||
$ref: 'write/topics/tid/move.yaml'
|
$ref: 'write/topics/tid/move.yaml'
|
||||||
|
/topics/{tid}/crossposts:
|
||||||
|
$ref: 'write/topics/tid/crossposts.yaml'
|
||||||
/tags/{tag}/follow:
|
/tags/{tag}/follow:
|
||||||
$ref: 'write/tags/tag/follow.yaml'
|
$ref: 'write/tags/tag/follow.yaml'
|
||||||
/posts/{pid}:
|
/posts/{pid}:
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ put:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
types:
|
types:
|
||||||
type: object
|
type: object
|
||||||
@@ -103,7 +102,6 @@ put:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
types:
|
types:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ get:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
isPrivate:
|
isPrivate:
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -65,7 +64,6 @@ get:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
types:
|
types:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ put:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
groups:
|
groups:
|
||||||
type: array
|
type: array
|
||||||
@@ -107,7 +106,6 @@ put:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
types:
|
types:
|
||||||
type: object
|
type: object
|
||||||
@@ -230,7 +228,6 @@ delete:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
groups:
|
groups:
|
||||||
type: array
|
type: array
|
||||||
@@ -244,7 +241,6 @@ delete:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
types:
|
types:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -71,5 +71,4 @@ get:
|
|||||||
privileges:
|
privileges:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: boolean
|
|
||||||
description: A set of privileges with either true or false
|
description: A set of privileges with either true or false
|
||||||
76
public/openapi/write/topics/tid/crossposts.yaml
Normal file
76
public/openapi/write/topics/tid/crossposts.yaml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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
|
||||||
@@ -213,3 +213,17 @@ Topics.move = async (req, res) => {
|
|||||||
|
|
||||||
helpers.formatApiResponse(200, res);
|
helpers.formatApiResponse(200, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Topics.crosspost = async (req, res) => {
|
||||||
|
const { cid } = req.body;
|
||||||
|
const crossposts = await topics.tools.crosspost(req.params.tid, cid, req.uid);
|
||||||
|
|
||||||
|
helpers.formatApiResponse(200, res, { crossposts });
|
||||||
|
};
|
||||||
|
|
||||||
|
Topics.uncrosspost = async (req, res) => {
|
||||||
|
const { cid } = req.body;
|
||||||
|
const crossposts = await topics.tools.uncrosspost(req.params.tid, cid, req.uid);
|
||||||
|
|
||||||
|
helpers.formatApiResponse(200, res, { crossposts });
|
||||||
|
};
|
||||||
|
|||||||
@@ -54,5 +54,8 @@ module.exports = function () {
|
|||||||
|
|
||||||
setupApiRoute(router, 'put', '/:tid/move', [...middlewares, middleware.assert.topic], controllers.write.topics.move);
|
setupApiRoute(router, 'put', '/:tid/move', [...middlewares, middleware.assert.topic], controllers.write.topics.move);
|
||||||
|
|
||||||
|
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;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ const _ = require('lodash');
|
|||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const topics = require('.');
|
const topics = require('.');
|
||||||
const categories = require('../categories');
|
const categories = require('../categories');
|
||||||
|
const posts = require('../posts');
|
||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
|
const activitypub = require('../activitypub');
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@@ -311,4 +313,109 @@ module.exports = function (Topics) {
|
|||||||
db.sortedSetAdd(set, timestamp, tid),
|
db.sortedSetAdd(set, timestamp, tid),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function getCrossposts(tid) {
|
||||||
|
const crosspostIds = await db.getSortedSetMembers(`tid:${tid}:crossposts`);
|
||||||
|
let crossposts = await db.getObjects(crosspostIds.map(id => `crosspost:${id}`));
|
||||||
|
crossposts = crossposts.map((crosspost, idx) => {
|
||||||
|
crosspost.id = crosspostIds[idx];
|
||||||
|
return crosspost;
|
||||||
|
});
|
||||||
|
|
||||||
|
return crossposts;
|
||||||
|
}
|
||||||
|
|
||||||
|
topicTools.crosspost = 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 getCrossposts(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),
|
||||||
|
db.sortedSetAdd(`uid:${uid}:crossposts`, now, crosspostId),
|
||||||
|
]);
|
||||||
|
await categories.onTopicsMoved([cid]);
|
||||||
|
} else {
|
||||||
|
throw new Error('[[error:topic-already-crossposted]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...crossposts, { id: crosspostId, uid, tid, cid, timestamp: now }];
|
||||||
|
};
|
||||||
|
|
||||||
|
topicTools.uncrosspost = async function (tid, cid, uid) {
|
||||||
|
let crossposts = await getCrossposts(tid);
|
||||||
|
const crosspostId = crossposts.reduce((id, { id: _id, cid: _cid, uid: _uid }) => {
|
||||||
|
if (String(cid) === String(_cid) && 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),
|
||||||
|
db.sortedSetRemove(`uid:${uid}:crossposts`, crosspostId),
|
||||||
|
]);
|
||||||
|
await categories.onTopicsMoved([cid]);
|
||||||
|
|
||||||
|
crossposts = await getCrossposts(tid);
|
||||||
|
return crossposts;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user