nodebb-plugin-ezoic-infinite 1.8.13 → 1.8.14

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
@@ -1,24 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const meta = require.main.require('./src/meta');
3
+ const meta = require.main.require('./src/meta');
4
4
  const groups = require.main.require('./src/groups');
5
- const db = require.main.require('./src/database');
5
+ const db = require.main.require('./src/database');
6
6
 
7
7
  const SETTINGS_KEY = 'ezoic-infinite';
8
8
  const plugin = {};
9
9
 
10
- // ── Helpers ────────────────────────────────────────────────────────────────
11
-
12
10
  function normalizeExcludedGroups(value) {
13
11
  if (!value) return [];
14
12
  if (Array.isArray(value)) return value;
15
- // NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
16
- const s = String(value).trim();
17
- if (s.startsWith('[')) {
18
- try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
19
- }
20
- // Fallback : séparation par virgule
21
- return s.split(',').map(v => v.trim()).filter(Boolean);
13
+ return String(value).split(',').map(s => s.trim()).filter(Boolean);
22
14
  }
23
15
 
24
16
  function parseBool(v, def = false) {
@@ -29,22 +21,34 @@ function parseBool(v, def = false) {
29
21
  }
30
22
 
31
23
  async function getAllGroups() {
24
+ const now = Date.now();
25
+ if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
26
+
32
27
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
33
28
  if (!names || !names.length) {
34
29
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
35
30
  }
36
31
  const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
37
32
  const data = await groups.getGroupsData(filtered);
38
- const valid = data.filter(g => g && g.name);
33
+ const valid = (data || []).filter(g => g && g.name);
39
34
  valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
35
+
36
+ _groupsCacheAt = now;
37
+ _groupsCache = valid;
40
38
  return valid;
41
39
  }
42
40
 
43
- // ── Settings cache (30s TTL) ────────────────────────────────────────────────
44
-
45
- let _settingsCache = null;
41
+ let _settingsCache = null;
42
+ _excludedCache.clear();
46
43
  let _settingsCacheAt = 0;
47
- const SETTINGS_TTL = 30_000;
44
+ const SETTINGS_TTL = 30000; // 30s
45
+
46
+ let _groupsCache = null;
47
+ let _groupsCacheAt = 0;
48
+ const GROUPS_TTL = 5 * 60 * 1000; // 5min
49
+
50
+ const _excludedCache = new Map(); // uid -> { at, excluded }
51
+ const EXCLUDED_TTL = 30 * 1000; // 30s
48
52
 
49
53
  async function getSettings() {
50
54
  const now = Date.now();
@@ -52,107 +56,100 @@ async function getSettings() {
52
56
  const s = await meta.settings.get(SETTINGS_KEY);
53
57
  _settingsCacheAt = Date.now();
54
58
  _settingsCache = {
55
- enableBetweenAds: parseBool(s.enableBetweenAds, true),
56
- showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
57
- placeholderIds: (s.placeholderIds || '').trim(),
58
- intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
59
- enableCategoryAds: parseBool(s.enableCategoryAds, false),
60
- showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
59
+ // Between-post ads (simple blocks) in category topic list
60
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
61
+ showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
62
+ placeholderIds: (s.placeholderIds || '').trim(),
63
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
64
+
65
+ // Home/categories list ads (between categories on / or /categories)
66
+ enableCategoryAds: parseBool(s.enableCategoryAds, false),
67
+ showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
61
68
  categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
62
- intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
63
- enableMessageAds: parseBool(s.enableMessageAds, false),
64
- showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
65
- messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
66
- messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
67
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
69
+ intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
70
+
71
+ // "Ad message" between replies (looks like a post)
72
+ enableMessageAds: parseBool(s.enableMessageAds, false),
73
+ showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
74
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
75
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
76
+
77
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
68
78
  };
69
79
  return _settingsCache;
70
80
  }
71
81
 
72
82
  async function isUserExcluded(uid, excludedGroups) {
73
83
  if (!uid || !excludedGroups.length) return false;
84
+
85
+ const now = Date.now();
86
+ const cached = _excludedCache.get(uid);
87
+ if (cached && (now - cached.at) < EXCLUDED_TTL) return cached.excluded;
88
+
74
89
  const userGroups = await groups.getUserGroups([uid]);
75
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
76
- }
90
+ const excluded = (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
77
91
 
78
- // ── Scripts Ezoic ──────────────────────────────────────────────────────────
92
+ _excludedCache.set(uid, { at: now, excluded });
93
+ // petite hygiene : éviter une map qui grossit sans limite
94
+ if (_excludedCache.size > 5000) _excludedCache.clear();
79
95
 
80
- const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
81
- <script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
82
- <script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
83
- <script>
84
- window.ezstandalone = window.ezstandalone || {};
85
- ezstandalone.cmd = ezstandalone.cmd || [];
86
- </script>`;
96
+ return excluded;
97
+ }
87
98
 
88
- // ── Hooks ──────────────────────────────────────────────────────────────────
89
99
 
90
100
  plugin.onSettingsSet = function (data) {
91
- if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
101
+ // Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
102
+ if (data && data.hash === SETTINGS_KEY) {
103
+ _settingsCache = null;
104
+ _excludedCache.clear();
105
+ }
92
106
  };
93
107
 
94
108
  plugin.addAdminNavigation = async (header) => {
95
109
  header.plugins = header.plugins || [];
96
- header.plugins.push({ route: '/plugins/ezoic-infinite', icon: 'fa-ad', name: 'Ezoic Infinite Ads' });
110
+ header.plugins.push({
111
+ route: '/plugins/ezoic-infinite',
112
+ icon: 'fa-ad',
113
+ name: 'Ezoic Infinite Ads'
114
+ });
97
115
  return header;
98
116
  };
99
117
 
100
- /**
101
- * Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
102
- *
103
- * NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
104
- * (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
105
- * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
106
- * et est rendu via req.app.renderAsync('header', hookReturn.templateData).
107
- * On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
108
- * tout en préservant ce dernier.
109
- */
110
- plugin.injectEzoicHead = async (data) => {
111
- try {
112
- const settings = await getSettings();
113
- const uid = data.req?.uid ?? 0;
114
- const excluded = await isUserExcluded(uid, settings.excludedGroups);
115
- if (!excluded) {
116
- // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
- data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
118
- }
119
- } catch (_) {}
120
- return data;
121
- };
122
-
123
118
  plugin.init = async ({ router, middleware }) => {
124
119
  async function render(req, res) {
125
- const settings = await getSettings();
120
+ const settings = await getSettings();
126
121
  const allGroups = await getAllGroups();
122
+
127
123
  res.render('admin/plugins/ezoic-infinite', {
128
124
  title: 'Ezoic Infinite Ads',
129
125
  ...settings,
130
126
  enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
131
- enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
127
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
132
128
  allGroups,
133
129
  });
134
130
  }
135
131
 
136
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
132
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
137
133
  router.get('/api/admin/plugins/ezoic-infinite', render);
138
134
 
139
135
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
140
136
  const settings = await getSettings();
141
137
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
138
+
142
139
  res.json({
143
140
  excluded,
144
- enableBetweenAds: settings.enableBetweenAds,
145
- showFirstTopicAd: settings.showFirstTopicAd,
146
- placeholderIds: settings.placeholderIds,
147
- intervalPosts: settings.intervalPosts,
148
- enableCategoryAds: settings.enableCategoryAds,
149
- showFirstCategoryAd: settings.showFirstCategoryAd,
141
+ enableBetweenAds: settings.enableBetweenAds,
142
+ showFirstTopicAd: settings.showFirstTopicAd,
143
+ placeholderIds: settings.placeholderIds,
144
+ intervalPosts: settings.intervalPosts,
145
+ enableCategoryAds: settings.enableCategoryAds,
146
+ showFirstCategoryAd: settings.showFirstCategoryAd,
150
147
  categoryPlaceholderIds: settings.categoryPlaceholderIds,
151
- intervalCategories: settings.intervalCategories,
152
- enableMessageAds: settings.enableMessageAds,
153
- showFirstMessageAd: settings.showFirstMessageAd,
154
- messagePlaceholderIds: settings.messagePlaceholderIds,
155
- messageIntervalPosts: settings.messageIntervalPosts,
148
+ intervalCategories: settings.intervalCategories,
149
+ enableMessageAds: settings.enableMessageAds,
150
+ showFirstMessageAd: settings.showFirstMessageAd,
151
+ messagePlaceholderIds: settings.messagePlaceholderIds,
152
+ messageIntervalPosts: settings.messageIntervalPosts,
156
153
  });
157
154
  });
158
155
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.13",
3
+ "version": "1.8.14",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -18,4 +18,4 @@
18
18
  "compatibility": "^4.0.0"
19
19
  },
20
20
  "private": false
21
- }
21
+ }
package/plugin.json CHANGED
@@ -4,14 +4,30 @@
4
4
  "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "library": "./library.js",
6
6
  "hooks": [
7
- { "hook": "static:app.load", "method": "init" },
8
- { "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
9
- { "hook": "action:settings.set", "method": "onSettingsSet" },
10
- { "hook": "filter:middleware.renderHeader","method": "injectEzoicHead" }
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
+ }
11
19
  ],
12
- "staticDirs": { "public": "public" },
13
- "acpScripts": [ "public/admin.js" ],
14
- "scripts": [ "public/client.js" ],
15
- "templates": "public/templates",
16
- "css": [ "public/style.css" ]
17
- }
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
+ }