nodebb-plugin-discord-onekite 1.0.10 → 1.0.12
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 +5 -2
- package/library.js +101 -57
- package/package.json +1 -1
- package/plugin.json +8 -5
- package/static/lib/acp-toast.js +23 -0
package/README.md
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
# nodebb-plugin-discord-onekite v1.
|
|
2
|
-
|
|
1
|
+
# nodebb-plugin-discord-onekite v1.1.1
|
|
2
|
+
|
|
3
|
+
Fix:
|
|
4
|
+
- Makes embeds ultra-minimal (title/description/url only).
|
|
5
|
+
- On Discord 400 embed validation error, retries with plain `content` so a notification is always delivered.
|
package/library.js
CHANGED
|
@@ -20,6 +20,32 @@ function normalizeCids(value) {
|
|
|
20
20
|
return [];
|
|
21
21
|
}
|
|
22
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 isValidHttpUrl(s) {
|
|
41
|
+
try {
|
|
42
|
+
const u = new URL(s);
|
|
43
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
async function getSettings() {
|
|
24
50
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
25
51
|
return {
|
|
@@ -35,41 +61,79 @@ function cidAllowed(topicCid, allowedCids) {
|
|
|
35
61
|
}
|
|
36
62
|
|
|
37
63
|
async function postToDiscord(webhookUrl, payload) {
|
|
38
|
-
if (!webhookUrl) return;
|
|
39
|
-
|
|
40
64
|
const res = await request(webhookUrl, {
|
|
41
65
|
method: 'POST',
|
|
42
66
|
headers: { 'Content-Type': 'application/json' },
|
|
43
67
|
body: JSON.stringify(payload),
|
|
44
68
|
});
|
|
45
69
|
|
|
70
|
+
const bodyText = await res.body.text().catch(() => '');
|
|
71
|
+
|
|
46
72
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
47
|
-
const
|
|
48
|
-
|
|
73
|
+
const err = new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${bodyText}`);
|
|
74
|
+
err.statusCode = res.statusCode;
|
|
75
|
+
err.responseBody = bodyText;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function sendDiscord(webhookUrl, payload, fallbackContent) {
|
|
81
|
+
if (!webhookUrl) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await postToDiscord(webhookUrl, payload);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Discord embed validation error is commonly 400 with {"embeds":["0"]}
|
|
87
|
+
// Retry with plain content so notifications still arrive.
|
|
88
|
+
if (e && e.statusCode === 400 && fallbackContent) {
|
|
89
|
+
try {
|
|
90
|
+
await postToDiscord(webhookUrl, { content: fallbackContent });
|
|
91
|
+
return;
|
|
92
|
+
} catch (e2) {
|
|
93
|
+
// eslint-disable-next-line no-console
|
|
94
|
+
console.error(e2);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error(e);
|
|
49
99
|
}
|
|
50
100
|
}
|
|
51
101
|
|
|
52
|
-
async function
|
|
53
|
-
const baseUrl = meta.config.url;
|
|
102
|
+
async function buildPayload({ tid, pid, isReply }) {
|
|
103
|
+
const baseUrl = normalizeBaseUrl(meta.config.url);
|
|
54
104
|
|
|
55
|
-
const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', '
|
|
105
|
+
const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'mainPid']);
|
|
56
106
|
if (!topicData) return null;
|
|
57
107
|
|
|
58
|
-
const topicUrl =
|
|
59
|
-
const
|
|
108
|
+
const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
|
|
109
|
+
const url = isReply && pid ? `${topicUrl}/${pid}` : topicUrl;
|
|
110
|
+
|
|
111
|
+
const u = await user.getUserFields(topicData.uid, ['username']);
|
|
60
112
|
|
|
61
|
-
const
|
|
113
|
+
const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
|
|
114
|
+
const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
|
|
115
|
+
|
|
116
|
+
// Super-minimal embed to avoid Discord rejecting unknown/invalid fields.
|
|
62
117
|
const embed = {
|
|
63
|
-
title
|
|
64
|
-
|
|
65
|
-
timestamp: new Date(isReply ? Date.now() : (topicData.timestamp || Date.now())).toISOString(),
|
|
66
|
-
author: u?.username ? { name: u.username, icon_url: u.picture || undefined } : undefined,
|
|
67
|
-
footer: { text: 'NodeBB → Discord (Onekite)' },
|
|
118
|
+
title,
|
|
119
|
+
description: isReply ? `Réponse de **${authorName}**` : `Sujet créé par **${authorName}**`,
|
|
68
120
|
};
|
|
69
121
|
|
|
70
|
-
if (
|
|
122
|
+
if (isValidHttpUrl(url)) {
|
|
123
|
+
embed.url = url;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const content = isReply
|
|
127
|
+
? `🗨️ Nouvelle réponse : ${title}\n${url}`
|
|
128
|
+
: `🆕 Nouveau sujet : ${title}\n${url}`;
|
|
129
|
+
|
|
130
|
+
return { topicData, embed, content };
|
|
131
|
+
}
|
|
71
132
|
|
|
72
|
-
|
|
133
|
+
function extractTidPid(data) {
|
|
134
|
+
const tid = data?.tid || data?.topic?.tid || data?.post?.tid;
|
|
135
|
+
const pid = data?.pid || data?.post?.pid;
|
|
136
|
+
return { tid, pid };
|
|
73
137
|
}
|
|
74
138
|
|
|
75
139
|
const Plugin = {};
|
|
@@ -110,57 +174,37 @@ Plugin.addAdminNavigation = (header) => {
|
|
|
110
174
|
return header;
|
|
111
175
|
};
|
|
112
176
|
|
|
113
|
-
Plugin.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!settings.webhookUrl) return;
|
|
117
|
-
|
|
118
|
-
const tid = data?.tid || data?.topic?.tid;
|
|
119
|
-
if (!tid) return;
|
|
177
|
+
Plugin.onTopicPost = async (data) => {
|
|
178
|
+
const settings = await getSettings();
|
|
179
|
+
if (!settings.webhookUrl) return;
|
|
120
180
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
data?.topic?.isNew === true ||
|
|
124
|
-
(typeof data?.topic?.postcount === 'number' && data.topic.postcount === 1);
|
|
181
|
+
const { tid } = extractTidPid(data);
|
|
182
|
+
if (!tid) return;
|
|
125
183
|
|
|
126
|
-
|
|
184
|
+
const built = await buildPayload({ tid, isReply: false });
|
|
185
|
+
if (!built) return;
|
|
127
186
|
|
|
128
|
-
|
|
129
|
-
if (!built) return;
|
|
187
|
+
if (!cidAllowed(built.topicData.cid, settings.cids)) return;
|
|
130
188
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await postToDiscord(settings.webhookUrl, { embeds: [built.embed] });
|
|
134
|
-
} catch (err) {
|
|
135
|
-
// eslint-disable-next-line no-console
|
|
136
|
-
console.error(err);
|
|
137
|
-
}
|
|
189
|
+
await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
|
|
138
190
|
};
|
|
139
191
|
|
|
140
|
-
Plugin.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!settings.webhookUrl) return;
|
|
192
|
+
Plugin.onTopicReply = async (data) => {
|
|
193
|
+
const settings = await getSettings();
|
|
194
|
+
if (!settings.notifyReplies) return;
|
|
195
|
+
if (!settings.webhookUrl) return;
|
|
145
196
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (!pid || !tid) return;
|
|
197
|
+
const { tid, pid } = extractTidPid(data);
|
|
198
|
+
if (!tid || !pid) return;
|
|
149
199
|
|
|
150
|
-
|
|
151
|
-
|
|
200
|
+
const built = await buildPayload({ tid, pid, isReply: true });
|
|
201
|
+
if (!built) return;
|
|
152
202
|
|
|
153
|
-
|
|
154
|
-
if (!built) return;
|
|
203
|
+
if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
|
|
155
204
|
|
|
156
|
-
|
|
157
|
-
if (!cidAllowed(built.topicData.cid, settings.cids)) return;
|
|
205
|
+
if (!cidAllowed(built.topicData.cid, settings.cids)) return;
|
|
158
206
|
|
|
159
|
-
|
|
160
|
-
} catch (err) {
|
|
161
|
-
// eslint-disable-next-line no-console
|
|
162
|
-
console.error(err);
|
|
163
|
-
}
|
|
207
|
+
await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
|
|
164
208
|
};
|
|
165
209
|
|
|
166
210
|
module.exports = Plugin;
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
"method": "addAdminNavigation"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"hook": "action:topic.
|
|
17
|
-
"method": "
|
|
16
|
+
"hook": "action:topic.post",
|
|
17
|
+
"method": "onTopicPost"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
"hook": "action:
|
|
21
|
-
"method": "
|
|
20
|
+
"hook": "action:topic.reply",
|
|
21
|
+
"method": "onTopicReply"
|
|
22
22
|
}
|
|
23
23
|
],
|
|
24
|
-
"templates": "templates"
|
|
24
|
+
"templates": "templates",
|
|
25
|
+
"acpScripts": [
|
|
26
|
+
"static/lib/acp-toast.js"
|
|
27
|
+
]
|
|
25
28
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* global $, app, ajaxify */
|
|
3
|
+
|
|
4
|
+
(function () {
|
|
5
|
+
function showToastIfSaved() {
|
|
6
|
+
if (!ajaxify || !ajaxify.data || ajaxify.data.template !== 'admin/plugins/discord-onekite') return;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(window.location.href);
|
|
10
|
+
const saved = url.searchParams.get('saved');
|
|
11
|
+
if (saved === '1' || saved === 'true') {
|
|
12
|
+
if (window.app && typeof app.alertSuccess === 'function') {
|
|
13
|
+
app.alertSuccess('Paramètres enregistrés !');
|
|
14
|
+
}
|
|
15
|
+
url.searchParams.delete('saved');
|
|
16
|
+
window.history.replaceState({}, document.title, url.toString());
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
$(window).on('action:ajaxify.end', showToastIfSaved);
|
|
22
|
+
$(showToastIfSaved);
|
|
23
|
+
})();
|