├── token.sample.js ├── .gitignore ├── config.js ├── .editorconfig ├── .github └── workflows │ └── main.yml ├── package.json ├── helper.js ├── index.js ├── test └── test.js ├── README.md └── commands.js /token.sample.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Token 5 | */ 6 | module.exports = 'Your Telegram token here'; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files 2 | token.js 3 | deploy.sh 4 | 5 | # IDE 6 | .idea 7 | 8 | # Build 9 | node_modules 10 | 11 | # Zip 12 | Archive.zip 13 | telegram-bot.zip 14 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Config 5 | */ 6 | module.exports = { 7 | lesterchanApiUrl: 'https://api.lesterchan.net/v1', 8 | 9 | defaultDateTimeFormat: 'HH:mm, Do MMMM YYYY', 10 | defaultDateFormat: 'Do MMMM YYYY', 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | insert_final_newline = false 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: GitHub CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | lint-test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v3 14 | - name: Lint and Test 15 | run: | 16 | cp token.sample.js token.js 17 | npm install 18 | npm run lint 19 | npm test 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telegram-bot", 3 | "version": "1.0.0", 4 | "description": "Telegram Bot", 5 | "license": "MIT", 6 | "author": "Lester Chan (https://lesterchan.net)", 7 | "homepage": "https://lesterchan.net", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/lesterchan/telegram-bot.git" 11 | }, 12 | "main": "index.js", 13 | "scripts": { 14 | "lint": "eslint *.js test/*.js", 15 | "test": "mocha" 16 | }, 17 | "eslintConfig": { 18 | "extends": "techinasia-base", 19 | "env": { 20 | "mocha": true, 21 | "node": true 22 | }, 23 | "rules": { 24 | "no-unused-expressions": 0, 25 | "func-names": 0, 26 | "strict": 0, 27 | "max-len": 0 28 | } 29 | }, 30 | "devDependencies": { 31 | "chai": "^4.3.4", 32 | "chai-as-promised": "^7.1.1", 33 | "eslint": "^7.26.0", 34 | "eslint-config-techinasia-base": "^0.8.0", 35 | "eslint-plugin-import": "^2.22.1", 36 | "mocha": "^10.1.0" 37 | }, 38 | "dependencies": { 39 | "moment": "^2.29.4", 40 | "request": "^2.88.2", 41 | "request-promise": "^4.2.6" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Helper 5 | */ 6 | module.exports = { 7 | ucWords(string) { 8 | return string.replace('/\w\S*/g', (str) => str.charAt(0).toUpperCase() + str.substr(1).toLowerCase()); // eslint-disable-line no-useless-escape 9 | }, 10 | formatNumber(x) { 11 | return x.toLocaleString('en'); 12 | }, 13 | formatBytes(bytes, decimals) { 14 | const b = parseInt(bytes, 10); 15 | if (b === 0) { 16 | return '0 Byte'; 17 | } 18 | const k = 1024; 19 | const dm = decimals + 1 || 3; 20 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 21 | const i = Math.floor(Math.log(b) / Math.log(k)); 22 | return `${(b / Math.pow(k, i)).toPrecision(dm)} ${sizes[i]}`; // eslint-disable-line no-restricted-properties 23 | }, 24 | parseCommand(message) { 25 | const tokens = message.split(' '); 26 | if (!tokens[0].match(/^\//)) { 27 | return null; 28 | } 29 | const command = []; 30 | const cmd = tokens.shift(); 31 | const match = cmd.match(/\/(\w*)/); 32 | if (match.length > 0) { 33 | command[match[1]] = tokens; 34 | } 35 | return command; 36 | }, 37 | getMessage(message) { 38 | let m = ''; 39 | if (message) { 40 | m = message.toString().trim(); 41 | } else { 42 | m = ''; 43 | } 44 | return (m.length > 0 ? m : 'N/A'); 45 | }, 46 | formatMessage(title, description, fields) { 47 | let message = ''; 48 | 49 | if (title.length > 0) { 50 | message = `${title}\n`; 51 | } 52 | if (description.length > 0) { 53 | message += `${description}\n`; 54 | } 55 | if (fields.length > 0) { 56 | message += `
${this.parseFields(fields)}
`; 57 | } 58 | 59 | return message; 60 | }, 61 | parseFields(fields) { 62 | const data = []; 63 | fields.forEach((entry) => { 64 | if (entry.title && entry.title.length > 0) { 65 | data.push(`${entry.title}: ${entry.value}`); 66 | } 67 | }); 68 | 69 | return data.join("\n"); // eslint-disable-line quotes 70 | }, 71 | validateIp(ip) { 72 | const matcher = /^(?:(?:2[0-4]\d|25[0-5]|1\d{2}|[1-9]?\d)\.){3}(?:2[0-4]\d|25[0-5]|1\d{2}|[1-9]?\d)$/; 73 | return matcher.test(ip); 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Requires (Custom Modules) 5 | */ 6 | const rp = require('request-promise'); 7 | const helper = require('./helper'); 8 | const commands = require('./commands'); 9 | const telegramToken = require('./token'); 10 | 11 | /** 12 | * Send message to Telegram 13 | * 14 | * @param {int} chatId Chat ID 15 | * @param {string} message Message to send 16 | * 17 | * @return {object} Request Promise 18 | */ 19 | function sendMessageToTelegram(chatId, message) { 20 | return rp({ 21 | method: 'POST', 22 | uri: `https://api.telegram.org/bot${telegramToken}/sendMessage`, 23 | form: { 24 | chat_id: chatId, 25 | text: message, 26 | parse_mode: 'HTML', 27 | }, 28 | }); 29 | } 30 | 31 | /** 32 | * Process Commands 33 | * 34 | * @param {object} message AWS Lambda Event 35 | * 36 | * @return {object} Request Promise 37 | */ 38 | function processCommands(message) { 39 | if (message) { 40 | const commandArguments = helper.parseCommand(message.trim()); 41 | if (commandArguments === null) { 42 | return commands.error('Invalid Command'); 43 | } 44 | 45 | const commandKeys = Object.keys(commandArguments); 46 | if (commandKeys.length === 0 && !commands[commandKeys[0]]) { 47 | return commands.error('Invalid Command'); 48 | } 49 | 50 | const command = commandKeys[0]; 51 | 52 | return commands[command](commandArguments[command]); 53 | } 54 | 55 | return commands.error('Event not specified'); 56 | } 57 | 58 | /** 59 | * Main Lambda function 60 | * 61 | * @param {object} event AWS Lambda uses this parameter to pass in event data to the handler. 62 | * @param {object} context AWS Lambda uses this parameter to provide your handler the runtime information of the Lambda function that is executing. 63 | * 64 | * @return {object} Request Promise 65 | */ 66 | exports.handler = (event, context) => { 67 | // Message 68 | let message; 69 | if (event.body.channel_post && event.body.channel_post.text) { 70 | message = event.body.channel_post.text; 71 | } else if (event.body.message && event.body.message.text) { 72 | message = event.body.message.text; 73 | } 74 | const processCommand = processCommands(message); 75 | 76 | // Chat ID 77 | let chatId; 78 | if (event.body.message && event.body.message.chat && event.body.message.chat.id) { 79 | chatId = event.body.message.chat.id; 80 | } else if (event.body.channel_post && event.body.channel_post.chat && event.body.channel_post.chat.id) { 81 | chatId = event.body.channel_post.chat.id; 82 | } 83 | 84 | if (chatId) { 85 | processCommand.then((response) => { 86 | const processTelegram = sendMessageToTelegram(chatId, response); 87 | processTelegram.then(() => { 88 | context.succeed(); 89 | }).catch(() => { 90 | context.fail(); 91 | }); 92 | }).catch((error) => { 93 | const processTelegram = sendMessageToTelegram(chatId, error.message); 94 | processTelegram.then(() => { 95 | context.succeed(); 96 | }).catch(() => { 97 | context.fail(); 98 | }); 99 | }); 100 | } else { 101 | processCommand.then(() => { 102 | context.succeed(); 103 | }).catch(() => { 104 | context.fail(); 105 | }); 106 | } 107 | 108 | return processCommand; 109 | }; 110 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Requires (Test Modules) 5 | */ 6 | const { expect } = require('chai'); 7 | 8 | /** 9 | * Requires (Main App) 10 | */ 11 | const lambda = require('../index'); 12 | 13 | /** 14 | * Timeout 15 | */ 16 | const timeout = 5000; 17 | 18 | /** 19 | * Mock AWS Lambda Context 20 | */ 21 | const context = { 22 | fail() {}, 23 | succeed() {}, 24 | }; 25 | 26 | describe('telegram-bot', () => { 27 | it('Should list down all buses arrival timing at the bus stop', (done) => { 28 | const output = lambda.handler({ 29 | body: { 30 | message: { 31 | text: '/bus 44591', 32 | }, 33 | }, 34 | }, context); 35 | 36 | output.then((response) => { 37 | expect(response).to.contain('Bus Stop 44591'); 38 | done(); 39 | }).catch(done); 40 | }).timeout(timeout); 41 | 42 | it('Should list down a single bus arrival timing at the bus stop', (done) => { 43 | const output = lambda.handler({ 44 | body: { 45 | message: { 46 | text: '/bus 30111 991', 47 | }, 48 | }, 49 | }, context); 50 | 51 | output.then((response) => { 52 | expect(response).to.contain('Bus Stop 30111'); 53 | expect(response).to.contain('Bus: 991'); 54 | done(); 55 | }).catch(done); 56 | }).timeout(timeout); 57 | 58 | it('Should validate against invalid bus stop number', (done) => { 59 | const output = lambda.handler({ 60 | body: { 61 | message: { 62 | text: '/bus invalidbustopno', 63 | }, 64 | }, 65 | }, context); 66 | 67 | output.then((response) => { 68 | expect(response).to.eql('Bus stop or number is invalid'); 69 | done(); 70 | }).catch(done); 71 | }).timeout(timeout); 72 | 73 | it('Should validate against invalid bus number', (done) => { 74 | const output = lambda.handler({ 75 | body: { 76 | message: { 77 | text: '/bus 14229 invalidbusno', 78 | }, 79 | }, 80 | }, context); 81 | 82 | output.then((response) => { 83 | expect(response).to.eql('Bus stop or number is invalid'); 84 | done(); 85 | }).catch(done); 86 | }).timeout(timeout); 87 | 88 | it('Should list down Singapore haze conditions', (done) => { 89 | const output = lambda.handler({ 90 | body: { 91 | message: { 92 | text: '/haze', 93 | }, 94 | }, 95 | }, context); 96 | 97 | output.then((response) => { 98 | expect(response).to.contain('Singapore Haze Conditions'); 99 | done(); 100 | }).catch(done); 101 | }).timeout(timeout); 102 | 103 | it('Should list down Singapore 3 hour forecast weather conditions', (done) => { 104 | const output = lambda.handler({ 105 | body: { 106 | message: { 107 | text: '/weather', 108 | }, 109 | }, 110 | }, context); 111 | 112 | output.then((response) => { 113 | expect(response).to.contain('Singapore Weather Conditions'); 114 | done(); 115 | }).catch(done); 116 | }).timeout(timeout); 117 | 118 | it('Should list down Google DNS information', (done) => { 119 | const output = lambda.handler({ 120 | body: { 121 | message: { 122 | text: '/ipinfo 8.8.8.8', 123 | }, 124 | }, 125 | }, context); 126 | 127 | output.then((response) => { 128 | expect(response).to.contain('8.8.8.8'); 129 | done(); 130 | }).catch(done); 131 | }).timeout(timeout); 132 | 133 | it('Should validate against invalid IP', (done) => { 134 | const output = lambda.handler({ 135 | body: { 136 | message: { 137 | text: '/ipinfo invalidip', 138 | }, 139 | }, 140 | }, context); 141 | 142 | output.catch((e) => { 143 | expect(e.message).to.eql('Invalid IP'); 144 | done(); 145 | }).catch(done); 146 | }).timeout(timeout); 147 | 148 | it('Should list down social stats count for a link', (done) => { 149 | const output = lambda.handler({ 150 | body: { 151 | message: { 152 | text: '/socialstats https://lesterchan.net/blog/2017/06/30/apple-ipad-pro-10-5-space-grey-256gb-wi-fi-cellular/', 153 | }, 154 | }, 155 | }, context); 156 | 157 | output.then((response) => { 158 | expect(response).to.contain('https://lesterchan.net/blog/2017/06/30/apple-ipad-pro-10-5-space-grey-256gb-wi-fi-cellular/'); 159 | done(); 160 | }).catch(done); 161 | }).timeout(timeout); 162 | }); 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Bot 2 | A tutorial on creating a Node.js Telegram bot using AWS Lambda with AWS API Gateway. 3 | 4 | ![Telegram Bot](https://c2.staticflickr.com/2/1528/25660481625_8438a20584_o.jpg) 5 | 6 | ## Build Status 7 | [![Build Status](https://github.com/lesterchan/telegram-bot/workflows/GitHub%20CI%2FCD/badge.svg)](https://github.com/lesterchan/telegram-bot/actions) 8 | 9 | ## Setup 10 | I am choosing Asia Pacific (Singapore) region for AWS Lambda and Asia Pacific (Singapore) region for AWS API Gateway. 11 | 12 | ### Telegram 13 | 1. Go to [Telegram Web](https://web.telegram.org/). 14 | 3. Start a chat with [@BotFather](https://telegram.me/BotFather). 15 | 4. Type "/start". 16 | 5. Type "/newbot" to create a new bot. I named my bot "lesterchan_bot". 17 | 6. Note the HTTP API access token that @BotFather will reply you after you created the bot. 18 | 19 | ### Checkout Code 20 | ``` 21 | $ git clone https://github.com/lesterchan/telegram-bot.git 22 | $ cd telegram-bot 23 | $ npm install --production 24 | $ cp token.sample.js token.js 25 | ``` 26 | Open up ```token.js``` and fill in your Telegram HTTP API access token obtained in the first step then run this command: 27 | ``` 28 | $ zip -r telegram-bot.zip *.js node_modules/* 29 | ``` 30 | 31 | ### AWS Lambda 32 | 1. Go to [AWS Lambda](https://ap-southeast-1.console.aws.amazon.com/lambda/home?region=ap-southeast-1). 33 | 2. Click "Get Started Now". 34 | 3. Under the "Select blueprint" screen, search for "hello-world"and you will see the hello-world blueprint which says "A starter AWS Lambda function.". 35 | 4. Click on "hello-world" (NOT "hello-world-python"). 36 | 5. You will be brought to the "Configure Function" page. 37 | 6. Under "Name", you can choose any name for your function. I called it "telegram-bot". 38 | 7. Under "Runtime", ensure it is "Node.js". 39 | 8. Under "Code entry type", choose "Upload a .ZIP file" and click the "Upload" button" to browse for the file "telegram-bot.zip" which you have zipped previously. 40 | 9. Under "Handler", we leave it as "index.handler". 41 | 10. Under "Role", we choose "Basic Execution Role". 42 | 11. You will be brought to a "Role Summary" page. 43 | 12. Under "IAM Role", choose "lambda_basic_execution". 44 | 13. Under "Role Name", choose "oneClick_lambda_basic_execution_.....". 45 | 14. Click "Allow". 46 | 15. You will be brought back to the "Configure Function" page. 47 | 16. Leave "Memory (MB)" as "128MB". 48 | 17. You might want to increase "Timeout" to "15" seconds. 49 | 18. Under VPC, choose "No VPC". 50 | 19. Click "Next". 51 | 20. Click "Create function". 52 | 53 | ### AWS API Gateway 54 | 1. Go to [AWS API Gateway](https://ap-southeast-1.console.aws.amazon.com/apigateway/home?region=ap-southeast-1). 55 | 2. Click "Get Started Now". 56 | 3. Under "API name", enter the name of your API. I will just name it "Telegram Bot". 57 | 4. Click "Create API". 58 | 5. You will be redirected to the "Resources" page. 59 | 6. Click "Create Method" and on the dropdown menu on the left, choose "POST" and click on the "tick" icon. 60 | 7. Now, you will see the "/ - POST - Setup" page on the right. 61 | 8. Under "Integration Type", choose "Lambda Function". 62 | 9. Under "Lambda Region", choose "ap-southeast-1". 63 | 10. Under "Lambda Function", type "telegram" and it should auto-complete it to "telegram-bot". 64 | 11. Click "Save" and "Ok" when the popup appears. 65 | 12. You will be brought to the "/ - POST - Method Execution" Page. 66 | 13. Click "Integration Request". 67 | 14. Click "Mapping Templates" and the section should expand. 68 | 15. Click "Add Mapping Template" and type in "application/json" and click on the "tick" icon. 69 | 16. Under "Input Passthrough" on the right, click on the "pencil" icon. 70 | 16. Choose "Mapping Template" on the dropdown that appears. 71 | 17. Copy and paste ```{"body": $input.json('$')}``` to the template box. 72 | 18. Click on the "tick" icon beside the dropdown once you are done. 73 | 19. Click on "Deploy API" button on the top left. 74 | 20. Under "Deployment Stage", click "New Stage". 75 | 21. Under "Stage Name", I will type in "production". 76 | 22. Click "Deploy". 77 | 23. Note the "Invoke URL" at the top and your API is now live. 78 | 79 | ### Set Telegram Webhook 80 | 1. Replace <ACCESS_TOKEN> with your Telegram HTTP API access token obtained in the first step. 81 | 2. Replace <INVOKE_URL> with your Invoke URL obtained in the previous step. 82 | 3. Run this command: 83 | ``` 84 | $ curl --data "url=" "https://api.telegram.org/bot/setWebhook" 85 | ``` 86 | You should get back a response similar to this: 87 | ``` 88 | $ {"ok":true,"result":true,"description":"Webhook was set"} 89 | ``` 90 | 91 | ### Testing via Telegram 92 | 1. Message your Telegram Bot that you have created. 93 | 2. Type in "/haze" (without the quotes). 94 | 3. You should get back a nicely formatting response as shown in the first screenshot. 95 | 96 | ## Commands 97 | ### Singapore Bus Arrival Timings 98 | Usage: ```/bus ``` 99 | Example: ```/bus 30111 991``` 100 | 101 | Usage: ```/bus ``` 102 | Example: ```/bus 44591``` 103 | 104 | ### Singapore Haze Situation 105 | Usage: ```/haze``` 106 | Example: ```/haze``` 107 | 108 | ### Singapore Weather 2 Hour Forecast 109 | Usage: ```/weather``` 110 | Example: ```/weather``` 111 | 112 | ### IP Information 113 | Usage: ```/ipinfo ``` 114 | Example: ```/ipinfo 8.8.8.8``` 115 | 116 | ### Social Stats Count For Links 117 | Usage: ```/socialstats ``` 118 | Example: ```/socialstats https://lesterchan.net``` 119 | 120 | ## See Also 121 | [Slack Bot using AWS API Gateway and AWS Lamda](https://github.com/lesterchan/slack-bot) 122 | -------------------------------------------------------------------------------- /commands.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Requires 5 | */ 6 | const rp = require('request-promise'); 7 | const moment = require('moment'); 8 | 9 | const config = require('./config'); 10 | const helper = require('./helper'); 11 | 12 | /** 13 | * Commands 14 | */ 15 | module.exports = { 16 | /** 17 | * Fake a error promise 18 | * 19 | * @param {string} error Error Message 20 | * 21 | * @return {object} Rejected Request Promise 22 | */ 23 | error(error) { 24 | return Promise.reject(new Error(error)); 25 | }, 26 | 27 | /** 28 | * Get Bus Arrival Timing 29 | * 30 | * @param {object} commandArguments Command Arguments 31 | * 32 | * @return {object} Request promise 33 | */ 34 | bus(commandArguments) { 35 | const busStopNo = commandArguments[0]; 36 | const busNo = commandArguments[1] || ''; 37 | let busQuery = busStopNo; 38 | 39 | if (busNo !== '') { 40 | busQuery += `/${busNo}`; 41 | } 42 | 43 | return rp({ 44 | uri: `${config.lesterchanApiUrl}/lta/bus-arrival/${busQuery}`, 45 | json: true, 46 | }).then((body) => { 47 | if (body.Services && body.Services.length > 0) { 48 | const busLoad = { 49 | SEA: 'Seats Available', 50 | SDA: 'Standing Available', 51 | LSD: 'Limited Standing', 52 | }; 53 | const busType = { 54 | SD: 'Single Deck', 55 | DD: 'Double Deck', 56 | BD: 'Bendy', 57 | }; 58 | // Fields 59 | const fields = []; 60 | body.Services.forEach((bus) => { 61 | fields.push({ 62 | title: 'Bus', 63 | value: bus.ServiceNo, 64 | }); 65 | 66 | // Bus Arrival Timings 67 | const nextBus = bus.NextBus || ''; 68 | const subBus = bus.NextBus2 || ''; 69 | const followBus = bus.NextBus3 || ''; 70 | 71 | if (nextBus !== '') { 72 | fields.push({ 73 | title: 'Next Bus', 74 | value: `${moment(nextBus.EstimatedArrival).fromNow()} (${busLoad[nextBus.Load]}, ${busType[nextBus.Type]})`, 75 | }); 76 | } else { 77 | fields.push({ 78 | title: 'Next Bus', 79 | value: 'Not Operating Now', 80 | }); 81 | } 82 | 83 | if (subBus !== '') { 84 | fields.push({ 85 | title: 'Subsequent Bus', 86 | value: `${moment(subBus.EstimatedArrival).fromNow()} (${busLoad[subBus.Load]}, ${busType[subBus.Type]})`, 87 | }); 88 | } else { 89 | fields.push({ 90 | title: 'Subsequent Bus', 91 | value: 'Not Operating Now', 92 | }); 93 | } 94 | 95 | if (followBus !== '') { 96 | fields.push({ 97 | title: 'Following Bus', 98 | value: `${moment(followBus.EstimatedArrival).fromNow()} (${busLoad[followBus.Load]}, ${busType[followBus.Type]})`, 99 | }); 100 | } else { 101 | fields.push({ 102 | title: 'Following Bus', 103 | value: 'Not Operating Now', 104 | }); 105 | } 106 | }); 107 | 108 | return helper.formatMessage(`Bus Stop ${body.BusStopCode}`, '', fields); 109 | } 110 | 111 | return 'Bus stop or number is invalid'; 112 | }); 113 | }, 114 | 115 | /** 116 | * Haze 117 | * 118 | * @return {object} Request promise 119 | */ 120 | haze() { 121 | return rp({ 122 | uri: `${config.lesterchanApiUrl}/nea/psipm25`, 123 | json: true, 124 | }).then((body) => { 125 | // Variables 126 | const northPsi = parseInt(body.items[0].readings.pm25_one_hourly.north, 10); 127 | const centralPsi = parseInt(body.items[0].readings.pm25_one_hourly.central, 10); 128 | const eastPsi = parseInt(body.items[0].readings.pm25_one_hourly.east, 10); 129 | const westPsi = parseInt(body.items[0].readings.pm25_one_hourly.west, 10); 130 | const southPsi = parseInt(body.items[0].readings.pm25_one_hourly.south, 10); 131 | const averagePsi = Math.ceil((northPsi + centralPsi + eastPsi + westPsi + southPsi) / 5); 132 | const { timestamp } = body.items[0]; 133 | const niceDate = moment(timestamp).add(8, 'hours'); 134 | 135 | // Fields 136 | const fields = [ 137 | { 138 | title: 'Average', 139 | value: helper.getMessage(averagePsi), 140 | }, 141 | { 142 | title: 'Central', 143 | value: helper.getMessage(centralPsi), 144 | }, 145 | { 146 | title: 'North', 147 | value: helper.getMessage(northPsi), 148 | }, 149 | { 150 | title: 'South', 151 | value: helper.getMessage(southPsi), 152 | }, 153 | { 154 | title: 'East', 155 | value: helper.getMessage(eastPsi), 156 | }, 157 | { 158 | title: 'West', 159 | value: helper.getMessage(westPsi), 160 | }, 161 | ]; 162 | 163 | return helper.formatMessage('Singapore Haze Conditions', `PM2.5 Hourly Update. Last updated at ${niceDate.format(config.defaultDateTimeFormat)}.`, fields); 164 | }); 165 | }, 166 | 167 | /** 168 | * Weather (2 hour Forecast) 169 | * 170 | * @return {object} Request promise 171 | */ 172 | weather() { 173 | return rp({ 174 | uri: `${config.lesterchanApiUrl}/nea/nowcast`, 175 | json: true, 176 | }).then((body) => { 177 | const fields = []; 178 | if (body.items[0].forecasts && body.items[0].forecasts.length > 0) { 179 | body.items[0].forecasts.forEach((nowcast) => { 180 | fields.push({ 181 | title: nowcast.area, 182 | value: helper.getMessage(nowcast.forecast), 183 | }); 184 | }); 185 | } 186 | 187 | return helper.formatMessage('Singapore Weather Conditions', `2 hour Forecast. ${moment(body.items[0].update_timestamp).add(8, 'hours').format(config.defaultDateTimeFormat)}.`, fields); 188 | }); 189 | }, 190 | 191 | /** 192 | * IP Info 193 | * 194 | * @param {object} commandArguments Command Arguments 195 | * 196 | * @return {object} Request promise 197 | */ 198 | ipinfo(commandArguments) { 199 | // Variables 200 | const ip = commandArguments[0] || '127.0.0.1'; 201 | 202 | // Validate IP Address 203 | if (!helper.validateIp(ip)) { 204 | return this.error('Invalid IP'); 205 | } 206 | 207 | return rp({ 208 | uri: `http://ipinfo.io/${ip}/json`, 209 | json: true, 210 | }).then((body) => { 211 | // Fields 212 | const fields = [ 213 | { 214 | title: 'IP', 215 | value: helper.getMessage(body.ip), 216 | }, 217 | { 218 | title: 'Hostname', 219 | value: helper.getMessage(body.hostname), 220 | }, 221 | { 222 | title: 'Country', 223 | value: helper.getMessage(body.country), 224 | }, 225 | { 226 | title: 'City', 227 | value: helper.getMessage(body.city), 228 | }, 229 | { 230 | title: 'Region', 231 | value: helper.getMessage(body.region), 232 | }, 233 | { 234 | title: 'Organization', 235 | value: helper.getMessage(body.org), 236 | }, 237 | ]; 238 | 239 | return helper.formatMessage('IP Information', body.ip, fields); 240 | }); 241 | }, 242 | 243 | /** 244 | * Social Site Sharing Count 245 | * 246 | * @param {object} commandArguments Command Arguments 247 | * 248 | * @return {object} Request promise 249 | */ 250 | socialstats(commandArguments) { 251 | const link = commandArguments[0] || 'https://lesterchan.net'; 252 | 253 | return rp({ 254 | uri: `${config.lesterchanApiUrl}/link?page=${link}`, 255 | json: true, 256 | }).then((body) => { 257 | // Fields 258 | const fields = [ 259 | { 260 | title: 'Total', 261 | value: helper.formatNumber(body.total_count), 262 | }, 263 | { 264 | title: 'Facebook', 265 | value: helper.formatNumber(body.count.facebook), 266 | }, 267 | { 268 | title: 'Twitter', 269 | value: helper.formatNumber(body.count.twitter), 270 | }, 271 | { 272 | title: 'Pinterest', 273 | value: helper.formatNumber(body.count.pinterest), 274 | }, 275 | ]; 276 | 277 | return helper.formatMessage('Link Social Stats', body.url, fields); 278 | }); 279 | }, 280 | }; 281 | --------------------------------------------------------------------------------