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 +8 -22
- package/library.js +37 -4
- package/package.json +8 -3
- package/public/admin.js +23 -28
- package/public/client.js +94 -16
- package/public/templates/admin/plugins/ezoic-infinite.tpl +55 -20
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
|
-
##
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
}
|
|
12
17
|
}
|
package/public/admin.js
CHANGED
|
@@ -1,35 +1,30 @@
|
|
|
1
|
-
/* globals app, ajaxify
|
|
1
|
+
/* globals app, ajaxify */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
function
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
function init() {
|
|
11
|
+
const $form = $('.ezoic-infinite-settings');
|
|
12
|
+
if (!$form.length) return;
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
app.alertError('CSRF token introuvable');
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
14
|
+
require(['settings'], function (Settings) {
|
|
15
|
+
Settings.load('ezoic-infinite', $form);
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
const $target = $posts.eq(
|
|
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
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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>
|