├── main.png
├── update.gif
├── screencast-url.gif
├── .clasp.json
├── src
├── smsAd.tpl.html
├── smsAds.tpl.html
├── appsscript.json
├── mail__summary.tpl.html
├── mail__preheader.tpl.html
├── wizardDialog.tpl.html
├── mailText.tpl.html
├── sms.js
├── userEmailWizard.tpl.html
├── spreadsheetTriggers.js
├── mail__listing.tpl.html
├── smsSend.js
├── mainTriggerWizard.tpl.html
├── mail.tpl.html
├── mail__ads.tpl.html
├── inlineStyles.js
├── spreadsheetUi.js
├── mailSend.js
├── mail.js
├── userParams.js
├── dataSend.js
├── spreadsheetUtils.js
├── data.js
├── utils.js
├── dayjs.min.js
├── ads.js
└── Code.js
├── version.json
├── messages.json
├── CHANGELOG.md
└── README.md
/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximelebreton/alertes-leboncoin/HEAD/main.png
--------------------------------------------------------------------------------
/update.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximelebreton/alertes-leboncoin/HEAD/update.gif
--------------------------------------------------------------------------------
/screencast-url.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximelebreton/alertes-leboncoin/HEAD/screencast-url.gif
--------------------------------------------------------------------------------
/.clasp.json:
--------------------------------------------------------------------------------
1 | {"scriptId":"15GE-TW-COB9rfq49nF38GDqytbwpK2HMxLQOzdC1JZMGkUCfLqWoG0T4", "projectId": "project-id-fxovycalkvjbwgkskdk", "rootDir": "src"}
--------------------------------------------------------------------------------
/src/smsAd.tpl.html:
--------------------------------------------------------------------------------
1 | = ad.title ?>
2 | = ad.textPlace ?>
3 | = ad.textDateTime ?>
4 | if (ad.textPrice) { ?>
5 | = ad.textPrice ?>
6 | } ?>
7 | = ad.shortUrl ?>
--------------------------------------------------------------------------------
/src/smsAds.tpl.html:
--------------------------------------------------------------------------------
1 |
2 | var pluralX = ads.length > 1 ? 'x' : '';
3 | var pluralS = ads.length > 1 ? 's' : '';
4 | ?>
5 | = ads.length ?> nouveau= pluralX ?> résultat= pluralS ?> pour = label ?> : = url ?>
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version" : "5.5.2",
3 | "description" : "Voir le changelog",
4 | "url": "https://github.com/maximelebreton/alertes-leboncoin/blob/master/CHANGELOG.md#change-log",
5 | "helpLabel": "Comment mettre à jour ?",
6 | "helpUrl": "https://github.com/maximelebreton/alertes-leboncoin#obtenir-la-dernière-mise-à-jour",
7 | "openLabel": "Ouvrir la feuille de calcul"
8 | }
9 |
--------------------------------------------------------------------------------
/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "active": true,
3 | "title": "Bloqués... par leboncoin.",
4 | "message": "Suite à la mise en place d'un système de protection (datadome) par leboncoin, il n'est plus possible d'utiliser Alertes leboncoin depuis le 31 Août 2018. J'étudie actuellement les potentielles solutions. Toutes vos aides et suggestions sont les bienvenues.",
5 | "date": "2018-09-06 09:09:09"
6 | }
7 |
--------------------------------------------------------------------------------
/src/appsscript.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeZone": "Europe/Paris",
3 | "dependencies": {
4 | "libraries": [{
5 | "userSymbol": "cheeriogasify",
6 | "libraryId": "1XFto3-D4AWTLPD7nh1MOheCLCTfu-WqwdAznn0jWlLoaglBXW4O5nDuT",
7 | "version": "4"
8 | }]
9 | },
10 | "webapp": {
11 | "access": "ANYONE",
12 | "executeAs": "USER_ACCESSING"
13 | },
14 | "exceptionLogging": "STACKDRIVER",
15 | "executionApi": {
16 | "access": "ANYONE"
17 | }
18 | }
--------------------------------------------------------------------------------
/src/mail__summary.tpl.html:
--------------------------------------------------------------------------------
1 |
2 | for (var i = 0; i < result.length; i++ ) {
3 |
4 | var id = result[i];
5 | var label = entities.labels[id].label;
6 | var url = entities.urls[id].url;
7 | var ads = entities.ads[id].toSend;
8 |
9 | ?>
10 |
11 | if (ads.length) { ?>
12 | - = label ?> (= ads.length ?>) —
13 | } ?>
14 |
15 | } ?>
16 |
--------------------------------------------------------------------------------
/src/mail__preheader.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | var preheaderCount = 0;
4 | var preheaderLimit = 10;
5 | var preheaderContent = "";
6 |
7 | for (var i = 0; i < result.length; i++ ) {
8 |
9 | var id = result[i];
10 | var label = entities.labels[id].label;
11 | var ads = entities.ads[id].toSend;
12 |
13 | if (ads.length) {
14 | for (var j = 0; j < ads.length; j++ ) {
15 |
16 | var ad = ads[j];
17 |
18 | if (preheaderCount < preheaderLimit) {
19 | preheaderContent += ad.title + ", "
20 | }
21 | preheaderCount++
22 | }
23 | }
24 | }
25 |
26 | ?>
27 | = preheaderContent ?>
28 |
--------------------------------------------------------------------------------
/src/wizardDialog.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 | != getUserEmailTemplate(); ?>
20 |
21 | != getMainTriggerTemplate(); ?>
22 |
23 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/mailText.tpl.html:
--------------------------------------------------------------------------------
1 |
2 | for (var i = 0; i < result.length; i++ ) {
3 |
4 | var id = result[i];
5 | var label = data.entities.labels[id].label;
6 | var url = data.entities.urls[id].url;
7 | var ads = data.entities.ads[id].toSend;
8 |
9 | var pluralX = ads.length > 1 ? 'x' : '';
10 | var pluralS = ads.length > 1 ? 's' : '';
11 |
12 | var title = ads.length + " résultat" + pluralS + " pour " + label;
13 | var titleLine = Array(title.length + 1).join("=");
14 |
15 | ?>
16 | = title ?>
17 | = titleLine ?>
18 |
19 |
20 |
21 | for (var j = 0; j < ads.length; j++) {
22 |
23 | var ad = ads[j];
24 |
25 | ?>
26 | - = ad.title ?>
27 | = ad.textPlace ?>
28 | = ad.textDateTime ?>
29 | if (ad.textPrice) { ?>
30 | = ad.textPrice ?>
31 | } ?>
32 | = ad.shortUrl ?>
33 |
34 |
35 |
36 | }
37 |
38 | ?>
39 |
40 |
41 |
42 | }
43 | ?>
44 |
45 |
46 | alertes-leboncoin version = data.version ?> : http://maximelebreton.github.io/alertes-leboncoin
47 |
48 | Éditer mes alertes : = data.sheetUrl ?>
--------------------------------------------------------------------------------
/src/sms.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ------------------ *
3 | * SMS
4 | * ------------------ *
5 | */
6 |
7 | /**
8 | * Get Sms Ads Template (multi)
9 | */
10 | function getSmsAdsTemplate( entities, result ) {
11 |
12 | var template = HtmlService.createTemplateFromFile('smsAds.tpl');
13 | var id = result[0];
14 | template.label = entities.labels[id].label;
15 | template.url = entities.urls[id].url;
16 | template.ads = entities.ads[id].toSend;
17 |
18 | return template.evaluate().getContent();
19 | }
20 |
21 |
22 | /**
23 | * Get Sms Ad Template (single)
24 | */
25 | function getSmsAdTemplate(ad) {
26 |
27 | var template = HtmlService.createTemplateFromFile('smsAd.tpl');
28 | template.ad = ad;
29 |
30 | return template.evaluate().getContent();
31 | }
32 |
33 |
34 | /**
35 | * Get sms messages
36 | */
37 | function getSmsMessages(data, result, maxSmsSendByResult) {
38 |
39 | var messages = [];
40 | var id = result[0];
41 | var ads = data.entities.ads[id].toSend;
42 |
43 | maxSmsSendByResult = maxSmsSendByResult || getParam('maxSmsSendByResult');
44 |
45 | if (ads.length > maxSmsSendByResult) {
46 | var message = getSmsAdsTemplate(data.entities, result);
47 | messages.push(message);
48 |
49 | } else {
50 |
51 | for (var i = 0; i < ads.length; i++ ) {
52 | var ad = ads[i];
53 | var message = getSmsAdTemplate(ad);
54 | messages.push(message);
55 |
56 | }
57 | }
58 |
59 | return messages;
60 | }
--------------------------------------------------------------------------------
/src/userEmailWizard.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
22 |
23 |
24 |
25 |
26 | ">
27 |
28 |
33 |
34 | L'email reste modifiable à tout moment dans la feuille "Variables"
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/spreadsheetTriggers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check main trigger
3 | */
4 | function checkMainTrigger(callbackString) {
5 |
6 | var triggers = ScriptApp.getProjectTriggers();
7 |
8 | if (!triggers.length) {
9 | showMainTriggerWizard(callbackString);
10 | return false;
11 | }
12 |
13 | return true;
14 | }
15 |
16 |
17 | /**
18 | * Set main trigger
19 | */
20 | function setMainTrigger(hours) {
21 |
22 | deleteProjectTriggers();
23 |
24 | var triggerHour = 12;
25 | var triggerMinute = 30;
26 | var trigger = ScriptApp.newTrigger('alertesLeBonCoin').timeBased().nearMinute(triggerMinute);
27 |
28 | if (hours == 0) {
29 |
30 | getSpreadsheetContext().toast("Vos alertes ont été mises en pause", 'Alertes LeBonCoin');
31 |
32 | } else {
33 |
34 | if (hours >= 1 && hours <= 12) {
35 | trigger = trigger.everyHours(hours);
36 | }
37 | if (hours == 24) {
38 | trigger = trigger.atHour(triggerHour).everyDays(1);
39 | }
40 | if (hours == 48) {
41 | trigger = trigger.atHour(triggerHour).everyDays(2);
42 | }
43 | if (hours == 168) {
44 | trigger = trigger.atHour(triggerHour).everyWeeks(1).onWeekDay(ScriptApp.WeekDay.MONDAY);
45 | }
46 |
47 | trigger = trigger.create();
48 | var triggerId = trigger.getUniqueId();
49 |
50 | if (triggerId) {
51 | getSpreadsheetContext().toast("Vos alertes ont été réglées sur \"toutes les " + hours + " heures\"", 'Alertes LeBonCoin');
52 | }
53 |
54 | }
55 |
56 | }
57 |
58 |
59 | /**
60 | * Delete project triggers
61 | */
62 | function deleteProjectTriggers() {
63 | var triggers = ScriptApp.getProjectTriggers();
64 |
65 | for (var i = 0; i < triggers.length; i++) {
66 | ScriptApp.deleteTrigger(triggers[i]);
67 | }
68 | }
--------------------------------------------------------------------------------
/src/mail__listing.tpl.html:
--------------------------------------------------------------------------------
1 | for (var i = 0; i < result.length; i++ ) {
2 |
3 | var id = result[i];
4 | var label = entities.labels[id].label;
5 | var url = entities.urls[id].url;
6 | var ads = entities.ads[id].toSend;
7 | var tags = entities.ads[id].tags;
8 | var singleParams = entities.advanced[id].params;
9 | var haveDuplicates = entities.advanced[id].haveDuplicates;
10 |
11 | var getUrlField = function(field) {
12 | var string = getQueryString(field, url);
13 | var result = string ? decodeURL(string) : null;
14 | return result;
15 | };
16 |
17 | var searchQuery = getUrlField('q');
18 | var locationQuery = getUrlField('location');
19 | //var minPrice = getUrlField('ps');
20 |
21 |
22 | ?>
23 |
24 | if (ads.length) { ?>
25 |
26 | if (haveDuplicates == true && ads.length <= 1) { ?>
27 |
28 | Le résultat pour = label ?> a été masqué pour éviter les doublons
29 |
30 | } else { ?>
31 |
32 |
33 | = label ?> (= ads.length ?>)
34 |
35 | if ( getParam('showTags') ) { ?>
36 |
37 |
38 | if (searchQuery) { ?>= searchQuery ?> } ?>
39 | if (locationQuery) { ?>= locationQuery ?> } ?>
40 |
41 |
42 | } ?>
43 |
44 |
45 |
46 |
47 | != getMailAdsTemplate( ads, singleParams, haveDuplicates ); ?>
48 |
49 |
50 | } ?>
51 | } ?>
52 |
53 | } ?>
--------------------------------------------------------------------------------
/src/smsSend.js:
--------------------------------------------------------------------------------
1 | /** ------
2 | * FREE
3 | * ------
4 | */
5 |
6 |
7 | /**
8 | * Free send sms
9 | */
10 | function freeSendSms(user, pass, message, callback, dataForCallback, resultForCallback) {
11 |
12 | //message = message.replace(/(\r\n|\n|\r)/gm,""); //les sauts de lignes ne passent pas en GET, alors on nettoie
13 | message = encodeURIComponent( message.substring(0, 480) ).replace(/'/g,"%27").replace(/"/g,"%22");
14 | var error;
15 |
16 | //Logger.log(message);
17 |
18 | var url = "https://smsapi.free-mobile.fr/sendmsg?user=" + user + "&pass=" + pass + "&msg=" + message;
19 |
20 | try {
21 | UrlFetchApp.fetch(url);
22 |
23 | } catch(exception) {
24 | Logger.log(exception);
25 | error = exception;
26 | }
27 |
28 | if (callback && typeof(callback) === "function") {
29 | //Logger.log("ok le callback");
30 | return callback(error, user, dataForCallback, resultForCallback);
31 | }
32 | }
33 |
34 |
35 | /**
36 | * Send sms with free
37 | */
38 | function sendSmsWithFreeGateway(data, selectedResult, user, pass, callback) {
39 |
40 | var messages = getSmsMessages(data, selectedResult);
41 |
42 | messages.map( function( message ) {
43 |
44 | freeSendSms(user, pass, message, callback, data, selectedResult);
45 | })
46 |
47 | }
48 |
49 |
50 |
51 |
52 | /** ------------------------
53 | * BOUYGUES (draft !)
54 | * ------------------------
55 | */
56 |
57 |
58 | function bouyguesSendSms(number, message) {
59 |
60 | message = encodeURIComponent( message.substring(0, 160) ).replace(/'/g,"%27").replace(/"/g,"%22");
61 |
62 | var postData = {
63 | 'fieldMsisdn': number,
64 | 'fieldMessage': message,
65 | 'Verif.x': '51',
66 | 'Verif.y': '16'
67 | };
68 |
69 | var options = {
70 | 'method' : 'post',
71 | 'payload' : postData
72 | };
73 |
74 | // ATTENTION : limite de bouygues à 5 sms / jour
75 | // inspiré de https://github.com/y3nd/bouygues-sms/blob/master/index.js, mais nécessite une athentification
76 |
77 | try {
78 | UrlFetchApp.fetch('https://www.secure.bbox.bouyguestelecom.fr/services/SMSIHD/confirmSendSMS.phtml', options);
79 |
80 | } catch(e) {
81 | Logger.log(e);
82 | }
83 |
84 | }
85 |
86 | /**
87 | * Send sms with bouygues
88 | */
89 | function sendSmsWithBouyguesGateway(data, selectedResult, user, pass) {
90 |
91 | var messages = getSmsMessages(data, selectedResult, 1);
92 |
93 | for (var i = 0; i < messages.length; i++ ) {
94 | var message = messages[i];
95 | bouyguesSendSms(user, pass, message);
96 | }
97 |
98 | }
--------------------------------------------------------------------------------
/src/mainTriggerWizard.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
22 |
23 |
24 |
25 |
26 |
59 |
60 |
65 |
66 |
Pour les connaisseurs, cette action va créer un déclencheur
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/mail.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
31 |
32 |
33 |
34 |
35 |
36 | != getMailPreheaderTemplate( data.entities, result ) ?>
37 |
38 | if (result.length > 1) { ?>
39 | != getMailSummaryTemplate( data.entities, result ) ?>
40 | } ?>
41 | != getMailListingTemplate( data.entities, result ) ?>
42 |
43 | if ( data.update ) { ?>
44 |
50 | } ?>
51 |
52 |
53 |
54 |
55 | ✂ avec ♥ par mlb
(idée originale Just docs it)
56 |
57 | if ( getParam('showMailEditLink') == true ) { ?>
58 |
Éditer mes alertes
59 | } ?>
60 |
61 | alertes-leboncoin version = version ?>
62 |
63 |
64 |
65 |
66 | Temps d'exécution : = data.runtimeInSeconds ?> secondes
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/mail__ads.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | var showMap = singleParams.hasOwnProperty("showMap") ? singleParams.showMap : getParam('showMap');
4 | var mapZoom = singleParams.hasOwnProperty("mapZoom") ? singleParams.mapZoom : getParam('mapZoom');
5 |
6 | var isMapAdLayoutImage = showMap ? inlineStyles.adLayoutImageWithMap : "";
7 |
8 | ?>
9 |
10 |
11 | for (var i = 0; i < ads.length; i++) {
12 |
13 | ?>
14 |
15 |
16 | var ad = ads[i];
17 |
18 | if (ad.haveDuplicateInResult && haveDuplicates == true) {
19 |
20 | ?>
21 |
22 | L'annonce "= ad.title ?>" a été masquée car elle apparait déjà dans "= ad.userLabel ?>"
23 |
24 |
25 |
26 | } else {
27 |
28 | ?>
29 |
30 |
31 |
32 |
33 |
43 |
44 | if (showMap) { ?>
45 |
46 |
47 |
 ?>&zoom=<?= mapZoom ?>&size=120x120&sensor=false&language=fr&sensor=false)
48 |
49 |
50 | } ?>
51 |
52 |
53 |
61 |
62 | = ad.textPlace ?>
63 |
64 |
65 | = ad.textDateTime ?>
66 |
67 |
68 | if (ad.price) { ?>= ad.textPrice ?> } ?>
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | }
79 |
80 | ?>
81 |
82 |
83 | }
84 |
85 | ?>
--------------------------------------------------------------------------------
/src/inlineStyles.js:
--------------------------------------------------------------------------------
1 | var brandPrimary = "#369";
2 | var brandSecondary = "#989898";
3 | var borderColor = "#eaeaea";
4 | var placeholderColor = "#f2f2f2";
5 |
6 | var inlineStyles = {
7 |
8 | label: "display: inline; padding: .2em .6em .3em; font-size: .75em; font-weight: 700; line-height: 1; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: .25em;",
9 | labelPrimary: "color: "+brandPrimary+"; border: 1px solid "+brandPrimary+";",
10 | labelSecondary: "border: 1px solid "+brandSecondary+"; color: "+brandSecondary+";",
11 |
12 | adLayoutImage: "padding: 0 20px 20px 0; min-width: 100px; max-width: 160px; float: left; vertical-align: middle; text-align: center; font-size: 0;",
13 | adLayoutImageWithMap: "width: 40%;",
14 | adLayoutMap: "float: right; padding: 0 0 20px 20px; font-size: 0;",
15 | adLayoutContent: "min-width: 200px; display: inline-block; padding:0 0 20px 0; font-size: 13px; line-height: 19.5px;",
16 |
17 | adImage: "display: inline-block; width: 100%; min-height: 120px; max-height: 120px; background-color: "+placeholderColor+"; vertical-align: middle;",
18 | adImageImg: "vertical-align:middle; display: inline-block; border: 0; max-width: 100%; max-height: 120px; height: auto;",
19 | adImagePlaceholder: "vertical-align: middle;",
20 | adImageAlignFix: "min-height: 120px; display: inline-block; font-size: 0; line-height: 0; vertical-align: middle;",
21 |
22 | adMap: "min-height: 120px;",
23 |
24 | adContentTitle: "display: inline-block;",
25 | adContentTitleLink: "font-size: 14px; font-weight: bold; color: "+brandPrimary+"; text-decoration: none; vertical-align: middle;",
26 | adContentPlace: "",
27 | adContentDate: "",
28 | adContentPrice: "line-height: 18px; font-size: 14px; font-weight: bold;",
29 |
30 | list: "padding-left:0px;",
31 | listItem: "list-style:none; margin-bottom: 0; margin-left: 0px; clear: both; border-top: 1px solid "+borderColor+";",
32 | listItemContainer: "width:auto; padding:20px 0 0 0;",
33 |
34 | headerList: "display:block;clear:both;padding-top:20px; margin-top: 0; font-size:14px; color: "+brandSecondary+";",
35 | headerListLink: "color: "+brandSecondary+"; font-weight: bold; font-size: 15px; text-decoration: none;",
36 | headerListTags: "float: right;",
37 |
38 | preheader: "display:none;font-size:1px;color:#fff;line-height:1px;;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;",
39 |
40 | summary: "text-align:right; color: "+brandSecondary+"; list-style: none; padding-left: 0; margin-bottom: 0;",
41 | summaryItem: "margin-left: 0;",
42 | summaryItemLink: "color: "+brandSecondary+"; text-decoration: none;",
43 |
44 | notification: "border:1px solid #FFECB3; background-color: #FFFDE7; text-align:center; margin-top:10px; margin-bottom:10px; padding:10px; clear:both; overflow:hidden;",
45 |
46 | footer: "border-top:1px solid #f7f7f7; text-align:center; margin-top:10px; padding-top:20px; line-height: 18px; clear:both; overflow:hidden;",
47 | footerLeft: "float: left; text-align: left;",
48 | footerMiddle: "margin-left: 10px; margin-right: 10px;",
49 | footerRight: "float: right;",
50 |
51 | secondaryFooter: "color: transparent;"
52 | }
--------------------------------------------------------------------------------
/src/spreadsheetUi.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * --------------------- *
4 | * ABOUT SPREADSHEET UI
5 | * --------------------- *
6 | */
7 |
8 |
9 | /**
10 | * Create menu
11 | */
12 | function createMenu() {
13 | var ui = SpreadsheetApp.getUi();
14 |
15 | // We need to set a local "handle" to call a library function
16 | ui.createMenu('Alertes LeBonCoin')
17 | //.addItem('Modifier email destinataire', 'handleUpdateRecipientEmail')
18 | .addItem('Paramètres utilisateur', 'handleOpenVariablesSheet')
19 | .addItem('Planification des alertes', 'handleShowMainTriggerWizard')
20 | .addSeparator()
21 | .addItem('Lancer manuellement', 'alertesLeBonCoin')
22 | .addToUi();
23 | }
24 |
25 |
26 | /**
27 | * On open variables sheet
28 | */
29 | function openVariablesSheet(userParams) {
30 | setParams(userParams);
31 | SpreadsheetApp.setActiveSheet(getVariablesSheetContext());
32 | }
33 |
34 |
35 | /**
36 | * Get main trigger wizard template
37 | */
38 | function getMainTriggerWizardTemplate(callbackString) {
39 |
40 | var template = HtmlService.createTemplateFromFile('mainTriggerWizard.tpl');
41 | template.callbackString = callbackString;
42 |
43 | return template.evaluate();
44 | }
45 |
46 |
47 | /**
48 | * Show main trigger wizard
49 | */
50 | function showMainTriggerWizard(callbackString) {
51 |
52 | var ui = SpreadsheetApp.getUi();
53 |
54 | var html = getMainTriggerWizardTemplate(callbackString).setWidth(360);
55 | var response = ui.showModelessDialog(html, "Voulez-vous planifier l'envoi des alertes ?");
56 | }
57 |
58 |
59 | /**
60 | * Show simple dialol
61 | */
62 | function showDialog(title, content) {
63 | var htmlOutput = HtmlService
64 | .createHtmlOutput('' + content + '
')
65 | .setWidth(250)
66 | .setHeight(80);
67 | SpreadsheetApp.getUi().showModelessDialog(htmlOutput, title);
68 | }
69 |
70 |
71 | /**
72 | * Highlight row
73 | */
74 | function highlightRow(row, backgroundColor, borderColor) {
75 | var backgroundColor = backgroundColor || getParam('colors').background.working;
76 | var borderColor = borderColor || getParam('colors').border.working;
77 | row.setBackground( backgroundColor );
78 | row.setBorder(true, true, true, true, false, false, borderColor, null);
79 | SpreadsheetApp.flush(); // see https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#flush
80 | }
81 |
82 |
83 | /**
84 | * Unhighlight row
85 | */
86 | function unhighlightRow(row) {
87 | row.setBackground('');
88 | row.setBorder(false, false, false, false, false, false);
89 | SpreadsheetApp.flush();
90 | }
91 |
92 |
93 | /**
94 | * Set active selection on email
95 | */
96 | function setActiveSelectionOnEmail() {
97 | SpreadsheetApp.setActiveSheet(getVariablesSheetContext());
98 | getVariablesSheetContext().setActiveSelection( getRecipientEmailCell() );
99 | }
100 |
101 | /**
102 | * Get recipient email range
103 | */
104 | function getRecipientEmailCell() {
105 | var cell = getCellByIndex(2, getParam('names').range.userVarValues, getParam('names').sheet.variables);
106 | //var range = getVariablesSheetContext().getRange( 2, getColumnByName( params.names.range.userVarValues ) );
107 | return cell;
108 | }
109 |
110 |
111 |
--------------------------------------------------------------------------------
/src/mailSend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Send email
3 | */
4 | function sendEmail(email, mail, callback, dataForCallback, resultForCallback) {
5 |
6 | var titlePrefix = getParam('debug') == true ? "[debug] " : "";
7 | var devTitlePrefix = getParam('dev') == true ? "[dev] " : "";
8 | var error;
9 |
10 | if ( getParam('sendMail') !== false ) {
11 |
12 | try {
13 |
14 | MailApp.sendEmail(
15 | email,
16 | devTitlePrefix + titlePrefix + mail.title,
17 | mail.text,
18 | {
19 | htmlBody: getParam('plainText') == true ? undefined : mail.html
20 | }
21 | );
22 |
23 | } catch(exception) {
24 |
25 | log( exception )
26 | error = exception;
27 | }
28 |
29 | }
30 |
31 | if (callback && typeof(callback) === "function") {
32 | //Logger.log("ok le callback");
33 | return callback(error, email, dataForCallback, resultForCallback);
34 | }
35 |
36 | //Logger.log( 'mail :'); Logger.log( email );
37 | //{"message":"Limite dépassée : Taille du corps de l'e-mail.","name":"Exception","fileName":"Code","lineNumber":566,"stack":"\tat Code:566 (sendEmail)\n\tat Code:557 (sendGroupedData)\n\tat Code:530 (sendDataTo)\n\tat Code:256 (start)\n"}
38 | }
39 |
40 |
41 | /**
42 | * Get grouped mail
43 | */
44 | function getGroupedMail( data, result ) {
45 |
46 | var mail = {
47 | title: getMailTitle( data.entities, result ),
48 | html: getMailTemplate( data, result ),
49 | text: getTextMailTemplate( data, result )
50 | }
51 |
52 | return mail;
53 | }
54 |
55 |
56 | /**
57 | * Get separate mails
58 | */
59 | function getSeparateMails( data, result ) {
60 |
61 | var mails = result.map( function( id ) {
62 |
63 | var singleResult = [id];
64 | var mail = {
65 | title: getMailTitle( data.entities, singleResult ),
66 | html: getMailTemplate( data, singleResult ),
67 | text: getTextMailTemplate( data, singleResult )
68 | };
69 |
70 | return mail;
71 | })
72 |
73 | return mails;
74 | }
75 |
76 |
77 | /**
78 | * Mail send grouped results
79 | */
80 | function mailSendGroupedResults( data, selectedResult, email, callback ) {
81 | var mail = getGroupedMail( data, selectedResult );
82 |
83 | sendEmail(email, mail, callback, data, selectedResult);
84 | }
85 |
86 |
87 | /**
88 | * Mail send separate results
89 | */
90 | function mailSendSeparateResults( data, selectedResult, email, callback ) {
91 |
92 | var mails = getSeparateMails(data, selectedResult);
93 |
94 | for (var i = 0; i < selectedResult.length; i++ ) {
95 | var mail = mails[i];
96 | var id = selectedResult[i];
97 | var singleResult = [id];
98 |
99 | sendEmail(email, mail, callback, data, singleResult);
100 | }
101 | }
102 |
103 |
104 | /**
105 | * Handle Mail send
106 | */
107 | function handleMailSend( data, selectedResult, email, callback ) {
108 |
109 | if ( getParam('groupedResults') ) {
110 |
111 | mailSendGroupedResults(data, selectedResult, email, function(error, callbackRecipient, callbackData, callbackResult) { // 1 : CATCH CALLBACK
112 | // If grouped mail is too big, try to send in separate results
113 | if (error) {
114 |
115 | mailSendSeparateResults(data, selectedResult, email, callback);
116 | } else {
117 |
118 | if (callback && typeof(callback) === "function") {
119 | return callback(error, callbackRecipient, callbackData, callbackResult); // 2 : BUT RE-SEND IT
120 | }
121 | }
122 | });
123 |
124 | } else {
125 | mailSendSeparateResults(data, selectedResult, email, callback);
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/mail.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ------------------ *
3 | * MAIL
4 | * ------------------ *
5 | */
6 |
7 |
8 | /**
9 | * Get mail title
10 | */
11 | function getMailTitle( entities, result ) {
12 |
13 | var length = getAdsTotalLength( entities, result );
14 |
15 | var onlyProAds = true;
16 | for (var i = 0; i < result.length; i++ ) {
17 | var id = result[i];
18 | var ads = entities.ads[id].toSend;
19 |
20 | if (ads.length) {
21 | for (var j = 0; j < ads.length; j++ ) {
22 | var ad = ads[j];
23 | if ( !ad.isPro ) { onlyProAds = false; }
24 | }
25 | }
26 | }
27 |
28 | var prefixTitle = 'Alertes leboncoin.fr : ';
29 | var suffixTitle = '';
30 |
31 | if (result.length == 1) {
32 | suffixTitle = ' pour "' + entities.labels[result[0]].label + '"'
33 | }
34 | if (result.length > 1) {
35 | suffixTitle = ' (groupés)'
36 | }
37 |
38 | var pluralS = length > 1 ? 's' : '';
39 |
40 | return prefixTitle + length + "\xa0nouveau" + (length > 1 ? "x" : "") + " résultat" + pluralS + (onlyProAds ? " (pro)" : "") + suffixTitle;
41 | }
42 |
43 |
44 | /**
45 | * Get ads length
46 | */
47 | function getAdsTotalLength( entities, result ) {
48 | var length = 0;
49 | for (var i = 0; i < result.length; i++ ) {
50 | var id = result[i];
51 | length += entities.ads[id].toSend.length;
52 | }
53 | return length;
54 | }
55 |
56 |
57 |
58 | /*
59 | * Get mail template
60 | */
61 | function getMailTemplate( data, result ) {
62 |
63 | var template = HtmlService.createTemplateFromFile('mail.tpl');
64 | template.result = result;
65 | template.data = data;
66 |
67 | return template.evaluate().getContent();
68 | }
69 |
70 |
71 | /*
72 | * Get mail text template
73 | */
74 | function getTextMailTemplate( data, result ) {
75 |
76 | var template = HtmlService.createTemplateFromFile('mailText.tpl');
77 | template.data = data;
78 | template.result = result;
79 |
80 | return template.evaluate().getContent();
81 | }
82 |
83 |
84 | /*
85 | * Get mail preheader template
86 | */
87 | function getMailPreheaderTemplate( entities, result ) {
88 |
89 | var template = HtmlService.createTemplateFromFile('mail__preheader.tpl');
90 | template.entities = entities;
91 | template.result = result;
92 |
93 | return template.evaluate().getContent();
94 | }
95 |
96 |
97 | /*
98 | * Get mail summary template
99 | */
100 | function getMailSummaryTemplate( entities, result ) {
101 |
102 | var template = HtmlService.createTemplateFromFile('mail__summary.tpl');
103 | template.entities = entities;
104 | template.result = result;
105 |
106 | return template.evaluate().getContent();
107 | }
108 |
109 |
110 | /*
111 | * Get mail listing template
112 | */
113 | function getMailListingTemplate( entities, result ) {
114 |
115 | var template = HtmlService.createTemplateFromFile('mail__listing.tpl');
116 | template.entities = entities;
117 | template.result = result;
118 |
119 | return template.evaluate().getContent();
120 | }
121 |
122 |
123 | /*
124 | * Get mail ads template
125 | */
126 | function getMailAdsTemplate( ads, singleParams, haveDuplicates ) {
127 |
128 | var template = HtmlService.createTemplateFromFile('mail__ads.tpl');
129 | template.ads = ads;
130 | template.singleParams = singleParams;
131 | template.haveDuplicates = haveDuplicates;
132 | return template.evaluate().getContent();
133 | }
134 |
135 |
136 | /**
137 | * Encode data
138 | * TODO : refactor
139 | */
140 | function encodeForStaticMapApi(s) {
141 | if (s) {
142 | var s = s.trim().replace(/\s\s+/g, '+').replace(/[!'()*]/g, '+');
143 | //return encodeURIComponent(s);
144 | return s;
145 | }
146 | }
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/src/userParams.js:
--------------------------------------------------------------------------------
1 | /**
2 | * --------------------- *
3 | * ABOUT PREREQUISITES
4 | * --------------------- *
5 | */
6 |
7 |
8 | /**
9 | * Set params
10 | */
11 | function getParams(defaults, userScriptParams) {
12 |
13 | var scriptParams = deepExtend({}, defaults, handleDepreciatedParams(userScriptParams) );
14 |
15 | //var params = scriptParams; // because getUserParamsFromSheet() need params... (maybe needs to refactor?)
16 | var sheetParams = getUserParamsFromSheet( scriptParams );
17 |
18 | var mergedParams = deepExtend({}, scriptParams, sheetParams)
19 | //params = mergedParams;
20 |
21 |
22 | var stringifiedParams = {};
23 | Object.keys(mergedParams).forEach( function(key) {
24 |
25 | var value = JSON.stringify( mergedParams[key], function(key, val) {
26 | if (typeof val === 'function') {
27 | return val + ''; // implicitly `toString` it
28 | }
29 | return val;
30 | });
31 |
32 | stringifiedParams[key] = value;
33 | });
34 |
35 | var documentProperties = PropertiesService.getDocumentProperties();
36 | documentProperties.setProperties( stringifiedParams );
37 |
38 | var names = getParam('names');
39 | Logger.log( stringifiedParams );
40 | Logger.log( typeof names );
41 | Logger.log( names.sheet );
42 | Logger.log( names['sheet'] );
43 |
44 | return mergedParams;
45 | }
46 |
47 |
48 | /**
49 | * Handle old versions params
50 | */
51 | function handleDepreciatedParams(userScriptParams) {
52 |
53 | var modifiedParams = userScriptParams || {};
54 |
55 | var rangeNames = (((userScriptParams || {}).names || {}).range || {});
56 |
57 | // manage deprecated adIdRange
58 | var adIdRange = rangeNames.adId
59 | var lastAdRange = rangeNames.lastAd;
60 |
61 | if (typeof adIdRange !== 'undefined' && typeof lastAdRange == 'undefined') {
62 | modifiedParams.names.range.lastAd = userScriptParams.names.range.adId;
63 | delete modifiedParams.names.range.adId;
64 | }
65 |
66 | modifiedParams.isAvailable = {};
67 |
68 | var userVarNamesRange = rangeNames.userVarNames;
69 | if (typeof userVarNamesRange == 'undefined') {
70 | modifiedParams.isAvailable.sheetParams = false;
71 | } else {
72 | modifiedParams.isAvailable.sheetParams = true;
73 | }
74 |
75 | var advancedOptionsRange = rangeNames.advancedOptions;
76 | if (typeof advancedOptionsRange == 'undefined') {
77 | modifiedParams.isAvailable.advancedOptions = false;
78 | } else {
79 | modifiedParams.isAvailable.advancedOptions = true;
80 | }
81 |
82 | var advancedMenuRange = rangeNames.advancedMenu;
83 | if (typeof advancedMenuRange == 'undefined') {
84 | modifiedParams.isAvailable.advancedMenu = false;
85 | } else {
86 | modifiedParams.isAvailable.advancedMenu = true;
87 | }
88 |
89 | return modifiedParams;
90 | }
91 |
92 |
93 | /**
94 | * Get user params from variables sheet
95 | */
96 | function getUserParamsFromSheet( params ) {
97 |
98 | var sheetUserParams = {};
99 |
100 | if (params.isAvailable.sheetParams) {
101 | forEachCellInRange( params.names.range.userVarNames, params.startIndex, function(index) {
102 |
103 | var name = getCellByIndex(index, params.names.range.userVarNames, params.names.sheet.variables).getValue();
104 | var value = getCellByIndex(index, params.names.range.userVarValues, params.names.sheet.variables).getValue();
105 |
106 | sheetUserParams[name] = value;
107 | });
108 | }
109 |
110 | return sheetUserParams;
111 | }
112 |
113 |
114 | /**
115 | * Get recipient email
116 | */
117 | function getRecipientEmail() {
118 |
119 | var recipientEmail;
120 |
121 | if ( getParam('isAvailable').sheetParams ) {
122 | recipientEmail = getParam('email');
123 | } else {
124 | recipientEmail = getValuesByRangeName( getParam('names').range.recipientEmail )[1][0];
125 | }
126 |
127 | return recipientEmail;
128 | }
--------------------------------------------------------------------------------
/src/dataSend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ------------------ *
3 | * DATA SEND
4 | * ------------------ *
5 | */
6 |
7 |
8 | /**
9 | * Split result by send type
10 | */
11 | function splitResultBySendType(selectedResult, entities) {
12 |
13 | var results = {
14 | main: [],
15 | custom: [],
16 | group: {},
17 | sms: [],
18 | sendLater: []
19 | }
20 |
21 | selectedResult.map( function( id ) {
22 |
23 | var singleParams = entities.advanced[id].params;
24 |
25 | if ( singleParams.hourFrequency ) {
26 |
27 | var lastAdSent = entities.advanced[id].lastAdSentDate;
28 | var now = new Date();
29 | var hourDiff = getHourDiff(lastAdSent, now);
30 |
31 | if (hourDiff < singleParams.hourFrequency) {
32 | results.sendLater.push( id );
33 | }
34 | }
35 |
36 | var splitGroupFromMainSend = entities.labels[id].isGroup == true && getParam('splitSendByCategory') == true ? true : false;
37 | if (splitGroupFromMainSend || singleParams.separateSend == true || (singleParams.email && (singleParams.email !== getParam('email'))) ) {
38 | if (entities.labels[id].isGroup == true) {
39 | var name = entities.labels[id].groupName;
40 | (results.group[ name ] = results.group[ name ] ? results.group[ name ] : []).push( id ); // yep!
41 | } else {
42 | results.custom.push( id );
43 | }
44 | } else {
45 | results.main.push( id );
46 | }
47 |
48 | if (singleParams.sendSms == true) {
49 | results.sms.push( id );
50 | }
51 |
52 | });
53 |
54 | return results;
55 | }
56 |
57 |
58 | /**
59 | * Handle send data
60 | */
61 | function handleSendData(data, callback) {
62 |
63 | var results = splitResultBySendType(data.result, data.entities);
64 |
65 | // main result
66 | var readyMainResults = filterResults(results.main, results.sendLater);
67 | var mainAds = getAllAdsFromResult( data, readyMainResults );
68 | var mainAdsData = getEnhancedData( data );
69 | sendMainResults( mainAdsData, readyMainResults, callback );
70 |
71 | /*if ( getParam('splitSendByCategory') == true ) {
72 | var mainAdsData = getEnhancedData( data );
73 | //var mainAdsData = data;
74 | mainAdsData = getCategorySortedData( mainAdsData, mainAds ); // reconstructed data
75 | sendResultsByCategory( mainAdsData, mainAdsData.result, callback );
76 | } else {
77 | var mainAdsData = getEnhancedData( data );
78 | //var mainAdsData = data;
79 | sendMainResults( mainAdsData, readyMainResults, callback );
80 | }*/
81 |
82 |
83 | var readySeparateGroupResults = {};
84 | Object.keys(results.group).map(function(key, index) {
85 | readySeparateGroupResults[key] = filterResults(results.group[key], results.sendLater)
86 | });
87 | sendSeparateGroupResults( data, readySeparateGroupResults, callback );
88 |
89 | // custom result
90 | var readyCustomResults = filterResults(results.custom, results.sendLater);
91 | sendCustomResults( data, readyCustomResults, callback );
92 |
93 | // sms result
94 | var readySmsResults = filterResults(results.sms, results.sendLater);
95 | sendSmsResults( data, readySmsResults, callback );
96 |
97 | }
98 |
99 |
100 | /**
101 | * Send main results
102 | */
103 | function sendMainResults(data, mainResults, callback) {
104 | if (mainResults.length) {
105 | handleMailSend(data, mainResults, getParam('email'), callback);
106 | }
107 | }
108 |
109 |
110 |
111 |
112 | /**
113 | * Send separate group results
114 | */
115 | function sendSeparateGroupResults(data, groupResults, callback) {
116 | Object.keys(groupResults).map( function( key ) {
117 | var groupResult = groupResults[key];
118 | if (groupResult.length) {
119 | mailSendGroupedResults(data, groupResult, getParam('email'), callback);
120 | }
121 | })
122 | }
123 |
124 |
125 | /**
126 | * Send results by category
127 | */
128 | function sendResultsByCategory(data, categoryResults, callback) {
129 | if (categoryResults.length) {
130 |
131 | categoryResults.map( function( id ) {
132 | var singleResult = [id];
133 |
134 | if (data.entities.ads[id].toSend.length) {
135 | mailSendSeparateResults(data, singleResult, getParam('email'), callback);
136 | }
137 | })
138 |
139 | }
140 | }
141 |
142 |
143 | /**
144 | * Send custom results
145 | */
146 | function sendCustomResults(data, customResults, callback) {
147 | if (customResults.length) {
148 |
149 | customResults.map( function( id ) {
150 | var singleResult = [id];
151 | var customEmail = data.entities.advanced[id].params.email || getParam('email');
152 |
153 | data.entities.advanced[id].haveDuplicates = false; // because they are separated mails & searches
154 |
155 | mailSendSeparateResults(data, singleResult, customEmail, callback);
156 | })
157 |
158 | }
159 | }
160 |
161 |
162 | /**
163 | * Send sms results
164 | */
165 | function sendSmsResults(data, smsResults, callback) {
166 | if (smsResults.length) {
167 |
168 | smsResults.map( function( id ) {
169 | var singleResult = [id];
170 |
171 | var freeUser = data.entities.advanced[id].params.freeUser || getParam('freeUser');
172 | var freePass = data.entities.advanced[id].params.freePass || getParam('freePass');
173 |
174 | if (freeUser && freePass) {
175 | sendSmsWithFreeGateway(data, singleResult, freeUser, freePass, callback);
176 | }
177 | })
178 |
179 | }
180 | }
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ### 5.5.2
4 | - Ajout du paramètre global `strictUrl`, qui permets d'ignorer totalement les urls invalides (au lieu d'afficher un avertissement).
5 |
6 | ### 5.5.1
7 | - Vérification de la validité des liens, suite au changement de structure des urls par leboncoin (qui n'a pas jugé utile de mettre en place des redirections...) [#35](https://github.com/maximelebreton/alertes-leboncoin/issues/35)
8 |
9 | ### 5.5.0 (baroud d'honneur)
10 | - Contournement de la protection mise en place par leboncoin [#39](https://github.com/maximelebreton/alertes-leboncoin/issues/39)
11 |
12 | ### 5.2.7
13 | - Refactoring et amélioration du correctif #22
14 | - ajout du paramètre individuel `pause`
15 |
16 | ### 5.2.6
17 | - correctif [#22](https://github.com/maximelebreton/alertes-leboncoin/issues/22)
18 |
19 | ### 5.2.5
20 | - correctif [#17](https://github.com/maximelebreton/alertes-leboncoin/issues/17)
21 |
22 | ### 5.2.4
23 | > Si vous avez utilisé une **version inférieure à la 5.2.4** lors du passage entre 2016 à 2017, il est possible que vous ne receviez plus les nouvelles annonces. Je vous invite donc à **faire la mise à jour**, et à **vérifier les dates** indiquées dans votre colonne 'Dernière annonce', car elles pourraient avoir 1 an d'avance !
24 |
25 | - Correction du bug de l'an 2000 (du même genre en tout cas, les annonces restaient bloquées au 31 décembre)
26 | - correctif [#14](https://github.com/maximelebreton/alertes-leboncoin/issues/14)
27 |
28 | ### 5.2.3
29 | - Refactoring + correctif [#13](https://github.com/maximelebreton/alertes-leboncoin/issues/13)
30 |
31 | ### 5.2.2
32 | - Amélioration du footer de notification de mise à jour
33 |
34 | ### 5.2.1
35 | - Correction d'un bug causé par le refactoring
36 |
37 | ## 5.2.0 - `30/12/2016`
38 | - Envoi de Sms (numéro Free Mobile uniquement) `sendSms`, `freeUser`, `freePass`
39 | - Possibilité de définir de manière plus fine le prix minimum et maximum `minprice` et `maxprice`
40 | - Possibilité d'ajuster la fréquence des envois de mail de manière individuelle `hourFrequency`
41 | - Pro indiqué entre parenthèses lorsque les résultats ne contiennent que des résultats profesionnels
42 | - Ajout du lien "Éditer mes alertes" en bas de mail (avec la possibilité de le désactiver) `showMailEditLink`
43 | - Refactoring du code, et séparation des fichiers
44 |
45 | ### 5.1.3
46 | - Ajout du mail en format texte
47 |
48 | ## 5.1.0 - `15/11/2016`
49 | - possibilité d'exécuter une fonction custom (`onDataResult()`) via les paramètres utilisateurs avancés
50 | - dans la planification d'alertes, ajout des entrées `tous les jours`, `tous les 2 jours`, `toutes les semaines` et `mettre en pause`.
51 | - ajout d'un 'One Click Action' dans Gmail (`Éditer`) pour accéder directement à la feuille de calcul
52 |
53 | ### 5.0.4
54 | - Les autorisations d'accès sont maintenant limitées au document (par défaut, cela demandait un accès total !)
55 |
56 | ### 5.0.2
57 | - Léger refactoring du code
58 |
59 | ## 5.0.0 - `01/08/2016`
60 | - Ajout d'un menu `Planification des alertes` permettant de paramétrer directement le déclencheur
61 | - Ajout d'une colonne `Options avancées`
62 | - Possibilité de définir un email par recherche via les `Options avancées`
63 | - Paramètres utilisateurs à présent modifiables directement dans la feuille de calcul
64 | - Centralisation des styles CSS de l'email dans inlineStyles.gs
65 | - Ajout d'un libellé 'pro' pour les annonces professionelles
66 |
67 | ### 4.3.2
68 | - améliorations du comportement de l'email
69 |
70 | ### 4.3.1
71 | - email responsive + modifications visuelles
72 |
73 | ## 4.3.0 - `12/07/2016`
74 | - utilisation des [templates](https://developers.google.com/apps-script/guides/html/templates) pour faciliter la maintenance du markup des emails
75 | - ajout du résumé des annonces dans l'aperçu du mail (mailPreheaderTemplate.html)
76 | - ajout d'un placeholder lorsqu'aucune photo n'est disponible
77 |
78 | ### 4.2.1
79 | - Correction d'un bug lié à l'affichage des cartes [#3](https://github.com/maximelebreton/alertes-leboncoin/issues/3)
80 | - Ajout d'une fonction de tri pour que le mail envoyé ne contienne que les dernières annonces même lorsque l'on trie par prix.
81 |
82 | ## 4.2.0 - `04/07/2016`
83 | - Changement de l'algorithme de détection des dernières annonces (anciennement basé sur un id, et remplacé par un timestamp qui est la combinaison de la date et l'id)
84 | - Améliorations visuelles (la progression est maintenant visible)
85 | - Ajout d'une notification en bas de mail lorsqu'une mise à jour est disponible
86 |
87 | ### 4.1.5
88 | - modification du titre des emails envoyés
89 |
90 | ### 4.1.4
91 | - ajout d'un footer
92 |
93 | ### 4.1.3
94 | - Correction d'un bug lié aux paramètres utilisateurs qui n'étaient pas correctement étendus (extend VS deepExtend)
95 |
96 | ### 4.1.2
97 | - Corrections de bugs, amélioration considérable des performances, données normalisées, et ajout de la possibilité de recevoir des mails individuels
98 |
99 | ## 4.0.0 - `08/06/2016`
100 | - Reprise intégrale du projet par [@maximelebreton](https://github.com/maximelebreton) et refactoring complet selon ces principes : https://github.com/maximelebreton/alertes-leboncoin/issues/2
101 |
102 | ## 3.x.x - `07/03/2016`
103 | - par [@jief](https://github.com/jief666) : https://github.com/jief666/alertes-leboncoin
104 |
105 | ## 2.x.x - `22/10/2012`
106 | - http://justdocsit.blogspot.fr/2012/11/alerte-leboncoin-v2.html
107 |
108 | ## 1.x.x - `09/07/2012`
109 | - version originale par [@St3ph-fr](https://github.com/St3ph-fr) : http://justdocsit.blogspot.fr/2012/07/creer-une-alerte-sur-le-bon-coin.html
110 |
--------------------------------------------------------------------------------
/src/spreadsheetUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ------------------------------------- *
3 | * ABOUT SPREADSHEET CUSTOM FUNCTIONS
4 | * ------------------------------------- *
5 | */
6 |
7 |
8 | /**
9 | * Get url content
10 | */
11 | function getUrlContent(url, remainingAttempts) {
12 |
13 | var content = "";
14 | var remainingAttempts = remainingAttempts || 2; // String
15 | remainingAttempts = Number(remainingAttempts); // Number
16 | var response = getUrlResponse(url);
17 |
18 | if ( response.getResponseCode() === 500 ) {
19 |
20 | if ( remainingAttempts == 0 ) {
21 |
22 | getSpreadsheetContext().toast( response.getContentText() );
23 |
24 | } else {
25 | Utilities.sleep(1000); // Waiting 1 sec and retry
26 | remainingAttempts = remainingAttempts-1; // Number
27 | getUrlContent(url, remainingAttempts.toString()); // Inside loop, remainingAttemps need to be a String and not a number because of 0
28 | }
29 |
30 | } else {
31 | //content = response.getContentText("iso-8859-15");
32 | content = response.getContentText("UTF-8");
33 | }
34 |
35 | return content;
36 | }
37 |
38 |
39 | /**
40 | * Get url response
41 | */
42 | function getUrlResponse(url) {
43 |
44 | var response = UrlFetchApp.fetch(url, {muteHttpExceptions: getParam('muteHttpExceptions'), followRedirects: true});
45 |
46 | return response;
47 | }
48 |
49 |
50 | /**
51 | * For each cell in range
52 | */
53 | function forEachCellInRange(columnName, startIndex, callback) {
54 |
55 | var range = getSpreadsheetContext().getRangeByName(columnName).getValues();
56 | var startIndex = startIndex - 1 || 0;
57 |
58 | for (var i = startIndex, length = range.length; i < length; i++) {
59 |
60 | var index = i + 1;
61 | var value = range[i][0];
62 |
63 | if (value.length && callback && typeof(callback) === "function") {
64 | callback(index);
65 | }
66 | }
67 |
68 | }
69 |
70 |
71 | /**
72 | * Get array index
73 | */
74 | function getArrayIndex(index, startIndex) {
75 | var startIndex = startIndex - 1 || 0;
76 | return index - startIndex;
77 | }
78 |
79 |
80 | /**
81 | * For each value
82 | */
83 | function forEachValue(values, startIndex, callback) {
84 |
85 | var startIndex = startIndex - 1 || 0;
86 |
87 | for (var i = startIndex, length = values.length; i < length; i++) {
88 |
89 | var index = i + 1;
90 | var value = values[i][0];
91 |
92 | if (value.length && callback && typeof(callback) === "function") {
93 | callback(index);
94 | }
95 | }
96 |
97 | }
98 |
99 |
100 | /**
101 | * Get row by index
102 | */
103 | function getRowByIndex( index, rangeName, sheetName ) {
104 |
105 | return getSheetByName( sheetName ).getRange(index, 1, 1, getColumnByName( rangeName ) );
106 | }
107 |
108 | /**
109 | * Get cell by index
110 | */
111 | function getCellByIndex( index, rangeName, sheetName ) {
112 |
113 | return getSheetByName( sheetName ).getRange(index, getColumnByName( rangeName ) );
114 | }
115 |
116 |
117 | /**
118 | * Get range by name
119 | */
120 | function getRangeByName( rangeName ) {
121 |
122 | return getSpreadsheetContext().getRangeByName( rangeName );
123 | }
124 |
125 |
126 | /**
127 | * Get column by name
128 | */
129 | function getColumnByName( rangeName ) {
130 |
131 | return getSpreadsheetContext().getRangeByName( rangeName ).getColumn();
132 | }
133 |
134 |
135 | /**
136 | * Get spreadsheet context
137 | */
138 | function getSpreadsheetContext() {
139 | var ss = SpreadsheetApp.getActiveSpreadsheet();
140 |
141 | return ss;
142 | }
143 |
144 |
145 | /**
146 | * Get data sheet context
147 | */
148 | function getSheetByName(name) {
149 | var sheet = getSpreadsheetContext().getSheetByName( name );
150 |
151 | return sheet;
152 | }
153 |
154 | /**
155 | * Get data sheet context
156 | */
157 | function getDataSheetContext() {
158 | var sheet = getSpreadsheetContext().getSheetByName( getParam('names').sheet.main );
159 |
160 | return sheet;
161 | }
162 |
163 | /**
164 | * Get variables sheet context
165 | */
166 | function getVariablesSheetContext() {
167 | var sheet = getSpreadsheetContext().getSheetByName( getParam('names').sheet.variables );
168 |
169 | return sheet;
170 | }
171 |
172 |
173 | /**
174 | * Get full range name
175 | */
176 | function getFullRangeName( rangeName ) {
177 |
178 | return names.sheet.main + '!' + rangeName;
179 | }
180 |
181 |
182 | /**
183 | * Get values by range name
184 | */
185 | function getValuesByRangeName(rangeName, asString) {
186 | // raw
187 | var asString = asString || true;
188 |
189 | //— for example, getRangeByName('TaxRates') or getRangeByName('Sheet Name!TaxRates'), but not getRangeByName('"Sheet Name"!TaxRates').
190 | var range = getSpreadsheetContext().getRangeByName(rangeName);
191 |
192 | if (asString) {
193 | return range.getDisplayValues();
194 | } else {
195 | return range.getValues();
196 | }
197 |
198 | }
199 |
200 |
201 | /**
202 | * Get cached content
203 | */
204 | function getCachedContent(url) {
205 |
206 | var cache = CacheService.getDocumentCache();
207 | var cached = cache.get( getUrlHashcode(url) );
208 |
209 | if (cached != null) {
210 | return cached;
211 | } else {
212 | return false;
213 | }
214 | }
215 |
216 |
217 | /**
218 | * Set cache
219 | */
220 | function setCache(url, content) {
221 | //var cache = CacheService.getPublicCache();
222 | try {
223 | var cache = CacheService.getDocumentCache();
224 | cache.put( getUrlHashcode(url), content, getParam('cacheTime') );
225 | } catch(e) {
226 | Logger.log(e);
227 | getSpreadsheetContext().toast( e , 'Alertes LeBonCoin');
228 | }
229 |
230 | }
231 |
232 |
233 | /**
234 | * Get url hashcode
235 | */
236 | function getUrlHashcode( url ) {
237 | return url.toString().split("/").pop().hashCode().toString();
238 | }
239 |
240 |
241 | /**
242 | * Hashcode function
243 | */
244 | String.prototype.hashCode = function() {
245 | for(var ret = 0, i = 0, len = this.length; i < len; i++) {
246 | ret = (26 * ret + this.charCodeAt(i)) << 0;
247 | }
248 | return ret;
249 | };
--------------------------------------------------------------------------------
/src/data.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ---------------- *
3 | * DATA
4 | * ---------------- *
5 | */
6 |
7 |
8 | /**
9 | * Set normalized data (inspired by redux & normalizr principles)
10 | */
11 | function getNormalizedData( data, datum ) {
12 |
13 | // Push new result
14 | data.result.push( datum.index );
15 |
16 | // Because GAS is not ecmascript 6, we need to use this method to set dynamic object names...
17 | var obj = {}; obj[ datum.index ] = {};
18 | var labels = extend({}, obj);
19 | var urls = extend({}, obj);
20 | var ads = extend({}, obj);
21 | var advanced = extend({}, obj);
22 |
23 | // Extend Labels
24 | labels[ datum.index ] = {
25 | id: datum.index,
26 | label: datum.label
27 | }
28 | labels = extend({}, data.entities.labels, labels);
29 |
30 | // Extend urls
31 | urls[ datum.index ] = {
32 | id: datum.index,
33 | url: datum.url
34 | }
35 | urls = extend({}, data.entities.urls, urls);
36 |
37 | // Extend ads
38 | ads[ datum.index ] = {
39 | id: datum.index,
40 | //all: datum.ads,
41 | toSend: datum.adsToSend,
42 | tags: datum.tags
43 | }
44 | ads = extend({}, data.entities.ads, ads);
45 |
46 | // Extend advanced
47 | advanced[ datum.index ] = {
48 | id: datum.index,
49 | params: datum.singleParams,
50 | lastAdSentDate: datum.lastAdSentDate
51 | }
52 | advanced = extend({}, data.entities.advanced, advanced);
53 |
54 | // Extend entities
55 | data.entities = extend({}, data.entities, {
56 | labels: labels,
57 | urls: urls,
58 | ads: ads,
59 | advanced: advanced
60 | });
61 |
62 | return data;
63 |
64 | }
65 |
66 |
67 | /**
68 | * Get enhanced data
69 | */
70 | function getEnhancedData( data ) {
71 |
72 | var allAds = getAllAdsFromResult( data, data.result );
73 | var adsIds = getAttrValuesFromAds( allAds, "id");
74 | var duplicates = getDuplicates( adsIds );
75 |
76 | var enhancedData = getDataWithDuplicates( data, duplicates );
77 | enhancedData = getDataWithGroups( enhancedData );
78 |
79 | return enhancedData;
80 | }
81 |
82 |
83 | /**
84 | * Get data with duplicates
85 | */
86 | function getDataWithDuplicates( data, duplicates ) {
87 |
88 | var alreadySeenDuplicates = [];
89 |
90 | data.result.map( function( id ) {
91 | var ads = data.entities.ads[id].toSend;
92 | ads.map( function( ad ) {
93 |
94 | duplicates.map( function( duplicatedId ) {
95 |
96 | if (ad.id == duplicatedId) {
97 | if ( alreadySeenDuplicates.indexOf( ad.id ) > -1 ) { // if match in array
98 | ad.haveDuplicateInResult = id;
99 | data.entities.advanced[id].haveDuplicates = true;
100 | } else {
101 | alreadySeenDuplicates.push( ad.id );
102 | }
103 | }
104 |
105 | })
106 | })
107 | })
108 |
109 | return data;
110 | }
111 |
112 |
113 | /**
114 | * Get data with duplicates
115 | */
116 | function getDataWithGroups( data ) {
117 |
118 | var adProperty = 'userLabel';
119 | var alreadySeenLabelGroups = [];
120 | var regex = /(?:^\[(.*)])?\s?(.*)/;
121 |
122 | data.result.map( function( id ) {
123 | var label = data.entities.labels[id];
124 |
125 | var matches = label.label.match(regex);
126 |
127 | var labelGroup = matches[1];
128 | var labelSingle = matches[2];
129 |
130 | var slug = labelGroup ? labelGroup : labelSingle;
131 | var indexOfSlug = alreadySeenLabelGroups.indexOf( slug );
132 |
133 | if (labelGroup) {
134 | data.entities.labels[id].isGroup = true;
135 | data.entities.labels[id].groupName = labelGroup;
136 | }
137 |
138 | if ( indexOfSlug > -1 ) { // if match in array
139 |
140 | } else {
141 | alreadySeenLabelGroups.push( slug );
142 | }
143 |
144 | })
145 |
146 |
147 | return data;
148 | }
149 |
150 |
151 | /**
152 | * Get category sorted data
153 | */
154 | function getCategorySortedData(data, ads) {
155 | var categorySortedData = JSON.parse(JSON.stringify(data));;
156 | categorySortedData.result = [];
157 | //categorySortedData.resultSource = [];
158 | categorySortedData.entities.ads = {};
159 | categorySortedData.entities.labels = {};
160 | categorySortedData.entities.urls = {}; // not used
161 | categorySortedData.entities.advanced = {}; // not used
162 |
163 | var adProperty = 'userLabel';
164 |
165 | var alreadySeenCategorySlugs = [];
166 |
167 | ads.map( function( ad ) {
168 |
169 | var id;
170 | //var indexOfCategorySlug = alreadySeenCategorySlugs.indexOf( ad.categorySlug );
171 |
172 | //var labelGroup = s.match(/\[(.*?)\]/g);
173 |
174 | var regex = /(?:^\[(.*)])?\s?(.*)/;
175 | var matches = ad[adProperty].match(regex);
176 |
177 | var labelGroup = matches[1];
178 | var label = matches[2];
179 |
180 | var categorySlug = labelGroup ? labelGroup : label;
181 |
182 | //var indexOfCategorySlug = alreadySeenCategorySlugs.indexOf( ad[adProperty] );
183 |
184 | var indexOfCategorySlug = alreadySeenCategorySlugs.indexOf( categorySlug );
185 |
186 | if ( indexOfCategorySlug > -1 ) { // if match in array
187 | id = indexOfCategorySlug;
188 | } else {
189 | id = alreadySeenCategorySlugs.length;
190 |
191 | alreadySeenCategorySlugs.push( categorySlug );
192 | categorySortedData.result.push( id );
193 |
194 | categorySortedData.entities.ads[ id ] = {
195 | id: id,
196 | toSend: []
197 | };
198 | categorySortedData.entities.labels[ id ] = {
199 | id: id,
200 | label: categorySlug,
201 | isGroup: true
202 | };
203 |
204 | // Not used but need to be non-empty
205 | categorySortedData.entities.urls[ id ] = {
206 | id: id,
207 | url: ""
208 | };
209 | // Not used but need to be non-empty
210 | categorySortedData.entities.advanced[ id ] = {
211 | id: id,
212 | params: {}
213 | };
214 | }
215 |
216 | categorySortedData.entities.ads[ id ].toSend.push( ad );
217 | })
218 |
219 | categorySortedData.result.map( function ( id ) {
220 | var sortedAds = categorySortedData.entities.ads[ id ].toSend.sort( dynamicSort("-timestamp") );
221 | categorySortedData.entities.ads[ id ].toSend = sortedAds;
222 | })
223 |
224 | return categorySortedData;
225 | }
226 |
227 |
228 | /**
229 | * Get all ads from result
230 | */
231 | function getAllAdsFromResult( data, result ) {
232 |
233 | var allAds = [];
234 |
235 | result.map( function( id ) {
236 | var ads = data.entities.ads[id].toSend;
237 |
238 | ads.map( function( ad ) {
239 | ad.sheetIndex = id;
240 | })
241 |
242 | Array.prototype.push.apply(allAds, ads);
243 | })
244 |
245 | return allAds;
246 | }
247 |
248 |
249 | /**
250 | * Get Attributes values from ads
251 | */
252 | function getAttrValuesFromAds( ads, attribute ) {
253 |
254 | var values = [];
255 |
256 | ads.map( function( ad ) {
257 | var value = ad[attribute];
258 | values.push( value );
259 | })
260 |
261 | return values;
262 | }
263 |
264 |
265 |
266 |
267 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ----------- *
3 | * UTILS
4 | * ----------- *
5 | */
6 |
7 |
8 | /**
9 | * Log
10 | */
11 | function log(value, stringify) {
12 | if (stringify == false) {
13 | Logger.log ( value );
14 | }
15 | return Logger.log( JSON.stringify(value) );
16 |
17 | }
18 |
19 |
20 | /**
21 | * Extract html
22 | */
23 | function extractHtml(html, startTag, endTag, startIndex, endIndex){
24 |
25 | var extractedHtml = "";
26 | var startIndex = startIndex || 0;
27 | var endIndex = endIndex || 0;
28 |
29 | var from = html.indexOf(startTag);
30 |
31 | if (from !== -1) {
32 | var to = html.indexOf(endTag, from);
33 |
34 | extractedHtml = html.substring( from + startIndex, to - endIndex);
35 | }
36 |
37 | return extractedHtml;
38 | }
39 |
40 |
41 | /**
42 | * Get hour diff (between date objects)
43 | */
44 | function getHourDiff(date1, date2) {
45 | return Math.floor(Math.abs(date1 - date2) / 3600000);
46 | }
47 |
48 |
49 | /**
50 | * Clean text
51 | */
52 | function cleanText(value) {
53 |
54 | return value.trim().replace(/\s{2,}/g, ' ');
55 |
56 | }
57 |
58 |
59 | /**
60 | * Filter results
61 | */
62 | function filterResults(a, b) {
63 |
64 | var results = a.filter(function(val) {
65 | return b.indexOf(val) == -1;
66 | });
67 |
68 | return results;
69 |
70 | }
71 |
72 |
73 | /**
74 | * Remove duplicate results
75 | */
76 | Array.prototype.uniq = function uniq() {
77 | return this.reduce(function(accum, cur) {
78 | if (accum.indexOf(cur) === -1) accum.push(cur);
79 | return accum;
80 | }, [] );
81 | }
82 |
83 |
84 | /**
85 | * Add protocol (https)
86 | */
87 | function addProtocol(url) {
88 | if ( url && !/^(f|ht)tps?:\/\//i.test(url) ) {
89 | url = "https:" + url;
90 | }
91 | return url;
92 | }
93 |
94 |
95 | /**
96 | * Decode URL
97 | */
98 | function decodeURL(url) {
99 | try {
100 | url = decodeURIComponent(url);
101 | } catch(e) {
102 | url = decodeURIComponent( escape(url) );
103 | }
104 | return url;
105 | }
106 |
107 |
108 | /**
109 | * Mimic jquery Extend function
110 | */
111 | function extend() {
112 | for(var i=1; i 0) || (parseInt(a[i]) > parseInt(b[i]))) {
174 | return 1;
175 | } else if ((b[i] && !a[i] && parseInt(b[i]) > 0) || (parseInt(a[i]) < parseInt(b[i]))) {
176 | return -1;
177 | }
178 | }
179 |
180 | return 0;
181 | }
182 |
183 |
184 | /**
185 | * Sort object properties
186 | */
187 | function sortObjectProperties(obj, sortValue, reverse){
188 |
189 | var keysSorted;
190 | if (reverse) {
191 | keysSorted = Object.keys(obj).sort(function(a,b){return obj[b][sortValue]-obj[a][sortValue]});
192 | } else {
193 | keysSorted = Object.keys(obj).sort(function(a,b){return obj[a][sortValue]-obj[b][sortValue]});
194 | }
195 |
196 | var objSorted = {};
197 | for(var i = 0; i < keysSorted.length; i++){
198 | objSorted[keysSorted[i]] = obj[keysSorted[i]];
199 | }
200 | return objSorted;
201 | }
202 |
203 |
204 | /**
205 | * Dynamic Sort
206 | */
207 | function dynamicSort(property) {
208 | var sortOrder = 1;
209 | if(property[0] === "-") {
210 | sortOrder = -1;
211 | property = property.substr(1);
212 | }
213 | return function (a,b) {
214 | var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
215 | return result * sortOrder;
216 | }
217 | }
218 |
219 | /**
220 | * Slug
221 | */
222 | var slug = function(str) {
223 | str = str.replace(/^\s+|\s+$/g, ''); // trim
224 | str = str.toLowerCase();
225 |
226 | // remove accents, swap ñ for n, etc
227 | var from = "ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;";
228 | var to = "aaaaaeeeeeiiiiooooouuuunc------";
229 | for (var i=0, l=from.length ; i= 1 ? counts[item] + 1 : 1;
264 | if (counts[item] === 2) {
265 | out.push(item);
266 | }
267 | }
268 |
269 | return out;
270 | }
271 |
272 |
273 | /**
274 | *
275 | */
276 | function runtimeCountStop(start) {
277 |
278 | var stop = new Date();
279 | var runtime = Number(stop) - Number(start);
280 |
281 | return runtime;
282 |
283 | }
284 |
285 | function getDateObjectFromString(string) {
286 |
287 | var compatibleString = string.replace(' ', 'T');
288 | var date = new Date(compatibleString);
289 |
290 | return date;
291 |
292 | }
293 |
294 | function formatPrice(price_value){
295 | var formatted_price = '';
296 | var price = ''+price_value;
297 | var price_length = price.length;
298 | var start;
299 | while(price_length > 0){
300 | start = price_length - 3 > 0 ? price_length - 3 : 0;
301 | formatted_price = ' ' + price.substring(start, start+3)+formatted_price;
302 | price = price.substring(0, start);
303 | price_length = price.length;
304 | }
305 | formatted_price = formatted_price.replace(/^\s+/, '');
306 | return formatted_price;
307 | }
--------------------------------------------------------------------------------
/src/dayjs.min.js:
--------------------------------------------------------------------------------
1 | // Dayjs 1.6.9
2 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.dayjs=e()}(this,function(){"use strict";var t="millisecond",e="second",n="minute",r="hour",s="day",i="week",a="month",u="year",c=/^(\d{4})-?(\d{1,2})-?(\d{0,2})(.*?(\d{1,2}):(\d{1,2}):(\d{1,2}))?.?(\d{1,3})?$/,o=/\[.*?\]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,h={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},d=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},$={padStart:d,padZoneStr:function(t){var e=Math.abs(t),n=Math.floor(e/60),r=e%60;return(t<=0?"+":"-")+d(n,2,"0")+":"+d(r,2,"0")},monthDiff:function(t,e){var n=12*(e.year()-t.year())+(e.month()-t.month()),r=t.clone().add(n,"months"),s=e-r<0,i=t.clone().add(n+(s?-1:1),"months");return Number(-(n+(e-r)/(s?r-i:i-r)))},absFloor:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},prettyUnit:function(c){return{M:a,y:u,w:i,d:s,h:r,m:n,s:e,ms:t}[c]||String(c||"").toLowerCase().replace(/s$/,"")},isUndefined:function(t){return void 0===t}},f="en",l={};l[f]=h;var m=function(t){return t instanceof D},y=function(t,e,n){var r;if(!t)return null;if("string"==typeof t)l[t]&&(r=t),e&&(l[t]=e,r=t);else{var s=t.name;l[s]=t,r=s}return n||(f=r),r},M=function(t,e){if(m(t))return t.clone();var n=e||{};return n.date=t,new D(n)},p=function(t,e){return M(t,{locale:e.$L})},S=$;S.parseLocale=y,S.isDayjs=m,S.wrapper=p;var D=function(){function h(t){this.parse(t)}var d=h.prototype;return d.parse=function(t){var e,n;this.$d=null===(e=t.date)?new Date(NaN):S.isUndefined(e)?new Date:e instanceof Date?e:"string"==typeof e&&/.*[^Z]$/i.test(e)&&(n=e.match(c))?new Date(n[1],n[2]-1,n[3]||1,n[5]||0,n[6]||0,n[7]||0,n[8]||0):new Date(e),this.init(t)},d.init=function(t){this.$y=this.$d.getFullYear(),this.$M=this.$d.getMonth(),this.$D=this.$d.getDate(),this.$W=this.$d.getDay(),this.$H=this.$d.getHours(),this.$m=this.$d.getMinutes(),this.$s=this.$d.getSeconds(),this.$ms=this.$d.getMilliseconds(),this.$L=this.$L||y(t.locale,null,!0)||f},d.$utils=function(){return S},d.isValid=function(){return!("Invalid Date"===this.$d.toString())},d.isLeapYear=function(){return this.$y%4==0&&this.$y%100!=0||this.$y%400==0},d.$compare=function(t){return this.valueOf()-M(t).valueOf()},d.isSame=function(t){return 0===this.$compare(t)},d.isBefore=function(t){return this.$compare(t)<0},d.isAfter=function(t){return this.$compare(t)>0},d.year=function(){return this.$y},d.month=function(){return this.$M},d.day=function(){return this.$W},d.date=function(){return this.$D},d.hour=function(){return this.$H},d.minute=function(){return this.$m},d.second=function(){return this.$s},d.millisecond=function(){return this.$ms},d.unix=function(){return Math.floor(this.valueOf()/1e3)},d.valueOf=function(){return this.$d.getTime()},d.startOf=function(t,c){var o=this,h=!!S.isUndefined(c)||c,d=function(t,e){var n=p(new Date(o.$y,e,t),o);return h?n:n.endOf(s)},$=function(t,e){return p(o.toDate()[t].apply(o.toDate(),h?[0,0,0,0].slice(e):[23,59,59,999].slice(e)),o)};switch(S.prettyUnit(t)){case u:return h?d(1,0):d(31,11);case a:return h?d(1,this.$M):d(0,this.$M+1);case i:return d(h?this.$D-this.$W:this.$D+(6-this.$W),this.$M);case s:case"date":return $("setHours",0);case r:return $("setMinutes",1);case n:return $("setSeconds",2);case e:return $("setMilliseconds",3);default:return this.clone()}},d.endOf=function(t){return this.startOf(t,!1)},d.$set=function(s,i){switch(S.prettyUnit(s)){case"date":this.$d.setDate(i);break;case a:this.$d.setMonth(i);break;case u:this.$d.setFullYear(i);break;case r:this.$d.setHours(i);break;case n:this.$d.setMinutes(i);break;case e:this.$d.setSeconds(i);break;case t:this.$d.setMilliseconds(i)}return this.init(),this},d.set=function(t,e){return this.clone().$set(t,e)},d.add=function(t,c){var o=this;t=Number(t);var h,d=S.prettyUnit(c),$=function(e,n){var r=o.set("date",1).set(e,n+t);return r.set("date",Math.min(o.$D,r.daysInMonth()))};if(d===a)return $(a,this.$M);if(d===u)return $(u,this.$y);switch(d){case n:h=6e4;break;case r:h=36e5;break;case s:h=864e5;break;case i:h=6048e5;break;case e:h=1e3;break;default:h=1}var f=this.valueOf()+t*h;return p(f,this)},d.subtract=function(t,e){return this.add(-1*t,e)},d.format=function(t){var e=this,n=t||"YYYY-MM-DDTHH:mm:ssZ",r=S.padZoneStr(this.$d.getTimezoneOffset()),s=this.$locale(),i=s.weekdays,a=s.months,u=function(t,e,n,r){return t&&t[e]||n[e].substr(0,r)};return n.replace(o,function(t){if(t.indexOf("[")>-1)return t.replace(/\[|\]/g,"");switch(t){case"YY":return String(e.$y).slice(-2);case"YYYY":return String(e.$y);case"M":return String(e.$M+1);case"MM":return S.padStart(e.$M+1,2,"0");case"MMM":return u(s.monthsShort,e.$M,a,3);case"MMMM":return a[e.$M];case"D":return String(e.$D);case"DD":return S.padStart(e.$D,2,"0");case"d":return String(e.$W);case"dd":return u(s.weekdaysMin,e.$W,i,2);case"ddd":return u(s.weekdaysShort,e.$W,i,3);case"dddd":return i[e.$W];case"H":return String(e.$H);case"HH":return S.padStart(e.$H,2,"0");case"h":case"hh":return 0===e.$H?12:S.padStart(e.$H<13?e.$H:e.$H-12,"hh"===t?2:1,"0");case"a":return e.$H<12?"am":"pm";case"A":return e.$H<12?"AM":"PM";case"m":return String(e.$m);case"mm":return S.padStart(e.$m,2,"0");case"s":return String(e.$s);case"ss":return S.padStart(e.$s,2,"0");case"SSS":return S.padStart(e.$ms,3,"0");case"Z":return r;default:return r.replace(":","")}})},d.diff=function(t,c,o){var h=S.prettyUnit(c),d=M(t),$=this-d,f=S.monthDiff(this,d);switch(h){case u:f/=12;break;case a:break;case"quarter":f/=3;break;case i:f=$/6048e5;break;case s:f=$/864e5;break;case r:f=$/36e5;break;case n:f=$/6e4;break;case e:f=$/1e3;break;default:f=$}return o?f:S.absFloor(f)},d.daysInMonth=function(){return this.endOf(a).$D},d.$locale=function(){return l[this.$L]},d.locale=function(t,e){var n=this.clone();return n.$L=y(t,e,!0),n},d.clone=function(){return p(this.toDate(),this)},d.toDate=function(){return new Date(this.$d)},d.toArray=function(){return[this.$y,this.$M,this.$D,this.$H,this.$m,this.$s,this.$ms]},d.toJSON=function(){return this.toISOString()},d.toISOString=function(){return this.toDate().toISOString()},d.toObject=function(){return{years:this.$y,months:this.$M,date:this.$D,hours:this.$H,minutes:this.$m,seconds:this.$s,milliseconds:this.$ms}},d.toString=function(){return this.$d.toUTCString()},h}();return M.extend=function(t,e){return t(e,D,M),M},M.locale=y,M.isDayjs=m,M.en=l[f],M});
3 |
4 | // locale fr
5 | ! function (e, n) {
6 | "object" == typeof exports && "undefined" != typeof module ? module.exports = n(require("dayjs")) : "function" == typeof define && define.amd ? define(["dayjs"], n) : e.dayjs_locale_fr = n(e.dayjs)
7 | }(this, function (e) {
8 | "use strict";
9 |
10 | var n = {
11 | name: "fr",
12 | weekdays: "Dimanche_Lundi_Mardi_Mercredi_Jeudi_Vendredi_Samedi".split("_"),
13 | months: "Janvier_Février_Mars_Avril_Mai_Juin_Juillet_Août_Septembre_Octobre_Novembre_Décembre".split("_"),
14 | relativeTime: {
15 | future: "dans %s",
16 | past: "il y a %s",
17 | s: "quelques secondes",
18 | m: "une minute",
19 | mm: "%d minutes",
20 | h: "une heure",
21 | hh: "%d heures",
22 | d: "un jour",
23 | dd: "%d jours",
24 | M: "un mois",
25 | MM: "%d mois",
26 | y: "un an",
27 | yy: "%d ans"
28 | },
29 | ordinal: function (e) {
30 | return e + "º"
31 | }
32 | };
33 | return e.locale(n, null, !0), n
34 | });
35 |
36 | dayjs.locale('fr');
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Suite à la mise en place d'un système de protection (datadome) par **leboncoin**, il n'est **plus possible d'utiliser Alertes leboncoin** depuis le 31 Août 2018. J'étudie actuellement les potentielles solutions. Toutes vos aides et suggestions sont les bienvenues.
4 |
5 | _______
6 |
7 | Alertes leboncoin - 5.5.2 [](https://github.com/maximelebreton/alertes-leboncoin)
8 | =============================
9 | Recevez par email vos recherches leboncoin.fr (via Google Sheets / App Script)
10 |
11 | 
12 |
13 |
14 | Pour commencer
15 | ------------------------------------------
16 | **Prérequis :** *vous devez avoir un compte Google et y être connecté.*
17 |
18 | 1. Créez votre **[copie de la feuille de calcul *Alertes leboncoin*](https://goo.gl/Awjw5f)**
19 |
20 | 2. Indiquez votre **email** dans l'onglet **`Paramètres utilisateur`**, et **lancez manuellement** votre première recherche via le **menu `Alertes LeBonCoin`.**
21 |
22 | 3. Pour être averti **automatiquement** des prochains résultats, réglez la **fréquence** à laquelle vous souhaitez être averti via le **menu dans `Planification des alertes`.**
23 |
24 | 4. Il ne vous reste plus qu'à vous rendre sur le site [leboncoin.fr](https://www.leboncoin.fr) pour **copier le lien** de votre recherche, puis le **coller** dans votre feuille de calcul (colonne `Lien` de l'onglet **`Vos alertes`**).
25 |
26 |
27 | Wiki
28 | -----
29 |
30 | - [Comment obtenir le lien de votre recherche ?](https://github.com/maximelebreton/alertes-leboncoin/wiki/Comment-obtenir-le-lien-de-votre-recherche-%3F)
31 |
32 |
33 | Vos alertes
34 | ----------------------------------
35 | Dans la **feuille** intitulée `Vos alertes`, **chaque ligne correspond à une recherche** :
36 | > Pour chaque recherche que vous souhaitez effectuer sur [leboncoin.fr](https://www.leboncoin.fr), après avoir **copié** le lien de votre recherche, il vous suffit de le **coller** dans la colonne prévue à cet effet, puis de lui donner un titre.
37 |
38 | Les colonnes (avec un exemple) :
39 |
40 | Titre | Lien | Dernière annonce | Paramètres avancés
41 | ------------ | ------------- | ------------- | -------------
42 | `Caravane` | `https://www.leboncoin.fr/caravaning/` | | `{"showMap":true}`
43 | *le titre de votre recherche* (*`obligatoire`*) | *l'url de votre recherche* (*`obligatoire`*) | *indique la date du dernier résultat qui vous a été envoyé par email* (*`automatique`*) | *est un champ qui s'adresse aux utilisateurs avancés* (*`facultatif`*)
44 |
45 |
46 | Paramètres utilisateur
47 | ----------------------
48 |
49 | ### Méthode simple
50 | Dans la **feuille** intitulée `Paramètres utilisateur`, accessible également via le menu `Alertes LeBonCoin` > `Paramètres utilisateur`.
51 |
52 | > *Les paramètres définis via la feuille `Paramètres utilisateur` s'appliquent à toutes les recherches*
53 |
54 | Paramètre | Valeur | Description
55 | ------------ | ------------- | -------------
56 | `email` | `mon@email.com` (exemple) | *l'adresse à laquelle sera envoyée les annonces. Possibilité de définir plusieurs destinataires en les séparant par une virgule*
57 | `showMap` | `=true` ou `=false` | *affiche une mini carte*
58 | `mapZoom` | nombre de `=0` à `=17` | *règle le niveau de zoom de la carte*
59 | `groupedResults` | `=true` ou `=false` | *permet de grouper les résultats dans un seul mail*
60 | `strictUrl` | `=true` ou `=false` | *ignore les urls non valides*
61 |
62 |
63 | ### Méthode avancée
64 |
65 | #### Paramères globaux
66 | > *Les paramètres globaux avancés s'appliquent à toutes les recherches*
67 |
68 | Via l'objet `userParams` (dans la feuille de calcul : `Outils > Editeur de scripts`), qui permet de personnaliser la totalité des **[variables de la librairie](https://github.com/maximelebreton/alertes-leboncoin/blob/master/src/Code.js#L10)**
69 |
70 | Exemple :
71 | ```
72 | var userParams = {
73 | startIndex: 2,
74 | selectors: {
75 | adItem: '.mainList ul > li'
76 | },
77 | onDataResult: function(result, entities) {
78 | // Custom callback
79 | }
80 | }
81 | ```
82 |
83 |
84 | #### Paramètres individuels
85 |
86 | > *Les paramètres individuels avancés s'appliquent uniquement à la recherche concernée*
87 |
88 | Via la colonne `Paramètres avancées` en passant un `objet JSON`.
89 |
90 | Exemple :
91 | ```
92 | {"email":"autre@email.com","showMap":true,"mapZoom":9}
93 | ```
94 |
95 | Paramètre (individuel) | Valeur | Description | Type
96 | ------------ | ------------- | ------------- | -------------
97 | `email` | `"mon@email.com"` (exemple) | *l'adresse à laquelle sera envoyée les annonces. Possibilité de définir plusieurs destinataires en les séparant par une virgule* | `String`
98 | `showMap` | `true` ou `false` | *Affiche une mini carte* | `Boolean`
99 | `mapZoom` | nombre de `0` à `17` | *Règle le niveau de zoom de la carte* | `Number`
100 | `hourFrequency` | `36` (exemple) | *Permet de modifier individuellement la fréquence des envois d'email (en nombre d'heures). Doit être __supérieur__ au déclencheur principal.* | `Number`
101 | `minPrice` | `150` (exemple) | *Spécifier un prix minimum (>=)* | `Number`
102 | `maxPrice` | `275` (exemple) | *Spécifier un prix maximum (<=)* | `Number`
103 | `sendSms` | `true` ou `false` | *[Experimental] Active l'envoi de Sms (uniquement compatible avec l'api __Free Mobile__ pour le moment)* | `Boolean`
104 | `freeUser` | `"0123456789"` (exemple) | *ID Free Mobile* | `String`
105 | `freePass` | `"xxxxxx"` (exemple) | *Clé d'identification (à générer dans [votre espace Free Mobile](http://www.universfreebox.com/article/26337/Nouveau-Free-Mobile-lance-un-systeme-de-notification-SMS-pour-vos-appareils-connectes))* | `String`
106 | `pause` | `true` ou `false` | *Mets en pause l'annonce* | `Boolean`
107 | `isValidUrl` | `true` | *Force la validité de l'url* | `Boolean`
108 |
109 |
110 | Limitations
111 | ---------------
112 | *Alertes leboncoin* est une **web application dont le code est open source**, mais basée sur le **service *App Script*** associé à votre compte *Google* (**qui lui ne l'est pas**).
113 | Bien que cette solution a l'**avantage d'être "gratuite"**, elle reste totalement **dépendante de la politique de *Google* et de ses limitations**.
114 | Il est plus que conseillé d'avoir un **usage raisonnable de la solution**, sans quoi vous seriez vite confrontés aux **[limitations du service](https://developers.google.com/apps-script/guides/services/quotas#top_of_page)**.
115 | D'autre part, n'étant pas une solution officielle d'alertes, *leboncoin.fr* peut tout à fait décider d'y mettre un terme s'il considère qu'il y'a des abus, ce qui viendrait pénaliser toute la communauté.
116 |
117 |
118 | Obtenir la dernière mise à jour
119 | ----------------------------------
120 | Pour mettre à jour la librairie, une fois dans la feuille de calcul, aller dans `Outils > Editeur de scripts`, puis `Ressources > Bibliothèques`, choisissez la version la plus récente, puis **cliquez sur Enregistrer**.
121 | > *IMPORTANT : La mise à jour de la librairie ne mets pas à jour la feuille de calcul.
122 | Donc si une nouvelle fonctionnalité n'apparait pas alors que vous venez de mettre à jour la librairie, pensez à récupérer la [dernière version de la feuille de calcul](https://goo.gl/Awjw5f).*
123 |
124 |
125 |
126 |
127 |
128 | Un problème ?
129 | --------------
130 | **Avant de vous inquiéter :**
131 | 1. vérifiez que votre **adresse email est bien renseignée** et qu'elle ne contient pas de caractères spéciaux (oui, même le +...)
132 | 2. vérifiez que votre **[version est bien à jour](#obtenir-la-dernière-mise-à-jour)** (et n'oubliez pas de cliquer sur enregistrer lors du changement)
133 | 3. si ça ne fonctionne toujours pas, et que vous ne savez pas pourquoi, tentez une **[réinstallation complète](#pour-commencer)**
134 | 4. si le problème n'est pas déjà signalé, je vous invite à **[créer une issue](https://github.com/maximelebreton/alertes-leboncoin/issues)**
135 |
136 |
137 | Pourquoi cette version, et quelle différence avec les autres ?
138 | -----------------
139 |
140 | J'explique les raisons de cette version ici :
141 | #### > [Vision et évolutions futures](https://github.com/maximelebreton/alertes-leboncoin/issues/2)
142 |
143 | **TL DR;**
144 | * refonte totale du code
145 | * intégration de [cheerio](https://github.com/3846masa/cheerio-gasify) (équivalent de jquery côté serveur)
146 | * **mise à jour semi-automatique du code** (`Outils > Editeur de scripts`, puis `Ressources > Bibliothèques` pour choisir la version)
147 | * ajout de paramètres utilisateur
148 | * ajout d'une mini carte pour localiser rapidement l'annonce (`showMap`)
149 | * possibilité de choisir l'envoi des résultats en mails individuels ou en mail groupé (`groupedResults`)
150 | * Markup HTML externalisé dans des fichiers `.html` gérés par [HTML service](https://developers.google.com/apps-script/guides/html/templates)
151 |
152 |
153 | CHANGELOG
154 | -------------
155 | Le détail des modifications se trouve dans le **[CHANGELOG](https://github.com/maximelebreton/alertes-leboncoin/blob/master/CHANGELOG.md)**
156 |
157 | version originale par http://justdocsit.blogspot.fr
158 | repris depuis la version `4.0.0` par [mlb](http://www.maximelebreton.com)
159 |
160 | Clé projet de la bilbiothèque : `M9iNq7X9ZWxS_D7pHmMGBb6YoFnfw0_Hk`
161 | Code de la bibliothèque : [script.google.com/...](https://script.google.com/d/15GE-TW-COB9rfq49nF38GDqytbwpK2HMxLQOzdC1JZMGkUCfLqWoG0T4/edit?usp=drive_web)
162 |
163 | _____________________________
164 |
165 |
166 | [Laisser un commentaire](http://maximelebreton.github.io/alertes-leboncoin/#disqus_thread)
167 | -------------
168 |
169 |
170 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7Y34RFD6WYVRA)
171 |
--------------------------------------------------------------------------------
/src/ads.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ------------------ *
3 | * ADS
4 | * ------------------ *
5 | */
6 |
7 | /**
8 | * Get ads from url
9 | */
10 | function getHtmlContentFromUrl( url ) {
11 |
12 | var htmlContent = {};
13 |
14 | // Cache is only necessary when debugging with large datasets
15 | if ( getParam('useCache') ) {
16 | var cachedUrlContent = getCachedContent( url );
17 | if ( cachedUrlContent ) {
18 | htmlContent = JSON.parse( cachedUrlContent );
19 | getSpreadsheetContext().toast('Récupération du cache');
20 | }
21 | }
22 | if ( ( getParam('useCache') && !cachedUrlContent) || !getParam('useCache') ) {
23 |
24 | var html = getUrlContent( url );
25 |
26 | htmlContent = {
27 | main: getMainHtml( html ),
28 | json: getJSONDataHtml( html ),
29 | tags: getTagDataHtml( html )
30 | };
31 |
32 | }
33 | if ( getParam('useCache') && !cachedUrlContent ) {
34 | setCache( url, JSON.stringify( htmlContent ));
35 | getSpreadsheetContext().toast('Mise en cache');
36 | }
37 |
38 | return htmlContent;
39 | }
40 |
41 |
42 | /**
43 | * Get Main Html
44 | */
45 | function getMainHtml( html ) {
46 | var mainHtml = extractHtml(html, getParam('selectors').mainStartTag, getParam('selectors').mainEndTag, getParam('selectors').mainStartTag.length );
47 |
48 | return mainHtml;
49 | }
50 |
51 |
52 | /**
53 | * Get Tag Data Html
54 | */
55 | function getTagDataHtml( html ) {
56 | var startTag = 'var utag_data = '
57 | var endTag = '';
58 |
59 | var tagDataHtml = extractHtml(html, startTag, endTag, 0, endTag.length).trim();
60 |
61 | return tagDataHtml;
62 | }
63 |
64 | /**
65 | * Get JSON Data Html
66 | */
67 | function getJSONDataHtml( html ) {
68 | var startTag = 'window.FLUX_STATE = '
69 | var endTag = '';
70 |
71 | var JSONDataHtml = extractHtml(html, startTag, endTag, startTag.length, 0).trim();
72 |
73 | return JSONDataHtml;
74 | }
75 |
76 |
77 | /**
78 | * Get Main JSDOM
79 | */
80 | function getMainJSDOM( html ) {
81 | var $main = $( getParam('selectors').adItem, getParam('selectors').adContext, html );
82 | //var $main = $('li[itemtype="http://schema.org/Offer"]', getParam('selectors').adContext, html); // reference
83 | //var $main = $('.mainList ul > li', getParam('selectors').adContext, html); // same
84 | //var $main = $('.mainList li[itemtype="http://schema.org/Offer"]', getParam('selectors').adContext, html); // same
85 | //var $main = $('.mainList', getParam('selectors').adContext, html).find('li'); // slower
86 | //var $main = $('#listingAds .mainList ul > li', html); //
87 |
88 | return $main;
89 | }
90 |
91 |
92 | /**
93 | * Get tags from html
94 | */
95 | function getTagDataFromHtml( html ) {
96 | var tagData = {};
97 |
98 | try {
99 | var separator = ' : ';
100 | var wantedTags = ['prixmin', 'prixmax', 'anneemin', 'anneemax', 'subcat', 'cat', 'region', 'departement', 'kmmin', 'kmmax', 'cp', 'city'];
101 |
102 | for (var i = 0; i < wantedTags.length; i++ ) {
103 | var tagLabel = wantedTags[i];
104 | var tagValue = extractHtml(html, tagLabel, ',', tagLabel.length + separator.length);
105 |
106 | if (tagValue) {
107 | tagData[tagLabel] = tagValue;
108 | }
109 | }
110 | } catch (e) {
111 | console.error("Parsing error:", e);
112 | }
113 |
114 | return tagData;
115 | }
116 |
117 |
118 | function getIdFromUrl(url) {
119 |
120 | var regex = /\/(\d+)[\.\/\?]/i;
121 | var matches = url.match(regex);
122 | var id = matches ? matches[1] : matches;
123 |
124 | return id;
125 | }
126 |
127 | function getRelativeUrl(url) {
128 |
129 | var regex = /(.+leboncoin\.fr)(.+)/i;
130 | var matches = url.match(regex);
131 | var relativeUrl = matches ? matches[2] : url;
132 |
133 | return relativeUrl;
134 | }
135 |
136 |
137 | function getListingAdsFromJSON( ads, userLabel ) {
138 |
139 | return ads.map( function( ad ) {
140 |
141 |
142 |
143 | return {
144 | id: ad.list_id,
145 | title: ad.subject,
146 | textPrice: ad.price && ad.price.length ? formatPrice( ad.price[0] ) + ' €' : '',
147 | price: ad.price && ad.price.length ? ad.price[0] : undefined,
148 | userLabel: userLabel,
149 | textPlace: ad.location.city_label,
150 | textDateTime: dayjs( ad.index_date ).format('dddd DD MMMM, HH:hh'),
151 | isPro: ad.owner.type == "pro" ? true : false,
152 | url: ad.url,
153 | img: {
154 | src: ad.images && ad.images.urls && ad.images.urls.length ? ad.images.urls[0] : undefined
155 | },
156 | //timestamp: getDateWithIdMilliseconds(new Date(ad.index_date), ad.list_id).getTime(),
157 | timestamp: getDateObjectFromString( ad.index_date ).getTime(),
158 | shortUrl: ad.url,
159 | isDuplicateOf: []
160 | }
161 | });
162 |
163 | }
164 |
165 |
166 | /**
167 | * Get listing ads data
168 | * @returns {Object} Returns data of the listing ads
169 | */
170 | function getListingAdsFromHtml( mainHtml, tagsHtml, userLabel ) {
171 |
172 | var ads = [];
173 | var protocol = 'https:';
174 |
175 | var $selector = getMainJSDOM( mainHtml );
176 |
177 | //var tags = getTagDataFromHtml( tagsHtml );
178 | //var searchCategorySlug = JSON.parse( tags.subcat );
179 |
180 | // liste des annonces
181 | $selector.each(function(i, element) {
182 |
183 | var $this = $(this);
184 |
185 | var $a = $this.find('a');
186 | var $item_supp = $this.find('.item_supp');
187 |
188 | var $title = $this.find('[itemprop="name"]') || $this.find('.item_title');
189 | var $price = $this.find('[itemprop="price"]') || $this.find('.item_price');
190 | var $category = $this.find('[itemprop="category"]') || $item_supp.eq( 0 );
191 | var $place = $this.find('[itemprop="availableAtOrFrom"]') || $item_supp.eq( 1 );
192 | var $img = $this.find('.item_image').find('.lazyload') || $this.find('[itemprop="image"]');
193 | var $date = $this.find('[itemprop="availabilityStarts"]') || $item_supp.eq( 2 );
194 | var isPro = $this.find('.ispro').length ? true : false;
195 |
196 | //var categorySlug = (searchCategorySlug == "toutes_categories") ? slug( $category.attr('content') ) : searchCategorySlug;
197 |
198 | var ad = {
199 | //id: Number($a.data( 'info' ).ad_listid),
200 | id: Number( getIdFromUrl($a.attr('href')) ),
201 | title: cleanText( $title.text() ),
202 | textPrice: Number( parseFloat( $price.text().replace(/\s/g, '') ) ).toLocaleString() + ' €',
203 | price: Number( parseFloat( $price.text().replace(/\s/g, '') ) ),
204 | //categorySlug: categorySlug,
205 | userLabel: userLabel,
206 | textPlace: cleanText( $place.text() ),
207 | textDateTime: cleanText( $date.text() ),
208 | textDate: String( $date.attr('content') ),
209 | isPro: isPro,
210 | url: 'https://www.leboncoin.fr' + getRelativeUrl( $a.attr('href') ),
211 | img: {
212 | src: addProtocol( $img.data('imgsrc') ) || $img.attr('content')
213 | }
214 | };
215 |
216 | // A real Date Object with milliseconds based on Ad Id to prevent conflicts
217 | ad.timestamp = getAdDateTime( ad.textDateTime, ad.id ).getTime();
218 |
219 | ad.shortUrl = 'https://leboncoin.fr/vi/' + ad.id;
220 | ad.isDuplicateOf = []; // for later use
221 |
222 | ads.push(ad);
223 |
224 | });
225 |
226 | return ads;
227 | }
228 |
229 |
230 | /**
231 | * Get data before Id
232 | */
233 | function getAdsBeforeId(ads, stopId) {
234 |
235 | var reducedAds = [];
236 |
237 | var stopIndex = ads.map(function(ad) {
238 | return ad.id;
239 | }).indexOf(stopId);
240 |
241 | reducedAds = ads.slice( 0, stopIndex );
242 |
243 | return reducedAds;
244 | }
245 |
246 |
247 | /**
248 | * Get Ads before time
249 | */
250 | function getAdsBeforeTime(ads, lastTime) {
251 |
252 | var reducedAds = [];
253 |
254 | ads.map(function(ad) {
255 |
256 | if (ad.timestamp > lastTime) {
257 | reducedAds.push( ad );
258 | }
259 | });
260 |
261 | return reducedAds;
262 | }
263 |
264 |
265 | /**
266 | * Get latest ads (based on stored value)
267 | */
268 | function getLatestAds(ads, latestAdValue) {
269 |
270 | var latestAds = [];
271 |
272 | var latestAdStoredTimestamp = null;
273 | if (typeof latestAdValue.getTime === 'function') {
274 | latestAdStoredTimestamp = latestAdValue.getTime();
275 | }
276 |
277 | var latestAdTimestamp = ads[0].timestamp;
278 |
279 | if (latestAdTimestamp !== latestAdStoredTimestamp) {
280 |
281 | if (latestAdStoredTimestamp) {
282 | //log('TIMESTAMP');
283 | latestAds = getAdsBeforeTime(ads, latestAdStoredTimestamp);
284 |
285 | } else if( Number(latestAdValue) !== 0 ) {
286 | //log('ID');
287 | latestAds = getAdsBeforeId(ads, Number(latestAdValue) ); // deprecated, replaced by getDataBeforeTime
288 |
289 | } else {
290 | //log('ALL');
291 | latestAds = ads;
292 | }
293 | }
294 |
295 | var latestAdsSorted = latestAds.sort( dynamicSort("-timestamp") );
296 |
297 | return latestAdsSorted;
298 | }
299 |
300 |
301 | /**
302 | * Filter Ads
303 | */
304 | function filterAds(ads, singleParams) {
305 |
306 | var filteredAds = ads;
307 |
308 | if (singleParams.minPrice || singleParams.maxPrice) {
309 | var minPrice = singleParams.minPrice || undefined;
310 | var maxPrice = singleParams.maxPrice || undefined;
311 |
312 | filteredAds = getAdsBetweenPrice(ads, minPrice, maxPrice)
313 | }
314 |
315 | return filteredAds;
316 |
317 | }
318 |
319 |
320 | /**
321 | * Get Ads between price
322 | */
323 | function getAdsBetweenPrice(ads, minPrice, maxPrice) {
324 |
325 | var filteredAds = [];
326 |
327 | ads.map(function(ad) {
328 |
329 | if ( ad.price && ((minPrice && ad.price < minPrice) || (maxPrice && ad.price > maxPrice) )) {
330 | // sorry ad !
331 | } else {
332 | filteredAds.push( ad );
333 | }
334 |
335 | });
336 |
337 | return filteredAds;
338 | }
339 |
340 |
341 | /**
342 | * Get Ad Date Time (with adId param to generate milliseconds)
343 | */
344 | var getAdDateTime = function(adTextDateTime, adId) {
345 |
346 | // Date is now
347 | var d = new Date();
348 | // Reset seconds and milliseconds because of Ad Id magic trick
349 | d.setSeconds(0);
350 | d.setMilliseconds(0);
351 |
352 | /*var dateSplit = adTextDate.split('-');
353 | var year = Number(dateSplit[0]);
354 | var month = Number(dateSplit[1]) - 1; // because months = 0-11
355 | var day = Number(dateSplit[2]);*/
356 |
357 | var dateTimeSeparator = adTextDateTime.indexOf(',');
358 | var dateString = adTextDateTime.substring(0, dateTimeSeparator).trim().toLowerCase();
359 | var timeString = adTextDateTime.substring(dateTimeSeparator + 1).trim();
360 | var timeSeparator = timeString.indexOf(":");
361 | var dateSeparator = dateString.indexOf(" ");
362 |
363 |
364 | // Month, Day
365 | var month;
366 | var day;
367 | switch( dateString ) {
368 | case "aujourd'hui":
369 | var today = d;
370 | month = today.getMonth();
371 | day = today.getDate();
372 | break;
373 | case "hier":
374 | var yesterday = new Date( d.setDate(d.getDate() - 1) );
375 | month = yesterday.getMonth();
376 | day = yesterday.getDate();
377 | break;
378 | default:
379 | var monthString = dateString.substring(dateSeparator + 1);
380 | var dayString = dateString.substring(0, dateSeparator);
381 | month = getMonthNumber( monthString );
382 | day = Number( dayString );
383 | }
384 |
385 | // Hours, minutes
386 | var hours = Number(timeString.substring(0, timeSeparator));
387 | var minutes = Number(timeString.substring(timeSeparator + 1));
388 |
389 | // Milliseconds based on Ad Id (magic trick)
390 | var milliseconds = getMillisecondsByMagic( adId );
391 |
392 | //d.setYear( year ); // TODO: find a way to prevent year changes (december->january)
393 | d.setMonth( month );
394 | d.setDate( day );
395 | d.setHours( hours );
396 | d.setMinutes( minutes );
397 | d.setMilliseconds( milliseconds );
398 |
399 | var date;
400 |
401 | if ( typeof d.getMonth === 'function' ) {
402 | date = d;
403 | }
404 |
405 | //log(date);
406 | return date;
407 | }
408 |
409 | function getDateWithIdMilliseconds(date, adId) {
410 |
411 | var d = new Date(date);
412 | var milliseconds = getMillisecondsByMagic( adId );
413 | d.setMilliseconds( milliseconds )
414 |
415 | return d;
416 |
417 | }
418 |
419 |
420 | /**
421 | * Get month number
422 | * DEPRECATED since leboncoin use microdata for date
423 | */
424 | function getMonthNumber(month) {
425 |
426 | var months = ["jan", "fév", "mars", "avr", "mai", "juin", "juil", "août", "sept", "oct", "nov", "déc"];
427 | var fullMonths = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"];
428 |
429 | var monthNumber = months.indexOf( month );
430 | var fullMonthNumber = fullMonths.indexOf( month );
431 |
432 | var number = (monthNumber >= 0) ? monthNumber : fullMonthNumber;
433 |
434 | return number;
435 | }
436 |
437 |
438 | /**
439 | * Get last digits
440 | */
441 | function getLastDigits(number, count) {
442 |
443 | var stringNumber = number.toString();
444 | var length = stringNumber.length;
445 | var lastDigits = Number( stringNumber.slice(length-count, length) );
446 |
447 | return lastDigits;
448 | }
449 |
450 |
451 | /**
452 | * Get milliseconds by magic
453 | */
454 | function getMillisecondsByMagic(id) {
455 |
456 | var secondInMilliseconds = 60000-1;
457 | var idInMilliseconds = getLastDigits(id,4); // fake, but that's the trick (needs 10000 consecutive ads with same dateTime to fail...)
458 | var milliseconds = secondInMilliseconds - idInMilliseconds;
459 |
460 | return milliseconds;
461 | }
462 |
463 |
464 |
465 |
466 |
--------------------------------------------------------------------------------
/src/Code.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @OnlyCurrentDoc
3 | */
4 |
5 | var cheerio = cheeriogasify.require('cheerio');
6 | var $ = cheerio;
7 |
8 | var version = "5.4.5";
9 |
10 | var defaults = {
11 | debug: false,
12 | showMap: false,
13 | mapZoom: 7,
14 | showTags: false,
15 | groupedResults: true,
16 | splitSendByCategory: true,
17 | startIndex: 2,
18 | plainText: false,
19 | sendMail: true,
20 | maxSmsSendByResult: 3,
21 | showMailEditLink: true,
22 | useCache: false,
23 | cacheTime: 1500, // in seconds
24 | muteHttpExceptions: true,
25 | dateFormat: {
26 | human: 'd MMMM, HH:mm',
27 | iso: 'YYYY-MM-DDTHH:mm:ss.sssZ'
28 | },
29 | colors: {
30 | background: {
31 | working: '#ECEFF1',
32 | success: '#DCEDC8',
33 | warning: '#FF0000'
34 | },
35 | border: {
36 | working: '#B0BEC5'
37 | }
38 | },
39 | names: {
40 | sheet: {
41 | main: 'Vos alertes',
42 | variables: 'Paramètres utilisateur',
43 | debug: 'debug'
44 | },
45 | range: {
46 | label: 'labelRange',
47 | url: 'urlRange',
48 | lastAd: 'lastAdRange',
49 | userVarNames: 'userVarNamesRange',
50 | userVarValues: 'userVarValuesRange',
51 | advancedOptions: 'advancedOptionsRange',
52 | advancedMenu: 'advancedMenuRange'
53 | },
54 | mail: {
55 | anchorPrefix: 'part-'
56 | }
57 | },
58 | selectors: {
59 | adItem: 'li[itemtype="http://schema.org/Offer"]', // old : '.mainList ul > li'
60 | adsContext: '#listingAds',
61 | mainStartTag: ''
63 | },
64 | freeUser: undefined,
65 | freePass: undefined
66 | };
67 |
68 | /*var defaults = {
69 | debug: false,
70 | showMap: false,
71 | mapZoom: 7,
72 | showTags: false,
73 | groupedResults: true,
74 | startIndex: 2,
75 | plainText: false,
76 | sendMail: true,
77 | maxSmsSendByResult: 3,
78 | showMailEditLink: true,
79 | useCache: false,
80 | cacheTime: 1500, // in seconds
81 | muteHttpExceptions: true,
82 | humanDateFormat: 'd MMMM, HH:mm',
83 | isoDateFormat: 'YYYY-MM-DDTHH:mm:ss.sssZ',
84 | workingBackgroundColor: '#ECEFF1',
85 | successBackgroundColor: '#DCEDC8',
86 | warningBackgroundColor: '#FF0000',
87 | workingBorderColor: '#B0BEC5',
88 | mainSheetName: 'Vos alertes',
89 | variablesSheetName: 'Paramètres utilisateur',
90 | debugSheetName: 'debug',
91 | labelRangeName: 'labelRange',
92 | urlRangeName: 'urlRange',
93 | lastAdRangeName: 'lastAdRange',
94 | userVarKeysRangeName: 'userVarKeysNamesRange',
95 | userVarValuesRangeName: 'userVarValuesRange',
96 | advancedOptionsRangeName: 'advancedOptionsRange',
97 | advancedMenuRangeName: 'advancedMenuRange',
98 | anchorPrefixMailName: 'part-',
99 | adItemSelector: 'li[itemtype="http://schema.org/Offer"]', // old : '.mainList ul > li'
100 | adsContextSelector: '#listingAds',
101 | mainStartTagSelector: '',
103 | freeUser: undefined,
104 | freePass: undefined
105 | };*/
106 |
107 | var normalizedData = {
108 | result: [],
109 | entities: {},
110 | update: false,
111 | sheetUrl: ''
112 | };
113 |
114 | // PARAMS global variable
115 | // todo : refactor with https://developers.google.com/apps-script/guides/properties ?
116 | var params;
117 |
118 |
119 | /*
120 | a tester :
121 | config = Object.assign({
122 | title: 'Foo',
123 | body: 'Bar',
124 | buttonText: 'Baz',
125 | cancellable: true
126 | }, config);*/
127 |
128 | /**
129 | * Init
130 | */
131 | function init(userParams, e) {
132 |
133 | params = getParams(defaults, userParams);
134 |
135 |
136 |
137 | createMenu();
138 |
139 | //checkCheck();
140 | /**/
141 |
142 | var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);
143 | status = authInfo.getAuthorizationStatus();
144 | url = authInfo.getAuthorizationUrl();
145 |
146 | //checkMainTrigger();
147 |
148 | }
149 |
150 | function getParam( param ) {
151 | return JSON.parse( PropertiesService.getDocumentProperties().getProperty( param ) );
152 | }
153 |
154 | function checkCheck() {
155 |
156 | var sheet = getSheetByName( getParam('names').sheet.main );
157 | var range = getRangeByName( getParam('names').range.advancedMenu );
158 | var col = range.getColumn();
159 |
160 |
161 | var row = getFirstEmptyRow( getRangeByName( getParam('names').range.url ) );
162 |
163 | var cell = sheet.getRange( 2, col, row-1);
164 |
165 | cell.clear();
166 |
167 | var rule = SpreadsheetApp.newDataValidation()
168 | .requireValueInList(['Modifier les paramètres avancés'], true)
169 | .setAllowInvalid(true)
170 | .setHelpText('')
171 | .build();
172 |
173 | cell.setDataValidation(rule);
174 |
175 | }
176 |
177 | function getFirstEmptyRow( range ) {
178 |
179 | var values = range.getValues(); // get all data in one call
180 | var ct = 0;
181 | while ( values[ct][0] != "" ) {
182 | ct++;
183 | }
184 | return (ct);
185 | }
186 |
187 | /**
188 | * Start, everything start from here
189 | */
190 | function start(userParams) {
191 |
192 | // Start counting execution time
193 | var runtimeCountStart = new Date();
194 |
195 | //setParams(userParams);
196 | //params = params || getParams(defaults, userParams);
197 | getParams(defaults, userParams);
198 |
199 | if ( !isRecipientEmail( ) ) {
200 | setActiveSelectionOnEmail();
201 | showDialog("Oups !", "Merci de remplir le champ email");
202 | return;
203 | }
204 |
205 | var labelRangeValues = getSpreadsheetContext().getRangeByName( getParam('names').range.label ).getValues();
206 | var urlRangeValues = getSpreadsheetContext().getRangeByName( getParam('names').range.url ).getValues();
207 | var lastAdRangeValues = getSpreadsheetContext().getRangeByName( getParam('names').range.lastAd ).getValues();
208 | var advancedOptionsRangeValues = getSpreadsheetContext().getRangeByName( getParam('names').range.advancedOptions ).getValues();
209 |
210 | // For each value in the url range sheet
211 | forEachValue( urlRangeValues, getParam('startIndex'), function(index) {
212 |
213 | var rangeNames = getParam('names').range;
214 | var sheetNames = getParam('names').sheet;
215 |
216 | var arrayIndex = getArrayIndex(index, getParam('startIndex') ); // Because sheet index and array index aren't the same
217 |
218 | var lastRangeName = getParam('isAvailable').advancedOptions ? rangeNames.advancedOptions : rangeNames.lastAd;
219 | var row = getRowByIndex(index, lastRangeName, sheetNames.main);
220 |
221 | highlightRow(row);
222 |
223 | var label = labelRangeValues[arrayIndex][0]; // String expected
224 | var url = urlRangeValues[arrayIndex][0]; // String URL expected
225 | //var category = getCategoryFromHtml();
226 | var htmlContent = getHtmlContentFromUrl( url );
227 |
228 | var ads;
229 |
230 | if (htmlContent.json.length) {
231 | var parsed = JSON.parse( htmlContent.json )
232 | var JSONAds = parsed.adSearch.data.ads;
233 | ads = getListingAdsFromJSON( JSONAds, label )
234 |
235 | } else {
236 | ads = getListingAdsFromHtml( htmlContent.main, htmlContent.tags, label );
237 | }
238 |
239 |
240 | var stringifiedSingleParams = getParam('isAvailable').advancedOptions ? advancedOptionsRangeValues[arrayIndex][0] : "";
241 | var singleParams = stringifiedSingleParams.length ? JSON.parse(stringifiedSingleParams) : {};
242 |
243 | if (singleParams.pause !== true && ads.length) {
244 |
245 | var lastAdSentDate = lastAdRangeValues[arrayIndex][0]; // Date Object expected
246 | var unsentAds = getLatestAds(ads, lastAdSentDate);
247 | var adsToSend = filterAds(unsentAds, singleParams);
248 |
249 | getSpreadsheetContext().toast(adsToSend.length + " annonce(s) à envoyer", 'Information');
250 |
251 | if (adsToSend.length) {
252 |
253 | var lastAdToSendDate = adsToSend[0].timestamp;
254 |
255 | // not ready (experimental)
256 | //var tags = getTagsFromHtml( html );
257 |
258 | var datum = {
259 | index: index,
260 | label: label,
261 | url: url,
262 | adsToSend: adsToSend,
263 | singleParams: singleParams,
264 | tags: '',
265 | lastAdSentDate: lastAdSentDate
266 | //readyToSend: isItTimeToSend( singleParams )
267 | }
268 |
269 | normalizedData = getNormalizedData( normalizedData, datum );
270 |
271 | }
272 | }
273 |
274 | unhighlightRow(row);
275 |
276 | });
277 |
278 |
279 | var update = checkForUpdates();
280 | if ( update ) {
281 | normalizedData.update = update;
282 | }
283 |
284 | // sheet url
285 | normalizedData.sheetUrl = getSpreadsheetContext().getUrl();
286 |
287 |
288 | // Stop counting execution time
289 | var runtimeInMilliseconds = runtimeCountStop(runtimeCountStart);
290 | var runtimeInSeconds = (runtimeInMilliseconds / 1000) % 60;
291 | normalizedData.runtimeInSeconds = runtimeInSeconds.toFixed(2);
292 | //getSpreadsheetContext().toast("Temps d'execution : " + runtimeInSeconds + " secondes", 'Information');
293 |
294 |
295 | // user custom callback
296 | if ( getParam('onDataResult') ) {
297 | //getParam('onDataResult')(normalizedData.result, normalizedData.entities);
298 | }
299 |
300 | //normalizedData.result.length
301 | var saveResult = [];
302 |
303 | if ( normalizedData.result.length ) {
304 |
305 | var data = getEnhancedData( normalizedData ); // yep
306 |
307 | handleSendData( normalizedData, function(error, callbackRecipient, callbackData, callbackResult) {
308 |
309 | if (error) {
310 | if (error.name == 'Exception') {
311 | getSpreadsheetContext().toast("Erreur lors de l'envoi à " + callbackRecipient, 'Alertes LeBonCoin');
312 | Logger.log( error.message );
313 | }
314 | } else {
315 | getSpreadsheetContext().toast("Annonces envoyées à " + callbackRecipient, 'Alertes LeBonCoin');
316 |
317 | // because it needs to be based on ads and not searches
318 | callbackResult.map( function( id ) {
319 |
320 | var adsSent = callbackData.entities.ads[ id ].toSend;
321 | adsSent.map( function( ad ) {
322 |
323 | if ( saveResult.indexOf( ad.sheetIndex ) > -1 ) {
324 | // ok nothing to do
325 | } else {
326 | saveResult.push( ad.sheetIndex );
327 | }
328 |
329 | })
330 | })
331 |
332 | }
333 | });
334 |
335 | if ( getParam('keepHistory') !== false ) {
336 | //forEachResult( saveResult, dataOk.entities, setLatestAdRangeValue );
337 | onDataSend( saveResult, normalizedData.entities ); // faster, but buggy display...
338 | }
339 |
340 | }
341 |
342 |
343 | if ( !normalizedData.result.length ) {
344 | //getSpreadsheetContext().toast("Aucune annonce à envoyer");
345 | }
346 |
347 | }
348 |
349 | /**
350 | * For each result
351 | */
352 | function onDataSend( result, entities ) {
353 |
354 | var lastAdValues = getSpreadsheetContext().getRangeByName( getParam('names').range.lastAd ).getValues();
355 | var lastAdRange = getDataSheetContext().getRange( 1, getColumnByName( getParam('names').range.lastAd ), lastAdValues.length, 1 );
356 |
357 | var numberFormats = lastAdRange.getNumberFormats();
358 | var backgroundColors = lastAdRange.getBackgrounds();
359 |
360 | result.map( function( id ) {
361 | var sheetIndex = id;
362 | var arrayIndex = sheetIndex - 1; // because 1 is 0 in array's
363 | var lastAd = entities.ads[ sheetIndex ].toSend[0];
364 |
365 | if (lastAd) {
366 | lastAdValues[arrayIndex] = [ new Date( lastAd.timestamp ) ];
367 | numberFormats[arrayIndex] = [ getParam('dateFormat').human];
368 | backgroundColors[arrayIndex] = [ getParam('colors').background.success ];
369 | }
370 | })
371 |
372 | lastAdRange.setValues( lastAdValues );
373 | lastAdRange.setNumberFormats( numberFormats );
374 | lastAdRange.setBackgrounds( backgroundColors );
375 | }
376 |
377 |
378 | /**
379 | * For each result
380 | */
381 | function forEachResult( result, entities, callback ) {
382 |
383 | for (var i = 0; i < result.length; i++ ) {
384 | var index = result[i];
385 |
386 | if (callback && typeof(callback) === "function") {
387 | callback(index, entities);
388 | }
389 |
390 | }
391 | }
392 |
393 |
394 | /**
395 | * Set latest Ad value
396 | */
397 | function setLatestAdRangeValue(index, entities) {
398 |
399 | var latestAdDate = new Date( entities.ads[index].toSend[0].timestamp );
400 | var latestAdRange = getDataSheetContext().getRange( index, getColumnByName( getParam('names').range.lastAd ) );
401 |
402 | latestAdRange.setValue( latestAdDate );
403 | latestAdRange.setNumberFormat( getParam('dateFormat').human );
404 | latestAdRange.setBackground( getParam('colors').background.success );
405 | }
406 |
407 |
408 | /**
409 | * Check if recipient email is defined
410 | */
411 | function isRecipientEmail( callbackString ) {
412 |
413 | var email = getRecipientEmail();
414 |
415 | if (!email.length) {
416 | return false;
417 | }
418 |
419 | return true;
420 | }
421 |
422 |
423 | /**
424 | * Check for updates
425 | */
426 | function checkForUpdates() {
427 |
428 | var update = false;
429 | var url = "https://raw.githubusercontent.com/maximelebreton/alertes-leboncoin/master/version.json";
430 |
431 | try {
432 | var response = UrlFetchApp.fetch(url);
433 | var data = JSON.parse(response.getContentText());
434 |
435 | if ( versionCompare( data.version, version ) == 1) {
436 |
437 | update = data;
438 | }
439 | } catch(e) {
440 | // handle error
441 | }
442 |
443 | return update;
444 | }
445 |
--------------------------------------------------------------------------------