nodebb-plugin-ezoic-infinite 0.2.1 → 0.4.2

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 CHANGED
@@ -2,34 +2,20 @@
2
2
 
3
3
  Plugin NodeBB 4.x pour intégrer Ezoic lors des chargements en infinite scroll.
4
4
 
5
- ## Installation (recommandée)
6
- Depuis le dossier NodeBB (où se trouve `package.json` du forum) :
5
+ ## Fonctionnalités
6
+ - Pubs **entre posts** : toutes les N réponses, avec pool d'IDs (fenêtre glissante)
7
+ - Pubs **type message** entre les réponses : toutes les M réponses, avec pool d'IDs séparé (fenêtre glissante)
8
+ - Exclusion par groupes (ACP)
7
9
 
8
- ### Option A — installer depuis un chemin local (simple)
10
+ ## Installation (local)
9
11
  ```bash
12
+ cd /path/to/nodebb/node_modules
13
+ unzip nodebb-plugin-ezoic-infinite.zip -d nodebb-plugin-ezoic-infinite
14
+ cd /path/to/nodebb
10
15
  npm i ./node_modules/nodebb-plugin-ezoic-infinite
11
- # ou si tu as le dossier ailleurs : npm i /chemin/vers/nodebb-plugin-ezoic-infinite
12
16
  ./nodebb build
13
17
  ./nodebb restart
14
18
  ```
15
19
 
16
- ### Option B — npm link (workflow dev)
17
- ```bash
18
- cd node_modules/nodebb-plugin-ezoic-infinite
19
- npm link
20
- cd /usr/src/app # dossier NodeBB
21
- npm link nodebb-plugin-ezoic-infinite
22
- ./nodebb build
23
- ./nodebb restart
24
- ```
25
-
26
- > Note: NodeBB n’est généralement pas publié sur le registre npm, donc on **ne met pas** `nodebb` en dépendance npm “résoluble”.
27
20
  ## Configuration
28
21
  ACP -> Plugins -> Ezoic Infinite Ads
29
- - **Pool d’IDs**: un par ligne (ou séparé par virgules)
30
- - **Intervalle**: ex 6 (pub après #6, #12, #18, ...)
31
- - **Groupes exclus**: multi-sélection
32
-
33
- ## Notes Ezoic
34
- Le plugin supprime les placeholders existants du DOM avant de les recréer, puis appelle
35
- `ezstandalone.destroyPlaceholders()` avant `ezstandalone.showAds(id)` afin d'éviter les doublons d'IDs.
package/library.js CHANGED
@@ -10,7 +10,14 @@ const plugin = {};
10
10
  function normalizeExcludedGroups(value) {
11
11
  if (!value) return [];
12
12
  if (Array.isArray(value)) return value;
13
- return value.split(',').map(s => s.trim()).filter(Boolean);
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';
14
21
  }
15
22
 
16
23
  async function getAllGroups() {
@@ -22,8 +29,16 @@ async function getAllGroups() {
22
29
  async function getSettings() {
23
30
  const s = await meta.settings.get(SETTINGS_KEY);
24
31
  return {
32
+ // Between-post ads (simple blocks)
33
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
25
34
  placeholderIds: (s.placeholderIds || '').trim(),
26
35
  intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
36
+
37
+ // "Ad message" between replies (looks like a post)
38
+ enableMessageAds: parseBool(s.enableMessageAds, false),
39
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
40
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
41
+
27
42
  excludedGroups: normalizeExcludedGroups(s.excludedGroups),
28
43
  };
29
44
  }
@@ -31,7 +46,7 @@ async function getSettings() {
31
46
  async function isUserExcluded(uid, excludedGroups) {
32
47
  if (!uid || !excludedGroups.length) return false;
33
48
  const userGroups = await groups.getUserGroups([uid]);
34
- return userGroups[0].some(g => excludedGroups.includes(g.name));
49
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
35
50
  }
36
51
 
37
52
  plugin.addAdminNavigation = async (header) => {
@@ -48,13 +63,31 @@ plugin.init = async ({ router, middleware }) => {
48
63
  router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, async (req, res) => {
49
64
  const settings = await getSettings();
50
65
  const allGroups = await getAllGroups();
51
- res.render('admin/plugins/ezoic-infinite', { title: 'Ezoic Infinite Ads', ...settings, allGroups });
66
+
67
+ res.render('admin/plugins/ezoic-infinite', {
68
+ title: 'Ezoic Infinite Ads',
69
+ ...settings,
70
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
71
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
72
+ allGroups,
73
+ });
52
74
  });
53
75
 
54
76
  router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
55
77
  const settings = await getSettings();
56
78
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
57
- res.json({ placeholderIds: settings.placeholderIds, intervalPosts: settings.intervalPosts, excluded });
79
+
80
+ res.json({
81
+ excluded,
82
+
83
+ enableBetweenAds: settings.enableBetweenAds,
84
+ placeholderIds: settings.placeholderIds,
85
+ intervalPosts: settings.intervalPosts,
86
+
87
+ enableMessageAds: settings.enableMessageAds,
88
+ messagePlaceholderIds: settings.messagePlaceholderIds,
89
+ messageIntervalPosts: settings.messageIntervalPosts,
90
+ });
58
91
  });
59
92
  };
60
93
 
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.2.1",
3
+ "version": "0.4.2",
4
4
  "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
+ "license": "MIT",
6
7
  "keywords": [
7
8
  "nodebb",
9
+ "nodebb-plugin",
8
10
  "ezoic",
9
- "ads"
11
+ "ads",
12
+ "infinite-scroll"
10
13
  ],
11
- "license": "MIT"
14
+ "engines": {
15
+ "node": ">=18"
16
+ }
12
17
  }
package/public/admin.js CHANGED
@@ -1,35 +1,30 @@
1
- /* globals app, ajaxify, config */
1
+ /* globals app, ajaxify */
2
2
  'use strict';
3
3
 
4
- function getCsrfToken() {
5
- return (ajaxify?.data?.csrf_token) || (config?.csrf_token) || '';
6
- }
4
+ (function () {
5
+ function isOurPage(data) {
6
+ const url = data?.url || ajaxify?.data?.url || window.location.pathname || '';
7
+ return url.includes('admin/plugins/ezoic-infinite') || url.includes('/plugins/ezoic-infinite');
8
+ }
7
9
 
8
- $(document).ready(() => {
9
- $('#save').on('click', function (e) {
10
- e.preventDefault();
10
+ function init() {
11
+ const $form = $('.ezoic-infinite-settings');
12
+ if (!$form.length) return;
11
13
 
12
- const csrf = getCsrfToken();
13
- if (!csrf) {
14
- app.alertError('CSRF token introuvable');
15
- return;
16
- }
14
+ require(['settings'], function (Settings) {
15
+ Settings.load('ezoic-infinite', $form);
17
16
 
18
- $.ajax({
19
- method: 'PUT',
20
- url: '/api/admin/settings/ezoic-infinite',
21
- headers: {
22
- 'x-csrf-token': csrf
23
- },
24
- data: {
25
- placeholderIds: $('#placeholderIds').val(),
26
- intervalPosts: $('#intervalPosts').val(),
27
- excludedGroups: ($('#excludedGroups').val() || []).join(',')
28
- }
29
- })
30
- .done(() => app.alertSuccess('Paramètres enregistrés'))
31
- .fail((xhr) => {
32
- app.alertError(xhr?.responseJSON?.error || 'Erreur lors de la sauvegarde');
17
+ $('#save').off('click.ezoicInfinite').on('click.ezoicInfinite', function (e) {
18
+ e.preventDefault();
19
+ Settings.save('ezoic-infinite', $form, function () {
20
+ app.alertSuccess('Paramètres enregistrés');
21
+ });
22
+ });
33
23
  });
24
+ }
25
+
26
+ $(document).ready(init);
27
+ $(window).on('action:ajaxify.end', function (ev, data) {
28
+ if (isOurPage(data)) init();
34
29
  });
35
- });
30
+ })();
package/public/client.js CHANGED
@@ -14,38 +14,116 @@ async function fetchConfig() {
14
14
  }
15
15
 
16
16
  function parsePool(raw) {
17
+ if (!raw) return [];
17
18
  return Array.from(new Set(
18
19
  raw.split(/[\n,;\s]+/)
19
20
  .map(x => parseInt(x, 10))
20
- .filter(n => n > 0)
21
+ .filter(n => Number.isFinite(n) && n > 0)
21
22
  ));
22
23
  }
23
24
 
24
- async function refreshAds() {
25
- const cfg = await fetchConfig();
26
- if (!cfg || cfg.excluded) return;
27
-
28
- const pool = parsePool(cfg.placeholderIds);
29
- const interval = cfg.intervalPosts;
30
- const $posts = $('.posts .post');
31
- if (!pool.length || !$posts.length) return;
25
+ function isTopicPage() {
26
+ return ajaxify?.data?.template?.name === 'topic' || $('body').hasClass('page-topic');
27
+ }
32
28
 
29
+ function removePlaceholdersByPool(pool) {
33
30
  pool.forEach(id => $('#ezoic-pub-ad-placeholder-' + id).remove());
31
+ }
34
32
 
35
- const slots = Math.floor($posts.length / interval);
36
- const start = Math.max(1, slots - pool.length + 1);
37
- const activeIds = [];
33
+ function removeAdMessageWrappers() {
34
+ $('.ezoic-ad-post').remove();
35
+ }
36
+
37
+ function computeWindowSlots(totalPosts, interval, poolSize) {
38
+ const slots = Math.floor(totalPosts / interval);
39
+ if (slots <= 0) return [];
40
+ const start = Math.max(1, slots - poolSize + 1);
41
+ const out = [];
42
+ for (let s = start; s <= slots; s++) out.push(s);
43
+ return out;
44
+ }
45
+
46
+ function insertBetweenPosts(pool, interval) {
47
+ if (!isTopicPage()) return [];
38
48
 
39
- for (let slot = start, i = 0; slot <= slots; slot++, i++) {
49
+ const $posts = $('.posts .post').not('.ezoic-ad-post');
50
+ const total = $posts.length;
51
+ if (!total) return [];
52
+
53
+ const slotsToRender = computeWindowSlots(total, interval, pool.length);
54
+ if (!slotsToRender.length) return [];
55
+
56
+ const activeIds = [];
57
+ for (let i = 0; i < slotsToRender.length; i++) {
58
+ const slotNumber = slotsToRender[i];
40
59
  const id = pool[i];
41
- const index = slot * interval - 1;
42
- const $target = $posts.eq(index);
60
+ const postIndex = slotNumber * interval - 1;
61
+ const $target = $posts.eq(postIndex);
43
62
  if (!$target.length) continue;
63
+
44
64
  $target.after('<div id="ezoic-pub-ad-placeholder-' + id + '"></div>');
45
65
  activeIds.push(id);
46
66
  }
67
+ return activeIds;
68
+ }
69
+
70
+ function insertAdMessages(pool, interval) {
71
+ if (!isTopicPage()) return [];
72
+
73
+ const $posts = $('.posts .post').not('.ezoic-ad-post');
74
+ const total = $posts.length;
75
+ if (!total) return [];
76
+
77
+ const slotsToRender = computeWindowSlots(total, interval, pool.length);
78
+ if (!slotsToRender.length) return [];
79
+
80
+ const activeIds = [];
81
+ for (let i = 0; i < slotsToRender.length; i++) {
82
+ const slotNumber = slotsToRender[i];
83
+ const id = pool[i];
84
+ const postIndex = slotNumber * interval - 1;
85
+ const $target = $posts.eq(postIndex);
86
+ if (!$target.length) continue;
87
+
88
+ const html =
89
+ '<div class="post ezoic-ad-post" data-ezoic-ad="1">' +
90
+ '<div class="content">' +
91
+ '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>' +
92
+ '</div>' +
93
+ '</div>';
94
+
95
+ $target.after(html);
96
+ activeIds.push(id);
97
+ }
98
+ return activeIds;
99
+ }
100
+
101
+ async function refreshAds() {
102
+ const cfg = await fetchConfig();
103
+ if (!cfg || cfg.excluded) return;
104
+ if (!isTopicPage()) return;
105
+
106
+ const betweenPool = parsePool(cfg.placeholderIds);
107
+ const betweenInterval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
108
+
109
+ const messagePool = parsePool(cfg.messagePlaceholderIds);
110
+ const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
111
+
112
+ removeAdMessageWrappers();
113
+ removePlaceholdersByPool(betweenPool);
114
+ removePlaceholdersByPool(messagePool);
115
+
116
+ const activeIds = [];
117
+
118
+ if (cfg.enableBetweenAds && betweenPool.length) {
119
+ activeIds.push(...insertBetweenPosts(betweenPool, betweenInterval));
120
+ }
121
+
122
+ if (cfg.enableMessageAds && messagePool.length) {
123
+ activeIds.push(...insertAdMessages(messagePool, messageInterval));
124
+ }
47
125
 
48
- if (window.ezstandalone?.destroyPlaceholders) {
126
+ if (activeIds.length && window.ezstandalone?.destroyPlaceholders) {
49
127
  window.ezstandalone.destroyPlaceholders();
50
128
  }
51
129
  activeIds.forEach(id => window.ezstandalone?.showAds?.(id));
@@ -1,24 +1,59 @@
1
1
  <div class="acp-page-container">
2
2
  <h2>Ezoic Infinite Ads</h2>
3
3
 
4
- <div class="mb-3">
5
- <label class="form-label">Pool d’IDs Ezoic</label>
6
- <textarea id="placeholderIds" class="form-control" rows="4">{placeholderIds}</textarea>
7
- </div>
8
-
9
- <div class="mb-3">
10
- <label class="form-label">Afficher une pub tous les N posts</label>
11
- <input type="number" id="intervalPosts" class="form-control" value="{intervalPosts}" min="1">
12
- </div>
13
-
14
- <div class="mb-3">
15
- <label class="form-label">Groupes exclus</label>
16
- <select id="excludedGroups" class="form-select" multiple>
17
- <!-- BEGIN allGroups -->
18
- <option value="{allGroups.name}">{allGroups.name}</option>
19
- <!-- END allGroups -->
20
- </select>
21
- </div>
22
-
23
- <button id="save" class="btn btn-primary">Enregistrer</button>
4
+ <form class="ezoic-infinite-settings" role="form">
5
+ <h4 class="mt-3">Pubs entre les posts (bloc simple)</h4>
6
+
7
+ <div class="form-check mb-3">
8
+ <input class="form-check-input" type="checkbox" id="enableBetweenAds" name="enableBetweenAds" {enableBetweenAds_checked}>
9
+ <label class="form-check-label" for="enableBetweenAds">Activer les pubs entre les posts</label>
10
+ </div>
11
+
12
+ <div class="mb-3">
13
+ <label class="form-label" for="placeholderIds">Pool d’IDs Ezoic (entre posts)</label>
14
+ <textarea id="placeholderIds" name="placeholderIds" class="form-control" rows="4">{placeholderIds}</textarea>
15
+ <p class="form-text">Un ID par ligne (ou séparé par virgules/espaces). Le nombre d’IDs = nombre max de pubs simultanées.</p>
16
+ </div>
17
+
18
+ <div class="mb-3">
19
+ <label class="form-label" for="intervalPosts">Afficher une pub tous les N posts</label>
20
+ <input type="number" id="intervalPosts" name="intervalPosts" class="form-control" value="{intervalPosts}" min="1">
21
+ </div>
22
+
23
+ <hr/>
24
+
25
+ <h4 class="mt-3">Pubs “message” entre les réponses</h4>
26
+ <p class="form-text">Insère un bloc qui ressemble à un post, toutes les N réponses (dans une page topic).</p>
27
+
28
+ <div class="form-check mb-3">
29
+ <input class="form-check-input" type="checkbox" id="enableMessageAds" name="enableMessageAds" {enableMessageAds_checked}>
30
+ <label class="form-check-label" for="enableMessageAds">Activer les pubs “message”</label>
31
+ </div>
32
+
33
+ <div class="mb-3">
34
+ <label class="form-label" for="messagePlaceholderIds">Pool d’IDs Ezoic (message)</label>
35
+ <textarea id="messagePlaceholderIds" name="messagePlaceholderIds" class="form-control" rows="4">{messagePlaceholderIds}</textarea>
36
+ <p class="form-text">Pool séparé recommandé pour éviter la réutilisation d’IDs.</p>
37
+ </div>
38
+
39
+ <div class="mb-3">
40
+ <label class="form-label" for="messageIntervalPosts">Afficher un “message pub” tous les N messages</label>
41
+ <input type="number" id="messageIntervalPosts" name="messageIntervalPosts" class="form-control" value="{messageIntervalPosts}" min="1">
42
+ </div>
43
+
44
+ <hr/>
45
+
46
+ <h4 class="mt-3">Exclusions</h4>
47
+ <div class="mb-3">
48
+ <label class="form-label" for="excludedGroups">Groupes exclus</label>
49
+ <select id="excludedGroups" name="excludedGroups" class="form-select" multiple>
50
+ <!-- BEGIN allGroups -->
51
+ <option value="{allGroups.name}">{allGroups.name}</option>
52
+ <!-- END allGroups -->
53
+ </select>
54
+ <p class="form-text">Si l’utilisateur appartient à un de ces groupes, aucune pub n’est injectée.</p>
55
+ </div>
56
+
57
+ <button id="save" class="btn btn-primary">Enregistrer</button>
58
+ </form>
24
59
  </div>