post/

Apologize

April 21, 20226 min read

JavaScript, Node.js, Serverless, IFTTT, Webhooks

IFTTT can trigger an event when an endpoint is hit with the use of Webhooks. Then when the event is triggered, I can use IFTTT to send a rich push notification to my device.

With the use of IFTTT and two serverless functions that I made at Napkin, I can now receive rich notifications when there's an update on BENECO's scheduled interruptions.

Screenshot my iPhone lock screen

I have set up three things for this project:

  1. A serverless function scheduled every hour to check for the updated scheduled power interruptions. If there's an update, it hits the webhook on IFTTT.
  2. A serverless function that displays the scheduled interruptions scraped from beneco.com.ph.
  3. Webhooks on IFTTT.

Function #1

A cloud function that regularly checks for updates with Function #2. If there's an update, it will make a web request to the IFTTT webhook endpoint.

The problem I encountered was how and where should I store the latest scheduled interruptions state. Luckily, Napkin made it easy building stateful apps with their napkin module.

I installed napkin from the "Modules" tab and imported it to the code workspace destructuring the store object.

Source code

import axios from 'axios';
import { store } from 'napkin';

const { IFTTT_WEBHOOKS_KEY, SCHEDULES_URL } = process.env;

const IFTTT_EVENT_TRIGGER = 'beneco_scheduled_interruptions_notification';
const IFTTT_EVENT_TRIGGER_ENDPOINT = `https://maker.ifttt.com/trigger/${IFTTT_EVENT_TRIGGER}/with/key/${IFTTT_WEBHOOKS_KEY}`;
const LATEST_SCHEDULES_CACHE_KEY = 'latest-schedules';

const handler = async (req, res) => {
  const newSchedules = await getSchedules();
  const newSchedulesString = JSON.stringify(newSchedules);

  const cachedSchedulesString = await getCachedSchedules();

  const isSchedulesUpdated = newSchedulesString != cachedSchedulesString;

  if (isSchedulesUpdated) {
    await setCachedSchedules(newSchedulesString);

    const sent = await sendNotification(); // Congratulations! You've fired the beneco_scheduled_interruptions_notification json event

    res.json({
      message: sent,
    });

    return;
  }

  res.json({
    message: 'No update on schedules.',
  });
};

const sendNotification = async () => {
  try {
    const { data } = await axios.post(IFTTT_EVENT_TRIGGER_ENDPOINT);
    return data;
  } catch (_) {
    return 'Something went wrong. Failed to send notification.';
  }
};

const getSchedules = async () => {
  const { data } = await axios.get(SCHEDULES_URL);

  return data;
};

const setCachedSchedules = async (schedules) => {
  await store.put(LATEST_SCHEDULES_CACHE_KEY, schedules);
};

const getCachedSchedules = async () => {
  const { data } = await store.get(LATEST_SCHEDULES_CACHE_KEY);

  return data;
};

export default handler;

License: MIT License · Copyright (c) 2022 Noel Earvin Piamonte

So how do I know if the schedules are updated?

First, I fetch the latest schedules from Function #2 API endpoint. I then store the result as a string with JSON.stringify.

After that, I get the stored schedules with napkin module. If the stringified schedules is not equal to the stringified cached schedules, then that would mean that the schedules are updated. If it's updated then I store the latest schedules as string.

Function #2

The function scrapes the latest schedules from beneco.com.ph and displays the result in JSON format. By default, without the json slug as the "viewFormat" payload, it displays as a webpage.

I use the webpage to view the schedules when I click on the rich notification from IFTTT on my device. I use the schedules JSON response to compare it to the cached schedules on Function #1.

Source code

import axios from 'axios';
import cheerio from 'cheerio';

const jsonString = 'json';

const handler = async (req, res) => {
  const { viewFormat = 'html' } = req.params;
  const schedules = await getSchedules();

  if (viewFormat == jsonString) {
    res.json(schedules);
    return;
  }

  const html = Html(schedules);
  res.send(html);
};

const getSchedules = async () => {
  const { data } = await axios.get('https://www.beneco.com.ph');
  const { schedules } = parseHtml(data);

  return schedules;
};

const parseHtml = (html) => {
  const $ = cheerio.load(html);
  const schedules = [];

  $('.table__interruptions tbody tr').each((_, tr) => {
    const $tr = $(tr);
    const dateTime = $tr.find('td:eq(0)').html().replace('<br>', ', ');
    const purpose = $tr.find('td:eq(1)').text();
    const areasAffected = $tr.find('td:eq(2)').text();

    const schedule = {
      dateTime,
      areasAffected,
      purpose,
    };

    schedules.push(schedule);
  });

  return { schedules };
};

const Html = (schedules) => {
  return `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
          * {
            box-sizing: border-box;
            padding: 0px;
            margin: 0px;
          }

          body {
            font-family: Arial, sans-serif;
            padding-top: 15px;
            padding-left: 15px;
            padding-right: 15px;
          }
        </style>
        <title>Beneco Scheduled Interruptions</title>
      </head>
      <body>
        <h1 style="margin-bottom: 15px;">Scheduled Interruptions</h1>
        <div style="margin-bottom: 30px;">
          ${Table(schedules)}
        </div>
      </body>
    </html>
  `;
};

const Table = (schedules) => {
  const styles = ['border-collapse: collapse', 'overflow: scroll'];

  return `
    <table style="${inlineStyles(styles)}">
      ${TableHead()}
      <tbody>
        ${TableRows(schedules)}
      </tbody>
    </table>
  `;
};

const TableHead = () => {
  const styles = [
    'font-weight: bold',
    'text-align: left',
    'border: 1px solid #ddd',
    'padding: 8px',
  ];

  return `
    <thead>
      <tr>
        <th style="${inlineStyles(styles)} width: 20%">Date/ Time</th>
        <th style="${inlineStyles(styles)} width: 45%">Areas</th>
        <th style="${inlineStyles(styles)} width: 35%">Purpose</th>
      </tr>
    </thead>
  `;
};

const TableRows = (schedules) => {
  return schedules
    .map((schedule) => {
      const cellValues = Object.values(schedule);

      return `
        <tr>
          ${cellValues.map((value) => TableCell(value)).join('')}
        <tr>
      `;
    })
    .join('');
};

const TableCell = (value) => {
  const styles = [
    'vertical-align: top',
    'border: 1px solid #ddd',
    'padding: 8px',
  ];

  return `
    <td style="${inlineStyles(styles)}">${value}</td>
  `;
};

const inlineStyles = (styles) =>
  styles.length > 0 ? `${styles.join('; ')};` : '';

export default handler;

License: MIT License · Copyright (c) 2022 Noel Earvin Piamonte

IFTTT Webhooks

Now for the IFTTT Webhooks, I created an Applet on IFTTT - https://ifttt.com/create.

For the If block condition, I used the "Receive a web request with a JSON payload" and set the "Event name" to beneco_scheduled_interruptions_notification. This event name is what I used on Function #1 - IFTTT_EVENT_TRIGGER.

Screenshot of Edit trigger fields on IFTTT

I then set the field values for the Then action. I use the API endpoint on Function #1 as the "Link URL".

Screenshot of Edit action fields on IFTTT

To get the IFTTT_WEBHOOKS_KEY for the IFTTT_EVENT_TRIGGER_ENDPOINT, log in to IFTTT > My Services, scroll down and click on Webhooks then click on the "Documentation" button.

Screenshot of Webhooks API key page on IFTTT

I was looking to publish this Applet on IFTTT but unfortunately, IFTTT doesn't allow publishing an Applet with Webhooks. More info at Webhooks service FAQ .

bye.

Now playing :Not playing any music.