nodebb-plugin-ezoic-infinite 1.4.73 → 1.4.74

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 ADDED
@@ -0,0 +1,15 @@
1
+ # NodeBB Plugin – Ezoic Infinite (Production)
2
+
3
+ This plugin injects Ezoic placeholders between topics and posts on NodeBB 4.x,
4
+ with full support for infinite scroll.
5
+
6
+ ## Key guarantees
7
+ - No duplicate ads back-to-back
8
+ - One showAds call per placeholder
9
+ - Fast reveal (MutationObserver on first child)
10
+ - Safe with ajaxify navigation
11
+ - Works with NodeBB 4.x + Harmony
12
+
13
+ ## Notes
14
+ - Placeholders must exist and be selected in Ezoic
15
+ - Use separate ID pools for topics vs messages
package/library.js ADDED
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const meta = require.main.require('./src/meta');
4
+ const groups = require.main.require('./src/groups');
5
+ const db = require.main.require('./src/database');
6
+
7
+ const SETTINGS_KEY = 'ezoic-infinite';
8
+ const plugin = {};
9
+
10
+ function normalizeExcludedGroups(value) {
11
+ if (!value) return [];
12
+ if (Array.isArray(value)) return value;
13
+ return String(value).split(',').map(s => s.trim()).filter(Boolean);
14
+ }
15
+
16
+ function parseBool(v, def = false) {
17
+ if (v === undefined || v === null || v === '') return def;
18
+ if (typeof v === 'boolean') return v;
19
+ const s = String(v).toLowerCase();
20
+ return s === '1' || s === 'true' || s === 'on' || s === 'yes';
21
+ }
22
+
23
+ async function getAllGroups() {
24
+ let names = await db.getSortedSetRange('groups:createtime', 0, -1);
25
+ if (!names || !names.length) {
26
+ names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
27
+ }
28
+ const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
+ const data = await groups.getGroupsData(filtered);
30
+ // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
31
+ const valid = data.filter(g => g && g.name);
32
+ valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
33
+ return valid;
34
+ }
35
+ let _settingsCache = null;
36
+ let _settingsCacheAt = 0;
37
+ const SETTINGS_TTL = 30000; // 30s
38
+
39
+ async function getSettings() {
40
+ const now = Date.now();
41
+ if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
42
+ const s = await meta.settings.get(SETTINGS_KEY);
43
+ _settingsCacheAt = Date.now();
44
+ _settingsCache = {
45
+ // Between-post ads (simple blocks) in category topic list
46
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
47
+ showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
48
+ placeholderIds: (s.placeholderIds || '').trim(),
49
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
50
+
51
+ // Home/categories list ads (between categories on / or /categories)
52
+ enableCategoryAds: parseBool(s.enableCategoryAds, false),
53
+ showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
54
+ categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
55
+ intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
56
+
57
+ // "Ad message" between replies (looks like a post)
58
+ enableMessageAds: parseBool(s.enableMessageAds, false),
59
+ showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
60
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
61
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
62
+
63
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
64
+ };
65
+ return _settingsCache;
66
+ }
67
+
68
+ async function isUserExcluded(uid, excludedGroups) {
69
+ if (!uid || !excludedGroups.length) return false;
70
+ const userGroups = await groups.getUserGroups([uid]);
71
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
72
+ }
73
+
74
+ plugin.onSettingsSet = function (data) {
75
+ // Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
76
+ if (data && data.hash === SETTINGS_KEY) {
77
+ _settingsCache = null;
78
+ }
79
+ };
80
+
81
+ plugin.addAdminNavigation = async (header) => {
82
+ header.plugins = header.plugins || [];
83
+ header.plugins.push({
84
+ route: '/plugins/ezoic-infinite',
85
+ icon: 'fa-ad',
86
+ name: 'Ezoic Infinite Ads'
87
+ });
88
+ return header;
89
+ };
90
+
91
+ plugin.init = async ({ router, middleware }) => {
92
+ async function render(req, res) {
93
+ const settings = await getSettings();
94
+ const allGroups = await getAllGroups();
95
+
96
+ res.render('admin/plugins/ezoic-infinite', {
97
+ title: 'Ezoic Infinite Ads',
98
+ ...settings,
99
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
100
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
101
+ allGroups,
102
+ });
103
+ }
104
+
105
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
106
+ router.get('/api/admin/plugins/ezoic-infinite', render);
107
+
108
+ router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
109
+ const settings = await getSettings();
110
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
111
+
112
+ res.json({
113
+ excluded,
114
+ enableBetweenAds: settings.enableBetweenAds,
115
+ showFirstTopicAd: settings.showFirstTopicAd,
116
+ placeholderIds: settings.placeholderIds,
117
+ intervalPosts: settings.intervalPosts,
118
+ enableCategoryAds: settings.enableCategoryAds,
119
+ showFirstCategoryAd: settings.showFirstCategoryAd,
120
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
121
+ intervalCategories: settings.intervalCategories,
122
+ enableMessageAds: settings.enableMessageAds,
123
+ showFirstMessageAd: settings.showFirstMessageAd,
124
+ messagePlaceholderIds: settings.messagePlaceholderIds,
125
+ messageIntervalPosts: settings.messageIntervalPosts,
126
+ });
127
+ });
128
+ };
129
+
130
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.4.73",
3
+ "version": "1.4.74",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "id": "nodebb-plugin-ezoic-infinite",
3
+ "name": "NodeBB Ezoic Infinite Ads",
4
+ "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
+ "library": "./library.js",
6
+ "hooks": [
7
+ {
8
+ "hook": "static:app.load",
9
+ "method": "init"
10
+ },
11
+ {
12
+ "hook": "filter:admin.header.build",
13
+ "method": "addAdminNavigation"
14
+ },
15
+ {
16
+ "hook": "action:settings.set",
17
+ "method": "onSettingsSet"
18
+ }
19
+ ],
20
+ "staticDirs": {
21
+ "public": "public"
22
+ },
23
+ "acpScripts": [
24
+ "public/admin.js"
25
+ ],
26
+ "scripts": [
27
+ "public/client.js"
28
+ ],
29
+ "templates": "public/templates",
30
+ "css": [
31
+ "public/style.css"
32
+ ]
33
+ }
@@ -23,6 +23,10 @@
23
23
  usedPosts: new Set(),
24
24
  usedCategories: new Set(),
25
25
 
26
+ liveTopics: [],
27
+ livePosts: [],
28
+ liveCategories: [],
29
+
26
30
  lastShowById: new Map(),
27
31
  pendingById: new Set(),
28
32
  definedIds: new Set(),
@@ -243,6 +247,7 @@
243
247
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
244
248
  wrap.setAttribute('data-ezoic-after', String(afterPos));
245
249
  wrap.style.width = '100%';
250
+ wrap.style.display = 'none';
246
251
 
247
252
  const ph = document.createElement('div');
248
253
  ph.id = `${PLACEHOLDER_PREFIX}${id}`;
@@ -318,26 +323,14 @@
318
323
  }
319
324
  }
320
325
 
321
- function markFilled(wrap) {
322
- try {
323
- if (!wrap) return;
324
- if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
325
- wrap.setAttribute('data-ezoic-filled', '1');
326
- } catch (e) {}
327
- }
328
-
326
+
329
327
  function forcePlaceholderAutoHeight(wrap, id) {
330
328
  try {
331
- if (!wrap || !id) return;
332
- const ph = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
329
+ if (!wrap) return;
330
+ const ph = id ? wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`) : (wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`));
333
331
  if (!ph) return;
334
- // Override any reserved height/min-height injected by ad scripts
335
- try {
336
332
  ph.style.setProperty('height', 'auto', 'important');
337
333
  ph.style.setProperty('min-height', '0px', 'important');
338
- } catch (e) {}
339
- // Some creatives adjust after a frame
340
- try {
341
334
  requestAnimationFrame(() => {
342
335
  try {
343
336
  ph.style.setProperty('height', 'auto', 'important');
@@ -345,6 +338,16 @@
345
338
  } catch (e) {}
346
339
  });
347
340
  } catch (e) {}
341
+ }
342
+
343
+ function markFilled(wrap, id) {
344
+ try {
345
+ if (!wrap) return;
346
+ if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
347
+ wrap.setAttribute('data-ezoic-filled', '1');
348
+ // show wrapper only when filled (avoid blank gaps if consent/CMP blocks)
349
+ wrap.style.display = 'block';
350
+ forcePlaceholderAutoHeight(wrap, id);
348
351
  } catch (e) {}
349
352
  }
350
353
 
@@ -358,15 +361,13 @@
358
361
  if (!ph) return;
359
362
  // Already filled?
360
363
  if (ph.childNodes && ph.childNodes.length > 0) {
361
- markFilled(wrap); // Afficher wrapper
362
- forcePlaceholderAutoHeight(wrap, id);
364
+ markFilled(wrap, id); // Afficher wrapper
363
365
  sessionDefinedIds.add(id);
364
366
  return;
365
367
  }
366
368
  const obs = new MutationObserver(() => {
367
369
  if (ph.childNodes && ph.childNodes.length > 0) {
368
- markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
369
- forcePlaceholderAutoHeight(wrap, id);
370
+ markFilled(wrap, id); // CRITIQUE: Afficher wrapper maintenant
370
371
  try { sessionDefinedIds.add(id); } catch (e) {}
371
372
  try { obs.disconnect(); } catch (e) {}
372
373
  }
@@ -386,7 +387,7 @@
386
387
  const filled = !!(ph.childNodes && ph.childNodes.length > 0);
387
388
  if (filled) {
388
389
  try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
389
- try { markFilled(wrap); } catch (e) {}
390
+ try { markFilled(wrap, id); } catch (e) {}
390
391
  }
391
392
  return filled;
392
393
  }
@@ -415,23 +416,17 @@
415
416
  batchShowAdsTimer = setTimeout(() => {
416
417
  if (pendingShowAdsIds.size === 0) return;
417
418
 
418
- // Only keep IDs whose placeholders still exist at execution time
419
- const idsArray = Array.from(pendingShowAdsIds).filter((id) => {
420
- const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
421
- return !!(el && el.isConnected);
422
- });
419
+ const idsArray = Array.from(pendingShowAdsIds);
423
420
  pendingShowAdsIds.clear();
424
421
 
425
- if (!idsArray.length) return;
426
-
427
422
  // Appeler showAds avec TOUS les IDs en une fois
428
423
  try {
429
424
  window.ezstandalone = window.ezstandalone || {};
430
425
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
431
426
  window.ezstandalone.cmd.push(function() {
432
427
  if (typeof window.ezstandalone.showAds === 'function') {
433
- // Call as array (most compatible with ezstandalone)
434
- window.ezstandalone.showAds(idsArray);
428
+ // Appel batch: showAds(id1, id2, id3...)
429
+ window.ezstandalone.showAds(...idsArray);
435
430
  // Tracker tous les IDs
436
431
  idsArray.forEach(id => {
437
432
  state.lastShowById.set(id, Date.now());
@@ -459,6 +454,8 @@
459
454
  window.ezstandalone = window.ezstandalone || {};
460
455
  if (typeof window.ezstandalone.showAds === 'function') {
461
456
 
457
+ const elNow = document.getElementById(phId);
458
+ if (!elNow || !elNow.isConnected) return false;
462
459
  state.lastShowById.set(id, Date.now());
463
460
  window.ezstandalone.showAds(id);
464
461
  sessionDefinedIds.add(id);
@@ -534,7 +531,7 @@
534
531
  return Array.from(new Set(out)).sort((a, b) => a - b);
535
532
  }
536
533
 
537
- function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
534
+ function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet, liveArr) {
538
535
  if (!items.length) return 0;
539
536
  const targets = computeTargets(items.length, interval, showFirst);
540
537
 
@@ -555,7 +552,7 @@
555
552
 
556
553
  if (findWrap(kindClass, afterPos)) continue;
557
554
 
558
- const pick = pickId(kindPool, []);
555
+ const pick = pickId(kindPool, liveArr || []);
559
556
  const id = pick.id;
560
557
  if (!id) break;
561
558
 
@@ -581,7 +578,7 @@
581
578
  callShowAdsWhenReady(id);
582
579
  }
583
580
 
584
- liveArr.push({ id, wrap });
581
+ if (liveArr) liveArr.push({ id, wrap });
585
582
  if (wrap && (
586
583
  (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
587
584
  )) {
@@ -681,7 +678,7 @@
681
678
  Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
682
679
  normalizeBool(cfg.showFirstMessageAd),
683
680
  state.poolPosts,
684
- state.usedPosts);
681
+ state.usedPosts, state.livePosts);
685
682
  }
686
683
  } else if (kind === 'categoryTopics') {
687
684
  if (normalizeBool(cfg.enableBetweenAds)) {
@@ -689,7 +686,7 @@
689
686
  Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
690
687
  normalizeBool(cfg.showFirstTopicAd),
691
688
  state.poolTopics,
692
- state.usedTopics);
689
+ state.usedTopics, state.liveTopics);
693
690
  }
694
691
  } else if (kind === 'categories') {
695
692
  if (normalizeBool(cfg.enableCategoryAds)) {
@@ -697,7 +694,7 @@
697
694
  Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
698
695
  normalizeBool(cfg.showFirstCategoryAd),
699
696
  state.poolCategories,
700
- state.usedCategories);
697
+ state.usedCategories, state.liveCategories);
701
698
  }
702
699
  }
703
700
 
@@ -0,0 +1,40 @@
1
+ /* Ezoic Infinite Ads (NodeBB) - production safe */
2
+ /* Hide wrappers until ad actually fills to avoid blank gaps (e.g., CMP/consent issues) */
3
+ .ezoic-ad{
4
+ display:none;
5
+ height:auto !important;
6
+ min-height:0 !important;
7
+ padding:0 !important;
8
+ margin:0 !important;
9
+ width:100%;
10
+ }
11
+
12
+ /* Marked filled => show */
13
+ .ezoic-ad[data-ezoic-filled="1"]{
14
+ display:block;
15
+ }
16
+
17
+ /* Placeholder must not reserve fixed height */
18
+ .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"]{
19
+ height:auto !important;
20
+ min-height:0 !important;
21
+ margin:0 !important;
22
+ padding:0 !important;
23
+ }
24
+
25
+ /* Prevent baseline gaps under iframes/ins */
26
+ .ezoic-ad iframe,
27
+ .ezoic-ad ins{
28
+ display:block !important;
29
+ }
30
+
31
+ /* Remove empty spacer divs if any */
32
+ .ezoic-ad > div:empty{
33
+ display:none !important;
34
+ }
35
+
36
+ /* Defensive: wipe margins/paddings inside wrapper */
37
+ .ezoic-ad *{
38
+ margin:0 !important;
39
+ padding:0 !important;
40
+ }
package/style.css DELETED
@@ -1,43 +0,0 @@
1
- /*
2
- NodeBB + Ezoic (standalone)
3
- Goal: never reserve space until the ad is truly filled, and never keep a fixed/min height.
4
- */
5
-
6
- /* Hide wrappers until we detect the placeholder has content (prevents empty gaps if CMP/ads are blocked) */
7
- .ezoic-ad {
8
- display: none;
9
- width: 100%;
10
- height: auto !important;
11
- min-height: 0 !important;
12
- padding: 0 !important;
13
- margin: 0 !important;
14
- }
15
-
16
- .ezoic-ad[data-ezoic-filled="1"] {
17
- display: block;
18
- }
19
-
20
- /* The placeholder must not reserve space */
21
- .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
22
- height: auto !important;
23
- min-height: 0 !important;
24
- margin: 0 !important;
25
- padding: 0 !important;
26
- }
27
-
28
- /* Avoid baseline gap under iframes/ins */
29
- .ezoic-ad iframe,
30
- .ezoic-ad ins {
31
- display: block !important;
32
- }
33
-
34
- /* Remove empty spacer divs that sometimes get injected */
35
- .ezoic-ad > div:empty {
36
- display: none !important;
37
- }
38
-
39
- /* Keep internal margins/paddings from creating vertical gaps */
40
- .ezoic-ad * {
41
- margin: 0 !important;
42
- padding: 0 !important;
43
- }
File without changes