nodebb-plugin-moving-topics 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # NodeBB Plugin: Moving Topics for Owners
2
+
3
+ Allow topic owners to move their own topics between categories, without granting full moderation privileges.
4
+
5
+ ## Features
6
+
7
+ - Adds **Move** to the thread tools for the topic owner.
8
+ - Owners can move **only their own topics**.
9
+ - Moves are blocked for **locked** or **deleted** topics.
10
+ - Target categories are filtered by `topics:create` + `topics:read`.
11
+ - Admins/moderators retain existing move permissions.
12
+
13
+ ## Requirements
14
+
15
+ - NodeBB (compatible with current core API used by this plugin)
16
+
17
+ ## Installation
18
+
19
+ ### Via NodeBB CLI
20
+
21
+ ```powershell
22
+ cd /path/to/nodebb
23
+ ./nodebb install nodebb-plugin-moving-topics
24
+ ```
25
+
26
+ ### Manual
27
+
28
+ ```powershell
29
+ cd /path/to/nodebb
30
+ npm install nodebb-plugin-moving-topics
31
+ ```
32
+
33
+ Then rebuild and restart:
34
+
35
+ ```powershell
36
+ ./nodebb build
37
+ ./nodebb restart
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ No settings are required. The plugin respects existing category permissions.
43
+
44
+ ## Usage
45
+
46
+ For topic owners, the **Move** option appears in the thread tools menu. Choose a destination category and confirm.
47
+
48
+ ## License
49
+
50
+ MIT
package/library.js ADDED
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const plugin = {};
4
+
5
+ let patchedMove = false;
6
+ let originalMove;
7
+
8
+ function getCore(path) {
9
+ return require.main.require(path);
10
+ }
11
+
12
+ plugin.init = async function () {
13
+ if (patchedMove) {
14
+ return;
15
+ }
16
+ patchedMove = true;
17
+
18
+ const topicsApi = getCore('./src/api/topics');
19
+ const privileges = getCore('./src/privileges');
20
+ const topics = getCore('./src/topics');
21
+ const categories = getCore('./src/categories');
22
+ const batch = getCore('./src/batch');
23
+ const socketHelpers = getCore('./src/socket.io/helpers');
24
+ const events = getCore('./src/events');
25
+ const activitypubApi = getCore('./src/api/activitypub');
26
+ const activitypub = getCore('./src/activitypub');
27
+ const user = getCore('./src/user');
28
+
29
+ originalMove = topicsApi.move;
30
+
31
+ topicsApi.move = async function (caller, data) {
32
+ const canMove = await privileges.categories.isAdminOrMod(data.cid, caller.uid);
33
+ if (canMove) {
34
+ return await originalMove(caller, data);
35
+ }
36
+
37
+ const tids = Array.isArray(data.tid) ? data.tid : [data.tid];
38
+ const targetCid = parseInt(data.cid, 10);
39
+
40
+ const [canCreate, canRead] = await Promise.all([
41
+ privileges.categories.can('topics:create', targetCid, caller.uid),
42
+ privileges.categories.can('topics:read', targetCid, caller.uid),
43
+ ]);
44
+
45
+ if (!canCreate || !canRead) {
46
+ throw new Error('[[error:no-privileges]]');
47
+ }
48
+
49
+ const uids = await user.getUidsFromSet('users:online', 0, -1);
50
+ const cids = [targetCid];
51
+
52
+ await batch.processArray(tids, async (batchTids) => {
53
+ await Promise.all(batchTids.map(async (tid) => {
54
+ const topicData = await topics.getTopicFields(tid, [
55
+ 'tid',
56
+ 'cid',
57
+ 'uid',
58
+ 'mainPid',
59
+ 'slug',
60
+ 'deleted',
61
+ 'locked',
62
+ ]);
63
+
64
+ if (!topicData) {
65
+ throw new Error('[[error:no-topic]]');
66
+ }
67
+
68
+ const isOwner = parseInt(topicData.uid, 10) === parseInt(caller.uid, 10);
69
+ if (!isOwner || topicData.locked || topicData.deleted) {
70
+ throw new Error('[[error:no-privileges]]');
71
+ }
72
+
73
+ if (!cids.includes(topicData.cid)) {
74
+ cids.push(topicData.cid);
75
+ }
76
+
77
+ await topics.tools.move(tid, {
78
+ cid: targetCid,
79
+ uid: caller.uid,
80
+ });
81
+
82
+ const notifyUids = await privileges.categories.filterUids('topics:read', topicData.cid, uids);
83
+ socketHelpers.emitToUids('event:topic_moved', topicData, notifyUids);
84
+
85
+ if (!topicData.deleted) {
86
+ socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic');
87
+ activitypubApi.announce.note(caller, { tid });
88
+ const { activity } = await activitypub.mocks.activities.create(topicData.mainPid, caller.uid);
89
+ await activitypub.feps.announce(topicData.mainPid, activity);
90
+ }
91
+
92
+ await events.log({
93
+ type: 'topic-move',
94
+ uid: caller.uid,
95
+ ip: caller.ip,
96
+ tid: tid,
97
+ fromCid: topicData.cid,
98
+ toCid: targetCid,
99
+ });
100
+ }));
101
+ }, { batch: 10 });
102
+
103
+ await categories.onTopicsMoved(cids);
104
+ };
105
+ };
106
+
107
+ plugin.filterTopicPrivileges = async function (data) {
108
+ const topics = getCore('./src/topics');
109
+ const topicData = await topics.getTopicFields(data.tid, ['uid', 'locked', 'deleted']);
110
+ if (!topicData) {
111
+ return data;
112
+ }
113
+
114
+ const isOwner = parseInt(topicData.uid, 10) === parseInt(data.uid, 10);
115
+ data.canMoveOwnTopic = !!(isOwner && !data.isAdminOrMod && !topicData.locked && !topicData.deleted);
116
+
117
+ if (data.canMoveOwnTopic && !data.view_thread_tools) {
118
+ data.view_thread_tools = true;
119
+ }
120
+
121
+ return data;
122
+ };
123
+
124
+ module.exports = plugin;
package/package.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "nodebb-plugin-moving-topics",
3
+ "version": "1.0.0",
4
+ "description": "Allow topic owners to move their own topics.",
5
+ "main": "library.js",
6
+ "license": "MIT"
7
+ }
package/plugin.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "nodebb-plugin-moving-topics",
3
+ "name": "Moving Topics for Owners",
4
+ "description": "Allow topic owners to move their own topics.",
5
+ "url": "https://github.com/palmoni5/nodebb-plugin-moving-topics",
6
+ "library": "library.js",
7
+ "hooks": [
8
+ { "hook": "static:app.load", "method": "init" },
9
+ { "hook": "filter:privileges.topics.get", "method": "filterTopicPrivileges" }
10
+ ],
11
+ "scripts": [
12
+ "static/lib/client.js"
13
+ ]
14
+ }
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /* global ajaxify */
4
+
5
+ $(window).on('action:app.load', function () {
6
+ require(['hooks', 'translator'], function (hooks, translator) {
7
+ function canMoveOwnTopic() {
8
+ const privs = ajaxify.data && ajaxify.data.privileges;
9
+ return !!(privs && privs.canMoveOwnTopic);
10
+ }
11
+
12
+ function addMoveItem(menuEl) {
13
+ const $menu = $(menuEl);
14
+ if ($menu.find('.plugin-move-own-topic').length) {
15
+ return;
16
+ }
17
+
18
+ translator.translate('[[topic:thread-tools.move]]', function (label) {
19
+ const item = [
20
+ '<li>',
21
+ '<a href="#" class="dropdown-item rounded-1 d-flex align-items-center gap-2 plugin-move-own-topic" role="menuitem">',
22
+ '<i class="fa fa-fw fa-arrows text-secondary"></i> ',
23
+ label,
24
+ '</a>',
25
+ '</li>',
26
+ ].join('');
27
+ $menu.append(item);
28
+ });
29
+ }
30
+
31
+ function bindMoveHandler(menuEl) {
32
+ const $menu = $(menuEl);
33
+ $menu.off('click', '.plugin-move-own-topic').on('click', '.plugin-move-own-topic', function () {
34
+ require(['forum/topic/move'], function (move) {
35
+ move.init([ajaxify.data.tid], ajaxify.data.cid);
36
+ });
37
+ return false;
38
+ });
39
+ }
40
+
41
+ hooks.on('action:topic.tools.load', function (data) {
42
+ if (!canMoveOwnTopic()) {
43
+ return;
44
+ }
45
+ addMoveItem(data.element);
46
+ bindMoveHandler(data.element);
47
+ });
48
+
49
+ hooks.on('action:category.selector.options', function (data) {
50
+ if (!canMoveOwnTopic()) {
51
+ return;
52
+ }
53
+ const el = data.el;
54
+ if (!el || !el.length) {
55
+ return;
56
+ }
57
+ const inMoveModal = el.closest('.tool-modal').find('#move_thread_commit').length > 0;
58
+ if (inMoveModal) {
59
+ data.options.privilege = 'topics:create';
60
+ }
61
+ });
62
+ });
63
+ });