nodebb-plugin-discord-onekite 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 +20 -0
- package/lib/controllers.js +9 -0
- package/library.js +169 -0
- package/package.json +14 -0
- package/plugin.json +14 -0
- package/static/lib/admin.js +45 -0
- package/static/templates/admin/plugins/discord-onekite.tpl +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# nodebb-plugin-discord-onekite (NodeBB v4.x)
|
|
2
|
+
|
|
3
|
+
Plugin NodeBB qui notifie un salon Discord via **webhook** lors :
|
|
4
|
+
- des **nouveaux sujets**
|
|
5
|
+
- et optionnellement des **réponses**
|
|
6
|
+
|
|
7
|
+
## Installation (local)
|
|
8
|
+
Depuis le dossier NodeBB :
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install /chemin/vers/nodebb-plugin-discord-onekite
|
|
12
|
+
./nodebb build
|
|
13
|
+
./nodebb restart
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
ACP → Plugins → **Discord Onekite**
|
|
18
|
+
- Webhook URL
|
|
19
|
+
- (option) Notifier les réponses
|
|
20
|
+
- Catégories à notifier (si aucune sélection : toutes)
|
package/library.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
|
|
8
|
+
const topics = require.main.require('./src/topics');
|
|
9
|
+
const user = require.main.require('./src/user');
|
|
10
|
+
|
|
11
|
+
const controllers = require('./lib/controllers');
|
|
12
|
+
|
|
13
|
+
// Settings are stored under this key in NodeBB's settings system
|
|
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
|
+
|
|
20
|
+
// Sometimes saved as "1,2,3"
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
return value
|
|
23
|
+
.split(',')
|
|
24
|
+
.map(s => s.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function getSettings() {
|
|
31
|
+
const s = await meta.settings.get(SETTINGS_KEY);
|
|
32
|
+
return {
|
|
33
|
+
webhookUrl: (s && s.webhookUrl) ? String(s.webhookUrl).trim() : '',
|
|
34
|
+
notifyReplies: !!(s && (s.notifyReplies === true || s.notifyReplies === 'on' || s.notifyReplies === 'true')),
|
|
35
|
+
cids: normalizeCids(s && s.cids),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cidAllowed(topicCid, allowedCids) {
|
|
40
|
+
// IMPORTANT (per request): if no categories are selected => notify ALL categories.
|
|
41
|
+
if (!allowedCids || allowedCids.length === 0) return true;
|
|
42
|
+
return allowedCids.includes(String(topicCid));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function postToDiscord(webhookUrl, payload) {
|
|
46
|
+
if (!webhookUrl) return;
|
|
47
|
+
|
|
48
|
+
const res = await request(webhookUrl, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify(payload),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Discord webhook success is commonly 204 No Content
|
|
55
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
56
|
+
const text = await res.body.text().catch(() => '');
|
|
57
|
+
throw new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${text}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function buildTopicEmbed({ tid, pid, type }) {
|
|
62
|
+
const baseUrl = meta.config.url;
|
|
63
|
+
|
|
64
|
+
// For v4.x, these fields are stable and enough for URL + title
|
|
65
|
+
const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'timestamp', 'mainPid']);
|
|
66
|
+
if (!topicData) return null;
|
|
67
|
+
|
|
68
|
+
const topicUrl = `${baseUrl}/topic/${topicData.slug || topicData.tid}`;
|
|
69
|
+
|
|
70
|
+
// Author (topic author). If you prefer reply author, adjust to read post author.
|
|
71
|
+
const u = await user.getUserFields(topicData.uid, ['username', 'picture']);
|
|
72
|
+
|
|
73
|
+
const isReply = type === 'reply';
|
|
74
|
+
const embed = {
|
|
75
|
+
title: topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet'),
|
|
76
|
+
url: isReply && pid ? `${topicUrl}/${pid}` : topicUrl,
|
|
77
|
+
timestamp: new Date(isReply ? Date.now() : (topicData.timestamp || Date.now())).toISOString(),
|
|
78
|
+
author: u?.username ? { name: u.username, icon_url: u.picture || undefined } : undefined,
|
|
79
|
+
footer: { text: 'NodeBB → Discord (Onekite)' },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (isReply) {
|
|
83
|
+
embed.description = 'Nouvelle réponse dans un sujet.';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { topicData, embed };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const Plugin = {};
|
|
90
|
+
|
|
91
|
+
// Mount ACP page route (NodeBB v4.x)
|
|
92
|
+
Plugin.init = async ({ router }) => {
|
|
93
|
+
routeHelpers.setupAdminPageRoute(
|
|
94
|
+
router,
|
|
95
|
+
'/admin/plugins/discord-onekite',
|
|
96
|
+
[],
|
|
97
|
+
controllers.renderAdminPage
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Add entry under ACP -> Plugins
|
|
102
|
+
Plugin.addAdminNavigation = (header) => {
|
|
103
|
+
header.plugins.push({
|
|
104
|
+
route: '/plugins/discord-onekite',
|
|
105
|
+
icon: 'fa-bell',
|
|
106
|
+
name: 'Discord Onekite',
|
|
107
|
+
});
|
|
108
|
+
return header;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// NEW TOPICS ONLY (unless notifyReplies enabled for posts)
|
|
112
|
+
Plugin.onTopicSave = async (data) => {
|
|
113
|
+
try {
|
|
114
|
+
const { webhookUrl, cids } = await getSettings();
|
|
115
|
+
if (!webhookUrl) return;
|
|
116
|
+
|
|
117
|
+
const tid = data?.tid || data?.topic?.tid;
|
|
118
|
+
if (!tid) return;
|
|
119
|
+
|
|
120
|
+
// In v4.x, new topics often expose isNew; as a fallback, we detect postcount=1 if present.
|
|
121
|
+
const isNew =
|
|
122
|
+
data?.isNew === true ||
|
|
123
|
+
data?.topic?.isNew === true ||
|
|
124
|
+
(typeof data?.topic?.postcount === 'number' && data.topic.postcount === 1);
|
|
125
|
+
|
|
126
|
+
if (!isNew) return;
|
|
127
|
+
|
|
128
|
+
const built = await buildTopicEmbed({ tid, type: 'topic' });
|
|
129
|
+
if (!built) return;
|
|
130
|
+
|
|
131
|
+
if (!cidAllowed(built.topicData.cid, cids)) return;
|
|
132
|
+
|
|
133
|
+
await postToDiscord(webhookUrl, { embeds: [built.embed] });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.error(err);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// REPLIES (optional)
|
|
141
|
+
Plugin.onPostSave = async (data) => {
|
|
142
|
+
try {
|
|
143
|
+
const { webhookUrl, notifyReplies, cids } = await getSettings();
|
|
144
|
+
if (!webhookUrl || !notifyReplies) return;
|
|
145
|
+
|
|
146
|
+
const pid = data?.pid || data?.post?.pid;
|
|
147
|
+
const tid = data?.tid || data?.post?.tid;
|
|
148
|
+
if (!pid || !tid) return;
|
|
149
|
+
|
|
150
|
+
// Only new posts (avoid edits). v4.x commonly provides isNew for creates.
|
|
151
|
+
const isNew = data?.isNew === true || data?.post?.isNew === true;
|
|
152
|
+
if (!isNew) return;
|
|
153
|
+
|
|
154
|
+
const built = await buildTopicEmbed({ tid, pid, type: 'reply' });
|
|
155
|
+
if (!built) return;
|
|
156
|
+
|
|
157
|
+
// Avoid notifying the main post (already covered by topic.save)
|
|
158
|
+
if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
|
|
159
|
+
|
|
160
|
+
if (!cidAllowed(built.topicData.cid, cids)) return;
|
|
161
|
+
|
|
162
|
+
await postToDiscord(webhookUrl, { embeds: [built.embed] });
|
|
163
|
+
} catch (err) {
|
|
164
|
+
// eslint-disable-next-line no-console
|
|
165
|
+
console.error(err);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
module.exports = Plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodebb-plugin-discord-onekite",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Discord webhook notifier for Onekite (NodeBB v4.x only)",
|
|
5
|
+
"main": "library.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": ["nodebb", "plugin", "discord", "webhook", "notifications"],
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"undici": "^6.0.0"
|
|
10
|
+
},
|
|
11
|
+
"nbbpm": {
|
|
12
|
+
"compatibility": "^4.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-discord-onekite",
|
|
3
|
+
"name": "Discord Onekite Notifier",
|
|
4
|
+
"description": "Notifie Discord via webhook pour nouveaux sujets et/ou réponses, filtrable par catégories (NodeBB v4.x uniquement).",
|
|
5
|
+
"library": "./library.js",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
8
|
+
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
|
9
|
+
{ "hook": "action:topic.save", "method": "onTopicSave" },
|
|
10
|
+
{ "hook": "action:post.save", "method": "onPostSave" }
|
|
11
|
+
],
|
|
12
|
+
"templates": "static/templates",
|
|
13
|
+
"acpScripts": ["static/lib/admin.js"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* global $, app */
|
|
4
|
+
|
|
5
|
+
define('admin/plugins/discord-onekite', ['settings', 'api'], function (settings, api) {
|
|
6
|
+
const ACP = {};
|
|
7
|
+
|
|
8
|
+
ACP.init = async function () {
|
|
9
|
+
// Charge settings existants (webhookUrl, notifyReplies, cids)
|
|
10
|
+
settings.load('discord-onekite', $('.discord-onekite-settings'));
|
|
11
|
+
|
|
12
|
+
// Charge les catégories via l'API NodeBB (client api module ajoute /api)
|
|
13
|
+
try {
|
|
14
|
+
const res = await api.get('/categories');
|
|
15
|
+
const categories = (res && res.categories) ? res.categories : [];
|
|
16
|
+
|
|
17
|
+
const $select = $('#cids');
|
|
18
|
+
$select.empty();
|
|
19
|
+
|
|
20
|
+
categories
|
|
21
|
+
.filter(c => c && typeof c.cid !== 'undefined' && c.name)
|
|
22
|
+
.forEach(c => {
|
|
23
|
+
$('<option />')
|
|
24
|
+
.val(String(c.cid))
|
|
25
|
+
.text(c.name)
|
|
26
|
+
.appendTo($select);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Recharge settings pour re-sélectionner les catégories déjà sauvegardées
|
|
30
|
+
settings.load('discord-onekite', $('.discord-onekite-settings'));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.error('[discord-onekite] Could not load categories', e);
|
|
34
|
+
app.alertError('Impossible de charger la liste des catégories.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
$('#save').on('click', function () {
|
|
38
|
+
settings.save('discord-onekite', $('.discord-onekite-settings'), function () {
|
|
39
|
+
app.alertSuccess('Paramètres enregistrés !');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return ACP;
|
|
45
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
<h4>Discord Onekite</h4>
|
|
3
|
+
<p class="text-muted">
|
|
4
|
+
Envoie une notification dans Discord via webhook, avec filtres par catégories.
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<form role="form" class="discord-onekite-settings">
|
|
8
|
+
<div class="mb-3">
|
|
9
|
+
<label class="form-label" for="webhookUrl">Discord Webhook URL</label>
|
|
10
|
+
<input type="text" class="form-control" id="webhookUrl" name="webhookUrl" placeholder="https://discord.com/api/webhooks/..." />
|
|
11
|
+
<p class="form-text text-muted">
|
|
12
|
+
Discord → Salon #notifications → Intégrations → Webhooks → Copier l’URL.
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="form-check mb-3">
|
|
17
|
+
<input class="form-check-input" type="checkbox" id="notifyReplies" name="notifyReplies">
|
|
18
|
+
<label class="form-check-label" for="notifyReplies">
|
|
19
|
+
Notifier aussi les réponses (si décoché : uniquement les nouveaux sujets)
|
|
20
|
+
</label>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="mb-3">
|
|
24
|
+
<label class="form-label" for="cids">Catégories à notifier</label>
|
|
25
|
+
<select class="form-select" id="cids" name="cids" multiple size="12"></select>
|
|
26
|
+
<p class="form-text text-muted">
|
|
27
|
+
Si aucune catégorie n’est sélectionnée : <strong>toutes les catégories</strong> seront notifiées.
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- IMPORT admin/partials/save_button.tpl -->
|
|
32
|
+
</form>
|
|
33
|
+
</div>
|