├── 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 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/smsAds.tpl.html: -------------------------------------------------------------------------------- 1 | 1 ? 'x' : ''; 3 | var pluralS = ads.length > 1 ? 's' : ''; 4 | ?> 5 | nouveau résultat pour : -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/mail__preheader.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 27 | 28 |
-------------------------------------------------------------------------------- /src/wizardDialog.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/mailText.tpl.html: -------------------------------------------------------------------------------- 1 | 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 | 17 | 18 | 19 | 26 | - 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 44 | 45 | 46 | alertes-leboncoin version : http://maximelebreton.github.io/alertes-leboncoin 47 | 48 | Éditer mes alertes : -------------------------------------------------------------------------------- /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 |
29 |
30 | 31 |
32 |
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 | 23 | 24 | 25 | 26 | 27 | 28 |

Le résultat pour a été masqué pour éviter les doublons

29 | 30 | 31 | 32 |

33 | () 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |

44 | 45 | 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 |
61 |
62 | 63 |
64 |
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 | 37 | 38 | 1) { ?> 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | Une nouvelle version est disponible :
47 | | 48 |
49 |
50 | 51 | 52 | 53 |
54 | 55 | ✂ avec ♥ par mlb
(idée originale Just docs it) 56 |
57 | 58 | Éditer mes alertes 59 | 60 | 61 | alertes-leboncoin version  62 | 63 |
64 | 65 | 66 |
Temps d'exécution : secondes
67 | 68 | 69 | -------------------------------------------------------------------------------- /src/mail__ads.tpl.html: -------------------------------------------------------------------------------- 1 | 9 | 14 | 21 | 22 |
  • L'annonce "" a été masquée car elle apparait déjà dans ""

  • 23 | 24 | 29 | 30 |
  • 31 |
    32 | 33 |
    34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
    43 | 44 | 45 |
    46 |
    47 | 48 |
    49 |
    50 | 51 | 52 |
    53 |
    54 | 55 | // 56 | 57 | 58 | pro 59 | 60 |
    61 |
    62 | 63 |
    64 |
    65 | 66 |
    67 |
    68 | 69 |
    70 |
    71 | 72 | 73 |
    74 |
  • 75 | 76 | 81 | -------------------------------------------------------------------------------- /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 [![GitHub watchers](https://img.shields.io/github/stars/maximelebreton/alertes-leboncoin.svg?style=social&label=Star)](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 | [![Faire un don](https://www.paypalobjects.com/fr_FR/FR/i/btn/btn_donate_LG.gif)](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 | --------------------------------------------------------------------------------