חזרה

אפליקציית NodeJS לאוטומציה של העדכונים בבלוג

בשימוש ב-webhook המובנה של ghost כתבתי אפליקציה לניהול של כל העדכונים על פוסט חדש באתר. בצורה אוטומטית ללא מגע יד אדם 😎 >>>


אם שמתם לב - יש כאן כמה אפשרויות להתעדכן על פוסטים חדשים באתר - מייל, פוש (התראות דפדפן) וגם ערוץ קטן בטלגרם.
בהתחלה עשיתי הכל באופן ידני יחסית (תכל'ס לא נכתב כאן פוסט בכל רגע).
את העדכונים בפוש (דרך oneSignal - מערכת מעולה) הוצאתי דרך האתר שלהם, עדכונים בטלגרם כתבתי בעצמי בערוץ, ואת המיילים שלחתי דרך mailgun בצורה אוטומטית (ולא מעוצבת או תומכת RTL) על ידי ה-API של המערכת בלוגינג - ghost. מה שמפעיל את האתר הזה.

זה כמובן יעיל לעבוד ככה, ולא מסובך, אבל לא מקצועי 😉 וגם לא מגניב מספיק.. אז החלטתי לכתוב לזה אפליקציה אחת שתהיה אחראית על כל העדכונים. הלכתי על nodeJS (שאגב היא גם הפלטפורמה ש-ghost רץ עליה) שהכרתי בחודשים האחרונים ואני נהנה ממנה בכל יום מחדש. אפליקציה פשוטה מבוססת express עם עוד שתי חבילות לצורך הבקשות וניתוח ה-body של בקשות נכנסות (fetch + bodyParser) עושה את העבודה מעולה!
האפליקציה רצה (כמו הבלוג הזה) על הרוקו heroku כך שזה חינמי לחלוטין.. בואו נתחיל:

WEBHOOK

מה זה webhook?

וֶובּהוּק זה בעצם דרך (או כמה דרכים) של תוכנה (כגון אתר אינטרנט) לתקשר ולשלוח מידע לתוכנות\אתרים אחרים - בעיקר בזמן-אמת. כאשר מתרחש אירוע מסוים שמוגדר עליו webhook - התכנה תשלח מידע מפורט אודות האירוע למיקום שהוגדר

כדי לשלוח את המידע בצורה אוטומטית אני משתמש באפשרויות ה-webhook המובנות של ghost. כמו שרואים ברשימה שם - יש webhook ל-post.published שזה אומר שכאשר מתפרסם פוסט באתר - כל המידע אודות האירוע של פרסום הפוסט יישלח לכתובת שהוגדרה ל-webhook.

יש הרבה כלים לבדיקת webhooks ולראות מראש איזה מין מידע מתקבל מה-webhook וכך להיערך בבניית התכנה שתקבל את המידע ותנתח אותו.
אני השתמשתי בזה Hookbin - פשוט לוחצים על create new endpoint ומקבלים כתובת אליה מגדירים את ה-webhook, כל המידע שנדחף לכתובת הזו נגיש בצורה נוחה שם באתר וכך אפשר לבנות את האפליקציה בצורה נוחה כי יודעים מראש לאיזה מין מידע (JSON) להיערך.

מבנה האפליקציה

האפליקציה צריכה לעשות (בשלב זה) את הדברים הבאים:

  • לקבל את התוכן מה-webhook, לנתח את ה-body - הגוף של בקשת ה-HTTP - שמכיל את כל המידע בפורמט JSON.
    להכניס את המידע הנ"ל למשתנים על מנת להשתמש בו בפונקציות חיצוניות.
  • שליחת העדכונים:
  1. שליחת פושים דרך ה-API של oneSignal
  2. שליחת מיילים לרשימת תפוצה באנשי קשר של גוגל דרך גוגל סקריפט
  3. יצירת הודעה בערוץ הטלגרם דרך בוט (דרך קריאת HTTP פשוטה ל-telegram API)

הקוד..

אחרי שאיתחלתי את כל החבילות בנוד והכנסתי את ההגדרות הנצרכות לעבודה עם הרוקו (בהרוקו צריך לבצע הגדרה של הפורט כך שיישב מתהליכי הריצה - process.env - שורה 5) -

const express = require('express')
const fetch = require('node-fetch')
const bodyParser = require('body-parser')

const app = express()
const port = process.env.PORT || 5000
app.use(bodyParser.json())

אני מכניס את כל הטוקן וה-secrets הנדרשים בשביל ה-API שהתכנה מתממשקת איתם -

const TGtoken = 'bot token in telegram'
const TGchatID = 'chat ID of the channel in telegram'
const GASmacroURL = 'google apps scripts excuting URL'
const oneSignalAppID = 'oneSignal app ID'
const oneSignalAPIkey = 'oneSignal api key'
const oneSignalSegment = 'oneSignal segment'

כיוון שה-webhook שולח בקשה בשיטת POST - על האפליקציה להאזין לבקשות אלו. זה קטע הקוד שמתפקד במקרה של POST -

app.post('/', async (req) => {
  let json = req.body.post.current
  if (!json) {
    console.log(`got undefined call.. this is the content: ${JSON.stringify(req.body)}`)
  } else {
    let { url, title, custom_excerpt } = json
    const oneSignal = await OneSignalPush(title, custom_excerpt, url)
    const GASrequest = await GASpost(json)
    const telegrampost = await telegramPush(custom_excerpt, url)
    return `result: telegram - error code is ${telegrampost.status}, description is ${telegrampost.description}.\ngoogle script status is ${GASrequest.status}\noneSignal status is ${oneSignal.status}, recipients is ${oneSignal.recipients}`
  }
})

המשתנה json נוצר מתוכן ה-body של הבקשה שהתקבלה, לאחר מכן בודקים האם אכן מדובר בבקשה מה-webhook - בקשה כזו תכיל נתונים במיקום ממנו המשתנה json שואב את התוכן שלו. כך שאם המשתנה json ריק - הבקשה לא הגיעה מה-webhook.

שלוש השורות הללו -

const oneSignal = await OneSignalPush(title, custom_excerpt, url)
const GASrequest = await GASpost(json)
const telegrampost = await telegramPush(custom_excerpt, url)

הן קריאה לפונקציות. את הפעילות של שליחת העדכונים עצמם לפי כל שירות (פוש, טלגרם, מייל) חילקתי לפונקציות נפרדות והן נקראות מהפונקציה הראשית הזו באסינכרוניות. לכל פונקציה משורשר המידע הנצרך (נתונים מה-webhook - כותרת הפוסט, תקציר ו-URL).
הפונקציות לשליחה:

טלגרם

בשביל לשלוח הודעות בצורה ממוחשבת בטלגרם יש צורך בבוט. בוט הוא פשוט נקודת גישה ל-API של טלגרם - שמהצד השני שלו פולט את מה ששולחים לו - בתוך הטלגרם.
כדי ליצור בוט בטלגרם צריך לעבוד לפי ההוראות בהתכתבות עם BotFather (האבא של כל הבוטים..) ממנו מקבלים בסופו של דבר טוקן שמשמש לכל התקשרות עם ה-API של טלגרם, לצורך אימות זהות.
אחרי שנוצר הבוט פשוט מצרפים אותו לערוץ ונותנים לו הרשאת כתיבה. עכשיו צריך לדעת את ה-chat ID של הערוץ (לכל צ'אט בטלגרם יש מזהה יחודי שהוא משמש בין היתר להגדרה לאיפה לשלוח הודעה, כששולחים הודעה דרך הבוט).

כדי לקבל את ה-chat ID של הערוץ אפשר פשוט לשלוח בקשה ל-API של טלגרם לקבלת העדכונים האחרונים לבוט - בכתובת הזו -

https://api.telegram.org/bot<token>/getUpdates

אם היתה פעילות שנכנסה לפיד של הבוט ב-24 שעות האחרונות (אחרת טלגרם לא שומרת את המידע) תראו בתשובה את ה-chat ID של כל אחת מההודעות.

הפונקציה של שליחת ההודעה בטלגרם מקבלת את הפרמטרים של תקציר הפוסט שפורסם + כתובת הפוסט, אותם היא מפרסמת בערוץ דרך ה-API של הטלגרם, מקבלת את התגובה שנשלחה מטלגרם ומחזירה אותה כתוצאה של הפונקציה -

async function telegramPush(custom_excerpt, url) {
  let message = encodeURI(`${custom_excerpt}\n${url}`); // '\n' is line-break
  const response = await fetch(`https://api.telegram.org/bot${TGtoken}/sendMessage?chat_id=${TGchatID}&text=${message}&parse_mode=markdown`);
  const result = await response.json();
  return { status: result.error_code, description: result.description };
}

הפרמטרים שמוכנסים לכתובת ה-URL של ה-API הם הטוקן של הבוט + מזהה הצ'אט לשליחה, ואז גם תוכן ההודעה + פירוט לפרסר את הסטרינג כ-markdown.

פושים - oneSignal

כדי לשלוח פוש דרך ה-API של oneSignal צריך להגדיר לאיזה segment (מעין רשימת תפוצה. הגדרה של קבוצה מתוך רשימת הרשומים באתר) לשלוח את הפוש, כמו כן צריך להגדיר איזו כתובת תיפתח כאשר לוחצים על הפוש, תוכן ההודעה + כותרת -

async function OneSignalPush(title, custom_excerpt, url) {
  let message = {
    app_id: `${oneSignalAppID}`,
    headings: {"en": `${title}`},
    contents: {"en": `${custom_excerpt}`},
    url: `${url}`,
    included_segments: [`${oneSignalSegment}`]
  }
  let headers = {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": `Basic ${oneSignalAPIkey}`
  }
  const response = await fetch('https://onesignal.com/api/v1/notifications', { method: "POST", headers: headers, body: JSON.stringify(message) })
  const result = await response.json()
  return { status: response.status, recipients: result.recipients }
}

מערך האובייקטים message מכיל את כל התוכן וההגדרות שקשורות אליו, headers הם חלקים בבקשה שמבהירים באיזה סוג תוכן מדובר (כך שהשרת איתו אנחנו מתקשרים יבין מה אנחנו רוצים ומה שלחנו לו) + מעבירים נתונים כמו מפתחות ואימותים.

לאחר שהבקשה נשלחת ל-API של oneSignal מקבלים את התשובה (היא מכילה פרטים על הצלחת\כישלון שליחת הפוש + מידע על כמה קיבלו את הפוש בהצלחה במקרה ואכן..) ומעבירים אותה כתוצאה של הפונקציה.

גוגל סקריפט לשליחת המיילים

קראו כאן על שליחת מיילים קבוצתית + HTML דרך גוגל סקריפט

כדי לשלוח דרך גוגל סקריפט אני מתממשק עם webapp של גוגל סקריפט, בעלת כתובת HTTP - כך שפשוט צריך לשלוח בקשה לכתובת עם פרמטרים (התוכן הרצוי, מסודר ב-JSON בגוף ההודעה) והסקריפט מפענח אותו ושולח את ההודעה. כך נראה הסקריפט בגוגל -

function doPost(e) {
  if (typeof e !== 'undefined') {
    let body = JSON.parse(e.postData.contents)
    let { url, title, feature_image, custom_excerpt } = body
    let htmlbody = `<style></style><div>תוכן ${url} משולב עם ${custom_excerpt} משתנים ${feature_image} מהבקשה ${title}</div>` // תוכן תבנית המייל
    title = `פוסט חדש 🎉 בבלוג! ${title}`
    let contacts = ContactsApp.getContactGroup('בלוג').getContacts()
    for (let contact of contacts) {
      let address = contact.getEmailAddresses()
      Logger.log(address)
      MailApp.sendEmail(address[0], title, ``, { htmlBody: htmlbody })
    }
    return ContentService.createTextOutput(JSON.stringify('OK good'))
  } else {
    Logger.log('e is undefined. need to check')
  }
}

הפונקציה doPost אחראית על קבלת בקשות HTTP מסוג POST לכתובת של ה-webapp.
דבר ראשון בודקת האם הבקשה הנוכחית היא אכן בקשה מאפליקציית העדכונים (אם יש תוכן בפרמטר הפונקציה e - כלומר הוא לא undefined - משמע יש גוף body לבקשה). במקרה ואכן כך הפונקציה יוצרת JSON מה-body (הוא מגיע ב-JSON סטרינגי, צריך לפרסר אותו כדי להשתמש בתוכן שלו) ומכניסה את כל התוכן למשתנים ( title url feature_image custom_excerpt ).

לגוף ההודעה צריך להכין את הסטרינג (פשוט סטרינג רגיל) שמופק ממנו HTML, אני דוחף את כל המשתנים (כותרת, תמונה וכו') לתוך הערך של htmlbody שהוא template-string.

וזהו, מקבל את רשימת הכתובות מהקבוצה באנשי קשר של גוגל, ועובר עליהם בלולאה כשלכל אחד מהם נשלח מייל בנפרד.


כל הפונקציות האלו נקראות מהפונקציה הראשית שרצה בכל קריאת POST לאפליקציה. הפונקציה מקבלת את התוצאות שכל אחת מהפונקציות האלה מחזירה, ורושמת אותם בסופו של דבר בלוג.

אפשר לראות את הקוד בגיטהב כאן: https://github.com/chaim-chv/ghost-webhook

אשמח להארות ושיפורים!!


אם יש לכם איזו שאלה ❔✨ או כל תגובה 💬, הארה 💡 והערה ❕ שהיא על הפוסט - אשמח מאוד! אם תכתבו אותה בהערות כאן למטה
פשוט להתחבר עם חשבון גיטהב ולהגיב 🎉