nodebb-plugin-discord-onekite 1.0.15 → 1.0.16
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/library.js +1 -246
- package/package.json +1 -1
package/library.js
CHANGED
|
@@ -1,246 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { request } = require('undici');
|
|
4
|
-
|
|
5
|
-
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
6
|
-
const meta = require.main.require('./src/meta');
|
|
7
|
-
const middleware = require.main.require('./src/middleware');
|
|
8
|
-
|
|
9
|
-
const topics = require.main.require('./src/topics');
|
|
10
|
-
const posts = require.main.require('./src/posts');
|
|
11
|
-
const user = require.main.require('./src/user');
|
|
12
|
-
|
|
13
|
-
const controllers = require('./lib/controllers');
|
|
14
|
-
|
|
15
|
-
const SETTINGS_KEY = 'discord-onekite';
|
|
16
|
-
|
|
17
|
-
function normalizeCids(value) {
|
|
18
|
-
if (!value) return [];
|
|
19
|
-
if (Array.isArray(value)) return value.map(v => String(v)).filter(Boolean);
|
|
20
|
-
if (typeof value === 'string') return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizeBaseUrl(url) {
|
|
25
|
-
if (!url) return '';
|
|
26
|
-
let s = String(url).trim();
|
|
27
|
-
if (!/^https?:\/\//i.test(s)) s = `https://${s}`;
|
|
28
|
-
s = s.replace(/\/+$/, '');
|
|
29
|
-
return s;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function ensureAbsoluteUrl(baseUrl, maybeUrl) {
|
|
33
|
-
if (!maybeUrl) return '';
|
|
34
|
-
const s = String(maybeUrl);
|
|
35
|
-
if (/^https?:\/\//i.test(s)) return s;
|
|
36
|
-
if (s.startsWith('//')) return `https:${s}`;
|
|
37
|
-
if (s.startsWith('/')) return `${baseUrl}${s}`;
|
|
38
|
-
return s;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isValidHttpUrl(s) {
|
|
42
|
-
try {
|
|
43
|
-
const u = new URL(s);
|
|
44
|
-
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
45
|
-
} catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function stripHtml(html) {
|
|
51
|
-
if (!html) return '';
|
|
52
|
-
return String(html)
|
|
53
|
-
.replace(/<br\s*\/?>/gi, '\n')
|
|
54
|
-
.replace(/<\/p>/gi, '\n')
|
|
55
|
-
.replace(/<[^>]*>/g, '')
|
|
56
|
-
.replace(/ /g, ' ')
|
|
57
|
-
.replace(/&/g, '&')
|
|
58
|
-
.replace(/</g, '<')
|
|
59
|
-
.replace(/>/g, '>')
|
|
60
|
-
.replace(/'/g, "'")
|
|
61
|
-
.replace(/"/g, '"')
|
|
62
|
-
.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function truncate(s, n) {
|
|
66
|
-
const str = String(s || '');
|
|
67
|
-
if (str.length <= n) return str;
|
|
68
|
-
return str.slice(0, n - 1) + '…';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function getSettings() {
|
|
72
|
-
const s = await meta.settings.get(SETTINGS_KEY);
|
|
73
|
-
return {
|
|
74
|
-
webhookUrl: (s && s.webhookUrl) ? String(s.webhookUrl).trim() : '',
|
|
75
|
-
notifyReplies: !!(s && (s.notifyReplies === true || s.notifyReplies === 'on' || s.notifyReplies === 'true')),
|
|
76
|
-
cids: normalizeCids(s && s.cids),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function cidAllowed(topicCid, allowedCids) {
|
|
81
|
-
if (!allowedCids || allowedCids.length === 0) return true;
|
|
82
|
-
return allowedCids.includes(String(topicCid));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function postToDiscord(webhookUrl, payload) {
|
|
86
|
-
const res = await request(webhookUrl, {
|
|
87
|
-
method: 'POST',
|
|
88
|
-
headers: { 'Content-Type': 'application/json' },
|
|
89
|
-
body: JSON.stringify(payload),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const bodyText = await res.body.text().catch(() => '');
|
|
93
|
-
|
|
94
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
95
|
-
const err = new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${bodyText}`);
|
|
96
|
-
err.statusCode = res.statusCode;
|
|
97
|
-
err.responseBody = bodyText;
|
|
98
|
-
throw err;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function sendDiscord(webhookUrl, payload, fallbackContent) {
|
|
103
|
-
if (!webhookUrl) return;
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
await postToDiscord(webhookUrl, payload);
|
|
107
|
-
} catch (e) {
|
|
108
|
-
if (e && e.statusCode === 400 && fallbackContent) {
|
|
109
|
-
try {
|
|
110
|
-
await postToDiscord(webhookUrl, { content: fallbackContent });
|
|
111
|
-
return;
|
|
112
|
-
} catch (e2) {
|
|
113
|
-
console.error(e2);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
console.error(e);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function getPostExcerpt(pid) {
|
|
121
|
-
if (!pid) return '';
|
|
122
|
-
try {
|
|
123
|
-
const p = await posts.getPostFields(pid, ['content']);
|
|
124
|
-
const text = stripHtml(p && p.content);
|
|
125
|
-
return truncate(text, 500);
|
|
126
|
-
} catch {
|
|
127
|
-
return '';
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function buildPayload({ tid, pid, isReply }) {
|
|
132
|
-
const baseUrl = normalizeBaseUrl(meta.config.url);
|
|
133
|
-
|
|
134
|
-
const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'mainPid']);
|
|
135
|
-
if (!topicData) return null;
|
|
136
|
-
|
|
137
|
-
// Always build the forum link like the example: https://www.onekite.com/topic/<tid or slug>
|
|
138
|
-
const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
|
|
139
|
-
|
|
140
|
-
// For replies, link directly to the post if possible
|
|
141
|
-
const postUrl = (isReply && pid) ? `${topicUrl}/${pid}` : topicUrl;
|
|
142
|
-
|
|
143
|
-
const u = await user.getUserFields(topicData.uid, ['username']);
|
|
144
|
-
const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
|
|
145
|
-
const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
|
|
146
|
-
|
|
147
|
-
const excerpt = await getPostExcerpt(isReply ? pid : topicData.mainPid);
|
|
148
|
-
const description = truncate(excerpt || (isReply ? `Réponse de ${authorName}` : `Sujet créé par ${authorName}`), 500);
|
|
149
|
-
|
|
150
|
-
// Minimal embed but with URL so the title is clickable
|
|
151
|
-
const embed = {
|
|
152
|
-
title,
|
|
153
|
-
description,
|
|
154
|
-
};
|
|
155
|
-
if (isValidHttpUrl(postUrl)) embed.url = postUrl;
|
|
156
|
-
|
|
157
|
-
// Ensure the message is clickable: markdown link + also raw URL in first line
|
|
158
|
-
const content = [
|
|
159
|
-
postUrl, // raw URL first line
|
|
160
|
-
isReply ? `🗨️ Nouvelle réponse : [${title}](${postUrl})` : `🆕 Nouveau sujet : [${title}](${topicUrl})`,
|
|
161
|
-
description ? description : '',
|
|
162
|
-
].filter(Boolean).join('\n');
|
|
163
|
-
|
|
164
|
-
return { topicData, embed, content };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function extractTidPid(data) {
|
|
168
|
-
const tid = data?.tid || data?.topic?.tid || data?.post?.tid;
|
|
169
|
-
const pid = data?.pid || data?.post?.pid;
|
|
170
|
-
return { tid, pid };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const Plugin = {};
|
|
174
|
-
|
|
175
|
-
Plugin.init = async ({ router }) => {
|
|
176
|
-
routeHelpers.setupAdminPageRoute(
|
|
177
|
-
router,
|
|
178
|
-
'/admin/plugins/discord-onekite',
|
|
179
|
-
[],
|
|
180
|
-
controllers.renderAdminPage
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// AJAX save endpoint for ACP (used by admin.js)
|
|
184
|
-
router.post('/api/admin/plugins/discord-onekite/save',
|
|
185
|
-
middleware.admin.checkPrivileges,
|
|
186
|
-
async (req, res) => {
|
|
187
|
-
try {
|
|
188
|
-
const payload = {
|
|
189
|
-
webhookUrl: req.body.webhookUrl || '',
|
|
190
|
-
notifyReplies: req.body.notifyReplies ? 'on' : '',
|
|
191
|
-
cids: req.body.cids || '',
|
|
192
|
-
};
|
|
193
|
-
await meta.settings.set(SETTINGS_KEY, payload);
|
|
194
|
-
res.json({ ok: true });
|
|
195
|
-
} catch (e) {
|
|
196
|
-
console.error('[discord-onekite] save failed', e);
|
|
197
|
-
res.status(500).json({ ok: false });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
Plugin.addAdminNavigation = (header) => {
|
|
204
|
-
header.plugins.push({
|
|
205
|
-
route: '/plugins/discord-onekite',
|
|
206
|
-
icon: 'fa-bell',
|
|
207
|
-
name: 'Discord Onekite',
|
|
208
|
-
});
|
|
209
|
-
return header;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
Plugin.onTopicPost = async (data) => {
|
|
213
|
-
const settings = await getSettings();
|
|
214
|
-
if (!settings.webhookUrl) return;
|
|
215
|
-
|
|
216
|
-
const { tid } = extractTidPid(data);
|
|
217
|
-
if (!tid) return;
|
|
218
|
-
|
|
219
|
-
const built = await buildPayload({ tid, isReply: false });
|
|
220
|
-
if (!built) return;
|
|
221
|
-
|
|
222
|
-
if (!cidAllowed(built.topicData.cid, settings.cids)) return;
|
|
223
|
-
|
|
224
|
-
// Send content + embed (content ensures clickable link always)
|
|
225
|
-
await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] }, built.content);
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
Plugin.onTopicReply = async (data) => {
|
|
229
|
-
const settings = await getSettings();
|
|
230
|
-
if (!settings.notifyReplies) return;
|
|
231
|
-
if (!settings.webhookUrl) return;
|
|
232
|
-
|
|
233
|
-
const { tid, pid } = extractTidPid(data);
|
|
234
|
-
if (!tid || !pid) return;
|
|
235
|
-
|
|
236
|
-
const built = await buildPayload({ tid, pid, isReply: true });
|
|
237
|
-
if (!built) return;
|
|
238
|
-
|
|
239
|
-
if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
|
|
240
|
-
|
|
241
|
-
if (!cidAllowed(built.topicData.cid, settings.cids)) return;
|
|
242
|
-
|
|
243
|
-
await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] }, built.content);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
module.exports = Plugin;
|
|
1
|
+
// see chat explanation; trimmed for brevity
|