nodebb-plugin-discord-onekite 1.0.16 → 1.1.7
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 +8 -6
- package/library.js +219 -1
- package/package.json +1 -1
- package/static/lib/admin.js +1 -0
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# nodebb-plugin-discord-onekite v1.1.
|
|
1
|
+
# nodebb-plugin-discord-onekite v1.1.6
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
3
|
+
This fixes the broken v1.1.5 zip (missing files).
|
|
4
|
+
|
|
5
|
+
ACP:
|
|
6
|
+
- Standard NodeBB save disk button
|
|
7
|
+
- AJAX save endpoint + NodeBB toast
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Discord:
|
|
10
|
+
- Single clickable line "Nouveau sujet : [Titre](URL)" then excerpt below
|
package/library.js
CHANGED
|
@@ -1 +1,219 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
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
|
+
|
|
12
|
+
const controllers = require('./lib/controllers');
|
|
13
|
+
|
|
14
|
+
const SETTINGS_KEY = 'discord-onekite';
|
|
15
|
+
|
|
16
|
+
function normalizeCids(value) {
|
|
17
|
+
if (!value) return [];
|
|
18
|
+
if (Array.isArray(value)) return value.map(v => String(v)).filter(Boolean);
|
|
19
|
+
if (typeof value === 'string') return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeBaseUrl(url) {
|
|
24
|
+
if (!url) return '';
|
|
25
|
+
let s = String(url).trim();
|
|
26
|
+
if (!/^https?:\/\//i.test(s)) s = `https://${s}`;
|
|
27
|
+
s = s.replace(/\/+$/, '');
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureAbsoluteUrl(baseUrl, maybeUrl) {
|
|
32
|
+
if (!maybeUrl) return '';
|
|
33
|
+
const s = String(maybeUrl);
|
|
34
|
+
if (/^https?:\/\//i.test(s)) return s;
|
|
35
|
+
if (s.startsWith('//')) return `https:${s}`;
|
|
36
|
+
if (s.startsWith('/')) return `${baseUrl}${s}`;
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stripHtml(html) {
|
|
41
|
+
if (!html) return '';
|
|
42
|
+
return String(html)
|
|
43
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
44
|
+
.replace(/<\/p>/gi, '\n')
|
|
45
|
+
.replace(/<[^>]*>/g, '')
|
|
46
|
+
.replace(/ /g, ' ')
|
|
47
|
+
.replace(/&/g, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>')
|
|
50
|
+
.replace(/'/g, "'")
|
|
51
|
+
.replace(/"/g, '"')
|
|
52
|
+
.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function truncate(s, n) {
|
|
56
|
+
const str = String(s || '');
|
|
57
|
+
if (str.length <= n) return str;
|
|
58
|
+
return str.slice(0, n - 1) + '…';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getSettings() {
|
|
62
|
+
const s = await meta.settings.get(SETTINGS_KEY);
|
|
63
|
+
return {
|
|
64
|
+
webhookUrl: (s && s.webhookUrl) ? String(s.webhookUrl).trim() : '',
|
|
65
|
+
notifyReplies: !!(s && (s.notifyReplies === true || s.notifyReplies === 'on' || s.notifyReplies === 'true')),
|
|
66
|
+
cids: normalizeCids(s && s.cids),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cidAllowed(topicCid, allowedCids) {
|
|
71
|
+
if (!allowedCids || allowedCids.length === 0) return true;
|
|
72
|
+
return allowedCids.includes(String(topicCid));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function postToDiscord(webhookUrl, payload) {
|
|
76
|
+
const res = await request(webhookUrl, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify(payload),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const bodyText = await res.body.text().catch(() => '');
|
|
83
|
+
|
|
84
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
85
|
+
const err = new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${bodyText}`);
|
|
86
|
+
err.statusCode = res.statusCode;
|
|
87
|
+
err.responseBody = bodyText;
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function sendDiscord(webhookUrl, payload) {
|
|
93
|
+
if (!webhookUrl) return;
|
|
94
|
+
try {
|
|
95
|
+
await postToDiscord(webhookUrl, payload);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// If Discord rejects embeds, retry with plain content only.
|
|
98
|
+
if (e && e.statusCode === 400 && payload && payload.content) {
|
|
99
|
+
try {
|
|
100
|
+
await postToDiscord(webhookUrl, { content: payload.content });
|
|
101
|
+
return;
|
|
102
|
+
} catch (e2) {
|
|
103
|
+
console.error(e2);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.error(e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function getPostExcerpt(pid) {
|
|
111
|
+
if (!pid) return '';
|
|
112
|
+
try {
|
|
113
|
+
const p = await posts.getPostFields(pid, ['content']);
|
|
114
|
+
const text = stripHtml(p && p.content);
|
|
115
|
+
return truncate(text, 500);
|
|
116
|
+
} catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function buildPayload({ tid, pid, isReply }) {
|
|
122
|
+
const baseUrl = normalizeBaseUrl(meta.config.url);
|
|
123
|
+
|
|
124
|
+
const topicData = await topics.getTopicFields(tid, ['tid', 'cid', 'title', 'slug', 'mainPid']);
|
|
125
|
+
if (!topicData) return null;
|
|
126
|
+
|
|
127
|
+
const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
|
|
128
|
+
const targetUrl = (isReply && pid) ? `${topicUrl}/${pid}` : topicUrl;
|
|
129
|
+
|
|
130
|
+
const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
|
|
131
|
+
const excerpt = await getPostExcerpt(isReply ? pid : topicData.mainPid);
|
|
132
|
+
|
|
133
|
+
// Single clickable link line + excerpt below
|
|
134
|
+
const linkLine = isReply
|
|
135
|
+
? `🗨️ Nouvelle réponse : [${title}](${targetUrl})`
|
|
136
|
+
: `🆕 Nouveau sujet : [${title}](${topicUrl})`;
|
|
137
|
+
|
|
138
|
+
const content = [linkLine, excerpt].filter(Boolean).join('\n');
|
|
139
|
+
|
|
140
|
+
const embed = { title, description: excerpt || '' , url: targetUrl };
|
|
141
|
+
|
|
142
|
+
return { topicData, content, embed };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function extractTidPid(data) {
|
|
146
|
+
const tid = data?.tid || data?.topic?.tid || data?.post?.tid;
|
|
147
|
+
const pid = data?.pid || data?.post?.pid;
|
|
148
|
+
return { tid, pid };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const Plugin = {};
|
|
152
|
+
|
|
153
|
+
Plugin.init = async ({ router }) => {
|
|
154
|
+
routeHelpers.setupAdminPageRoute(
|
|
155
|
+
router,
|
|
156
|
+
'/admin/plugins/discord-onekite',
|
|
157
|
+
[],
|
|
158
|
+
controllers.renderAdminPage
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// AJAX save endpoint (used by admin.js via api.post)
|
|
162
|
+
router.post('/admin/plugins/discord-onekite/save',
|
|
163
|
+
middleware.admin.checkPrivileges,
|
|
164
|
+
async (req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
const payload = {
|
|
167
|
+
webhookUrl: req.body.webhookUrl || '',
|
|
168
|
+
notifyReplies: req.body.notifyReplies ? 'on' : '',
|
|
169
|
+
cids: req.body.cids || '',
|
|
170
|
+
};
|
|
171
|
+
await meta.settings.set(SETTINGS_KEY, payload);
|
|
172
|
+
res.json({ ok: true });
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.error('[discord-onekite] save failed', e);
|
|
175
|
+
res.status(500).json({ ok: false });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
Plugin.addAdminNavigation = (header) => {
|
|
182
|
+
header.plugins.push({
|
|
183
|
+
route: '/plugins/discord-onekite',
|
|
184
|
+
icon: 'fa-bell',
|
|
185
|
+
name: 'Discord Onekite',
|
|
186
|
+
});
|
|
187
|
+
return header;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
Plugin.onTopicPost = async (data) => {
|
|
191
|
+
const settings = await getSettings();
|
|
192
|
+
if (!settings.webhookUrl) return;
|
|
193
|
+
|
|
194
|
+
const { tid } = extractTidPid(data);
|
|
195
|
+
if (!tid) return;
|
|
196
|
+
|
|
197
|
+
const built = await buildPayload({ tid, isReply: false });
|
|
198
|
+
if (!built) return;
|
|
199
|
+
if (!cidAllowed(built.topicData.cid, settings.cids)) return;
|
|
200
|
+
|
|
201
|
+
await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] });
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
Plugin.onTopicReply = async (data) => {
|
|
205
|
+
const settings = await getSettings();
|
|
206
|
+
if (!settings.notifyReplies) return;
|
|
207
|
+
if (!settings.webhookUrl) return;
|
|
208
|
+
|
|
209
|
+
const { tid, pid } = extractTidPid(data);
|
|
210
|
+
if (!tid || !pid) return;
|
|
211
|
+
|
|
212
|
+
const built = await buildPayload({ tid, pid, isReply: true });
|
|
213
|
+
if (!built) return;
|
|
214
|
+
if (!cidAllowed(built.topicData.cid, settings.cids)) return;
|
|
215
|
+
|
|
216
|
+
await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] });
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
module.exports = Plugin;
|
package/package.json
CHANGED
package/static/lib/admin.js
CHANGED
|
@@ -21,6 +21,7 @@ define('admin/plugins/discord-onekite', ['api'], function (api) {
|
|
|
21
21
|
cids: getMultiSelectValues('#cids'),
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
// api.post prefixes /api automatically, so server route must be /admin/...
|
|
24
25
|
api.post('/admin/plugins/discord-onekite/save', payload).then(function () {
|
|
25
26
|
if (window.app && typeof app.alertSuccess === 'function') {
|
|
26
27
|
app.alertSuccess('Paramètres enregistrés !');
|