nodebb-plugin-integration 0.5.0 → 0.6.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 +156 -145
- package/README.ru.md +147 -136
- package/docs/MCP.md +718 -0
- package/docs/MCP.ru.md +719 -0
- package/package.json +3 -2
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +294 -76
- package/src/nodebb/posts.js +72 -21
- package/src/nodebb/topics.js +121 -24
- package/src/nodebb/users.js +19 -4
- package/src/routes/api.js +297 -35
- package/src/routes/index.js +24 -31
package/src/nodebb/topics.js
CHANGED
|
@@ -3,22 +3,19 @@
|
|
|
3
3
|
const NodeBB = require("./index");
|
|
4
4
|
|
|
5
5
|
const FIELDS = [
|
|
6
|
-
"tid",
|
|
7
|
-
"uid",
|
|
8
|
-
"title",
|
|
9
|
-
"cid",
|
|
10
|
-
"timestamp",
|
|
11
|
-
"mainPid",
|
|
12
|
-
"deleted"
|
|
6
|
+
"tid", "uid", "title", "cid", "timestamp", "mainPid", "deleted", "postcount"
|
|
13
7
|
];
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
throw new TypeError("Invalid topic ID");
|
|
9
|
+
function validId(value, name) {
|
|
10
|
+
const id = Number(value);
|
|
11
|
+
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
12
|
+
throw new TypeError(`Invalid ${name}`);
|
|
20
13
|
}
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
21
16
|
|
|
17
|
+
async function getTopic(tid) {
|
|
18
|
+
tid = validId(tid, "topic ID");
|
|
22
19
|
const topics = NodeBB.get("topics");
|
|
23
20
|
|
|
24
21
|
if (typeof topics.getTopicFields !== "function") {
|
|
@@ -28,13 +25,19 @@ async function getTopic(tid) {
|
|
|
28
25
|
return topics.getTopicFields(tid, FIELDS);
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
async function
|
|
32
|
-
tid =
|
|
28
|
+
async function getTopicData(tid) {
|
|
29
|
+
tid = validId(tid, "topic ID");
|
|
30
|
+
const topics = NodeBB.get("topics");
|
|
33
31
|
|
|
34
|
-
if (
|
|
35
|
-
throw new
|
|
32
|
+
if (typeof topics.getTopicData !== "function") {
|
|
33
|
+
throw new Error("NodeBB topics.getTopicData is unavailable");
|
|
36
34
|
}
|
|
37
35
|
|
|
36
|
+
return topics.getTopicData(tid);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function getTopicPids(tid) {
|
|
40
|
+
tid = validId(tid, "topic ID");
|
|
38
41
|
const topics = NodeBB.get("topics");
|
|
39
42
|
|
|
40
43
|
if (typeof topics.getPids !== "function") {
|
|
@@ -61,9 +64,7 @@ async function getTopics(tids = []) {
|
|
|
61
64
|
)
|
|
62
65
|
)];
|
|
63
66
|
|
|
64
|
-
if (!unique.length)
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
+
if (!unique.length) return [];
|
|
67
68
|
|
|
68
69
|
const topics = NodeBB.get("topics");
|
|
69
70
|
|
|
@@ -75,18 +76,114 @@ async function getTopics(tids = []) {
|
|
|
75
76
|
|
|
76
77
|
for (const tid of unique) {
|
|
77
78
|
const topic = await getTopic(tid);
|
|
78
|
-
|
|
79
|
-
if (topic) {
|
|
80
|
-
result.push(topic);
|
|
81
|
-
}
|
|
79
|
+
if (topic) result.push(topic);
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
return result;
|
|
85
83
|
}
|
|
86
84
|
|
|
85
|
+
async function createTopic(data) {
|
|
86
|
+
const topics = NodeBB.get("topics");
|
|
87
|
+
|
|
88
|
+
if (typeof topics.post !== "function") {
|
|
89
|
+
throw new Error("NodeBB topics.post is unavailable");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return topics.post(data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function createReply(data) {
|
|
96
|
+
const topics = NodeBB.get("topics");
|
|
97
|
+
|
|
98
|
+
if (typeof topics.reply !== "function") {
|
|
99
|
+
throw new Error("NodeBB topics.reply is unavailable");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return topics.reply(data);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function deleteTopic(tid, uid) {
|
|
106
|
+
tid = validId(tid, "topic ID");
|
|
107
|
+
uid = validId(uid, "user ID");
|
|
108
|
+
|
|
109
|
+
const topics = NodeBB.get("topics");
|
|
110
|
+
|
|
111
|
+
if (!topics.tools || typeof topics.tools.delete !== "function") {
|
|
112
|
+
throw new Error("NodeBB topics.tools.delete is unavailable");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return topics.tools.delete(tid, uid);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function restoreTopic(tid, uid) {
|
|
119
|
+
tid = validId(tid, "topic ID");
|
|
120
|
+
uid = validId(uid, "user ID");
|
|
121
|
+
|
|
122
|
+
const topics = NodeBB.get("topics");
|
|
123
|
+
|
|
124
|
+
if (!topics.tools || typeof topics.tools.restore !== "function") {
|
|
125
|
+
throw new Error("NodeBB topics.tools.restore is unavailable");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return topics.tools.restore(tid, uid);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function listTopics(options) {
|
|
132
|
+
const topics = NodeBB.get("topics");
|
|
133
|
+
|
|
134
|
+
if (typeof topics.getSortedTopics !== "function") {
|
|
135
|
+
throw new Error("NodeBB topics.getSortedTopics is unavailable");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const params = {
|
|
139
|
+
uid: validId(options.uid, "user ID"),
|
|
140
|
+
start: options.start,
|
|
141
|
+
stop: options.stop,
|
|
142
|
+
sort: options.sort || "recent"
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (options.cid !== undefined) {
|
|
146
|
+
params.cids = validId(options.cid, "category ID");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return topics.getSortedTopics(params);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function getTopicPosts(tid, uid, start, limit, reverse = false) {
|
|
153
|
+
tid = validId(tid, "topic ID");
|
|
154
|
+
uid = validId(uid, "user ID");
|
|
155
|
+
|
|
156
|
+
const topics = NodeBB.get("topics");
|
|
157
|
+
const topicData = await getTopicData(tid);
|
|
158
|
+
|
|
159
|
+
if (!topicData) return [];
|
|
160
|
+
|
|
161
|
+
if (typeof topics.getTopicPosts !== "function") {
|
|
162
|
+
throw new Error("NodeBB topics.getTopicPosts is unavailable");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const stop = start + limit - 1;
|
|
166
|
+
|
|
167
|
+
return topics.getTopicPosts(
|
|
168
|
+
topicData,
|
|
169
|
+
`tid:${tid}:posts`,
|
|
170
|
+
start,
|
|
171
|
+
stop,
|
|
172
|
+
uid,
|
|
173
|
+
Boolean(reverse)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
87
177
|
module.exports = {
|
|
88
178
|
getTopic,
|
|
179
|
+
getTopicData,
|
|
89
180
|
getTopics,
|
|
90
181
|
getTopicPids,
|
|
182
|
+
createTopic,
|
|
183
|
+
createReply,
|
|
184
|
+
deleteTopic,
|
|
185
|
+
restoreTopic,
|
|
186
|
+
listTopics,
|
|
187
|
+
getTopicPosts,
|
|
91
188
|
FIELDS
|
|
92
|
-
};
|
|
189
|
+
};
|
package/src/nodebb/users.js
CHANGED
|
@@ -36,9 +36,7 @@ async function getUsers(uids = []) {
|
|
|
36
36
|
)
|
|
37
37
|
)];
|
|
38
38
|
|
|
39
|
-
if (!unique.length)
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
39
|
+
if (!unique.length) return [];
|
|
42
40
|
|
|
43
41
|
const users = NodeBB.get("users");
|
|
44
42
|
|
|
@@ -56,8 +54,25 @@ async function getUsers(uids = []) {
|
|
|
56
54
|
return result;
|
|
57
55
|
}
|
|
58
56
|
|
|
57
|
+
async function getPostIds(uid, start, stop) {
|
|
58
|
+
uid = Number(uid);
|
|
59
|
+
|
|
60
|
+
if (!Number.isSafeInteger(uid) || uid <= 0) {
|
|
61
|
+
throw new TypeError("Invalid user ID");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const users = NodeBB.get("users");
|
|
65
|
+
|
|
66
|
+
if (typeof users.getPostIds !== "function") {
|
|
67
|
+
throw new Error("NodeBB users.getPostIds is unavailable");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return users.getPostIds(uid, start, stop);
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
module.exports = {
|
|
60
74
|
getUser,
|
|
61
75
|
getUsers,
|
|
76
|
+
getPostIds,
|
|
62
77
|
FIELDS
|
|
63
|
-
};
|
|
78
|
+
};
|
package/src/routes/api.js
CHANGED
|
@@ -16,13 +16,17 @@ function validId(value) {
|
|
|
16
16
|
return Number.isSafeInteger(id) && id > 0 ? id : 0;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function validPage(value, fallback, maximum) {
|
|
20
|
+
const number = value === undefined ? fallback : Number(value);
|
|
21
|
+
return Number.isSafeInteger(number) && number >= 0 && number <= maximum
|
|
22
|
+
? number : -1;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
function internalTokenMatches(req, expected) {
|
|
20
26
|
const authorization = req.headers.authorization || "";
|
|
21
27
|
const match = authorization.match(/^Bearer\s+([^\s]+)$/i);
|
|
22
28
|
|
|
23
|
-
if (!match || typeof expected !== "string" || !expected)
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
29
|
+
if (!match || typeof expected !== "string" || !expected) return false;
|
|
26
30
|
|
|
27
31
|
const actualBuffer = Buffer.from(match[1]);
|
|
28
32
|
const expectedBuffer = Buffer.from(expected);
|
|
@@ -31,6 +35,17 @@ function internalTokenMatches(req, expected) {
|
|
|
31
35
|
crypto.timingSafeEqual(actualBuffer, expectedBuffer);
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
function serviceUid(req) {
|
|
39
|
+
return validId(req.integrationSettings?.mcpServiceUid);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function operationError(res, error, label) {
|
|
43
|
+
console.error(`[Integration] ${label}:`, error.stack || error);
|
|
44
|
+
return res.status(400).json({
|
|
45
|
+
error: error.message || `${label} failed`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
exports.internalAuth = async function (req, res, next) {
|
|
35
50
|
try {
|
|
36
51
|
const settings = await Config.get();
|
|
@@ -46,7 +61,7 @@ exports.internalAuth = async function (req, res, next) {
|
|
|
46
61
|
req.integrationSettings = settings;
|
|
47
62
|
return next();
|
|
48
63
|
} catch (error) {
|
|
49
|
-
console.error("[Integration] Internal authentication failed:", error
|
|
64
|
+
console.error("[Integration] Internal authentication failed:", error);
|
|
50
65
|
return res.status(500).json({ error: "Internal API error" });
|
|
51
66
|
}
|
|
52
67
|
};
|
|
@@ -54,15 +69,13 @@ exports.internalAuth = async function (req, res, next) {
|
|
|
54
69
|
exports.apiSearch = async function (req, res) {
|
|
55
70
|
try {
|
|
56
71
|
const query = typeof req.body?.query === "string"
|
|
57
|
-
? req.body.query.trim()
|
|
58
|
-
: "";
|
|
72
|
+
? req.body.query.trim() : "";
|
|
59
73
|
|
|
60
74
|
if (!query || query.length > 2000) {
|
|
61
75
|
return res.status(400).json({ error: "Invalid search query" });
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
const results = await Search.searchText(query, req.uid);
|
|
65
|
-
|
|
66
79
|
return res.json({ results });
|
|
67
80
|
} catch (error) {
|
|
68
81
|
if (error.code === "AI_DISABLED") {
|
|
@@ -78,7 +91,7 @@ exports.internalInfo = async function (req, res) {
|
|
|
78
91
|
try {
|
|
79
92
|
return res.json(await Meta.getInfo());
|
|
80
93
|
} catch (error) {
|
|
81
|
-
console.error("[Integration] Forum info failed:", error
|
|
94
|
+
console.error("[Integration] Forum info failed:", error);
|
|
82
95
|
return res.status(500).json({ error: "Unable to get forum information" });
|
|
83
96
|
}
|
|
84
97
|
};
|
|
@@ -86,10 +99,10 @@ exports.internalInfo = async function (req, res) {
|
|
|
86
99
|
exports.internalPost = async function (req, res) {
|
|
87
100
|
try {
|
|
88
101
|
const pid = validId(req.body?.pid);
|
|
89
|
-
const uid = req
|
|
102
|
+
const uid = serviceUid(req);
|
|
90
103
|
|
|
91
|
-
if (!pid) {
|
|
92
|
-
return res.status(400).json({ error: "Invalid post ID" });
|
|
104
|
+
if (!pid || !uid) {
|
|
105
|
+
return res.status(400).json({ error: "Invalid post ID or service user" });
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
const post = await Posts.getPostMeta(pid);
|
|
@@ -100,9 +113,7 @@ exports.internalPost = async function (req, res) {
|
|
|
100
113
|
|
|
101
114
|
const tid = validId(post.tid);
|
|
102
115
|
|
|
103
|
-
if (!tid) {
|
|
104
|
-
return res.status(404).json({ error: "Post not found" });
|
|
105
|
-
}
|
|
116
|
+
if (!tid) return res.status(404).json({ error: "Post not found" });
|
|
106
117
|
|
|
107
118
|
const [postAllowed, topicAllowed] = await Promise.all([
|
|
108
119
|
Privileges.canReadPost(pid, uid),
|
|
@@ -115,9 +126,7 @@ exports.internalPost = async function (req, res) {
|
|
|
115
126
|
|
|
116
127
|
const content = await Posts.getPostContent(pid);
|
|
117
128
|
|
|
118
|
-
if (!content) {
|
|
119
|
-
return res.status(404).json({ error: "Post not found" });
|
|
120
|
-
}
|
|
129
|
+
if (!content) return res.status(404).json({ error: "Post not found" });
|
|
121
130
|
|
|
122
131
|
return res.json({
|
|
123
132
|
pid: Number(post.pid),
|
|
@@ -128,7 +137,7 @@ exports.internalPost = async function (req, res) {
|
|
|
128
137
|
timestamp: post.timestamp
|
|
129
138
|
});
|
|
130
139
|
} catch (error) {
|
|
131
|
-
console.error("[Integration] MCP post retrieval failed:", error
|
|
140
|
+
console.error("[Integration] MCP post retrieval failed:", error);
|
|
132
141
|
return res.status(500).json({ error: "Unable to get post" });
|
|
133
142
|
}
|
|
134
143
|
};
|
|
@@ -136,10 +145,10 @@ exports.internalPost = async function (req, res) {
|
|
|
136
145
|
exports.internalTopic = async function (req, res) {
|
|
137
146
|
try {
|
|
138
147
|
const tid = validId(req.body?.tid);
|
|
139
|
-
const uid = req
|
|
148
|
+
const uid = serviceUid(req);
|
|
140
149
|
|
|
141
|
-
if (!tid) {
|
|
142
|
-
return res.status(400).json({ error: "Invalid topic ID" });
|
|
150
|
+
if (!tid || !uid) {
|
|
151
|
+
return res.status(400).json({ error: "Invalid topic ID or service user" });
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
if (!(await Privileges.canReadTopic(tid, uid))) {
|
|
@@ -161,7 +170,7 @@ exports.internalTopic = async function (req, res) {
|
|
|
161
170
|
mainPid: Number(topic.mainPid)
|
|
162
171
|
});
|
|
163
172
|
} catch (error) {
|
|
164
|
-
console.error("[Integration] MCP topic retrieval failed:", error
|
|
173
|
+
console.error("[Integration] MCP topic retrieval failed:", error);
|
|
165
174
|
return res.status(500).json({ error: "Unable to get topic" });
|
|
166
175
|
}
|
|
167
176
|
};
|
|
@@ -170,9 +179,7 @@ exports.internalUser = async function (req, res) {
|
|
|
170
179
|
try {
|
|
171
180
|
const uid = validId(req.body?.uid);
|
|
172
181
|
|
|
173
|
-
if (!uid) {
|
|
174
|
-
return res.status(400).json({ error: "Invalid user ID" });
|
|
175
|
-
}
|
|
182
|
+
if (!uid) return res.status(400).json({ error: "Invalid user ID" });
|
|
176
183
|
|
|
177
184
|
const user = await Users.getUser(uid);
|
|
178
185
|
|
|
@@ -187,7 +194,7 @@ exports.internalUser = async function (req, res) {
|
|
|
187
194
|
picture: user.picture
|
|
188
195
|
});
|
|
189
196
|
} catch (error) {
|
|
190
|
-
console.error("[Integration] MCP user retrieval failed:", error
|
|
197
|
+
console.error("[Integration] MCP user retrieval failed:", error);
|
|
191
198
|
return res.status(500).json({ error: "Unable to get user" });
|
|
192
199
|
}
|
|
193
200
|
};
|
|
@@ -195,9 +202,7 @@ exports.internalUser = async function (req, res) {
|
|
|
195
202
|
exports.internalSearch = async function (req, res) {
|
|
196
203
|
try {
|
|
197
204
|
const query = typeof req.body?.query === "string"
|
|
198
|
-
? req.body.query.trim()
|
|
199
|
-
: "";
|
|
200
|
-
|
|
205
|
+
? req.body.query.trim() : "";
|
|
201
206
|
const requestedLimit = Number(req.body?.limit ?? 5);
|
|
202
207
|
|
|
203
208
|
if (!query || query.length > 2000) {
|
|
@@ -208,10 +213,11 @@ exports.internalSearch = async function (req, res) {
|
|
|
208
213
|
return res.status(400).json({ error: "Invalid search limit" });
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
const results = await Search.searchText(
|
|
217
|
+
query,
|
|
218
|
+
serviceUid(req),
|
|
219
|
+
Math.min(requestedLimit, 20)
|
|
220
|
+
);
|
|
215
221
|
|
|
216
222
|
return res.json({ results });
|
|
217
223
|
} catch (error) {
|
|
@@ -219,11 +225,268 @@ exports.internalSearch = async function (req, res) {
|
|
|
219
225
|
return res.status(503).json({ error: error.message });
|
|
220
226
|
}
|
|
221
227
|
|
|
222
|
-
console.error("[Integration] MCP search failed:", error
|
|
228
|
+
console.error("[Integration] MCP search failed:", error);
|
|
223
229
|
return res.status(500).json({ error: "Search failed" });
|
|
224
230
|
}
|
|
225
231
|
};
|
|
226
232
|
|
|
233
|
+
exports.internalCreateTopic = async function (req, res) {
|
|
234
|
+
try {
|
|
235
|
+
const uid = serviceUid(req);
|
|
236
|
+
const cid = validId(req.body?.cid);
|
|
237
|
+
const title = req.body?.title;
|
|
238
|
+
const content = req.body?.content;
|
|
239
|
+
const tags = req.body?.tags ?? [];
|
|
240
|
+
|
|
241
|
+
if (!uid || !cid || typeof title !== "string" ||
|
|
242
|
+
!title.trim() || title.length > 255 ||
|
|
243
|
+
typeof content !== "string" || !content.trim() ||
|
|
244
|
+
content.length > 20000 || !Array.isArray(tags) ||
|
|
245
|
+
tags.length > 10 ||
|
|
246
|
+
tags.some(tag => typeof tag !== "string" || !tag.trim() || tag.length > 32)) {
|
|
247
|
+
return res.status(400).json({ error: "Invalid topic data" });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const result = await Topics.createTopic({
|
|
251
|
+
uid,
|
|
252
|
+
cid,
|
|
253
|
+
title: title.trim(),
|
|
254
|
+
content,
|
|
255
|
+
tags
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return res.json(result);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return operationError(res, error, "Topic creation");
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
exports.internalDeleteTopic = async function (req, res) {
|
|
265
|
+
try {
|
|
266
|
+
const tid = validId(req.body?.tid);
|
|
267
|
+
const uid = serviceUid(req);
|
|
268
|
+
|
|
269
|
+
if (!tid || !uid) return res.status(400).json({ error: "Invalid topic ID" });
|
|
270
|
+
|
|
271
|
+
return res.json(await Topics.deleteTopic(tid, uid));
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return operationError(res, error, "Topic deletion");
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
exports.internalRestoreTopic = async function (req, res) {
|
|
278
|
+
try {
|
|
279
|
+
const tid = validId(req.body?.tid);
|
|
280
|
+
const uid = serviceUid(req);
|
|
281
|
+
|
|
282
|
+
if (!tid || !uid) return res.status(400).json({ error: "Invalid topic ID" });
|
|
283
|
+
|
|
284
|
+
return res.json(await Topics.restoreTopic(tid, uid));
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return operationError(res, error, "Topic restoration");
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
exports.internalListTopics = async function (req, res) {
|
|
291
|
+
try {
|
|
292
|
+
const uid = serviceUid(req);
|
|
293
|
+
const start = validPage(req.body?.start, 0, 10000000);
|
|
294
|
+
const limit = validPage(req.body?.limit, 20, 100);
|
|
295
|
+
|
|
296
|
+
const sorts = ["recent", "old", "create", "posts", "votes", "views"];
|
|
297
|
+
const sort = req.body?.sort ?? "recent";
|
|
298
|
+
const cid = req.body?.cid === undefined ? undefined : validId(req.body.cid);
|
|
299
|
+
|
|
300
|
+
if (!uid || start < 0 || limit < 1 || !sorts.includes(sort) ||
|
|
301
|
+
(req.body?.cid !== undefined && !cid)) {
|
|
302
|
+
return res.status(400).json({ error: "Invalid topic listing parameters" });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = await Topics.listTopics({
|
|
306
|
+
uid,
|
|
307
|
+
start,
|
|
308
|
+
stop: start + limit - 1,
|
|
309
|
+
sort,
|
|
310
|
+
cid
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const nextStart = Math.min(start + limit, result.topicCount);
|
|
314
|
+
|
|
315
|
+
return res.json({
|
|
316
|
+
...result,
|
|
317
|
+
nextStart,
|
|
318
|
+
hasMore: nextStart < result.topicCount
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return operationError(res, error, "Topic listing");
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
exports.internalCreatePost = async function (req, res) {
|
|
326
|
+
try {
|
|
327
|
+
const uid = serviceUid(req);
|
|
328
|
+
const tid = validId(req.body?.tid);
|
|
329
|
+
const content = req.body?.content;
|
|
330
|
+
|
|
331
|
+
if (!uid || !tid || typeof content !== "string" ||
|
|
332
|
+
!content.trim() || content.length > 20000) {
|
|
333
|
+
return res.status(400).json({ error: "Invalid post data" });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return res.json(await Topics.createReply({ uid, tid, content }));
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return operationError(res, error, "Post creation");
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
exports.internalEditPost = async function (req, res) {
|
|
343
|
+
try {
|
|
344
|
+
const uid = serviceUid(req);
|
|
345
|
+
const pid = validId(req.body?.pid);
|
|
346
|
+
const content = req.body?.content;
|
|
347
|
+
|
|
348
|
+
if (!uid || !pid || typeof content !== "string" ||
|
|
349
|
+
!content.trim() || content.length > 20000) {
|
|
350
|
+
return res.status(400).json({ error: "Invalid post data" });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return res.json(await Posts.editPost(pid, uid, content));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
return operationError(res, error, "Post editing");
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
exports.internalDeletePost = async function (req, res) {
|
|
360
|
+
try {
|
|
361
|
+
const uid = serviceUid(req);
|
|
362
|
+
const pid = validId(req.body?.pid);
|
|
363
|
+
|
|
364
|
+
if (!uid || !pid) return res.status(400).json({ error: "Invalid post ID" });
|
|
365
|
+
|
|
366
|
+
return res.json(await Posts.deletePost(pid, uid));
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return operationError(res, error, "Post deletion");
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
exports.internalRestorePost = async function (req, res) {
|
|
373
|
+
try {
|
|
374
|
+
const uid = serviceUid(req);
|
|
375
|
+
const pid = validId(req.body?.pid);
|
|
376
|
+
|
|
377
|
+
if (!uid || !pid) return res.status(400).json({ error: "Invalid post ID" });
|
|
378
|
+
|
|
379
|
+
return res.json(await Posts.restorePost(pid, uid));
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return operationError(res, error, "Post restoration");
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
exports.internalDeleteUserPosts = async function (req, res) {
|
|
386
|
+
try {
|
|
387
|
+
const actorUid = serviceUid(req);
|
|
388
|
+
const targetUid = validId(req.body?.uid);
|
|
389
|
+
const start = validPage(req.body?.start, 0, 10000000);
|
|
390
|
+
const limit = validPage(req.body?.limit, 100, 100);
|
|
391
|
+
|
|
392
|
+
if (!actorUid || !targetUid || start < 0 || limit < 1) {
|
|
393
|
+
return res.status(400).json({ error: "Invalid bulk deletion parameters" });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const pids = await Posts.getUserPostIds(targetUid, start, start + limit - 1);
|
|
397
|
+
const results = {
|
|
398
|
+
uid: targetUid,
|
|
399
|
+
scanned: pids.length,
|
|
400
|
+
deleted: 0,
|
|
401
|
+
skipped: 0,
|
|
402
|
+
failed: [],
|
|
403
|
+
hasMore: pids.length === limit
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
for (const rawPid of pids) {
|
|
407
|
+
const pid = validId(rawPid);
|
|
408
|
+
|
|
409
|
+
if (!pid) {
|
|
410
|
+
results.skipped++;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const post = await Posts.getPostMeta(pid);
|
|
416
|
+
|
|
417
|
+
if (!post || post.deleted || Number(post.uid) !== targetUid) {
|
|
418
|
+
results.skipped++;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const topic = await Topics.getTopic(post.tid);
|
|
423
|
+
|
|
424
|
+
if (!topic || Number(topic.mainPid) === pid) {
|
|
425
|
+
results.skipped++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
await Posts.deletePost(pid, actorUid);
|
|
430
|
+
results.deleted++;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
results.failed.push({
|
|
433
|
+
pid,
|
|
434
|
+
error: error.message || "Deletion failed"
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
results.nextStart = start + pids.length - results.deleted;
|
|
440
|
+
|
|
441
|
+
return res.json(results);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return operationError(res, error, "Bulk post deletion");
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
exports.internalListPosts = async function (req, res) {
|
|
448
|
+
try {
|
|
449
|
+
const uid = serviceUid(req);
|
|
450
|
+
const tid = validId(req.body?.tid);
|
|
451
|
+
const start = validPage(req.body?.start, 0, 10000000);
|
|
452
|
+
const limit = validPage(req.body?.limit, 20, 100);
|
|
453
|
+
const reverse = req.body?.reverse ?? false;
|
|
454
|
+
|
|
455
|
+
if (!uid || !tid || start < 0 || limit < 1 ||
|
|
456
|
+
typeof reverse !== "boolean") {
|
|
457
|
+
return res.status(400).json({ error: "Invalid post listing parameters" });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!(await Privileges.canReadTopic(tid, uid))) {
|
|
461
|
+
return res.status(404).json({ error: "Topic not found" });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const topic = await Topics.getTopic(tid);
|
|
465
|
+
|
|
466
|
+
if (!topic || topic.deleted) {
|
|
467
|
+
return res.status(404).json({ error: "Topic not found" });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const posts = await Topics.getTopicPosts(tid, uid, start, limit, reverse);
|
|
471
|
+
|
|
472
|
+
const total = Math.max(0, Number(topic.postcount) || 0);
|
|
473
|
+
const nextStart = Math.min(start + limit, total);
|
|
474
|
+
|
|
475
|
+
return res.json({
|
|
476
|
+
tid,
|
|
477
|
+
start,
|
|
478
|
+
limit,
|
|
479
|
+
reverse,
|
|
480
|
+
posts,
|
|
481
|
+
total,
|
|
482
|
+
nextStart,
|
|
483
|
+
hasMore: nextStart < total
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return operationError(res, error, "Post listing");
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
227
490
|
exports.renderAdmin = async function (req, res) {
|
|
228
491
|
try {
|
|
229
492
|
const [settings, allCategories] = await Promise.all([
|
|
@@ -232,7 +495,6 @@ exports.renderAdmin = async function (req, res) {
|
|
|
232
495
|
]);
|
|
233
496
|
|
|
234
497
|
const excluded = new Set(settings.excludedCategories.map(Number));
|
|
235
|
-
|
|
236
498
|
const categories = allCategories
|
|
237
499
|
.filter(category => category.cid > 0)
|
|
238
500
|
.map(category => ({
|