Membuat Bot WhatsApp Twitter dengan Javascript dan API KirimWA.id

5 min read

Disclaimer
Saya bekerja di AWS, semua opini adalah dari saya pribadi. (I work for AWS, my opinions are my own.)
Bot WhatsApp Twitter dengan API KirimWA.id
Bot WhatsApp Twitter dengan API KirimWA.id

TeknoCerdas.com – Salam cerdas untuk kita semua. WhatsApp adalah salah satu aplikasi yang paling sering digunakan saat ini, hampir menggantikan SMS. Dengan unofficial WhatsApp API Gateway dari API KirimWA.id kita dapat menjadikan WhatsApp memiliki lebih banyak fungsi. Salah satunya adalah menjadikannya sebuah Bot WhatsApp. Tulisan kali ini akan membahas bagaimana membuat WhatsApp Bot untuk melihat trending topics Twitter dengan API KirimWA.id.

Bot atau Chatbot adalah sebuah layanan yang otomatis melakukan respon terhadap chat yang dikirimkan pengguna. Chatbot banyak dimanfaatkan sebagai layanan Customer Service pelanggan. Namun semua itu tergantung dari pengembang bot untuk menginterpretasikan setiap chat yang masuk akan diproses sedemikian rupa.

Baca Juga
Kirim WhatsApp dengan API KirimWA.id dan Node.js
Seri Tutorial WhatsApp API Gateway dengan API KirimWA.id

Tujuan tutorial kali ini adalah kita akan membuat bot yang akan merespon WhatsApp chat atau perintah berikut:

twitter /trends

Jika nomor WhatsApp menerima pesan dengan format tersebut maka otomatis pesan balik yang dikirimkan adalah daftar trending topics dari Twitter.

Daftar Isi

0. Persiapan

Terdapat beberapa hal yang perlu dipenuhi sebelum mengikuti tutorial ini.

1. Install Dependencies

Kita akan membuat direktori baru untuk menyimpan kode tutorial yang ada pada artikel ini.

$ mkdir bot-twitter
$ cd bot-twitter

Buat sebuah package.json baru untuk menginstal Express framework dan Puppeteer.

{
  "dependencies": {
    "express": "^4.17.1",
    "puppeteer": "^10.2.0"
  }
}

Kemudian lanjutkan dengan menginstall dependencies atau ketergantungan pustaka.

$ npm install

Express digunakan sebagai webhook yang akan dipanggil oleh API KirimWA.id ketika sebuah pesan masuk. Sedangkan pustaka Puppeter digunakan untuk melakukan scraping Twitter dengan memanfaatkan headless Chromium.

2. Menulis Kode Bot WhatsApp Twitter

Saatnya bagian yang menyenangkan yaitu menulis kode utama bot untuk mendapatkan trending topics Twitter dan mengirimkannya ke WhatsApp pengirim.

Pastikan berada pada direktori kerja bot-twitter kemudian kita akan membuat index.js. Ini adalah satu-satunya file yang harus dibuat. Ini adalah bot sederhana jadi cukup satu file saja.

const puppeteer = require('puppeteer');
const express = require('express');
const app = express();
const port = process.env.APP_PORT || 4000;
const caches = {};
const https = require('http');

/**
 * @param Object browser
 * @return Promise
 */
async function getTwitterTrendingTopics(browser) {
  const br = await browser;
  const page = await br.newPage();
  await page.setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1');
  await page.setViewport({
    width: 375,
    height: 667,
    deviceScaleFactor: 1,
    isMobile: true
  })

  console.log('Opening /i/tends/ page...');
  await page.goto('https://mobile.twitter.com/i/trends');

  console.log('Waiting content be ready...');
  await page.waitForFunction("document.body.innerText.indexOf('Trending in') > -1");

  console.log('Trying to scroll...');
  await page.evaluate(async () => {
    const delay = 1000;
    const wait = (ms) => new Promise(res => setTimeout(res, ms));
    const count = async () => document.querySelectorAll('div[data-testid="trend"][role="link"]').length;
    const scrollDown = async () => {
      const last = document.querySelectorAll('div[data-testid="trend"][role="link"]').length;

      if (last) {
        document.querySelectorAll('div[data-testid="trend"][role="link"]')[last - 1]
          .scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' });
      }
    }

    let preCount = 0;
    let postCount = 0;

    do {
      preCount = await count();
      await scrollDown();
      await wait(delay);
      postCount = await count();
    } while (postCount > preCount);

    await wait(delay);
  });

  console.log('Collecting trending elements...');
  const trendingElements = await page.$$eval('div[data-testid="trend"][role="link"]', (divs) => {
    let tmp = [];

    for (let i=0; i -1) {
          return parseFloat(n.split(' ')[0]) * 1000;
        }

        if (n.indexOf('m') > -1) {
          return parseFloat(n) * 1000000;
        }

        return parseFloat(n);
      };


      tmp.push({
        trending_location: spans[0] ? spans[0].innerText : '?',
        text: spans[1] ? spans[1].innerText : '?',
        tweets: numberOfTweets(spans),
        tweets_raw: getRawTweets(numberOfTweets(spans))
      });
    }

    tmp.sort(function compare(a, b) {
      if (a.tweets_raw < b.tweets_raw) { return 1 }
      if (a.tweets_raw > b.tweets_raw) { return -1 }
      return 0;
    })

    return tmp;
  });

  return trendingElements;
}

/**
 * @param Object browser
 * @return Promise
 */
async function getTwitterTrendingTopicsFromCache(browser) {
  let trendings, needToFetch;
  const now = Math.floor(Date.now() / 1000);

  needToFetch = caches.hasOwnProperty('trendings') === false;

  if (caches.hasOwnProperty('trendings') === true) {
    const cacheMaxAge = 300;
    const cacheAge = now - caches.trendings.timestamp;

    if (cacheAge > cacheMaxAge) {
      needToFetch = true;
    }
  }

  if (needToFetch) {
    trendings = await getTwitterTrendingTopics(browser);
    caches.trendings = {
      data: trendings,
      timestamp: now
    }

    return trendings;
  }

  return caches.trendings.data;
}

/**
 * Make a request to API KirimWA.id
 *
 * @param Object params
 * @return Promise
 */
function apiKirimWaRequest(params)
{
  return new Promise((resolve, reject) => {
    let responseBody = '';
    const options = {
      method: params.method || 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${params.token}`
      }
    }
    const payloadBuffer = Buffer.from(params.payload);

    if (params.method === 'POST') {
      options.headers['Content-Length'] = payloadBuffer.length;
    }

    const req = https.request(params.url, options, function(res) {
      res.on('data', function concatBody(chunk) {
        responseBody += chunk;
      });

      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve({ body: responseBody, response: res });
          return;
        }

        // Non 2XX response
        reject({ body: responseBody, response: res });
      });
    });

    if (params.method === 'POST') {
      req.write(params.payload);
    }

    req.end();
  }); // Promise
}

const browser = puppeteer.launch({
  headless: true
});

app.use(express.json());
app.post('/webhook', async (req, res) => {
  const cmdPrefix = 'twitter /trends';

  const skipMessage = req.body.payload.from_me === false ||
                      req.body.payload.is_group_message === true ||
                      req.body.payload.text !== cmdPrefix;

  if (skipMessage) {
    res.send({ 'message': 'SKIP '});
    return;
  }

  const trendings = await getTwitterTrendingTopicsFromCache(browser);
  let trendingMessage = `${cmdPrefix} response:\n\n`;
  for (item of trendings) {
    trendingMessage += item.trending_location + "\n" +
                       `*${item.text}*` + "\n" +
                       `${item.tweets}` + "\n" +
                       '--' + "\n"
  }

  const url = new URL(process.env.API_BASE_URL + '/messages');
  const message = {
    message: trendingMessage,
    phone_number: req.body.payload.sender,
    message_type: 'text',
    device_id: req.body.payload.device_id
  };

  const reqParams = {
    token: process.env.API_TOKEN,
    url: url,
    method: 'POST',
    payload: JSON.stringify(message)
  };

  try {
    const { body, response } = await apiKirimWaRequest(reqParams);
    console.log(body);
  } catch (error) {
    console.error(error);
    console.error('Something went wrong', {
      body: error.body,
      statusCode: error.response.statusCode
    });
  }

  res.send({ 'message': 'OK '});
});

app.listen(port, function() {
  console.log(`Twitter API running at http://localhost:${port}`);
});

Simpan file tersebut dengan nama index.js.

Fungsi getTwitterTrendingTopics mensimulasikan sebuah browser bernavigasi ke https://mobile.twitter.com/i/trends/ kemudian menunggu konten untuk muncul dan melakukan scroll ke bawah hingga semua trending topics muncul.

Fungsi getTwitterTrendingTopicsFromCache berguna untuk melakukan cache dari hasil fungsi sebelumnya karena fungsi tersebut membutuhkan waktu beberapa detik. Jadi hasil akan dicache selama 5 menit.

Fungsi apiKirimWaRequest berguna untuk melakukan HTTP request ke server API KirimWA.id. Fungsi ini melakukan abstraksi pemanggilan dan akan melemparkan error ketika hasil tidak berstatus 2XX.

Endpoint Express app.post('/webhook', async (req, res) berfungsi untuk untuk memproses webhook call dari API KirimWA.id. Webhook ini akan menerima data pesan yang masuk, jika pesan yang masuk adalah sebuah pesan dengan format twitter /trends maka panggil fungsi untuk mendapatkan trending topics diatas kemudian kembalikan hasilnya kepada pengirim.

Jalankan script Node.js yang akan berjalan pada port 4000 dengan perintah berikut:

$ API_TOKEN=TOKEN_API_KIRIMWA_ANDA \
API_BASE_URL=https://api.kirimwa.id/v1 \
node index.js
Twitter API running at http://localhost:4000

3. Expose Localhost ke Internet

Saat ini webhook masih berjalan dilocalhost sehingga belum bisa dihubungi oleh API KirimWA.id dari internet. Untuk itu kita perlu melakukan expose localhost ke internet. Anda bisa menggunakan ngrok atau Cloudflare Tunnel. Pada contoh ini saya menggunakan Cloudflared Tunnel dan menghubungkannya dengan domain teknocerdas.com

Perintah yang saya gunakan untuk melakukan expose localhost port 4000 ke internet adalah:

$ cloudflared tunnel --hostname localhost.teknocerdas.com --url http://localhost:4000

Untuk mencobanya silahkan lakukan POST request ke URL https://localhost.teknocerdas.com/webhook.

$ curl -XPOST https://localhost.teknocerdas.com/webhook \
-H "Content-Type: application/json" \
-d '{ "Hello": "World" }'

Lihat pada terminal Window yang menjalankan script Node.js tersebut harusnya terdapat log body yang dikirim.

Baca Juga
Mengakses localhost dari Internet dengan SSH Tunneling

4. Menambahkan Webhook

Agar API KirimWA.id mengirimkan data pesan masuk ke webhook yang berlokasi di https://localhost.teknocerdas.com/webhook maka URL tersebut perlu didaftarkan.

Gunakan perintah berikut untuk mendaftarkan Webhook.

$ curl -XPOST https://api.kirimwa.id/v1/webhooks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer API_TOKEN_ANDA" \
-d '{ "webhook_url": "https://localhost.teknocerdas.com/webhook" }'

Jika berhasil maka respon yang dikembalikan kurang lebih seperti berikut.

{
  "id": "whu-YOUR@EMAIL",
  "status": "active",
  "data": "https://localhost.teknocerdas.com/webhook",
  "created_at": "2021-06-20T17:17:06.333Z",
  "meta": {
    "location": "https://api.kirimwa.id/v1/webhooks"
  }
}

5. Tes Bot WhatsApp

Untuk mengetes Bot yang telah dibuat anda dapat mengirimkan pesan ke diri sendiri. Pada perangkat HP anda buka browser dan buka https://kirimwa.id/ kemudian ketikkan nomor anda sendiri.

Setelah chat WhatsApp terbuka maka ketikkan pesan twitter /trends lalu kirim. Selang beberapa detik maka harusnya otomatis akan mendapat balasan berupa daftar trending topik di Twitter.

WhatsApp Bot Twitter API KirimWA.id
WhatsApp Bot Twitter dengan Unoffical WhatsApp API Gateway https://developer.kirimwa.id/