nodebb-plugin-ezoic-infinite 1.8.78 → 1.8.79
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 +11 -4
- package/package.json +1 -1
- package/public/client.js +41 -43
- package/CLAUDE.md +0 -67
- package/nodebb-plugin-onekite-ezoic.zip +0 -0
package/library.js
CHANGED
|
@@ -60,7 +60,7 @@ async function getSettings() {
|
|
|
60
60
|
if (_settingsCache && (t - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
61
61
|
|
|
62
62
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
63
|
-
_settingsCacheAt =
|
|
63
|
+
_settingsCacheAt = t;
|
|
64
64
|
_settingsCache = {
|
|
65
65
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
66
66
|
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
@@ -93,10 +93,17 @@ async function isUserExcluded(uid, excludedGroups) {
|
|
|
93
93
|
|
|
94
94
|
_excludeCache.set(key, { value, at: Date.now() });
|
|
95
95
|
if (_excludeCache.size > 1000) {
|
|
96
|
+
const now = Date.now();
|
|
96
97
|
let toDel = 100;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (--toDel <= 0) break;
|
|
98
|
+
// Supprimer en priorité les entrées expirées, puis les plus anciennes
|
|
99
|
+
for (const [k, v] of _excludeCache) {
|
|
100
|
+
if (now - v.at >= EXCLUDE_TTL) { _excludeCache.delete(k); if (--toDel <= 0) break; }
|
|
101
|
+
}
|
|
102
|
+
if (toDel > 0) {
|
|
103
|
+
for (const k of _excludeCache.keys()) {
|
|
104
|
+
_excludeCache.delete(k);
|
|
105
|
+
if (--toDel <= 0) break;
|
|
106
|
+
}
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -93,8 +93,7 @@
|
|
|
93
93
|
mutGuard: 0,
|
|
94
94
|
blockedUntil: 0,
|
|
95
95
|
inflight: 0,
|
|
96
|
-
pending:
|
|
97
|
-
pendingSet: new Set(),
|
|
96
|
+
pending: new Set(),
|
|
98
97
|
runQueued: false,
|
|
99
98
|
burstActive: false,
|
|
100
99
|
burstDeadline: 0,
|
|
@@ -213,11 +212,11 @@
|
|
|
213
212
|
// ── GC disconnected wraps ──────────────────────────────────────────────────
|
|
214
213
|
|
|
215
214
|
function gcDisconnectedWraps() {
|
|
216
|
-
for (const [key, w] of
|
|
215
|
+
for (const [key, w] of S.wrapByKey) {
|
|
217
216
|
if (!w?.isConnected) S.wrapByKey.delete(key);
|
|
218
217
|
}
|
|
219
|
-
for (const [klass, set] of
|
|
220
|
-
for (const w of
|
|
218
|
+
for (const [klass, set] of S.wrapsByClass) {
|
|
219
|
+
for (const w of set) {
|
|
221
220
|
if (w?.isConnected) continue;
|
|
222
221
|
set.delete(w);
|
|
223
222
|
const id = parseInt(w.getAttribute?.(ATTR.WRAPID), 10);
|
|
@@ -268,13 +267,17 @@
|
|
|
268
267
|
return true;
|
|
269
268
|
}
|
|
270
269
|
|
|
270
|
+
function addWrapTimer(id, ms, fn) {
|
|
271
|
+
const timers = S.wrapTimers.get(id) || [];
|
|
272
|
+
timers.push(setTimeout(() => { try { fn(); } catch (_) {} }, ms));
|
|
273
|
+
S.wrapTimers.set(id, timers);
|
|
274
|
+
}
|
|
275
|
+
|
|
271
276
|
function scheduleUncollapseChecks(wrap, id) {
|
|
272
277
|
if (!wrap || !id) return;
|
|
273
|
-
const ids = S.wrapTimers.get(id) || [];
|
|
274
278
|
for (const ms of TIMING.UNCOLLAPSE_CHECK_MS) {
|
|
275
|
-
|
|
279
|
+
addWrapTimer(id, ms, () => clearEmptyIfFilled(wrap));
|
|
276
280
|
}
|
|
277
|
-
S.wrapTimers.set(id, ids);
|
|
278
281
|
}
|
|
279
282
|
|
|
280
283
|
// ── Pool management ────────────────────────────────────────────────────────
|
|
@@ -401,12 +404,8 @@
|
|
|
401
404
|
}
|
|
402
405
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
403
406
|
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
S.wrapsByClass.get(cls)?.delete(w);
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
407
|
+
const colonIdx = key?.indexOf(':') ?? -1;
|
|
408
|
+
if (colonIdx > 0) S.wrapsByClass.get(key.slice(0, colonIdx))?.delete(w);
|
|
410
409
|
w.remove();
|
|
411
410
|
} catch (_) {}
|
|
412
411
|
}
|
|
@@ -505,7 +504,7 @@
|
|
|
505
504
|
if (!id || isBlocked()) return;
|
|
506
505
|
if (now() - (S.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
507
506
|
if (S.inflight >= MAX_INFLIGHT) {
|
|
508
|
-
|
|
507
|
+
S.pending.add(id);
|
|
509
508
|
return;
|
|
510
509
|
}
|
|
511
510
|
startShow(id);
|
|
@@ -513,9 +512,9 @@
|
|
|
513
512
|
|
|
514
513
|
function drainQueue() {
|
|
515
514
|
if (isBlocked()) return;
|
|
516
|
-
while (S.inflight < MAX_INFLIGHT && S.pending.
|
|
517
|
-
const id = S.pending.
|
|
518
|
-
S.
|
|
515
|
+
while (S.inflight < MAX_INFLIGHT && S.pending.size) {
|
|
516
|
+
const id = S.pending.values().next().value;
|
|
517
|
+
S.pending.delete(id);
|
|
519
518
|
startShow(id);
|
|
520
519
|
}
|
|
521
520
|
}
|
|
@@ -557,22 +556,18 @@
|
|
|
557
556
|
}
|
|
558
557
|
|
|
559
558
|
function scheduleEmptyCheck(id, showTs) {
|
|
560
|
-
const ids = S.wrapTimers.get(id) || [];
|
|
561
559
|
for (const delay of [TIMING.EMPTY_CHECK_MS_1, TIMING.EMPTY_CHECK_MS_2]) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
} catch (_) {}
|
|
573
|
-
}, delay));
|
|
560
|
+
addWrapTimer(id, delay, () => {
|
|
561
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
562
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
563
|
+
if (!wrap || !ph?.isConnected) return;
|
|
564
|
+
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
565
|
+
if (clearEmptyIfFilled(wrap)) return;
|
|
566
|
+
if (ph.querySelector('[id^="div-gpt-ad"]')) return;
|
|
567
|
+
if (ph.offsetHeight > 10) return;
|
|
568
|
+
wrap.classList.add('is-empty');
|
|
569
|
+
});
|
|
574
570
|
}
|
|
575
|
-
S.wrapTimers.set(id, ids);
|
|
576
571
|
}
|
|
577
572
|
|
|
578
573
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
@@ -692,8 +687,7 @@
|
|
|
692
687
|
S.wrapsByClass.clear();
|
|
693
688
|
S.kind = null;
|
|
694
689
|
S.inflight = 0;
|
|
695
|
-
S.pending
|
|
696
|
-
S.pendingSet.clear();
|
|
690
|
+
S.pending.clear();
|
|
697
691
|
S.burstActive = false;
|
|
698
692
|
S.runQueued = false;
|
|
699
693
|
}
|
|
@@ -797,10 +791,9 @@
|
|
|
797
791
|
|
|
798
792
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
799
793
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const MUTED = [
|
|
794
|
+
const MUTED_RE = (() => {
|
|
795
|
+
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
796
|
+
return new RegExp([
|
|
804
797
|
`with id ${PH_PREFIX}`,
|
|
805
798
|
'adsbygoogle.push() error',
|
|
806
799
|
'already been defined',
|
|
@@ -815,12 +808,17 @@
|
|
|
815
808
|
'Error in custom getTCData',
|
|
816
809
|
'no interstitial API',
|
|
817
810
|
'JS-Enable should only',
|
|
818
|
-
];
|
|
811
|
+
].map(esc).join('|'));
|
|
812
|
+
})();
|
|
813
|
+
|
|
814
|
+
function muteConsole() {
|
|
815
|
+
if (window.__nbbEzMuted) return;
|
|
816
|
+
window.__nbbEzMuted = true;
|
|
819
817
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
820
818
|
const orig = console[method];
|
|
821
819
|
if (typeof orig !== 'function') continue;
|
|
822
820
|
console[method] = function (...args) {
|
|
823
|
-
if (typeof args[0] === 'string'
|
|
821
|
+
if (typeof args[0] === 'string' && MUTED_RE.test(args[0])) return;
|
|
824
822
|
return orig.apply(console, args);
|
|
825
823
|
};
|
|
826
824
|
}
|
|
@@ -863,10 +861,10 @@
|
|
|
863
861
|
warmNetwork(); patchShowAds(); getIO(); ensureDomObserver();
|
|
864
862
|
requestBurst();
|
|
865
863
|
});
|
|
864
|
+
// action:ajaxify.contentLoaded et action:category.loaded ne passent pas par hooks → jQuery uniquement
|
|
866
865
|
const burstEvts = [
|
|
867
|
-
'action:ajaxify.contentLoaded',
|
|
868
|
-
'action:
|
|
869
|
-
'action:category.loaded', 'action:topic.loaded',
|
|
866
|
+
'action:ajaxify.contentLoaded',
|
|
867
|
+
'action:category.loaded',
|
|
870
868
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
871
869
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
872
870
|
try {
|
package/CLAUDE.md
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
A NodeBB 4.x plugin that integrates Ezoic Infinite Ads with infinite scroll support. It injects Ezoic placeholder ads between topics, posts, and categories using a pool of placeholder IDs.
|
|
8
|
-
|
|
9
|
-
## Development Setup
|
|
10
|
-
|
|
11
|
-
This plugin has no build step, no test suite, and no dependencies beyond NodeBB's core. To test it:
|
|
12
|
-
|
|
13
|
-
1. Copy/symlink the plugin directory into a NodeBB instance's `node_modules/nodebb-plugin-ezoic-infinite/`
|
|
14
|
-
2. Activate the plugin in NodeBB Admin > Extend > Plugins
|
|
15
|
-
3. Configure placeholder IDs at Admin > Plugins > Ezoic Infinite Ads
|
|
16
|
-
|
|
17
|
-
To package for distribution:
|
|
18
|
-
```bash
|
|
19
|
-
zip -r nodebb-plugin-onekite-ezoic.zip . --exclude "*.zip" --exclude ".git/*" --exclude "CLAUDE.md"
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Architecture
|
|
23
|
-
|
|
24
|
-
### Server-side (`library.js`)
|
|
25
|
-
Single file exporting a `plugin` object with NodeBB hooks:
|
|
26
|
-
- `filter:middleware.renderHeader` → `injectEzoicHead`: Injects Ezoic scripts + inline config into `<head>`. For excluded users, injects a stub that stubs `ezstandalone` without loading ad scripts.
|
|
27
|
-
- `static:app.load` → `init`: Registers admin routes (`/admin/plugins/ezoic-infinite`) and a config API endpoint (`/api/plugins/ezoic-infinite/config`).
|
|
28
|
-
- `filter:admin.header.build` → `addAdminNavigation`: Adds the plugin to the ACP nav.
|
|
29
|
-
- `action:settings.set` → `onSettingsSet`: Invalidates the settings cache on save.
|
|
30
|
-
|
|
31
|
-
Settings are cached for 30s. Group membership for ad exclusion is cached for 60s per uid+groups combination (up to 1000 entries).
|
|
32
|
-
|
|
33
|
-
The inline config is serialized as `window.__nbbEzoicCfg` in the HTML head, so `client.js` can read it without an extra network request.
|
|
34
|
-
|
|
35
|
-
### Client-side (`public/client.js`)
|
|
36
|
-
Self-contained IIFE with no external dependencies beyond jQuery (available globally in NodeBB) and the Ezoic `ezstandalone` API.
|
|
37
|
-
|
|
38
|
-
**Page type detection** (`detectKind`): Determines if the current page is a topic, category topic list, or categories index, then selects the corresponding ad kind (`ezoic-ad-message`, `ezoic-ad-between`, `ezoic-ad-categories`).
|
|
39
|
-
|
|
40
|
-
**Ad injection flow**:
|
|
41
|
-
1. `requestBurst()` → `scheduleRun()` → `runCore()` → `injectBetween()`
|
|
42
|
-
2. `injectBetween` iterates DOM items, checks ordinal position against the configured interval, inserts wrapper divs after qualifying elements using `insertAfter()`
|
|
43
|
-
3. Each wrapper contains an Ezoic placeholder `<div id="ezoic-pub-ad-placeholder-{id}">`
|
|
44
|
-
4. `IntersectionObserver` triggers `enqueueShow(id)` → `startShow(id)` → `ezstandalone.showAds(id)` when placeholders approach the viewport
|
|
45
|
-
|
|
46
|
-
**Pool/recycling**: Placeholder IDs are organized into three pools (topics, posts, categories). When all IDs in a pool are in use, `recycleWrap` finds the oldest off-screen wrap, calls `destroyPlaceholders`, and reuses it for the new position.
|
|
47
|
-
|
|
48
|
-
**State resets**: On `action:ajaxify.start` (SPA navigation), `cleanup()` destroys all wraps, resets all state. On `action:ajaxify.end`, re-initializes and triggers a burst.
|
|
49
|
-
|
|
50
|
-
### Admin UI
|
|
51
|
-
- `public/templates/admin/plugins/ezoic-infinite.tpl`: NodeBB Benchpress template (French labels), uses `Settings.load`/`Settings.save` from NodeBB's ACP framework
|
|
52
|
-
- `public/admin.js`: Minimal ACP script wiring the save button to NodeBB's `settings` module
|
|
53
|
-
- `public/style.css`: Visual styles for the `.nodebb-ezoic-wrap` and `.is-empty` states
|
|
54
|
-
|
|
55
|
-
### Key Constants (client.js)
|
|
56
|
-
- `WRAP_CLASS = 'nodebb-ezoic-wrap'`: CSS class on all injected wrappers
|
|
57
|
-
- `PH_PREFIX = 'ezoic-pub-ad-placeholder-'`: Prefix for placeholder element IDs
|
|
58
|
-
- `MAX_INSERTS_RUN = 6`: Max ad insertions per `runCore()` call
|
|
59
|
-
- `MAX_INFLIGHT = 4`: Max concurrent `showAds()` calls
|
|
60
|
-
- Three placeholder ID pools must not overlap between ad types
|
|
61
|
-
- `S.wrapTimers: Map<id, timerId[]>` tracks all pending timers per wrap — cancelled in `dropWrap()` and `cleanup()`
|
|
62
|
-
- `cleanup()` disconnects `S.domObs` and `S.io` so they are re-created fresh on next SPA page
|
|
63
|
-
|
|
64
|
-
### Important Constraints
|
|
65
|
-
- `sa.min.js` must load **synchronously** (no `async` attribute) — loading it async causes "Timed out waiting for loadingStatus LOADING" because it may execute before the CMP is ready
|
|
66
|
-
- Placeholder IDs must be distinct across the three pools (topics, posts, categories)
|
|
67
|
-
- The `showAds()` function is monkey-patched to deduplicate calls and guard against disconnected placeholders
|
|
Binary file
|