Slide 1

Slide 1 text

Building Malaysia Vaccine Tracker Twitter Bot Henry Lim ๓ฐžฆ Google Developer Expert, Web Technologies Creator of Remote for Slides Last modified: 10 July 2021

Slide 2

Slide 2 text

๐Ÿšฝ๐Ÿ”ฅ

Slide 3

Slide 3 text

๐Ÿ’‰๓ฐ™ฅ

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

DISCLAIMER: No, COVID-19 vaccine wonโ€™t give you โ€œmagnetic armsโ€ youtu.be/bVi-GlOY9iM

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

covidbucketbbc.s3-ap-southeast-1.amazonaws.com/heatdata.json { "data": [ { "nme": "Malaysia", "regtotal": "13046616", "pop_18": "24259200", "vakdose1": 2835178, "vakdose2": 1267758, "vakdosecomplete": 2835178 }, { "nme": "Johor", "regtotal": "1621696", "pop_18": "2838600", "vakdose1": "265985", "vakdose2": "117725", "vakdosecomplete": "265985" }, ... ], "updated": 1623340740000 }

Slide 13

Slide 13 text

โฑ ๐Ÿ˜ช

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

Search Tweet(s) const q = { "query": "from:JKJAVMY \"Total cumulative dose administered as\"", "start_time": "2021-06-11T00:00:00.000Z", "expansions": "attachments.media_keys", "tweet.fields": "attachments", "media.fields": "url", } const query = Object.keys(q).map((key) => `${key}=${encodeURIComponent(q[key])}`).join("&"); // Get Bearer Token from developer.twitter.com/en/portal/dashboard const headers = {Authorization: this.config.twitterToken}; // Call "search/recent" API // developer.twitter.com/en/docs/twitter-api/tweets/search/introduction const baseUrl = "https://api.twitter.com/2/tweets/search/recent"; const tweetReq = await fetch(`${baseUrl}?${query}`, {headers}); const tweetRes = await tweetReq.json(); return tweetRes;

Slide 16

Slide 16 text

๐ŸŒ ๐Ÿง  ๐Ÿ’ฌ

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

โ˜ ๐Ÿ‘€

Slide 21

Slide 21 text

Cloud Vision API

Slide 22

Slide 22 text

Detect Text using Google Cloud Vision API const Vision = require("@google-cloud/vision"); const vision = new Vision.ImageAnnotatorClient(); const imageReq = await fetch(imageUrl); const imageBuffer = await imageReq.buffer(); const imgContent = imageBuffer.toString("base64"); const [textDetections] = await vision.textDetection({ image: { content: imgContent }, imageContext: { languageHints: ["en", "ms"] }, }); return textDetections.textAnnotations.map((e) => ({ vertices: e.boundingPoly.vertices, text: e.description, }));

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

๐Ÿ”ฅ โšพ ๐Ÿ’ป

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

โ˜ โฑ ๓ฐ–ง

Slide 28

Slide 28 text

Cloud Scheduler

Slide 29

Slide 29 text

๓ฐข ๐Ÿ’ธ

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

Send Tweet const Twitter = require("twitter-lite"); this.twitterClient = new Twitter({ subdomain: "api" , version: "1.1", consumer_key: config.consumerKey , consumer_secret: config.consumerSecret, access_token_key: config.accessTokenKey , access_token_secret: config.accessTokenSecret }); const text = ` 1st dose: 21.372% (+0.634 p.p.) โ–“โ–“โ–“โ–“โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 2nd dose: 9.273% (+0.513 p.p.) โ–“โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 1st dose: 6,999,554 (+207,795) 2nd dose: 3,036,807 (+168,047) Total doses: 10,036,361 (+375,842) (As of 07/07/21, 23:59) `; await this.twitterClient.post("statuses/update", {status: text});

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

๐Ÿ”” ๐Ÿšจ ๐Ÿ’ฌ

Slide 35

Slide 35 text

Send Message to Telegram const FormData = require("form-data"); const sendMsgToTelegram = async (text) => { const form = new FormData(); form.append("chat_id", ""); form.append("text", text); const url = `https://api.telegram.org//sendMessage` return await fetch(url, { method: "POST", body: form }); } exports.someCuteNameHere = functions.https.onRequest(async (req, res) => { try { // um ... write some code here ? sendMsgToTelegram("2021-07-08: OK"); return res.send(200); } catch (e) { sendMsgToTelegram(`๐Ÿšจ๐Ÿšจ๐Ÿšจ ERROR ๐Ÿšจ๐Ÿšจ๐Ÿšจ\n\n${e.stack}`); return res.send(500); } })

Slide 36

Slide 36 text

๐Ÿง ๐Ÿค”

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

const {createCanvas, loadImage} = require("canvas"); // 1. Draw the background image (syringe): const img = await loadImage(path.resolve(__dirname, "icon.png")); const canvas = createCanvas(400, 400); const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, 400, 400); // 2. Calculate the math for the ring: const radius = 200 - (28 / 2); const angle = Math.PI * 1.5; const offset = ((28 / 2) / (Math.PI * 400) * 360) * (Math.PI / 180); ctx.lineWidth = 28; ctx.lineCap = "round"; // 3. Outer Ring: ctx.beginPath(); ctx.arc(200, 200, radius, angle, (Math.PI * 2) + angle, false); ctx.strokeStyle = "#2c41a4"; ctx.stroke(); // 4. First Dose Ring: ctx.beginPath(); โ†“ 1st dose % ctx.arc(200, 200, radius, angle + offset, (21.372 / 100 * Math.PI * 2) + angle - offset, false); ctx.strokeStyle = "#c0f4b8"; ctx.stroke(); // 5. Second Dose Ring: ctx.beginPath(); โ†“ 2nd dose % ctx.arc(200, 200, radius, angle + offset, (9.273 / 100 * Math.PI * 2) + angle - offset, false); ctx.strokeStyle = "#34cf1c"; ctx.stroke(); // 6. Convert to base64 and POST account/update_profile_image: await this.twitterClient.post("account/update_profile_image", {image: canvas.toDataURL().split(",")[1]});

Slide 40

Slide 40 text

๐Ÿ“ˆ ๐Ÿ“Š

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

๓ฐžฆ ๐Ÿช‘ ๐Ÿ“ˆ Because thereโ€™s no emoji for โ€œtableโ€, so I replaced that with โ€ฆ chair

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

bit.ly/malaysia-vaccine-data

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

๓ฐขฃ ๓ฐขฃ ๓ฐขฃ

Slide 49

Slide 49 text

One more thing ...

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

raw.githubusercontent.com/CITF-Malaysia/citf-public/main/vaccination/vax_malaysia.csv

Slide 54

Slide 54 text

raw.githubusercontent.com/CITF-Malaysia/citf-public/main/vaccination/vax_state.csv

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

๐Ÿšฎ ๐Ÿ—‘ ๓ฐฃน

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

Resources MY Vaccine Tracker Twitter: twitter.com/MYVaccineCount MY Vaccine Tracker Google Sheets: bit.ly/malaysia-vaccine-data CITF GitHub: github.com/CITF-Malaysia/citf-public Terrasect: github.com/tesseract-ocr/tesseract Firebase: firebase.google.com Cloud Vision API: cloud.google.com/vision twitter-lite: npmjs.com/package/twitter-lite chartjs-node-canvas: npmjs.com/package/chartjs-node-canvas google-spreadsheet: npmjs.com/package/google-spreadsheet chartjs-node-canvas: npmjs.com/package/chartjs-node-canvas cron-job.org: cron-job.org Telegram Bot API: core.telegram.org/bots/api

Slide 59

Slide 59 text

Thank you! ๐Ÿก ๐Ÿ˜ท Follow me: @henrylim96 @MYVaccineCount @RemoteForSlides