@arnaudw38/nodebb-plugin-spam-be-gone 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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Aziz Khoury
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # nodebb-plugin-spam-be-gone
2
+
3
+ Anti-spam plugin for **NodeBB 4.x** with **Cloudflare Turnstile** (Turnstile-only fork).
4
+
5
+ > This fork removes all **reCAPTCHA** and **hCaptcha** support and uses **Cloudflare Turnstile only** for human verification.
6
+
7
+ ## Highlights
8
+
9
+ - ✅ **NodeBB 4.x compatible**
10
+ - ✅ **Cloudflare Turnstile only** (no reCAPTCHA / no hCaptcha)
11
+ - ✅ Turnstile challenge on **registration**
12
+ - ✅ Optional Turnstile challenge on **login**
13
+ - ✅ **Akismet** checks for posts/topics
14
+ - ✅ **StopForumSpam** reporting + queue tooling
15
+ - ✅ **Project Honeypot (http:BL)** checks during registration
16
+
17
+ ## Compatibility
18
+
19
+ | Component | Version |
20
+ |---|---|
21
+ | NodeBB | `^4.0.0` |
22
+ | nbbpm compatibility | `^4.0.0` |
23
+
24
+ ## Installation (npm)
25
+
26
+ From your NodeBB root directory:
27
+
28
+ ```bash
29
+ npm install nodebb-plugin-spam-be-gone
30
+ ./nodebb build
31
+ ./nodebb restart
32
+ ```
33
+
34
+ Then enable the plugin in:
35
+
36
+ **Admin → Extend → Plugins** (or your NodeBB plugin management page)
37
+
38
+ ## Configuration
39
+
40
+ Open:
41
+
42
+ **Admin → Plugins → Spam Be Gone**
43
+
44
+ ### Cloudflare Turnstile setup
45
+
46
+ 1. Create a **Turnstile widget** in your Cloudflare dashboard.
47
+ 2. Copy your **Site Key** and **Secret Key**.
48
+ 3. In the plugin settings, enable Turnstile and paste both keys.
49
+ 4. Save settings.
50
+ 5. Rebuild & restart NodeBB if needed.
51
+
52
+ ### Turnstile options supported (plugin settings)
53
+
54
+ - `turnstileEnabled`
55
+ - `turnstileSiteKey`
56
+ - `turnstileSecretKey`
57
+ - `loginTurnstileEnabled`
58
+ - `turnstileTheme`
59
+ - `turnstileSize`
60
+ - `turnstileAppearance`
61
+
62
+ ## What changed in this fork
63
+
64
+ ### Removed
65
+
66
+ - ❌ Google **reCAPTCHA** integration
67
+ - ❌ **hCaptcha** integration
68
+ - ❌ Legacy captcha UI/assets/styles related to those providers
69
+
70
+ ### Added / refactored
71
+
72
+ - ✅ **Cloudflare Turnstile** verification (server-side + client-side)
73
+ - ✅ Cleaner settings handling / flag parsing
74
+ - ✅ More robust client injection (idempotent script/widget rendering)
75
+ - ✅ Cleanup for modern NodeBB 4.x plugin usage
76
+
77
+ ## Migration notes (from reCAPTCHA / hCaptcha)
78
+
79
+ If you were previously using reCAPTCHA or hCaptcha in this plugin:
80
+
81
+ 1. Remove old keys from the plugin settings (if still present in your DB/config).
82
+ 2. Create new Cloudflare Turnstile keys.
83
+ 3. Configure Turnstile settings in the plugin admin page.
84
+ 4. Test registration and (optionally) login challenge flows.
85
+
86
+ > Old captcha providers are no longer used by this fork.
87
+
88
+
89
+ ## npm publishing checklist (fork maintainers)
90
+
91
+ Before publishing your fork, update these fields in `package.json`:
92
+
93
+ - `name` (your npm package name / scope)
94
+ - `version`
95
+ - `repository`
96
+ - `bugs`
97
+ - `homepage`
98
+
99
+ The current values are placeholders (`your-scope` / `your-org`) and should be replaced with your actual npm scope and GitHub repository.
100
+
101
+ ## Troubleshooting
102
+
103
+ ### Turnstile widget does not appear
104
+
105
+ - Check that `turnstileEnabled` is enabled.
106
+ - Confirm your **Site Key** is valid.
107
+ - Ensure your forum domain matches the domain configured in Cloudflare Turnstile.
108
+ - Rebuild NodeBB assets (`./nodebb build`) and restart.
109
+
110
+ ### Verification fails on submit
111
+
112
+ - Confirm your **Secret Key** is correct.
113
+ - Check server connectivity to Cloudflare Turnstile verification endpoint.
114
+ - Inspect NodeBB logs for plugin validation errors.
115
+
116
+ ### Login challenge not showing
117
+
118
+ - Confirm `loginTurnstileEnabled` is enabled.
119
+ - Verify your active theme/login template is using the plugin hook output correctly.
120
+
121
+ ## Development notes
122
+
123
+ This package is intentionally kept minimal for easier deployment:
124
+
125
+ - No ESLint/tooling requirements for runtime
126
+ - No `package-lock.json` included in this fork package
127
+ - Turnstile-only code path
128
+
129
+ ## License
130
+
131
+ MIT
132
+
133
+ ## Credits
134
+
135
+ - Original plugin authors/maintainers of `nodebb-plugin-spam-be-gone`
136
+ - Refactor/fork updates for NodeBB 4.x + Cloudflare Turnstile
package/lib/akismet.js ADDED
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const NodeBBVersion = require(require.main.require('./src/constants').paths.currentPackage).version;
4
+ const SpamBeGoneVersion = require('../package.json').version;
5
+
6
+ const Akismet = module.exports;
7
+
8
+ Akismet.verified = false;
9
+ Akismet.api_key = '';
10
+ Akismet.blog = '';
11
+
12
+ async function callAkismet(uri, data) {
13
+ const headers = {
14
+ 'Content-Type': 'application/x-www-form-urlencoded',
15
+ 'User-Agent': `NodeBB/${NodeBBVersion} | nodebb-plugin-spam-be-gone/${SpamBeGoneVersion}`,
16
+ };
17
+ data.api_key = data.api_key || Akismet.api_key;
18
+ data.blog = data.blog || Akismet.blog;
19
+
20
+ const response = await fetch(uri, {
21
+ method: 'POST',
22
+ headers,
23
+ body: new URLSearchParams(data).toString(),
24
+ });
25
+
26
+ const responseBody = await response.text();
27
+ return responseBody;
28
+ }
29
+
30
+ Akismet.verifyKey = async (key, blog) => {
31
+ Akismet.api_key = key;
32
+ Akismet.blog = blog;
33
+
34
+ try {
35
+ const responseBody = await callAkismet(
36
+ 'https://rest.akismet.com/1.1/verify-key',
37
+ { api_key: key, blog }
38
+ );
39
+
40
+ Akismet.verified = responseBody === 'valid';
41
+
42
+ return Akismet.verified;
43
+ } catch (error) {
44
+ console.error('Error verifying Akismet API key:', error);
45
+ return false;
46
+ }
47
+ };
48
+
49
+ Akismet.checkSpam = async (data) => {
50
+ try {
51
+ const responseBody = await callAkismet('https://rest.akismet.com/1.1/comment-check', data);
52
+ const isSpam = responseBody === 'true';
53
+ return isSpam;
54
+ } catch (error) {
55
+ console.error('Error checking spam:', error);
56
+ throw error;
57
+ }
58
+ };
59
+
60
+ Akismet.submitSpam = async (data) => {
61
+ try {
62
+ await callAkismet('https://rest.akismet.com/1.1/submit-spam', data);
63
+ } catch (error) {
64
+ console.error('Error submittin spam:', error);
65
+ }
66
+ };
67
+
68
+ Akismet.submitHam = async (data) => {
69
+ try {
70
+ await callAkismet('https://rest.akismet.com/1.1/submit-ham', data);
71
+ } catch (error) {
72
+ console.error('Error submittin ham:', error);
73
+ }
74
+ };
75
+
package/library.js ADDED
@@ -0,0 +1,404 @@
1
+ 'use strict';
2
+
3
+ const util = require('util');
4
+ const https = require('https');
5
+ const querystring = require('querystring');
6
+ const Honeypot = require('project-honeypot');
7
+ const stopforumspam = require('stopforumspam');
8
+
9
+ const winston = require.main.require('winston');
10
+ const nconf = require.main.require('nconf');
11
+ const Meta = require.main.require('./src/meta');
12
+ const User = require.main.require('./src/user');
13
+ const Topics = require.main.require('./src/topics');
14
+ const db = require.main.require('./src/database');
15
+
16
+ const pluginData = require('./plugin.json');
17
+ const akismet = require('./lib/akismet');
18
+
19
+ let honeypot;
20
+ let pluginSettings = {};
21
+
22
+ const Plugin = module.exports;
23
+ pluginData.nbbId = pluginData.id.replace(/nodebb-plugin-/, '');
24
+ Plugin.nbbId = pluginData.nbbId;
25
+ Plugin.middleware = {};
26
+
27
+ function isOn(value) {
28
+ return value === 'on' || value === true;
29
+ }
30
+
31
+ async function getSettings() {
32
+ return Meta.settings.get(pluginData.nbbId);
33
+ }
34
+
35
+ function getTurnstileConfigFromSettings(settings) {
36
+ if (!isOn(settings.turnstileEnabled)) {
37
+ return null;
38
+ }
39
+ if (!settings.turnstileSiteKey || !settings.turnstileSecretKey) {
40
+ return null;
41
+ }
42
+ return {
43
+ siteKey: settings.turnstileSiteKey,
44
+ targetId: `${pluginData.nbbId}-turnstile-target`,
45
+ addLoginTurnstile: isOn(settings.loginTurnstileEnabled),
46
+ theme: settings.turnstileTheme || 'auto',
47
+ size: settings.turnstileSize || 'normal',
48
+ appearance: settings.turnstileAppearance || 'always',
49
+ language: (Meta.config.defaultLang || 'auto').toLowerCase(),
50
+ };
51
+ }
52
+
53
+ Plugin.middleware.isAdminOrGlobalMod = function (req, res, next) {
54
+ User.isAdminOrGlobalMod(req.uid, (err, isAdminOrGlobalMod) => {
55
+ if (err) {
56
+ return next(err);
57
+ }
58
+ if (isAdminOrGlobalMod) {
59
+ return next();
60
+ }
61
+ res.status(401).json({ message: '[[spam-be-gone:not-allowed]]' });
62
+ });
63
+ };
64
+
65
+ Plugin.middleware.checkStopForumSpam = function (req, res, next) {
66
+ if (!pluginSettings.stopforumspamEnabled) {
67
+ return res.status(400).send({ message: '[[spam-be-gone:sfs-not-enabled]]' });
68
+ }
69
+ if (!pluginSettings.stopforumspamApiKey) {
70
+ return res.status(400).send({ message: '[[spam-be-gone:sfs-api-key-not-set]]' });
71
+ }
72
+ next();
73
+ };
74
+
75
+ Plugin.load = async function (params) {
76
+ const settings = await getSettings();
77
+ if (!settings) {
78
+ winston.warn(`[plugins/${pluginData.nbbId}] Settings not set or could not be retrieved!`);
79
+ return;
80
+ }
81
+
82
+ if (isOn(settings.akismetEnabled)) {
83
+ if (settings.akismetApiKey) {
84
+ if (!await akismet.verifyKey(settings.akismetApiKey, nconf.get('url'))) {
85
+ winston.error(`[plugins/${pluginData.nbbId}] Unable to verify Akismet API key.`);
86
+ }
87
+ } else {
88
+ winston.error(`[plugins/${pluginData.nbbId}] Akismet API Key not set!`);
89
+ }
90
+ }
91
+
92
+ if (isOn(settings.honeypotEnabled)) {
93
+ if (settings.honeypotApiKey) {
94
+ honeypot = Honeypot(settings.honeypotApiKey);
95
+ } else {
96
+ winston.error(`[plugins/${pluginData.nbbId}] Honeypot API Key not set!`);
97
+ }
98
+ }
99
+
100
+ if (!settings.akismetMinReputationHam) {
101
+ settings.akismetMinReputationHam = 10;
102
+ }
103
+ if (settings.stopforumspamApiKey) {
104
+ stopforumspam.Key(settings.stopforumspamApiKey);
105
+ }
106
+
107
+ pluginSettings = settings;
108
+
109
+ const routeHelpers = require.main.require('./src/routes/helpers');
110
+ routeHelpers.setupAdminPageRoute(params.router, `/admin/plugins/${pluginData.nbbId}`, renderAdmin);
111
+
112
+ params.router.post(`/api/user/:userslug/${pluginData.nbbId}/report`, Plugin.middleware.isAdminOrGlobalMod, Plugin.middleware.checkStopForumSpam, Plugin.report);
113
+ params.router.post(`/api/user/:username/${pluginData.nbbId}/report/queue`, Plugin.middleware.isAdminOrGlobalMod, Plugin.middleware.checkStopForumSpam, Plugin.reportFromQueue);
114
+ };
115
+
116
+ async function renderAdmin(req, res) {
117
+ let akismetStats = await db.getObject(`${pluginData.nbbId}:akismet`);
118
+ akismetStats = { ...{ checks: 0, spam: 0 }, ...akismetStats };
119
+ res.render(`admin/plugins/${pluginData.nbbId}`, {
120
+ nbbId: pluginData.nbbId,
121
+ akismet: akismetStats,
122
+ title: 'Spam Be Gone',
123
+ });
124
+ }
125
+
126
+ Plugin.report = async function (req, res, next) {
127
+ try {
128
+ const uid = await User.getUidByUserslug(req.params.userslug);
129
+ if (!uid) {
130
+ return next(new Error('[[error:no-user]]'));
131
+ }
132
+ const [isAdmin, fields, ips] = await Promise.all([
133
+ User.isAdministrator(uid),
134
+ User.getUserFields(uid, ['username', 'email', 'uid']),
135
+ User.getIPs(uid, 0),
136
+ ]);
137
+ if (isAdmin) {
138
+ return res.status(403).send({ message: '[[spam-be-gone:cant-report-admin]]' });
139
+ }
140
+ await stopforumspam.submit({ ip: ips[0], email: fields.email, username: fields.username }, `Manual submission from user: ${req.uid} to user: ${fields.uid} via ${pluginData.id}`);
141
+ res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
142
+ } catch (err) {
143
+ winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}`);
144
+ res.status(400).json({ message: err.message || 'Something went wrong' });
145
+ }
146
+ };
147
+
148
+ Plugin.reportFromQueue = async (req, res) => {
149
+ const data = await db.getObject(`registration:queue:name:${req.params.username}`);
150
+ if (!data) {
151
+ return res.status(400).json({ message: '[[error:no-user]]' });
152
+ }
153
+ const submitData = { ip: data.ip, email: data.email, username: data.username };
154
+ try {
155
+ await stopforumspam.submit(submitData, `Manual submission from user: ${req.uid} to user: ${data.username} via ${pluginData.id}`);
156
+ res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
157
+ } catch (err) {
158
+ winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}\n${JSON.stringify(submitData, null, 4)}`);
159
+ res.status(400).json({ message: err.message || 'Something went wrong' });
160
+ }
161
+ };
162
+
163
+ Plugin.appendConfig = async (data) => {
164
+ data['spam-be-gone'] = data['spam-be-gone'] || {};
165
+ const settings = await getSettings();
166
+ const turnstile = getTurnstileConfigFromSettings(settings || {});
167
+ if (turnstile) {
168
+ data['spam-be-gone'].turnstile = {
169
+ siteKey: turnstile.siteKey,
170
+ theme: turnstile.theme,
171
+ size: turnstile.size,
172
+ appearance: turnstile.appearance,
173
+ language: turnstile.language,
174
+ targetId: turnstile.targetId,
175
+ };
176
+ }
177
+ return data;
178
+ };
179
+
180
+ Plugin.addCaptcha = async (data) => {
181
+ function addChallenge(templateData, enableOnLogin, challenge) {
182
+ if (Array.isArray(templateData.regFormEntry)) {
183
+ templateData.regFormEntry.push(challenge);
184
+ } else if (Array.isArray(templateData.loginFormEntry)) {
185
+ if (enableOnLogin) {
186
+ templateData.loginFormEntry.push(challenge);
187
+ }
188
+ } else {
189
+ templateData.captcha = challenge;
190
+ }
191
+ }
192
+
193
+ if (!data.templateData) {
194
+ return data;
195
+ }
196
+
197
+ const settings = await getSettings();
198
+ const turnstile = getTurnstileConfigFromSettings(settings || {});
199
+ if (turnstile) {
200
+ data.templateData.turnstileArgs = turnstile;
201
+ addChallenge(data.templateData, turnstile.addLoginTurnstile, {
202
+ label: 'Turnstile',
203
+ html: `<div id="${turnstile.targetId}"></div>`,
204
+ styleName: pluginData.nbbId,
205
+ });
206
+ }
207
+
208
+ return data;
209
+ };
210
+
211
+ Plugin.onPostEdit = async function (data) {
212
+ const cid = await Topics.getTopicField(data.post.tid, 'cid');
213
+ await Plugin.checkReply({ content: data.post.content, uid: data.post.uid, cid, req: data.req }, { type: 'post', edit: true });
214
+ return data;
215
+ };
216
+ Plugin.onTopicEdit = async (data) => { await Plugin.checkReply({ title: data.topic.title || '', uid: data.topic.uid, cid: data.topic.cid, req: data.req }, { type: 'topic', edit: true }); return data; };
217
+ Plugin.onTopicPost = async (data) => { await Plugin.checkReply(data, { type: 'topic' }); return data; };
218
+ Plugin.onTopicReply = async (data) => { await Plugin.checkReply(data, { type: 'post' }); return data; };
219
+
220
+ Plugin.checkReply = async function (data, options) {
221
+ options = options || {};
222
+ if (!akismet.verified || !data || !data.req || data.fromQueue) {
223
+ return;
224
+ }
225
+ const [isAdmin, isModerator, userData] = await Promise.all([
226
+ User.isAdministrator(data.req.uid),
227
+ User.isModerator(data.req.uid, data.cid),
228
+ User.getUserFields(data.req.uid, ['username', 'reputation', 'email']),
229
+ ]);
230
+ if (isAdmin || isModerator) {
231
+ return;
232
+ }
233
+ const akismetData = {
234
+ referrer: data.req.headers.referer,
235
+ user_ip: data.req.ip,
236
+ user_agent: data.req.headers['user-agent'],
237
+ permalink: nconf.get('url').replace(/\/$/, '') + data.req.path,
238
+ comment_content: (data.title ? `${data.title}\n\n` : '') + (data.content || ''),
239
+ comment_author: userData.username,
240
+ comment_author_email: userData.email,
241
+ comment_type: options.type === 'topic' ? 'forum-post' : 'comment',
242
+ };
243
+ if (options.edit) {
244
+ akismetData.recheck_reason = 'edit';
245
+ }
246
+ const isSpam = await akismet.checkSpam(akismetData);
247
+ await db.incrObjectField(`${pluginData.nbbId}:akismet`, 'checks');
248
+ if (!isSpam) {
249
+ return;
250
+ }
251
+ await db.incrObjectField(`${pluginData.nbbId}:akismet`, 'spam');
252
+ if (parseInt(userData.reputation, 10) >= parseInt(pluginSettings.akismetMinReputationHam, 10)) {
253
+ await akismet.submitHam(akismetData);
254
+ }
255
+ winston.verbose(`[plugins/${pluginData.nbbId}] Post by uid: ${data.req.uid} username: ${userData.username}@${data.req.ip} was flagged as spam and rejected.`);
256
+ throw new Error('Post content was flagged as spam by Akismet.com');
257
+ };
258
+
259
+ Plugin.checkRegister = async function (data) {
260
+ await Promise.all([
261
+ Plugin._honeypotCheck(data.req, data.userData),
262
+ Plugin._turnstileCheck(data.req),
263
+ ]);
264
+ return data;
265
+ };
266
+
267
+ Plugin.checkLogin = async function (data) {
268
+ const settings = await getSettings();
269
+ const turnstile = getTurnstileConfigFromSettings(settings || {});
270
+ if (turnstile && turnstile.addLoginTurnstile) {
271
+ await Plugin._turnstileCheck(data.req);
272
+ }
273
+ return data;
274
+ };
275
+
276
+ Plugin.getRegistrationQueue = async function (data) {
277
+ if (pluginSettings.stopforumspamEnabled) {
278
+ await Promise.all(data.users.map(augmentWitSpamData));
279
+ }
280
+ return data;
281
+ };
282
+
283
+ async function augmentWitSpamData(user) {
284
+ try {
285
+ user.ip = user.ip.replace('::ffff:', '');
286
+ let body = await stopforumspam.isSpammer({ ip: user.ip, email: user.email, username: user.username, f: 'json' });
287
+ if (!body) {
288
+ body = { success: 1, username: { frequency: 0, appears: 0 }, email: { frequency: 0, appears: 0 }, ip: { frequency: 0, appears: 0, asn: null } };
289
+ }
290
+ user.spamChecked = true;
291
+ user.spamData = body;
292
+ user.usernameSpam = body.username ? (body.username.frequency > 0 || body.username.appears > 0) : true;
293
+ user.emailSpam = body.email ? (body.email.frequency > 0 || body.email.appears > 0) : true;
294
+ user.ipSpam = body.ip ? (body.ip.frequency > 0 || body.ip.appears > 0) : true;
295
+ user.customActions = user.customActions || [];
296
+ if (pluginSettings.stopforumspamApiKey) {
297
+ user.customActions.push({ title: '[[spam-be-gone:report-user]]', id: `report-spam-user-${user.username}`, class: 'btn-warning report-spam-user', icon: 'fa-flag' });
298
+ }
299
+ } catch (err) {
300
+ if (err) {
301
+ winston.error(err);
302
+ }
303
+ }
304
+ }
305
+
306
+ Plugin.userProfileMenu = function (data, next) {
307
+ if (pluginSettings.stopforumspamEnabled && pluginSettings.stopforumspamApiKey) {
308
+ data.links.push({
309
+ id: 'spamBeGoneReportUserBtn', route: 'report-user', icon: 'fa-flag', name: '[[spam-be-gone:report-user]]',
310
+ visibility: { self: false, other: false, moderator: false, globalMod: true, admin: true },
311
+ });
312
+ }
313
+ next(null, data);
314
+ };
315
+
316
+ Plugin.onPostFlagged = async function (data) {
317
+ const flagObj = data.flag;
318
+ if (flagObj.type !== 'post' || flagObj.description !== 'Spam') {
319
+ return;
320
+ }
321
+ if (akismet.verified && pluginSettings.akismetFlagReporting && parseInt(flagObj.reporter.reputation, 10) >= parseInt(pluginSettings.akismetFlagReporting, 10)) {
322
+ const [userData, permalink, ip] = await Promise.all([
323
+ User.getUserFields(flagObj.target.uid, ['username', 'email']),
324
+ Topics.getTopicField(flagObj.target.tid, 'slug'),
325
+ db.getSortedSetRevRange(`uid:${flagObj.target.uid}:ip`, 0, 1),
326
+ ]);
327
+ const submitted = {
328
+ user_ip: ip ? ip[0] : '', permalink: `${nconf.get('url').replace(/\/$/, '')}/topic/${permalink}`,
329
+ comment_author: userData.username, comment_author_email: userData.email, comment_content: flagObj.target.content, comment_type: 'forum-post',
330
+ };
331
+ try { await akismet.submitSpam(submitted); winston.info('Spam reported to Akismet.', submitted); } catch (err) { winston.error(`Error reporting to Akismet ${err.message}\n${JSON.stringify(submitted, null, 4)}`); }
332
+ }
333
+ };
334
+
335
+ Plugin._honeypotCheck = async function (req, userData) {
336
+ if (!(honeypot && req && req.ip)) {
337
+ return;
338
+ }
339
+ const honeypotQuery = util.promisify(honeypot.query);
340
+ const results = await honeypotQuery(req.ip);
341
+ if (results && results.found && results.type && (results.type.spammer || results.type.suspicious)) {
342
+ const message = `${userData.username} | ${userData.email} was detected as ${(results.type.spammer ? 'spammer' : 'suspicious')}`;
343
+ winston.warn(`[plugins/${pluginData.nbbId}] ${message} and was denied registration.`);
344
+ throw new Error(message);
345
+ }
346
+ winston.verbose(`[plugins/${pluginData.nbbId}] username: ${userData.username} ip: ${req.ip} was not found in Honeypot database`);
347
+ };
348
+
349
+ Plugin._turnstileCheck = async function (req) {
350
+ const settings = await getSettings();
351
+ if (!isOn(settings.turnstileEnabled)) {
352
+ return;
353
+ }
354
+ if (!req || !req.body) {
355
+ throw new Error('[[spam-be-gone:captcha-not-verified]]');
356
+ }
357
+ const token = req.body['cf-turnstile-response'];
358
+ if (!token || !settings.turnstileSecretKey) {
359
+ throw new Error('[[spam-be-gone:captcha-not-verified]]');
360
+ }
361
+ const payload = querystring.stringify({
362
+ secret: settings.turnstileSecretKey,
363
+ response: token,
364
+ remoteip: req.ip,
365
+ });
366
+ const options = {
367
+ hostname: 'challenges.cloudflare.com',
368
+ path: '/turnstile/v0/siteverify',
369
+ method: 'POST',
370
+ headers: {
371
+ 'Content-Type': 'application/x-www-form-urlencoded',
372
+ 'Content-Length': Buffer.byteLength(payload),
373
+ },
374
+ };
375
+ await new Promise((resolve, reject) => {
376
+ const request = https.request(options, (res) => {
377
+ let responseData = '';
378
+ res.on('data', (chunk) => { responseData += chunk; });
379
+ res.on('end', () => {
380
+ let parsed;
381
+ try {
382
+ parsed = JSON.parse(responseData || '{}');
383
+ } catch (err) {
384
+ return reject(new Error('[[spam-be-gone:captcha-not-verified]]'));
385
+ }
386
+ if (parsed.success === true) {
387
+ return resolve();
388
+ }
389
+ winston.verbose(`[plugins/${pluginData.nbbId}] Turnstile verification failed: ${JSON.stringify(parsed['error-codes'] || [])}`);
390
+ reject(new Error('[[spam-be-gone:captcha-not-verified]]'));
391
+ });
392
+ });
393
+ request.on('error', (error) => reject(new Error(error.message || '[[spam-be-gone:captcha-not-verified]]')));
394
+ request.write(payload);
395
+ request.end();
396
+ });
397
+ };
398
+
399
+ Plugin.admin = {
400
+ menu: function (custom_header, callback) {
401
+ custom_header.plugins.push({ route: `/plugins/${pluginData.nbbId}`, icon: pluginData.faIcon, name: pluginData.name });
402
+ callback(null, custom_header);
403
+ },
404
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@arnaudw38/nodebb-plugin-spam-be-gone",
3
+ "version": "1.0.0",
4
+ "description": "Anti-spam plugin for NodeBB 4.x using Akismet, StopForumSpam, ProjectHoneyPot, and Cloudflare Turnstile (Turnstile-only fork)",
5
+ "main": "library.js",
6
+ "scripts": {},
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/aworobel/nodebb-plugin-spam-be-gone.git"
10
+ },
11
+ "keywords": [
12
+ "nodebb",
13
+ "plugin",
14
+ "spam",
15
+ "stopforumspam",
16
+ "akismet",
17
+ "honeypot",
18
+ "projecthoneypot",
19
+ "antispam",
20
+ "turnstile",
21
+ "cloudflare",
22
+ "nodebb-plugin",
23
+ "security",
24
+ "turnstile-only"
25
+ ],
26
+ "author": {
27
+ "name": "Aziz khoury",
28
+ "email": "bentael@gmail.com"
29
+ },
30
+ "license": "MIT",
31
+ "bugs": {
32
+ "url": "https://github.com/aworobel/nodebb-plugin-spam-be-gone/issues"
33
+ },
34
+ "dependencies": {
35
+ "async": "^3.2.0",
36
+ "project-honeypot": "~0.0.0",
37
+ "stopforumspam": "^1.3.8"
38
+ },
39
+ "nbbpm": {
40
+ "compatibility": "^4.0.0"
41
+ },
42
+ "homepage": "https://github.com/aworobel/nodebb-plugin-spam-be-gone#readme",
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "files": [
47
+ "library.js",
48
+ "lib/",
49
+ "public/",
50
+ "upgrades/",
51
+ "plugin.json",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }
package/plugin.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "id": "nodebb-plugin-spam-be-gone",
3
+ "name": "Spam Be Gone",
4
+ "description": "Anti-spam using Akismet.com, StopForumSpam.com, ProjectHoneyPot.org and Cloudflare Turnstile",
5
+ "url": "https://github.com/akhoury/nodebb-plugin-spam-be-gone",
6
+ "scss": [
7
+ "public/scss/styles.scss"
8
+ ],
9
+ "acpScripts": [
10
+ "public/js/scripts.js"
11
+ ],
12
+ "scripts": [
13
+ "public/js/scripts.js"
14
+ ],
15
+ "modules": {
16
+ "../admin/plugins/spam-be-gone.js": "public/js/admin.js"
17
+ },
18
+ "templates": "public/templates",
19
+ "languages": "public/languages",
20
+ "defaultLang": "en_GB",
21
+ "upgrades": [
22
+ "upgrades/enable_stopforumspam_by_default.js"
23
+ ],
24
+ "hooks": [
25
+ {
26
+ "hook": "static:app.load",
27
+ "method": "load"
28
+ },
29
+ {
30
+ "hook": "filter:admin.header.build",
31
+ "method": "admin.menu"
32
+ },
33
+ {
34
+ "hook": "filter:login.build",
35
+ "method": "addCaptcha",
36
+ "priority": 5
37
+ },
38
+ {
39
+ "hook": "filter:register.build",
40
+ "method": "addCaptcha",
41
+ "priority": 5
42
+ },
43
+ {
44
+ "hook": "filter:login.check",
45
+ "method": "checkLogin",
46
+ "priority": 5
47
+ },
48
+ {
49
+ "hook": "filter:register.check",
50
+ "method": "checkRegister",
51
+ "priority": 5
52
+ },
53
+ {
54
+ "hook": "filter:user.profileMenu",
55
+ "method": "userProfileMenu",
56
+ "priority": 5
57
+ },
58
+ {
59
+ "hook": "filter:user.getRegistrationQueue",
60
+ "method": "getRegistrationQueue",
61
+ "priority": 5
62
+ },
63
+ {
64
+ "hook": "filter:topic.post",
65
+ "method": "onTopicPost",
66
+ "priority": 5
67
+ },
68
+ {
69
+ "hook": "filter:topic.reply",
70
+ "method": "onTopicReply",
71
+ "priority": 5
72
+ },
73
+ {
74
+ "hook": "filter:topic.edit",
75
+ "method": "onTopicEdit"
76
+ },
77
+ {
78
+ "hook": "filter:post.edit",
79
+ "method": "onPostEdit"
80
+ },
81
+ {
82
+ "hook": "action:flags.create",
83
+ "method": "onPostFlagged",
84
+ "priority": 5
85
+ },
86
+ {
87
+ "hook": "filter:config.get",
88
+ "method": "appendConfig"
89
+ }
90
+ ],
91
+ "faIcon": "fa-shield"
92
+ }
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ define('admin/plugins/spam-be-gone', ['settings', 'alerts'], function (Settings, alerts) {
4
+ var Admin = {};
5
+
6
+ Admin.init = function () {
7
+ var nbbId = ajaxify.data.nbbId;
8
+ var klass = nbbId + '-settings';
9
+ var wrapper = $('.' + klass);
10
+
11
+ function onChange(e) {
12
+ var target = $(e.target);
13
+ var input = wrapper.find(target.attr('data-toggle-target'));
14
+ input.prop('disabled', !target.is(':checked'));
15
+ }
16
+
17
+ wrapper.find('input[type="checkbox"][data-toggle-target]').on('change', onChange);
18
+
19
+ Settings.load(nbbId, wrapper, function () {
20
+ wrapper.find('input[type="checkbox"][data-toggle-target]').each(function () {
21
+ onChange({ target: this });
22
+ });
23
+ });
24
+
25
+ $('#save').on('click', function (e) {
26
+ e.preventDefault();
27
+ wrapper.find('.has-error').removeClass('has-error');
28
+
29
+ var invalidSelector = '';
30
+ var invalidCount = 0;
31
+ wrapper.find('input[type="checkbox"][data-toggle-target]').each(function (i, checkbox) {
32
+ checkbox = $(checkbox);
33
+ if (checkbox.is(':checked') && !wrapper.find(checkbox.attr('data-toggle-target')).val()) {
34
+ invalidSelector += (!invalidCount++ ? '' : ', ') + checkbox.attr('data-toggle-target');
35
+ }
36
+ });
37
+
38
+ if (invalidSelector) {
39
+ wrapper.find(invalidSelector).each(function (i, el) {
40
+ el = $(el);
41
+ el.parent().addClass('has-error');
42
+ });
43
+ alerts.error('Empty fields not allowed!');
44
+ } else {
45
+ Settings.save(nbbId, wrapper, function () {
46
+ alerts.alert({
47
+ type: 'success',
48
+ alert_id: nbbId,
49
+ title: 'Reload Required',
50
+ message: 'Please reload your NodeBB to have your changes take effect',
51
+ clickfn: function () {
52
+ socket.emit('admin.reload');
53
+ },
54
+ });
55
+ });
56
+ }
57
+ });
58
+ };
59
+
60
+ return Admin;
61
+ });
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ /* global turnstile */
4
+
5
+ $(function () {
6
+ var pluginName = 'spam-be-gone';
7
+ var turnstileScriptUrl = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
8
+
9
+ function getTurnstileArgs() {
10
+ return ajaxify.data && ajaxify.data.turnstileArgs;
11
+ }
12
+
13
+ function injectScriptOnce(src) {
14
+ if (document.querySelector('script[src*="turnstile/v0/api.js"]')) {
15
+ return Promise.resolve();
16
+ }
17
+ return new Promise(function (resolve, reject) {
18
+ var s = document.createElement('script');
19
+ s.src = src;
20
+ s.async = true;
21
+ s.defer = true;
22
+ s.onload = resolve;
23
+ s.onerror = reject;
24
+ document.head.appendChild(s);
25
+ });
26
+ }
27
+
28
+ function renderTurnstileIfNeeded(isLoginPage) {
29
+ var args = getTurnstileArgs();
30
+ if (!args || (isLoginPage && !args.addLoginTurnstile)) {
31
+ return;
32
+ }
33
+
34
+ injectScriptOnce(turnstileScriptUrl)
35
+ .then(function () {
36
+ if (typeof turnstile === 'undefined') {
37
+ return;
38
+ }
39
+ var target = document.getElementById(args.targetId);
40
+ if (!target || target.dataset.turnstileRendered === '1') {
41
+ return;
42
+ }
43
+ turnstile.render('#' + args.targetId, {
44
+ sitekey: args.siteKey,
45
+ theme: args.theme || 'auto',
46
+ size: args.size || 'normal',
47
+ appearance: args.appearance || 'always',
48
+ language: args.language || 'auto',
49
+ callback: function () {
50
+ var error = utils.param('error');
51
+ if (error) {
52
+ require(['alerts'], function (alerts) { alerts.error(error); });
53
+ }
54
+ },
55
+ 'error-callback': function () {
56
+ require(['alerts'], function (alerts) { alerts.error('[[spam-be-gone:captcha-not-verified]]'); });
57
+ },
58
+ });
59
+ target.dataset.turnstileRendered = '1';
60
+ })
61
+ .catch(function () {
62
+ require(['alerts'], function (alerts) { alerts.error('Failed to load Cloudflare Turnstile'); });
63
+ });
64
+ }
65
+
66
+ function onAccountProfilePage() {
67
+ var $btn = $('#spamBeGoneReportUserBtn');
68
+ $btn.off('click').on('click', function (e) {
69
+ e.preventDefault();
70
+ reportUser('/api/user/' + ajaxify.data.userslug + '/' + pluginName + '/report');
71
+ var $parentBtn = $btn.parents('.account-fab').find('[data-toggle="dropdown"]');
72
+ if ($parentBtn.dropdown) {
73
+ $parentBtn.dropdown('toggle');
74
+ }
75
+ return false;
76
+ });
77
+ }
78
+
79
+ function onManageRegistrationPage() {
80
+ $('button.report-spam-user').off('click').on('click', function (e) {
81
+ e.preventDefault();
82
+ var username = $(this).parents('[data-username]').attr('data-username');
83
+ reportUser('/api/user/' + username + '/' + pluginName + '/report/queue');
84
+ return false;
85
+ });
86
+ }
87
+
88
+ function reportUser(url) {
89
+ require(['alerts'], function (alerts) {
90
+ $.post(url)
91
+ .then(function () { alerts.success('User reported!'); })
92
+ .catch(function (e) { alerts.error(e.responseJSON && e.responseJSON.message || '[spam-be-gone:something-went-wrong]'); });
93
+ });
94
+ }
95
+
96
+ $(window).on('action:ajaxify.end', function (evt, data) {
97
+ switch (data.tpl_url) {
98
+ case 'register':
99
+ renderTurnstileIfNeeded(false);
100
+ break;
101
+ case 'login':
102
+ renderTurnstileIfNeeded(true);
103
+ break;
104
+ case 'account/profile':
105
+ onAccountProfilePage();
106
+ break;
107
+ case 'admin/manage/registration':
108
+ onManageRegistrationPage();
109
+ break;
110
+ }
111
+ });
112
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "report-user": "Report spammer",
3
+ "something-went-wrong": "Something went wrong",
4
+ "not-allowed": "You are not allowed to perform this action",
5
+ "cant-report-admin": "You can't report an admin as spammer",
6
+ "user-reported": "User reported!",
7
+ "sfs-not-enabled": "Stop Forum Spam is not enabled",
8
+ "sfs-api-key-not-set": "Stop Forum Spam API Key is not set, get one from stopforumspam.com/keys",
9
+ "captcha-not-verified": "Captcha not verified, are you a robot?",
10
+ "admin-topic-start": "This plugin uses",
11
+ "admin-topic-end": "Submit pull requests or ideas at the the project repository",
12
+ "enable": "Enable",
13
+ "akismet-topic-1": "To check every user post. Get yours from ",
14
+ "akismet-topic-2": "Minimum reputation level to classify flagged posts as false positives (HAM). Posts made by users with at least this level reputation will never be flagged as spam.",
15
+ "akismet-topic-3": "Allow users with minimum reputation of X to submit posts to Akismet as spam via flagging (leave blank to disable)",
16
+ "honeypot-topic-1": "To check every user registration. Get yours from ",
17
+ "stopforumspam-topic-1": "To report a user you need an API key, get yours from ",
18
+ "turnstile-topic-1": "To check registrations (and optionally login). Create keys from ",
19
+ "turnstile-topic-2": "Keep your secret key private",
20
+ "enable-turnstile-login": "Enable Turnstile on login page as well"
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "report-user": "Report spammer",
3
+ "something-went-wrong": "Something went wrong",
4
+ "not-allowed": "You are not allowed to perform this action",
5
+ "cant-report-admin": "You can't report an admin as spammer",
6
+ "user-reported": "User reported!",
7
+ "sfs-not-enabled": "Stop Forum Spam is not enabled",
8
+ "sfs-api-key-not-set": "Stop Forum Spam API Key is not set, get one from stopforumspam.com/keys",
9
+ "captcha-not-verified": "Captcha not verified, are you a robot?",
10
+ "admin-topic-start": "This plugin uses",
11
+ "admin-topic-end": "pull requests or ideas at the",
12
+ "enable": "Enable",
13
+ "akismet-topic-1": "To check every user post. Get yours from ",
14
+ "akismet-topic-2": "Minimum reputation level to classify flagged posts as false positives (HAM). Posts made by users with at least this level reputation will never be flagged as spam.",
15
+ "akismet-topic-3": "Allow users with minimum reputation of X to submit posts to Akismet as spam via flagging (leave blank to disable)",
16
+ "honeypot-topic-1": "To check every user registration. Get yours from ",
17
+ "stopforumspam-topic-1": "To report a user you need an API key, get yours from ",
18
+ "turnstile-topic-1": "To check registrations (and optionally login). Create keys from ",
19
+ "turnstile-topic-2": "Keep your secret key private",
20
+ "enable-turnstile-login": "Enable Turnstile on login page as well"
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "report-user": "Zgłoś spam",
3
+ "something-went-wrong": "Coś poszło nie tak",
4
+ "not-allowed": "Nie masz uprawnień do wykonania tej czynności",
5
+ "cant-report-admin": "Nie możesz zgłosić administratora jako spamera",
6
+ "user-reported": "Użytkownik zgłoszony!",
7
+ "sfs-not-enabled": "Stop Forum Spam jest włączone",
8
+ "sfs-api-key-not-set": "Klucz API dla Stop Forum Spam nie został wprowadzony - uzyskasz go na stopforumspam.com/keys",
9
+ "captcha-not-verified": "Brak weryfikacji Captcha, jesteś automatem?",
10
+ "admin-topic-start": "Wtyczka wykorzystuje",
11
+ "admin-topic-end": "Na stronie projektu zgłosisz zmiany kodu czy własne pomysły",
12
+ "enable": "Włącz",
13
+ "akismet-topic-1": "Pozwoli sprawdzić każdy wpis. Uzyskaj własny na ",
14
+ "akismet-topic-2": "Poziom reputacji aby kwalifikować wpisy jako błędnie zakwalifikowane (HAM). Wpisy użytkowników z wysoką reputacją nie będą oznaczane jako spam.",
15
+ "akismet-topic-3": "Zezwól użytkownikom z reputacją X i wyższą aby zgłaszali spam do bazy Akismet (pozostaw puste aby wyłączyć)",
16
+ "honeypot-topic-1": "Pozwoli sprawdzać nowe konta. Uzyskaj własny na ",
17
+ "stopforumspam-topic-1": "Aby zgłosić użytkownika potrzebujesz klucz API stąd ",
18
+ "turnstile-topic-1": "To check registrations (and optionally login). Create keys from ",
19
+ "turnstile-topic-2": "Keep your secret key private",
20
+ "enable-turnstile-login": "Enable Turnstile on login page as well"
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "report-user": "举报垃圾邮件",
3
+ "something-went-wrong": "未知错误",
4
+ "not-allowed": "你没有权限进行这个操作",
5
+ "cant-report-admin": "你无法举报管理员",
6
+ "user-reported": "用户举报!",
7
+ "sfs-not-enabled": "未启用“停止论坛垃圾邮件”",
8
+ "sfs-api-key-not-set": "未从stopforumspam.com/keys设置阻止论坛垃圾邮件的 API 密钥",
9
+ "captcha-not-verified": "验证码校验失败,请尝试重新校验或刷新页面",
10
+ "admin-topic-start": "这个插件引用了以下项目:",
11
+ "admin-topic-end": "拉取项目或提交issue请前往:",
12
+ "enable": "启用",
13
+ "akismet-topic-1": "要检查所有的提交,可前往 ",
14
+ "akismet-topic-2": "将被标记的帖子分类为误报 (HAM) 的最低信誉级别。具有此级别声誉的用户所发布的帖子将不会被标记为垃圾邮件。",
15
+ "akismet-topic-3": "允许最低信誉等级为X的用户通过标记将帖子作为垃圾邮件提交给Akismet(留空以禁用)",
16
+ "honeypot-topic-1": "要对所有的用户注册进行检查,可前往 ",
17
+ "stopforumspam-topic-1": "你需要一个API密钥用于举报用户,可前往 ",
18
+ "turnstile-topic-1": "To check registrations (and optionally login). Create keys from ",
19
+ "turnstile-topic-2": "Keep your secret key private",
20
+ "enable-turnstile-login": "Enable Turnstile on login page as well"
21
+ }
@@ -0,0 +1,3 @@
1
+ #spam-be-gone-turnstile-target {
2
+ margin: 10px 0;
3
+ }
@@ -0,0 +1,87 @@
1
+ <div class="acp-page-container">
2
+ <!-- IMPORT admin/partials/settings/header.tpl -->
3
+
4
+ <div>
5
+ <div class="alert alert-info mb-0 text-xs">
6
+ <p>
7
+ [[spam-be-gone:admin-topic-start]]
8
+ <a target="_blank" href="https://github.com/oozcitak/akismet-js">akismet-js</a>,
9
+ <a target="_blank" href="https://github.com/julianlam/project-honeypot">project-honeypot</a>,
10
+ <a target="_blank" href="https://developers.cloudflare.com/turnstile/">Cloudflare Turnstile</a>,
11
+ <a target="_blank" href="https://github.com/deltreey/stopforumspam">stopforumspam</a>
12
+ </p>
13
+ <p class="mb-0">
14
+ [[spam-be-gone:admin-topic-end]]
15
+ <a target="_blank" href="https://github.com/akhoury/nodebb-plugin-spam-be-gone">spam-be-gone</a>
16
+ </p>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="row m-0">
21
+ <div id="spy-container" class="col-12 px-0 mb-4" tabindex="0">
22
+ <ul class="nav nav-tabs mb-3" role="tablist">
23
+ <li role="presentation" class="nav-item"><a class="nav-link active" href="#akismet" aria-controls="akismet" role="tab" data-bs-toggle="tab">Akismet</a></li>
24
+ <li role="presentation" class="nav-item"><a class="nav-link" href="#honeypot" aria-controls="honeypot" role="tab" data-bs-toggle="tab">Project Honeypot</a></li>
25
+ <li role="presentation" class="nav-item"><a class="nav-link" href="#turnstile" aria-controls="turnstile" role="tab" data-bs-toggle="tab">Cloudflare Turnstile</a></li>
26
+ <li role="presentation" class="nav-item"><a class="nav-link" href="#sfs" aria-controls="sfs" role="tab" data-bs-toggle="tab">StopForumSpam</a></li>
27
+ </ul>
28
+
29
+ <form role="form" class="{nbbId}-settings">
30
+ <fieldset>
31
+ <div class="tab-content">
32
+ <div role="tabpanel" class="tab-pane fade show active" id="akismet">
33
+ <div class="row"><div class="col-sm-12">
34
+ <div class="form-check">
35
+ <input class="form-check-input" data-toggle-target="#akismetApiKey,#akismetMinReputationHam,#akismetFlagReporting" type="checkbox" id="akismetEnabled" name="akismetEnabled"/>
36
+ <label class="section-title form-check-label">[[spam-be-gone:enable]] Akismet</label>
37
+ </div>
38
+ <p class="form-text">[[spam-be-gone:akismet-topic-1]] <a target="_blank" href="http://akismet.com/">akismet.com</a></p>
39
+ {{{ if akismet.checks }}}<p>Akismet checked <strong>{akismet.checks}</strong> posts and caught <strong>{akismet.spam}</strong> spam posts.</p>{{{ end }}}
40
+ <div class="mb-3"><label class="form-label" for="akismetApiKey">Akismet API Key</label><input placeholder="Akismet API Key here" type="text" class="form-control" id="akismetApiKey" name="akismetApiKey"/></div>
41
+ <div class="mb-3"><label class="form-label" for="akismetMinReputationHam">HAM Minimum Reputation</label><input placeholder="10" type="number" class="form-control" id="akismetMinReputationHam" name="akismetMinReputationHam"/></div>
42
+ <p class="form-text">[[spam-be-gone:akismet-topic-2]]</p>
43
+ <div class="mb-3"><label class="form-label" for="akismetFlagReporting">Flagging Minimum Reputation</label><input placeholder="5" type="text" class="form-control" id="akismetFlagReporting" name="akismetFlagReporting"/></div>
44
+ <p class="form-text">[[spam-be-gone:akismet-topic-3]]</p>
45
+ </div></div>
46
+ </div>
47
+
48
+ <div role="tabpanel" class="tab-pane fade" id="honeypot">
49
+ <div class="row"><div class="col-sm-12">
50
+ <div class="form-check"><input class="form-check-input" data-toggle-target="#honeypotApiKey" type="checkbox" id="honeypotEnabled" name="honeypotEnabled"/><label class="form-check-label">[[spam-be-gone:enable]] Honeypot</label></div>
51
+ <p class="form-text">[[spam-be-gone:honeypot-topic-1]]<a target="_blank" href="http://www.projecthoneypot.org/">projecthoneypot.org</a></p>
52
+ <div class="mb-3"><label class="form-label" for="honeypotApiKey">Honeypot API Key</label><input placeholder="Honeypot API Key here" type="text" class="form-control" id="honeypotApiKey" name="honeypotApiKey"/></div>
53
+ </div></div>
54
+ </div>
55
+
56
+ <div role="tabpanel" class="tab-pane fade" id="turnstile">
57
+ <div class="row"><div class="col-sm-12">
58
+ <div class="form-check">
59
+ <input class="form-check-input" data-toggle-target="#turnstileSiteKey,#turnstileSecretKey,#loginTurnstileEnabled,#turnstileTheme,#turnstileSize,#turnstileAppearance" type="checkbox" id="turnstileEnabled" name="turnstileEnabled"/>
60
+ <label class="form-check-label">[[spam-be-gone:enable]] Cloudflare Turnstile</label>
61
+ </div>
62
+ <p class="form-text">[[spam-be-gone:turnstile-topic-1]]<a target="_blank" href="https://developers.cloudflare.com/turnstile/">developers.cloudflare.com/turnstile</a></p>
63
+ <div class="mb-3"><label class="form-label" for="turnstileSiteKey">Turnstile Site Key</label><input placeholder="Site Key here" type="text" class="form-control" id="turnstileSiteKey" name="turnstileSiteKey"/></div>
64
+ <div class="mb-3"><label class="form-label" for="turnstileSecretKey">Turnstile Secret Key</label><input placeholder="Secret Key here" type="text" class="form-control" id="turnstileSecretKey" name="turnstileSecretKey"/></div>
65
+ <div class="row g-3">
66
+ <div class="col-md-4"><label class="form-label" for="turnstileTheme">Theme</label><select class="form-select" id="turnstileTheme" name="turnstileTheme"><option value="auto">auto</option><option value="light">light</option><option value="dark">dark</option></select></div>
67
+ <div class="col-md-4"><label class="form-label" for="turnstileSize">Size</label><select class="form-select" id="turnstileSize" name="turnstileSize"><option value="normal">normal</option><option value="compact">compact</option><option value="flexible">flexible</option></select></div>
68
+ <div class="col-md-4"><label class="form-label" for="turnstileAppearance">Appearance</label><select class="form-select" id="turnstileAppearance" name="turnstileAppearance"><option value="always">always</option><option value="execute">execute</option><option value="interaction-only">interaction-only</option></select></div>
69
+ </div>
70
+ <p class="form-text mt-2">[[spam-be-gone:turnstile-topic-2]]</p>
71
+ <div class="form-check"><input class="form-check-input" type="checkbox" id="loginTurnstileEnabled" name="loginTurnstileEnabled"/><label class="form-check-label">[[spam-be-gone:enable-turnstile-login]]</label></div>
72
+ </div></div>
73
+ </div>
74
+
75
+ <div role="tabpanel" class="tab-pane fade" id="sfs">
76
+ <div class="row"><div class="col-sm-12">
77
+ <div class="form-check"><input class="form-check-input" data-toggle-target="#stopforumspamApiKey" type="checkbox" id="stopforumspamEnabled" name="stopforumspamEnabled"/><label class="form-check-label">Enable StopForumSpam</label></div>
78
+ <p class="form-text">[[spam-be-gone:stopforumspam-topic-1]]<a target="_blank" href="https://www.stopforumspam.com/keys">stopforumspam.com/keys</a></p>
79
+ <div class="mb-3"><label class="form-label" for="stopforumspamApiKey">StopForumSpam API Key</label><input placeholder="API key here" type="text" class="stopforumspamApiKey form-control" id="stopforumspamApiKey" name="stopforumspamApiKey"/></div>
80
+ </div></div>
81
+ </div>
82
+ </div>
83
+ </fieldset>
84
+ </form>
85
+ </div>
86
+ </div>
87
+ </div>
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ var Meta = require.main.require('./src/meta');
4
+ var async = require.main.require('async');
5
+ var winston = require.main.require('winston');
6
+ var Plugin = require('../library');
7
+
8
+ module.exports = {
9
+ name: 'Enable StopForumSpam by default without api key',
10
+ timestamp: Date.UTC(2019, 0, 21),
11
+ method: function (callback) {
12
+ Meta.settings.get(Plugin.nbbId, function (err, settings) {
13
+ if (err) {
14
+ return callback(err);
15
+ }
16
+ if (!settings) {
17
+ settings = {};
18
+ }
19
+ if (settings.stopforumspamEnabled !== 'on') {
20
+ settings.stopforumspamEnabled = 'on';
21
+ Meta.settings.set(Plugin.nbbId, settings, function (err) {
22
+ callback(err);
23
+ });
24
+ } else {
25
+ callback();
26
+ }
27
+ });
28
+ }
29
+ };