nodebb-plugin-ezoic-infinite 1.0.2 → 1.0.5

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,5 @@
1
+ nodebb-plugin-ezoic-infinite v1.0.5
2
+
3
+ - ACP kept in the same spirit as v0.9.5 (server-rendered groups + admin.settings.get/set)
4
+ - Injection logic matches v0.9.7: showAds called one placeholder at a time, separate pools, FIFO recycling.
5
+ - Frontend settings are read from window.config.ezoicInfinite (filter:config.get), no admin endpoints used on frontend.
package/library.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  'use strict';
3
2
 
4
3
  const meta = require.main.require('./src/meta');
@@ -6,7 +5,7 @@ const groups = require.main.require('./src/groups');
6
5
 
7
6
  const Plugin = {};
8
7
 
9
- Plugin.init = async ({ router, middleware }) => {
8
+ Plugin.init = async function ({ router, middleware }) {
10
9
  router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
11
10
  router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
12
11
  };
@@ -15,24 +14,33 @@ async function renderAdmin(req, res) {
15
14
  const settings = await meta.settings.get('ezoic-infinite');
16
15
 
17
16
  let groupNames = [];
18
- try {
19
- groupNames = await groups.getGroupsFromSet('groups:createtime', 0, -1);
20
- } catch (e) {}
17
+ const candidates = ['groups:createtime', 'groups:visible:createtime', 'groups:alphabetical', 'groups:visible:alphabetical'];
18
+ for (const set of candidates) {
19
+ try {
20
+ groupNames = await groups.getGroupsFromSet(set, 0, -1);
21
+ if (Array.isArray(groupNames) && groupNames.length) break;
22
+ } catch (e) {}
23
+ }
21
24
 
22
25
  let groupList = [];
23
26
  try {
24
27
  groupList = await groups.getGroupsData(groupNames);
25
28
  } catch (e) {
26
- groupList = groupNames.map(name => ({ name }));
29
+ groupList = (groupNames || []).map((name) => ({ name }));
27
30
  }
28
31
 
32
+ groupList = (groupList || [])
33
+ .filter(g => g && g.name)
34
+ .sort((a, b) => (a.name || '').localeCompare(b.name || '', 'fr', { sensitivity: 'base' }));
35
+
29
36
  res.render('admin/plugins/ezoic-infinite', {
37
+ title: 'Ezoic - Publicités Infinite Scroll',
30
38
  settings,
31
39
  groups: groupList,
32
40
  });
33
41
  }
34
42
 
35
- Plugin.addAdminNavigation = async (header) => {
43
+ Plugin.addAdminNavigation = async function (header) {
36
44
  header.plugins = header.plugins || [];
37
45
  header.plugins.push({
38
46
  route: '/plugins/ezoic-infinite',
@@ -42,8 +50,9 @@ Plugin.addAdminNavigation = async (header) => {
42
50
  return header;
43
51
  };
44
52
 
45
- Plugin.addConfig = async (config) => {
46
- config.ezoicInfinite = await meta.settings.get('ezoic-infinite');
53
+ Plugin.addConfig = async function (config) {
54
+ const settings = await meta.settings.get('ezoic-infinite');
55
+ config.ezoicInfinite = settings || {};
47
56
  return config;
48
57
  };
49
58
 
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.0.2",
4
- "description": "Ezoic Infinite (rollback to 0.8.2 behaviour)",
3
+ "version": "1.0.5",
4
+ "description": "Ezoic ads injection for NodeBB 4.x (ACP stable like 0.9.5 + injection logic like 0.9.7).",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
+ "keywords": [
8
+ "nodebb",
9
+ "nodebb-plugin",
10
+ "ezoic",
11
+ "ads",
12
+ "infinite-scroll"
13
+ ],
7
14
  "nbbpm": {
8
15
  "compatibility": "^4.0.0"
9
16
  }
package/plugin.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "nodebb-plugin-ezoic-infinite",
3
3
  "name": "Ezoic Infinite",
4
+ "description": "Ezoic ads injection (topics list + topic posts) with pool recycling for infinite scroll.",
4
5
  "library": "./library.js",
5
6
  "hooks": [
6
7
  {
@@ -25,5 +26,8 @@
25
26
  "scripts": [
26
27
  "public/client.js"
27
28
  ],
29
+ "css": [
30
+ "public/style.css"
31
+ ],
28
32
  "templates": "public/templates"
29
33
  }
package/public/admin.js CHANGED
@@ -1,35 +1,59 @@
1
+ /* global $, app, socket */
2
+ 'use strict';
1
3
 
2
- /* global $, socket, app */
3
- $(function () {
4
- const ns = 'ezoic-infinite';
5
- const form = $('.ezoic-infinite-settings');
6
-
7
- socket.emit('admin.settings.get', { hash: ns }, (err, data) => {
8
- if (!data) return;
9
- Object.keys(data).forEach(k => {
10
- const el = form.find('[name="' + k + '"]');
11
- if (!el.length) return;
12
- if (el.attr('type') === 'checkbox') el.prop('checked', data[k]);
13
- else el.val(data[k]);
14
- });
15
-
16
- if (data.excludedGroups) {
17
- data.excludedGroups.split(',').forEach(g =>
18
- form.find('option[value="' + g + '"]').prop('selected', true)
19
- );
4
+ (function () {
5
+ if (window.ezoicInfiniteAdminLoaded) return;
6
+ window.ezoicInfiniteAdminLoaded = true;
7
+
8
+ $(document).ready(function () {
9
+ const namespace = 'ezoic-infinite';
10
+
11
+ function load() {
12
+ socket.emit('admin.settings.get', { hash: namespace }, function (err, data) {
13
+ if (err) return;
14
+ data = data || {};
15
+
16
+ const form = $('.ezoic-infinite-settings');
17
+
18
+ form.find('[name="enableBetweenAds"]').prop('checked', data.enableBetweenAds === true || data.enableBetweenAds === 'on');
19
+ form.find('[name="intervalTopics"]').val(parseInt(data.intervalTopics, 10) || 6);
20
+ form.find('[name="placeholderIds"]').val(data.placeholderIds || '');
21
+
22
+ form.find('[name="enableMessageAds"]').prop('checked', data.enableMessageAds === true || data.enableMessageAds === 'on');
23
+ form.find('[name="messageIntervalPosts"]').val(parseInt(data.messageIntervalPosts, 10) || 3);
24
+ form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || '');
25
+
26
+ const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
27
+ form.find('[name="excludedGroups"] option').each(function () {
28
+ $(this).prop('selected', selected.includes($(this).val()));
29
+ });
30
+ });
31
+ }
32
+
33
+ function save() {
34
+ const form = $('.ezoic-infinite-settings');
35
+ const payload = {
36
+ enableBetweenAds: form.find('[name="enableBetweenAds"]').is(':checked'),
37
+ intervalTopics: parseInt(form.find('[name="intervalTopics"]').val(), 10) || 6,
38
+ placeholderIds: form.find('[name="placeholderIds"]').val() || '',
39
+
40
+ enableMessageAds: form.find('[name="enableMessageAds"]').is(':checked'),
41
+ messageIntervalPosts: parseInt(form.find('[name="messageIntervalPosts"]').val(), 10) || 3,
42
+ messagePlaceholderIds: form.find('[name="messagePlaceholderIds"]').val() || '',
43
+
44
+ excludedGroups: (form.find('[name="excludedGroups"]').val() || []).join(','),
45
+ };
46
+
47
+ socket.emit('admin.settings.set', { hash: namespace, values: payload }, function (err) {
48
+ if (err) {
49
+ app.alertError(err.message || err);
50
+ return;
51
+ }
52
+ app.alertSuccess('Paramètres enregistrés');
53
+ });
20
54
  }
21
- });
22
55
 
23
- $('.ezoic-save').on('click', () => {
24
- const values = {};
25
- form.serializeArray().forEach(o => values[o.name] = o.value);
26
- values.enableBetweenAds = form.find('[name=enableBetweenAds]').is(':checked');
27
- values.enableMessageAds = form.find('[name=enableMessageAds]').is(':checked');
28
- values.excludedGroups = (form.find('[name=excludedGroups]').val() || []).join(',');
29
-
30
- socket.emit('admin.settings.set', { hash: ns, values }, err => {
31
- if (err) app.alertError(err.message);
32
- else app.alertSuccess('Enregistré');
33
- });
56
+ $(document).on('click', '.ezoic-infinite-save', save);
57
+ load();
34
58
  });
35
- });
59
+ })();
package/public/client.js CHANGED
@@ -1,6 +1,271 @@
1
+ /* global $, ajaxify, app */
2
+ 'use strict';
1
3
 
2
- /* global config */
3
4
  (function () {
4
- if (!config || !config.ezoicInfinite) return;
5
- // Same logic as stable 0.8.x
5
+ if (window.ezoicInfiniteLoaded) return;
6
+ window.ezoicInfiniteLoaded = true;
7
+
8
+ function getSettings() {
9
+ return (window.config && window.config.ezoicInfinite) ? window.config.ezoicInfinite : {};
10
+ }
11
+
12
+ let settings = getSettings();
13
+ let pageKey = null;
14
+
15
+ let usedBetween = new Set();
16
+ let usedMessage = new Set();
17
+ let fifoBetween = [];
18
+ let fifoMessage = [];
19
+
20
+ let refreshInFlight = false;
21
+ let refreshQueued = false;
22
+
23
+ function parsePool(text) {
24
+ return String(text || '')
25
+ .split(/\r?\n/)
26
+ .map(s => s.trim())
27
+ .filter(Boolean)
28
+ .map(s => parseInt(s, 10))
29
+ .filter(n => Number.isFinite(n) && n > 0);
30
+ }
31
+
32
+ function userExcluded() {
33
+ try {
34
+ const raw = (settings && settings.excludedGroups) ? String(settings.excludedGroups) : '';
35
+ if (!raw) return false;
36
+ const excluded = raw.split(',').map(s => s.trim()).filter(Boolean);
37
+ if (!excluded.length) return false;
38
+ const myGroups = (app.user && app.user.groups) ? app.user.groups : [];
39
+ return excluded.some(g => myGroups.includes(g));
40
+ } catch (e) { return false; }
41
+ }
42
+
43
+ function getPageKey() {
44
+ try {
45
+ if (ajaxify && ajaxify.data) {
46
+ if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
47
+ if (ajaxify.data.cid) return 'cid:' + ajaxify.data.cid + ':' + window.location.pathname;
48
+ }
49
+ } catch (e) {}
50
+ return window.location.pathname;
51
+ }
52
+
53
+ function isTopicPage() {
54
+ try { return !!(ajaxify && ajaxify.data && ajaxify.data.tid); } catch (e) {}
55
+ return /^\/topic\//.test(window.location.pathname);
56
+ }
57
+
58
+ function isCategoryTopicList() {
59
+ return $('li[component="category/topic"]').length > 0 && !isTopicPage();
60
+ }
61
+
62
+ function cleanupForNewPage() {
63
+ $('.ezoic-ad').remove();
64
+ usedBetween = new Set();
65
+ usedMessage = new Set();
66
+ fifoBetween = [];
67
+ fifoMessage = [];
68
+ }
69
+
70
+ function destroyPlaceholder(id) {
71
+ try {
72
+ window.ezstandalone = window.ezstandalone || {};
73
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
74
+ if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
75
+ window.ezstandalone.destroyPlaceholders(id);
76
+ return;
77
+ }
78
+ window.ezstandalone.cmd.push(function () {
79
+ try { window.ezstandalone.destroyPlaceholders(id); } catch (e) {}
80
+ });
81
+ } catch (e) {}
82
+ }
83
+
84
+ function ensureUniquePlaceholder(id) {
85
+ const existing = document.getElementById('ezoic-pub-ad-placeholder-' + id);
86
+ if (!existing) return;
87
+ const wrap = existing.closest('.ezoic-ad');
88
+ if (wrap) {
89
+ try { $(wrap).remove(); } catch (e) { wrap.remove(); }
90
+ } else {
91
+ existing.remove();
92
+ }
93
+ destroyPlaceholder(id);
94
+ }
95
+
96
+ function callEzoicSingle(id) {
97
+ if (!id) return;
98
+
99
+ const now = Date.now();
100
+ window.__ezoicLastSingle = window.__ezoicLastSingle || {};
101
+ const last = window.__ezoicLastSingle[id] || 0;
102
+ if (now - last < 1200) return;
103
+ window.__ezoicLastSingle[id] = now;
104
+
105
+ try {
106
+ window.ezstandalone = window.ezstandalone || {};
107
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
108
+
109
+ const run = function () {
110
+ try {
111
+ if (typeof window.ezstandalone.showAds === 'function') {
112
+ window.ezstandalone.showAds(id);
113
+ return true;
114
+ }
115
+ } catch (e) {}
116
+ return false;
117
+ };
118
+
119
+ window.ezstandalone.cmd.push(function () { run(); });
120
+
121
+ let tries = 0;
122
+ const tick = function () {
123
+ tries++;
124
+ if (run() || tries >= 8) return;
125
+ setTimeout(tick, 800);
126
+ };
127
+ setTimeout(tick, 800);
128
+ } catch (e) {}
129
+ }
130
+
131
+ function pickNextId(pool, usedSet) {
132
+ for (const id of pool) if (!usedSet.has(id)) return id;
133
+ return null;
134
+ }
135
+
136
+ function recycle(fifo, usedSet, selectorFn) {
137
+ fifo.sort((a, b) => a.after - b.after);
138
+ while (fifo.length) {
139
+ const old = fifo.shift();
140
+ const $el = selectorFn(old);
141
+ if (!$el.length) continue;
142
+
143
+ $el.remove();
144
+ usedSet.delete(old.id);
145
+ destroyPlaceholder(old.id);
146
+ return old.id;
147
+ }
148
+ return null;
149
+ }
150
+
151
+ function insertAfter($target, id, cls, afterVal) {
152
+ ensureUniquePlaceholder(id);
153
+ const wrap = $(
154
+ '<div class="ezoic-ad ' + cls + '" data-ezoic-id="' + id + '" data-ezoic-after="' + afterVal + '">' +
155
+ '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>' +
156
+ '</div>'
157
+ );
158
+ $target.after(wrap);
159
+ }
160
+
161
+ function injectBetweenTopics() {
162
+ if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
163
+
164
+ const interval = parseInt(settings.intervalTopics, 10) || 6;
165
+ const pool = parsePool(settings.placeholderIds);
166
+ if (!pool.length) return;
167
+
168
+ const $items = $('li[component="category/topic"]');
169
+ if (!$items.length) return;
170
+
171
+ $items.each(function (idx) {
172
+ const pos = idx + 1;
173
+ if (pos % interval !== 0) return;
174
+ if (idx === $items.length - 1) return;
175
+
176
+ const $li = $(this);
177
+ if ($li.next('.ezoic-ad-between').length) return;
178
+
179
+ let id = pickNextId(pool, usedBetween);
180
+ if (!id) {
181
+ id = recycle(
182
+ fifoBetween, usedBetween,
183
+ (old) => $('.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]')
184
+ );
185
+ if (!id) return;
186
+ }
187
+
188
+ usedBetween.add(id);
189
+ fifoBetween.push({ id, after: pos });
190
+
191
+ insertAfter($li, id, 'ezoic-ad-between', pos);
192
+ callEzoicSingle(id);
193
+ });
194
+ }
195
+
196
+ function injectBetweenMessages() {
197
+ if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
198
+
199
+ const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
200
+ const pool = parsePool(settings.messagePlaceholderIds);
201
+ if (!pool.length) return;
202
+
203
+ const $posts = $('[component="post"][data-pid]');
204
+ if (!$posts.length) return;
205
+
206
+ $posts.each(function (idx) {
207
+ const postNo = idx + 1;
208
+ if (postNo % interval !== 0) return;
209
+ if (idx === $posts.length - 1) return;
210
+
211
+ const $post = $(this);
212
+ if ($post.next('.ezoic-ad-message').length) return;
213
+
214
+ let id = pickNextId(pool, usedMessage);
215
+ if (!id) {
216
+ id = recycle(
217
+ fifoMessage, usedMessage,
218
+ (old) => $('.ezoic-ad-message[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]')
219
+ );
220
+ if (!id) return;
221
+ }
222
+
223
+ usedMessage.add(id);
224
+ fifoMessage.push({ id, after: postNo });
225
+
226
+ insertAfter($post, id, 'ezoic-ad-message', postNo);
227
+ callEzoicSingle(id);
228
+ });
229
+ }
230
+
231
+ function refresh() {
232
+ settings = getSettings();
233
+ if (!settings) return;
234
+ if (userExcluded()) return;
235
+
236
+ if (refreshInFlight) { refreshQueued = true; return; }
237
+ refreshInFlight = true;
238
+
239
+ try {
240
+ const key = getPageKey();
241
+ if (pageKey !== key) {
242
+ pageKey = key;
243
+ cleanupForNewPage();
244
+ }
245
+
246
+ if (isTopicPage()) injectBetweenMessages();
247
+ else if (isCategoryTopicList()) injectBetweenTopics();
248
+ } finally {
249
+ refreshInFlight = false;
250
+ if (refreshQueued) { refreshQueued = false; setTimeout(refresh, 50); }
251
+ }
252
+ }
253
+
254
+ function boot() {
255
+ refresh();
256
+ setTimeout(refresh, 1200);
257
+ }
258
+
259
+ $(document).ready(boot);
260
+ $(window).on('action:ajaxify.end', boot);
261
+
262
+ $(window).on('action:posts.loaded action:topics.loaded action:topic.loaded action:category.loaded', function () {
263
+ refresh();
264
+ setTimeout(refresh, 600);
265
+ });
266
+
267
+ $(window).on('action:ajaxify.start', function () {
268
+ pageKey = null;
269
+ cleanupForNewPage();
270
+ });
6
271
  })();
@@ -0,0 +1,3 @@
1
+ .ezoic-ad{height:auto !important; min-height:0 !important; padding:0 !important; margin:0.5rem 0;}
2
+ .ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
3
+ .ezoic-ad .ezoic-ad-inner > div{padding:0;margin:0;}
@@ -1,23 +1,78 @@
1
+ <div class="row">
2
+ <div class="col-lg-9">
3
+ <div class="card">
4
+ <div class="card-header">
5
+ <strong>Ezoic Infinite</strong>
6
+ </div>
7
+ <div class="card-body">
8
+ <form class="ezoic-infinite-settings">
9
+ <div class="mb-3">
10
+ <label class="form-label">Groupes exclus</label>
11
+ <select multiple class="form-control" name="excludedGroups" size="10">
12
+ <!-- BEGIN groups -->
13
+ <option value="{groups.name}">{groups.name}</option>
14
+ <!-- END groups -->
15
+ </select>
16
+ <p class="form-text">Aucune publicité ne sera affichée pour ces groupes.</p>
17
+ </div>
1
18
 
2
- <form class="ezoic-infinite-settings">
3
- <label>Groupes exclus</label>
4
- <select name="excludedGroups" multiple>
5
- <!-- BEGIN groups -->
6
- <option value="{groups.name}">{groups.name}</option>
7
- <!-- END groups -->
8
- </select>
19
+ <hr />
9
20
 
10
- <hr>
21
+ <h5>Publicités entre les topics (liste des sujets)</h5>
11
22
 
12
- <label><input type="checkbox" name="enableBetweenAds"> Activer pubs entre topics</label>
13
- <input type="number" name="intervalTopics" value="6">
14
- <textarea name="placeholderIds"></textarea>
23
+ <div class="form-check mb-3">
24
+ <input class="form-check-input" type="checkbox" name="enableBetweenAds" id="enableBetweenAds">
25
+ <label class="form-check-label" for="enableBetweenAds">Activer</label>
26
+ </div>
15
27
 
16
- <hr>
28
+ <div class="mb-3">
29
+ <label class="form-label">Intervalle (après chaque N topics)</label>
30
+ <input type="number" class="form-control" name="intervalTopics" min="1" step="1">
31
+ </div>
17
32
 
18
- <label><input type="checkbox" name="enableMessageAds"> Activer pubs entre messages</label>
19
- <input type="number" name="messageIntervalPosts" value="3">
20
- <textarea name="messagePlaceholderIds"></textarea>
33
+ <div class="mb-3">
34
+ <label class="form-label">Pool d'IDs placeholder (un par ligne)</label>
35
+ <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
36
+ </div>
21
37
 
22
- <button type="button" class="ezoic-save">Enregistrer</button>
23
- </form>
38
+ <hr />
39
+
40
+ <h5>Publicités entre les messages (dans un topic)</h5>
41
+
42
+ <div class="form-check mb-3">
43
+ <input class="form-check-input" type="checkbox" name="enableMessageAds" id="enableMessageAds">
44
+ <label class="form-check-label" for="enableMessageAds">Activer</label>
45
+ </div>
46
+
47
+ <div class="mb-3">
48
+ <label class="form-label">Intervalle (après chaque N messages)</label>
49
+ <input type="number" class="form-control" name="messageIntervalPosts" min="1" step="1">
50
+ </div>
51
+
52
+ <div class="mb-3">
53
+ <label class="form-label">Pool d'IDs placeholder messages (un par ligne)</label>
54
+ <textarea class="form-control" name="messagePlaceholderIds" rows="6"></textarea>
55
+ </div>
56
+
57
+ <button type="button" class="btn btn-primary ezoic-infinite-save">
58
+ <i class="fa fa-save"></i> Enregistrer
59
+ </button>
60
+ </form>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="col-lg-3">
66
+ <div class="card">
67
+ <div class="card-header">
68
+ <strong>Aide</strong>
69
+ </div>
70
+ <div class="card-body">
71
+ <p>Placeholder Ezoic :</p>
72
+ <pre class="mb-0">&lt;div id="ezoic-pub-ad-placeholder-123"&gt;&lt;/div&gt;</pre>
73
+ <hr />
74
+ <p class="mb-0">Les IDs doivent exister côté Ezoic (ad units configurées).</p>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>