@arnaudw38/nodebb-plugin-spam-be-gone 1.0.8 → 1.0.9

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 CHANGED
@@ -4,6 +4,7 @@ const util = require('util');
4
4
  const https = require('https');
5
5
  const querystring = require('querystring');
6
6
  const Honeypot = require('project-honeypot');
7
+ const stopforumspam = require('stopforumspam');
7
8
 
8
9
  const winston = require.main.require('winston');
9
10
  const nconf = require.main.require('nconf');
@@ -99,6 +100,9 @@ Plugin.load = async function (params) {
99
100
  if (!settings.akismetMinReputationHam) {
100
101
  settings.akismetMinReputationHam = 10;
101
102
  }
103
+ if (settings.stopforumspamApiKey) {
104
+ stopforumspam.Key(settings.stopforumspamApiKey);
105
+ }
102
106
 
103
107
  pluginSettings = settings;
104
108
 
@@ -133,7 +137,7 @@ Plugin.report = async function (req, res, next) {
133
137
  if (isAdmin) {
134
138
  return res.status(403).send({ message: '[[spam-be-gone:cant-report-admin]]' });
135
139
  }
136
- await stopForumSpamSubmit({ ip: ips[0], email: fields.email, username: fields.username }, `Manual submission from user: ${req.uid} to user: ${fields.uid} via ${pluginData.id}`);
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}`);
137
141
  res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
138
142
  } catch (err) {
139
143
  winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}`);
@@ -148,7 +152,7 @@ Plugin.reportFromQueue = async (req, res) => {
148
152
  }
149
153
  const submitData = { ip: data.ip, email: data.email, username: data.username };
150
154
  try {
151
- await stopForumSpamSubmit(submitData, `Manual submission from user: ${req.uid} to user: ${data.username} via ${pluginData.id}`);
155
+ await stopforumspam.submit(submitData, `Manual submission from user: ${req.uid} to user: ${data.username} via ${pluginData.id}`);
152
156
  res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
153
157
  } catch (err) {
154
158
  winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}\n${JSON.stringify(submitData, null, 4)}`);
@@ -279,7 +283,7 @@ Plugin.getRegistrationQueue = async function (data) {
279
283
  async function augmentWitSpamData(user) {
280
284
  try {
281
285
  user.ip = user.ip.replace('::ffff:', '');
282
- let body = await stopForumSpamLookup({ ip: user.ip, email: user.email, username: user.username });
286
+ let body = await stopforumspam.isSpammer({ ip: user.ip, email: user.email, username: user.username, f: 'json' });
283
287
  if (!body) {
284
288
  body = { success: 1, username: { frequency: 0, appears: 0 }, email: { frequency: 0, appears: 0 }, ip: { frequency: 0, appears: 0, asn: null } };
285
289
  }
@@ -392,71 +396,6 @@ Plugin._turnstileCheck = async function (req) {
392
396
  });
393
397
  };
394
398
 
395
-
396
- async function stopForumSpamLookup({ ip, email, username }) {
397
- const params = new URLSearchParams();
398
- params.set('f', 'json');
399
- if (ip) params.set('ip', ip);
400
- if (email) params.set('email', email);
401
- if (username) params.set('username', username);
402
-
403
- const res = await fetch(`https://api.stopforumspam.org/api?${params.toString()}`, {
404
- headers: {
405
- accept: 'application/json',
406
- 'user-agent': pluginData.id,
407
- },
408
- });
409
-
410
- if (!res.ok) {
411
- throw new Error(`StopForumSpam lookup failed (${res.status})`);
412
- }
413
-
414
- return await res.json();
415
- }
416
-
417
- async function stopForumSpamSubmit({ ip, email, username }, evidence) {
418
- if (!pluginSettings.stopforumspamApiKey) {
419
- throw new Error('[[spam-be-gone:sfs-api-key-not-set]]');
420
- }
421
-
422
- const body = new URLSearchParams();
423
- body.set('api_key', pluginSettings.stopforumspamApiKey);
424
- body.set('api', 'json');
425
- if (ip) body.set('ip_addr', ip);
426
- if (email) body.set('email', email);
427
- if (username) body.set('username', username);
428
- if (evidence) body.set('evidence', evidence);
429
-
430
- const res = await fetch('https://www.stopforumspam.com/add.php', {
431
- method: 'POST',
432
- headers: {
433
- 'content-type': 'application/x-www-form-urlencoded',
434
- accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
435
- 'user-agent': pluginData.id,
436
- },
437
- body: body.toString(),
438
- });
439
-
440
- const text = await res.text();
441
- if (!res.ok) {
442
- throw new Error(`StopForumSpam submit failed (${res.status})`);
443
- }
444
-
445
- try {
446
- const parsed = JSON.parse(text);
447
- if (parsed.success === 0 || parsed.error) {
448
- throw new Error(parsed.error || 'StopForumSpam submit failed');
449
- }
450
- return parsed;
451
- } catch (err) {
452
- // Some SFS responses can be plain text; consider HTTP 200 success as accepted.
453
- if (/error/i.test(text)) {
454
- throw err instanceof Error ? err : new Error('StopForumSpam submit failed');
455
- }
456
- return { success: 1, raw: text };
457
- }
458
- }
459
-
460
399
  Plugin.admin = {
461
400
  menu: function (custom_header, callback) {
462
401
  custom_header.plugins.push({ route: `/plugins/${pluginData.nbbId}`, icon: pluginData.faIcon, name: pluginData.name });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arnaudw38/nodebb-plugin-spam-be-gone",
3
- "version": "1.0.8",
4
- "description": "Anti-spam plugin for NodeBB 4.x using Akismet, StopForumSpam API, ProjectHoneyPot, and Cloudflare Turnstile (Turnstile-only fork)",
3
+ "version": "1.0.9",
4
+ "description": "Anti-spam plugin for NodeBB 4.x using Akismet, StopForumSpam, ProjectHoneyPot, and Cloudflare Turnstile (Turnstile-only fork)",
5
5
  "main": "library.js",
6
6
  "scripts": {},
7
7
  "repository": {
@@ -33,7 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "async": "^3.2.0",
36
- "project-honeypot": "~0.0.0"
36
+ "project-honeypot": "~0.0.0",
37
+ "stopforumspam": "^1.3.8"
37
38
  },
38
39
  "nbbpm": {
39
40
  "compatibility": "^4.0.0"
@@ -49,8 +50,7 @@
49
50
  "upgrades/",
50
51
  "plugin.json",
51
52
  "README.md",
52
- "LICENSE",
53
- "CHANGELOG.md"
53
+ "LICENSE"
54
54
  ],
55
55
  "publishConfig": {
56
56
  "access": "public"
@@ -5,14 +5,6 @@
5
5
  $(function () {
6
6
  var pluginName = 'spam-be-gone';
7
7
  var turnstileScriptUrl = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
8
- var turnstileState = {
9
- widgets: {},
10
- loginBindingsAttached: false,
11
- loginResetTimers: [],
12
- loginErrorObserver: null,
13
- loginAttemptArmed: false,
14
- navigatingAway: false,
15
- };
16
8
 
17
9
  function getTurnstileArgs() {
18
10
  return ajaxify.data && ajaxify.data.turnstileArgs;
@@ -33,180 +25,7 @@ $(function () {
33
25
  });
34
26
  }
35
27
 
36
- function getCurrentTurnstileResponse() {
37
- var input = document.querySelector('input[name="cf-turnstile-response"]');
38
- return input && input.value;
39
- }
40
-
41
- function getCurrentLoginTurnstileTargetId() {
42
- var args = getTurnstileArgs();
43
- return args && args.targetId;
44
- }
45
-
46
- function resetTurnstileWidget(targetId) {
47
- if (typeof turnstile === 'undefined') {
48
- return false;
49
- }
50
- targetId = targetId || getCurrentLoginTurnstileTargetId();
51
- if (!targetId) {
52
- return false;
53
- }
54
- var widgetId = turnstileState.widgets[targetId];
55
- if (widgetId === undefined || widgetId === null) {
56
- return false;
57
- }
58
- try {
59
- turnstile.reset(widgetId);
60
- return true;
61
- } catch (err) {
62
- // Ignore reset errors when the widget is already destroyed by navigation.
63
- return false;
64
- }
65
- }
66
-
67
- function onLoginPageNow() {
68
- return !ajaxify.data || !ajaxify.data.tpl_url || ajaxify.data.tpl_url === 'login';
69
- }
70
-
71
- function hasLoginErrorVisible() {
72
- var selectors = [
73
- '.alert-danger',
74
- '.alert-error',
75
- '[component="alerts"] .alert.alert-danger',
76
- '[component="alerts/error"]',
77
- '.login-error',
78
- '.text-danger',
79
- ];
80
- for (var i = 0; i < selectors.length; i += 1) {
81
- var nodes = document.querySelectorAll(selectors[i]);
82
- for (var j = 0; j < nodes.length; j += 1) {
83
- var n = nodes[j];
84
- if (n && (n.offsetParent !== null || (n.textContent && n.textContent.trim()))) {
85
- return true;
86
- }
87
- }
88
- }
89
- return false;
90
- }
91
-
92
- function clearLoginResetTimers() {
93
- turnstileState.loginResetTimers.forEach(function (timerId) {
94
- window.clearTimeout(timerId);
95
- });
96
- turnstileState.loginResetTimers = [];
97
- }
98
-
99
- function disarmLoginAttemptWatch() {
100
- turnstileState.loginAttemptArmed = false;
101
- clearLoginResetTimers();
102
- if (turnstileState.loginErrorObserver) {
103
- try {
104
- turnstileState.loginErrorObserver.disconnect();
105
- } catch (err) {
106
- // noop
107
- }
108
- turnstileState.loginErrorObserver = null;
109
- }
110
- }
111
-
112
- function resetLoginTurnstileOnDetectedError() {
113
- if (!turnstileState.loginAttemptArmed) {
114
- return;
115
- }
116
- if (turnstileState.navigatingAway || !onLoginPageNow()) {
117
- disarmLoginAttemptWatch();
118
- return;
119
- }
120
- if (!hasLoginErrorVisible()) {
121
- return;
122
- }
123
- var targetId = getCurrentLoginTurnstileTargetId();
124
- if (targetId && document.getElementById(targetId)) {
125
- resetTurnstileWidget(targetId);
126
- }
127
- disarmLoginAttemptWatch();
128
- }
129
-
130
- function scheduleLoginTurnstileReset() {
131
- // Arm a short-lived watcher and reset only when an actual login error is rendered.
132
- turnstileState.navigatingAway = false;
133
- disarmLoginAttemptWatch();
134
- turnstileState.loginAttemptArmed = true;
135
-
136
- if (window.MutationObserver) {
137
- turnstileState.loginErrorObserver = new MutationObserver(function () {
138
- resetLoginTurnstileOnDetectedError();
139
- });
140
- turnstileState.loginErrorObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
141
- }
142
-
143
- // Poll briefly as a fallback for themes that toggle classes/text without obvious DOM insertions.
144
- [250, 600, 1000, 1600, 2400, 3400].forEach(function (delay) {
145
- var timerId = window.setTimeout(function () {
146
- resetLoginTurnstileOnDetectedError();
147
- }, delay);
148
- turnstileState.loginResetTimers.push(timerId);
149
- });
150
-
151
- // Stop watching after a while to avoid stale observers.
152
- var cleanupTimerId = window.setTimeout(function () {
153
- if (!turnstileState.navigatingAway && onLoginPageNow()) {
154
- // no-op: just let the user try again manually if no explicit error was detected
155
- }
156
- disarmLoginAttemptWatch();
157
- }, 6000);
158
- turnstileState.loginResetTimers.push(cleanupTimerId);
159
- }
160
-
161
- window.addEventListener('beforeunload', function () {
162
- turnstileState.navigatingAway = true;
163
- disarmLoginAttemptWatch();
164
- });
165
- window.addEventListener('pagehide', function () {
166
- turnstileState.navigatingAway = true;
167
- disarmLoginAttemptWatch();
168
- });
169
-
170
- function bindLoginRetryReset() {
171
- if (turnstileState.loginBindingsAttached) {
172
- return;
173
- }
174
- turnstileState.loginBindingsAttached = true;
175
-
176
- function loginSubmitTrigger() {
177
- scheduleLoginTurnstileReset();
178
- }
179
-
180
- // Native capture listeners only (avoid duplicate handlers/race conditions with jQuery delegates).
181
- document.addEventListener('click', function (ev) {
182
- var btn = ev.target && ev.target.closest ? ev.target.closest('[component="login/submit"], #login [type="submit"], form[action*="/login"] [type="submit"], form[data-action="login"] [type="submit"], button[type="submit"]') : null;
183
- if (!btn) {
184
- return;
185
- }
186
- var inLogin = btn.closest && btn.closest('#login, form[action*="/login"], form[data-action="login"], [component="login"]');
187
- if (inLogin || (ajaxify.data && ajaxify.data.tpl_url === 'login')) {
188
- loginSubmitTrigger();
189
- }
190
- }, true);
191
-
192
- // Enter key in login fields for themes that trigger login without a click event.
193
- document.addEventListener('keydown', function (ev) {
194
- if (ev.key !== 'Enter') {
195
- return;
196
- }
197
- var el = ev.target;
198
- if (!el || !el.closest) {
199
- return;
200
- }
201
- var inLogin = el.closest('#login, form[action*="/login"], form[data-action="login"], [component="login"]');
202
- if (inLogin || (ajaxify.data && ajaxify.data.tpl_url === 'login')) {
203
- loginSubmitTrigger();
204
- }
205
- }, true);
206
-
207
- }
208
-
209
- function renderTurnstileIfNeeded(isLoginPage) {
28
+ function renderTurnstileIfNeeded(isLoginPage) {
210
29
  var args = getTurnstileArgs();
211
30
  if (!args || (isLoginPage && !args.addLoginTurnstile)) {
212
31
  return;
@@ -218,18 +37,10 @@ function renderTurnstileIfNeeded(isLoginPage) {
218
37
  return;
219
38
  }
220
39
  var target = document.getElementById(args.targetId);
221
- if (!target) {
222
- return;
223
- }
224
-
225
- if (target.dataset.turnstileRendered === '1') {
226
- if (isLoginPage) {
227
- bindLoginRetryReset();
228
- }
40
+ if (!target || target.dataset.turnstileRendered === '1') {
229
41
  return;
230
42
  }
231
-
232
- var widgetId = turnstile.render('#' + args.targetId, {
43
+ turnstile.render('#' + args.targetId, {
233
44
  sitekey: args.siteKey,
234
45
  theme: args.theme || 'auto',
235
46
  size: args.size || 'normal',
@@ -244,18 +55,8 @@ function renderTurnstileIfNeeded(isLoginPage) {
244
55
  'error-callback': function () {
245
56
  require(['alerts'], function (alerts) { alerts.error('[[spam-be-gone:captcha-not-verified]]'); });
246
57
  },
247
- 'expired-callback': function () {
248
- resetTurnstileWidget(args.targetId);
249
- },
250
- 'timeout-callback': function () {
251
- resetTurnstileWidget(args.targetId);
252
- },
253
58
  });
254
59
  target.dataset.turnstileRendered = '1';
255
- turnstileState.widgets[args.targetId] = widgetId;
256
- if (isLoginPage) {
257
- bindLoginRetryReset();
258
- }
259
60
  })
260
61
  .catch(function () {
261
62
  require(['alerts'], function (alerts) { alerts.error('Failed to load Cloudflare Turnstile'); });
package/CHANGELOG.md DELETED
@@ -1,78 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- ## [1.0.9] - 2026-02-25
6
-
7
- ### Fixed
8
- - Fixed login success flow causing an unnecessary Turnstile refresh shortly before redirect.
9
- - Login retry reset now triggers only when a login error is actually detected on the page.
10
-
11
- ### Improved
12
- - Added navigation-aware cleanup (`beforeunload`/`pagehide`) to avoid reset races during successful login redirects.
13
- - Reworked login retry watcher to use short-lived DOM error detection + fallback polling instead of blind timed resets.
14
-
15
- ## [1.0.8] - 2026-02-25
16
-
17
- ### Fixed
18
- - Fixed Turnstile login retry reset in themes/login flows where previous click/enter hooks did not reliably trigger the widget reset.
19
- - Made login retry reset logic target the current Turnstile container dynamically instead of relying on a stale target id.
20
- - Added a fallback reset trigger when login error alerts are rendered on the login page.
21
-
22
- ## [1.0.7] - 2026-02-25
23
-
24
- ### Fixed
25
- - Login retry Turnstile reset now uses native event listeners only (removed jQuery delegated login listeners).
26
- - Prevented duplicate retry-reset triggers caused by mixed jQuery + native listeners on some NodeBB themes.
27
-
28
- ### Changed
29
- - Simplified login retry detection to click + Enter key flows (removed submit listener).
30
-
31
- ## [1.0.6] - 2026-02-25
32
-
33
- ### Fixed
34
- - Fixed login retry Turnstile reset trigger on NodeBB login flows that do not emit the expected jQuery submit/click events.
35
- - Prevented missing reset after failed login without page reload by adding capture-phase submit/click/Enter listeners.
36
-
37
- ## [1.0.5] - 2026-02-25
38
-
39
- ### Fixed
40
- - Fixed Turnstile not resetting after a failed login attempt when retrying on the same page.
41
- - Reset is now triggered after login submit if the user remains on the login page, even if the hidden Turnstile token input has already been cleared by client-side login logic.
42
-
43
- ## [1.0.4] - 2026-02-25
44
-
45
- ### Fixed
46
- - Fixed login UX regression where the Turnstile widget was resetting while typing in the username/password fields.
47
- - Turnstile now resets only after a login submission retry flow (failed login without page reload), not on every keystroke.
48
-
49
- ## [1.0.3] - 2026-02-25
50
-
51
- ### Fixed
52
- - Fixed Cloudflare Turnstile failure on login retry without page reload.
53
- - Reset Turnstile widget automatically after a failed login attempt (Turnstile tokens are single-use).
54
- - Improved login flow reliability when users retry authentication on the same page.
55
-
56
- ### Improved
57
- - Added safer Turnstile widget state handling on the login form.
58
- - Added callbacks handling for expired/timeout token states.
59
- - Better UX during repeated login attempts without manual refresh.
60
- - Removed deprecated `stopforumspam` npm dependency and replaced it with direct StopForumSpam API requests using native `fetch` (eliminates `q` / `node-domexception` install warnings).
61
-
62
- ## [1.0.2] - 2026-02-25
63
-
64
- ### UI
65
- - Replaced visible 'Turnstile' label with a more user-friendly label:
66
- - French: `Vérification de sécurité`
67
-
68
- ## [1.0.1] - 2026-02-25
69
-
70
- ### Changed
71
- - Refactored plugin for NodeBB 4.x compatibility.
72
- - Removed legacy reCAPTCHA and hCaptcha integrations.
73
- - Added Cloudflare Turnstile support (register + optional login protection).
74
- - Simplified package/tooling for a minimal runtime-focused plugin setup.
75
-
76
- ### Docs
77
- - Rewrote README in English (Turnstile-only, no images).
78
- - Added npm-ready/publish-ready package metadata and documentation.