Have you ever wanted to convert currencies instantly while in Google Chat with colleagues and clients? In this tutorial, we are going to build a Currency Converter Google Chat App with Google Apps Script to do just that.
This tutorial is a multi-media series containing step-by-step video instructions for each stage of the process along with code snippets for each stage, important links and some further details on the more unusual parts of the code.
We start off our project adventure having already made a copy of the Google Apps Script Chat App project, connecting it to a Google Cloud Platform (GCP) project and deploying it for the first time. You can find out how to set up this stage by heading over to the Google Chat Apps for Google Apps Script Developers tutorial or directly to the YouTube tutorial.
It is important to note that Chat Apps and their development are only available for Google Workspace paid accounts.
Table of Contents
What we are Building
In this tutorial, we will be creating a Currency Converter chat app that generates a currency conversion based on a slash command inside Google Chat or Space. The syntax will be:
/xe amount from_currency_code:to_currency_code
For example:
/xe 1,230.95AUD:USD
This will return
1,230.95 AUD = 795.2 USD
1 AUD = 0.64601 USD
1 USD = 1.54798 AUD(Last updated: 2022-10-07)
We will also provide two more slash commands:
- /xe-help – This will provide instructions for the user on how to enter a conversion.
- /xe-list – This will provide a list of all currency codes.
Need help with Google Workspace development?
Go something to solve bigger than Chat GPT?
I can help you with all of your Google Workspace development needs, from custom app development to integrations and security. I have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
Schedule a free consultation today to discuss your needs and get started or learn more about our services here.
1. Create the /xe and /xe-help slash commands
In this first part of the tutorial series, we need to create two main slash commands /xe and /xe-help. To do this we will update the onMessage()
trigger function. Then we will connect the slash commands in the GCP Google Chat API configuration GUI.
We will also update the Google Apps Script Chat App template to return messages more specific to our currency converter.
Video 1
If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.
The Code
Code.gs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
/** * Responds to a MESSAGE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onMessage(event) { console.log(JSON.stringify(event, null, " ")) const message = event.message; if(message.slashCommand){ switch (message.slashCommand.commandId){ case 1: // /xe return {"text": attemptConversion(message.text)}; case 2: // /xe-help return {"text": conversionHelp()} } } } /** * Responds to an ADDED_TO_SPACE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onAddToSpace(event) { var message = ""; if (event.space.singleUserBotDm) { message = "Currency Converter bot added to DM. Thanks, " + event.user.displayName + "! \n\n" + errorInstructions; } else { message = "Currency Converter bot added to " + (event.space.displayName ? event.space.displayName : "this chat. \n\n" + errorInstructions); } if (event.message) { // Bot added through @mention. message = message + " and you said: \"" + event.message.text + "\" \n\n" + errorInstructions; } return { "text": message }; } /** * Responds to a REMOVED_FROM_SPACE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onRemoveFromSpace(event) { console.info("Currency Converter Bot removed from ", (event.space.name ? event.space.name : "this chat")); } |
CurrencyGlobals.gs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Error instrucions applied to Errors returned to the user. */ const errorInstructions = `To make a conversion: 1. Type */xe* 2. Select the amount to convert. 3. The origin currency as a 3-letter code. 4. Add a colon: 5. The currency to convert to as a 3 letter code. e.g. _/xe 1,000.00USD:EUR_ You can always retrieve this help at any time by using the */xe-help* slash command. ` |
SlashCommands.gs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * Attempts to convert amounts and currency codes that the users submits to * the chatbot after validating. * @param {String} text input from user in chat after add the /xe slash command. * @returns {String} Either currency conversion or Error text. */ function attemptConversion(text) { // NOTE just a placemarker for this stage of the tutorial we will update this soon. return `xe text = "${text}"` }; /** * Returns a string containing usage instructions for the currency converter chat app. * @returns {String} Help title plus error instructions. */ function conversionHelp(){ return errorInstructions; } |
appsscript.json
In your appsscript.json file, add the following:
1 2 3 |
"chat": { "addToSpaceFallbackMessage": "Chat App now added. Thanks!" } |
For me this would look like this:
1 2 3 4 5 6 7 8 9 |
{ "timeZone": "Australia/Sydney", "dependencies": {}, "exceptionLogging": "STACKDRIVER", "runtimeVersion": "V8", "chat": { "addToSpaceFallbackMessage": "Chat App now added. Thanks!" } } |
2. Connecting our Google Apps Script to the Exchange Rates Data API
At this stage of the tutorial, we will build our connector to our currency exchange API. We will create a quasi-class (CurrencyAPI()
) with a method to get a list of all currencies (.getCurrencyList()
) and retrieve a currency conversion (.convertCurrency())
.
Accessing the API is done through the Google Apps Script UrlFetchApp Class with the .fetch()
method. This will return two important methods worth noting:
.getContentText()
– The returned text from the fetch. For us, this will be a stringified JSON..getResponseCode()
– The response code. 200 to indicate a good response and the rest of the codes are errors. You can see the full list of error codes from the API here.
After that, we will need a way to check for any error codes that our fetch request may generate. We will create a private function for this to return either the text if the request is successful or error information.
We will be connecting to the Exchange Rates Data API. The API has a free tier of 250 requests each month. There is no requirement for a credit card or anything.
Video 2
Tutorial Links – For part 2
- Exchange Rates Data API main info page
- Exchange Rates Data API – List of Error Codes
- The Documentation page
- The Live Demo page – where I found my API key
- Pricing Page we added at 11:12 of the tutorial
The Code
CurrencyAPI.gs
Create this file to store your Currency API connector.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
/** * Clearing house for call to the Exchange Rates Data API https://apilayer.com/marketplace/exchangerates_data-api# */ function CurrencyAPI(){ const url = "https://api.apilayer.com/exchangerates_data/"; const headers = { "apikey": "1Wrt55***NONE FOR YOU!!!***Plr3w21" } const options = { method: 'GET', followRedirects: true, muteHttpExceptions: true, headers } const publicAPI = {}; /** * Checks the response code and returns error message or data respecive. * @param {UrlFetchApp.HTTPResponse} The HTTP response. * @returns {Object|String} Response Data Object if true or | Error message if false. */ function checkResponse(resp, functionCalled){ switch(resp.getResponseCode()){ case 200: return JSON.parse(resp.getContentText()); case 429: console.error(`ERROR: Out of JUICE Resp Code: 429 Message: ${resp.getContentText()} Call: ${functionCalled}`) return `ERROR: Oh no! We've maxed out our requests to the Exchange Rates API. Maybe it's time for an upgrade? UPGRADE NOW >> https://apilayer.com/marketplace/exchangerates_data-api?preview=true#pricing` default: console.error(`ERROR: Resp Code: ${resp.getResponseCode()} Message: ${resp.getContentText()} Call: ${functionCalled}`) return `ERROR: Something happened connecting to the Exchange Rates API. Please wait a few and try again. If the error persists, please contact your friendly dev. \n\n${errorInstructions}` } }; /** * Get Complete currency list for the Currency API * @returns {Object|String} Object of all symbols {"USD": "United States Dollar"} or String Error "Error: ..." */ publicAPI.getCurrencyList = () => { const resp = UrlFetchApp.fetch(url+'symbols', options); const result = checkResponse(resp, 'getCurrencyList'); return result.hasOwnProperty("symbols")? result.symbols : result; }; /** * Get Complete currency list for the Currency API * @param {String} source - 3-letter source currency. * @param {String} destination - 3-letter destination currency. * @param {String} amount - floating point number as a string. * @param {String} [date] - date as a string (format YYYY-MM-DD) * @returns {Object|String} Object of all symbols {"USD": "United States Dollar"} or String Error "Error: ..." */ publicAPI.convertCurrency = (source, destination, amount, date = null) => { let urlInput = `${url}convert?to=${destination}&from=${source}&amount=${amount}` urlInput = (date)? `${urlInput}&date=${date}` : urlInput; const resp = UrlFetchApp.fetch(urlInput, options); return checkResponse(resp, 'convertCurrency'); }; return publicAPI; }; |
Test.gs
Create this file to use for testing certain stages of your project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function getCurrencyList_test() { const list = CurrencyAPI().getCurrencyList(); console.log(list) } function convertCurrency_test(){ const list = [ ["AUD", "USD", "20.50", "2000-10-01"], ["EUR", "USD", "1423.22"], ["AUX", "EUR", "123.45"] ]; list.forEach(item => { const result = CurrencyAPI().convertCurrency(...item); console.log(result) }) }; |
CurrencyGlobals.gs – Add to.
Add the currency code to the bottom of this file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
//List of all available currencies in the API const CurrencyCodes = { AED: 'United Arab Emirates Dirham', AFN: 'Afghan Afghani', ALL: 'Albanian Lek', AMD: 'Armenian Dram', ANG: 'Netherlands Antillean Guilder', AOA: 'Angolan Kwanza', ARS: 'Argentine Peso', AUD: 'Australian Dollar', AWG: 'Aruban Florin', AZN: 'Azerbaijani Manat', BAM: 'Bosnia-Herzegovina Convertible Mark', BBD: 'Barbadian Dollar', BDT: 'Bangladeshi Taka', BGN: 'Bulgarian Lev', BHD: 'Bahraini Dinar', BIF: 'Burundian Franc', BMD: 'Bermudan Dollar', BND: 'Brunei Dollar', BOB: 'Bolivian Boliviano', BRL: 'Brazilian Real', BSD: 'Bahamian Dollar', BTC: 'Bitcoin', BTN: 'Bhutanese Ngultrum', BWP: 'Botswanan Pula', BYN: 'New Belarusian Ruble', BYR: 'Belarusian Ruble', BZD: 'Belize Dollar', CAD: 'Canadian Dollar', CDF: 'Congolese Franc', CHF: 'Swiss Franc', CLF: 'Chilean Unit of Account (UF)', CLP: 'Chilean Peso', CNY: 'Chinese Yuan', COP: 'Colombian Peso', CRC: 'Costa Rican Colón', CUC: 'Cuban Convertible Peso', CUP: 'Cuban Peso', CVE: 'Cape Verdean Escudo', CZK: 'Czech Republic Koruna', DJF: 'Djiboutian Franc', DKK: 'Danish Krone', DOP: 'Dominican Peso', DZD: 'Algerian Dinar', EGP: 'Egyptian Pound', ERN: 'Eritrean Nakfa', ETB: 'Ethiopian Birr', EUR: 'Euro', FJD: 'Fijian Dollar', FKP: 'Falkland Islands Pound', GBP: 'British Pound Sterling', GEL: 'Georgian Lari', GGP: 'Guernsey Pound', GHS: 'Ghanaian Cedi', GIP: 'Gibraltar Pound', GMD: 'Gambian Dalasi', GNF: 'Guinean Franc', GTQ: 'Guatemalan Quetzal', GYD: 'Guyanaese Dollar', HKD: 'Hong Kong Dollar', HNL: 'Honduran Lempira', HRK: 'Croatian Kuna', HTG: 'Haitian Gourde', HUF: 'Hungarian Forint', IDR: 'Indonesian Rupiah', ILS: 'Israeli New Sheqel', IMP: 'Manx pound', INR: 'Indian Rupee', IQD: 'Iraqi Dinar', IRR: 'Iranian Rial', ISK: 'Icelandic Króna', JEP: 'Jersey Pound', JMD: 'Jamaican Dollar', JOD: 'Jordanian Dinar', JPY: 'Japanese Yen', KES: 'Kenyan Shilling', KGS: 'Kyrgystani Som', KHR: 'Cambodian Riel', KMF: 'Comorian Franc', KPW: 'North Korean Won', KRW: 'South Korean Won', KWD: 'Kuwaiti Dinar', KYD: 'Cayman Islands Dollar', KZT: 'Kazakhstani Tenge', LAK: 'Laotian Kip', LBP: 'Lebanese Pound', LKR: 'Sri Lankan Rupee', LRD: 'Liberian Dollar', LSL: 'Lesotho Loti', LTL: 'Lithuanian Litas', LVL: 'Latvian Lats', LYD: 'Libyan Dinar', MAD: 'Moroccan Dirham', MDL: 'Moldovan Leu', MGA: 'Malagasy Ariary', MKD: 'Macedonian Denar', MMK: 'Myanma Kyat', MNT: 'Mongolian Tugrik', MOP: 'Macanese Pataca', MRO: 'Mauritanian Ouguiya', MUR: 'Mauritian Rupee', MVR: 'Maldivian Rufiyaa', MWK: 'Malawian Kwacha', MXN: 'Mexican Peso', MYR: 'Malaysian Ringgit', MZN: 'Mozambican Metical', NAD: 'Namibian Dollar', NGN: 'Nigerian Naira', NIO: 'Nicaraguan Córdoba', NOK: 'Norwegian Krone', NPR: 'Nepalese Rupee', NZD: 'New Zealand Dollar', OMR: 'Omani Rial', PAB: 'Panamanian Balboa', PEN: 'Peruvian Nuevo Sol', PGK: 'Papua New Guinean Kina', PHP: 'Philippine Peso', PKR: 'Pakistani Rupee', PLN: 'Polish Zloty', PYG: 'Paraguayan Guarani', QAR: 'Qatari Rial', RON: 'Romanian Leu', RSD: 'Serbian Dinar', RUB: 'Russian Ruble', RWF: 'Rwandan Franc', SAR: 'Saudi Riyal', SBD: 'Solomon Islands Dollar', SCR: 'Seychellois Rupee', SDG: 'Sudanese Pound', SEK: 'Swedish Krona', SGD: 'Singapore Dollar', SHP: 'Saint Helena Pound', SLL: 'Sierra Leonean Leone', SOS: 'Somali Shilling', SRD: 'Surinamese Dollar', STD: 'São Tomé and Príncipe Dobra', SVC: 'Salvadoran Colón', SYP: 'Syrian Pound', SZL: 'Swazi Lilangeni', THB: 'Thai Baht', TJS: 'Tajikistani Somoni', TMT: 'Turkmenistani Manat', TND: 'Tunisian Dinar', TOP: 'Tongan Paʻanga', TRY: 'Turkish Lira', TTD: 'Trinidad and Tobago Dollar', TWD: 'New Taiwan Dollar', TZS: 'Tanzanian Shilling', UAH: 'Ukrainian Hryvnia', UGX: 'Ugandan Shilling', USD: 'United States Dollar', UYU: 'Uruguayan Peso', UZS: 'Uzbekistan Som', VEF: 'Venezuelan Bolívar Fuerte', VND: 'Vietnamese Dong', VUV: 'Vanuatu Vatu', WST: 'Samoan Tala', XAF: 'CFA Franc BEAC', XAG: 'Silver (troy ounce)', XAU: 'Gold (troy ounce)', XCD: 'East Caribbean Dollar', XDR: 'Special Drawing Rights', XOF: 'CFA Franc BCEAO', XPF: 'CFP Franc', YER: 'Yemeni Rial', ZAR: 'South African Rand', ZMK: 'Zambian Kwacha (pre-2013)', ZMW: 'Zambian Kwacha', ZWL: 'Zimbabwean Dollar' } |
3. Connecting our Google Apps Script to the Exchange Rates Data API
Before sending our slash command info from our Google Chat App to the API to retrieve the currency conversion, we need to ensure that the user has provided valid input.
The expected input from the user is as follows:
/xe [amount]source_currrency_code:destination_currency_code
For example,
/xe 115.44AUD:USD
Note! Before we dive into our validation, it’s important for me to point out that I am basing my validation rule on the UK and US English separator convention of 1,000,000.00 or 1000000.00. Please modify the rules to meet your own country’s requirements.
What we will allow
It’s okay, particularly when working with text inputs, to be a little flexible in how a user might input their currency conversion.
If you have ever gone on Google search and run a currency conversion, you know that you can make a number of combinations to generate a currency conversion.
While coding out the full extent of Google’s allowable types would be far too complex and perhaps, dare I say, boring, we can provide a little help. Here is what we can do to support user input variation:
- Allow for any number before the currency code section. E.g. 1 or 1,110,00 or $2300.00.
- Allow for the use of commas or no commas in the amount that users input. E.g. 1,000,000,000.00 or even mistakes 1,1,1,222,1,.00.
- Provide some spacing flexibility between:
- The amount and the currency code section. E.g. 20 AUD:USD or 20AUD:USD.
- The source currency code and the colon ( Up to 3 spaces should be enough). E.g. 20AUD :USD or 20AUD :USD or 20AUD:USD.
- The colon and the destination currency code (Again, up to 3 spaces should be adequate). E.g. 20AUD: USD or 20AUD: USD or 20AUD:USD
- Permit lowercase currency code or mixed case code. It costs us nothing to convert everything using the toUpperCase() JavaScript method. E.g. 20aud:UsD.
What will generate an error
On the other hand, there are some necessary items for us to send to the API in order for it to respond successfully with currency conversion. This means we should handle:
- Missing number. No number no conversion. Error e.g. /xe or /xe AUD:EUR
- Multiple decimal places. It is hard for us to guess where the user intended to add their decimal place so we need to return an error here. Error e.g. 2,333.21.24 or E.g. 2..34.561.01.
- Ridonculous amount. 🤯 Extreme amounts may be difficult for the API to handle are likely someone is being a little silly. We should respond in kind. Error e.g. 1126545465165198998118991981891.1656116165165156165165161651165
- Non-3 letter currency codes. All currency codes are 3 letters in length. Error e.g. 2A:USD or 4AUD:US
- Missing source and destination currency code or colon. If we don’t have a source or a destination code we can’t convert anything. Error e.g. 2:EUR or 2AUD or 2AUD:.
- Erroneous currency codes. We should check with our stored
CurrencyCodes
list before we waste valuable requests with the Currency Exchange API. Error e.g. 2XXX:USD or 2AUD:YYY.
Setting up the code
We will create the validateAndRetrieveCurrenciesAndAmount(text)
function to handle our validation. This will be called from the attemptConversion()
function after it receives the text from the /xe
slash command.
Inside our validation function, we will extract our amount and currencies separately. This is because they require us to look at different things to ensure that they are accurate and ready to be sent to the API. This also helps us vary the spacing between the amount and the currency codes should they add a space.
It is much less costly and more efficient for us to run validation Apps Script-side rather than lose a request credit and let the API handle the error.
If we discover an error in the user’s input, we will return a text string to then containing information about the nature of the error. We will also include our instruction information contained in the errorInstructions
variable.
If the user successfully enters their currency code, then our validation function will return an object containing the amount as a float, the source currency code and the destination currency code.
conversion = {source, destination, amount}
Regular Expressions
We will be using a variety of regular expression rules to achieve the majority of our validation here.
Because they can be a little tricky we will explain them here in a little more detail:
- Extract the amount:
/([\d.,]+)/
:[]
– Indicates a character class or range to look for.\d
– Search for any digit..,
– Search for any decimal (.) or comma (,)()
– Ensures that all elements are captured in a group where we can apply a quantifier to it like we have with the plus symbol.+
– matches one or more occurrences of the selected characters.
- All periods or decimal symbols:
/\./g
:\.
– Search for a period./g
– The global flag matching all occurrences of the selected search.
-
From and to currency code range:
/[A-Za-z]{3}[\s]{0,3}:[\s]{0,3}[A-Za-z]{3}/
[A-Za-z]
– Character class searching for any character within the alphabet with either upper or lower case.{3}
– Curly braces indicate a match of a specific number of times. If the braces have one argument it must strictly meet that number of occurrences.[\s]
– Character class search for spaces.{0,3}
– Matching a range of the preceding character or character class between two values.:
– Match a colon.
- Get each currency code:
/[A-Za-z]{3}/g
:[A-Za-z]{3}
– The 3-letter code containing any letter from A to Z in any case./g
-Any occurrence of the selected search item.
Video 3
The Code – SlashCommands.gs
attemptConversion()
Remove the placemarker: return
.xe text = "${text}"
Add:
1 2 3 4 5 6 7 8 9 |
function attemptConversion(text) { // If success Object else String const conversion = validateAndRetrieveCurrenciesAndAmount(text); // Make the currency conversion. return `xe text = "${(typeof conversion === "string")? conversion: JSON.stringify(conversion)}"` }; |
validateAndRetrieveCurrenciesAndAmount(text)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
/** * Validates the text input and returns object containing the start and end conversion and amount. * @param {string} text - text input containg conversion request. * @returns {String | Object} if error "Error: info" else {amount<Number>, source<String>, destination<String>} */ function validateAndRetrieveCurrenciesAndAmount(text){ // Extract the amount const amount = (() => { const regex = /([\d.,]+)/; // Immediately Invoke anonymous function. const textAmount = text.match(regex) // All numbers commas and decimals. // If no number, return error. if(!textAmount) return "Error: no amount detected. \n\n" + errorInstructions; // More than one decimal. if((textAmount[0].match(/\./g) || []).length > 1) {return "Error: Decimal overload!!! One or none, please \n\n" + errorInstructions; }; // Check for redonculous lenghts. if(textAmount[0].length > 21){ return "Error: Wow, bud! That's some crazy moulah! 💰💰💰. It's too much for me to comprehend!\n\n"+ errorInstructions; } // Remove commas and parse as a float const floatAmount = parseFloat(textAmount[0].replaceAll(",", "")); return floatAmount })(); if(typeof amount === "string") return amount; // Extract currencies as Obj const currencies = (() => { // find the currencies between the colon (:) e.g. AUD:USD (Allow for upt to 3 spaces between the code and the colon) const regex = /[A-Za-z]{3}[\s]{0,3}:[\s]{0,3}[A-Za-z]{3}/ const currencyGroup = text.match(regex); if(!currencyGroup) return "Error: Missing 3-letter currency code or colon (:).\n\n"+ errorInstructions; // Match 3-letter currency code. const regexCurrency = /[A-Za-z]{3}/g const cur = currencyGroup[0].match(regexCurrency); // Store codes as object. const currency = { source: cur[0].toUpperCase(), destination: cur[1].toUpperCase() } // Validate if currency codes are available. if(!CurrencyCodes.hasOwnProperty(currency.source)) { return `Error: No source currency with the code ${currency.source}. Please check the */xe-list* to see what currencies are available\n\n`+ errorInstructions; } // Validate if currency codes are available. if(!CurrencyCodes.hasOwnProperty(currency.destination)) { return `Error: No destination currency with the code ${currency.destination}. Please check the */xe-list* to see what currencies are available\n\n`+ errorInstructions; } return currency; })(); if(typeof currencies === "string") return currencies; const conversion = {...currencies, amount} return conversion; }; |
4. Connecting /xe slash command to the API and validation
Now we finally get to deploy our /xe slash command and get some results.
First, we need to update the returned item in our attemptConversion()
function with the currencyConversion(
) function (see code below).
The currency conversion function will call the Exchange Rate API via our CurrencyAPI().convertCurrency()
method. If successful, it will return the currency based on the inputs we have validated and send it as part of the payload to the API.
We could simply return a value (e.g. xe/ 10AUD:USD = 6.19865 ) but that does not provide a lot of context for our users instead we want to provide something with a bit more valuable that will include:
- The returned result: 10 AUD = 6.19865 USD
- 1 source value = n destination value: 1 AUD = 0.619865 USD
- 1 destination value = n source value: 1 USD = 1.613254 AUD
- The date the exchange rate was found: (Last updated: 2022-10-16)
We can retrieve all but one bit of these from the object that is returned from our request:
1 2 3 4 5 |
{ success: true, query: { from: 'AUD', to: 'EUR', amount: 22.5 }, info: { timestamp: 1665622924, rate: 0.647157 }, date: '2022-10-13', result: 14.561033 } |
The only thing we need to work out is the conversion of 1 destination value base to the source. We can do this by dividing 1 by the exchange rate.
1/result.info.rate
However, there is a spanner in our works…
JavaScript Decimal Rounding Errors
The Problem
Our exchange rate returns a value up to 6 decimal places (e.g. 1.123456). This is more than enough to get a fine-grained indication of the exchange rate. Besides, it would look pretty message with a huge string of trailing decimal digits.
A problem arises in Javascript when we try and round up a value using the JavaScript toFixed() method.
Let’s say we have the number, 5.5555555, and we want to round up from 7 decimal places to 6. Our primary school education taught us that this should be 5.555556. However, using the toFixed()
method we get. 5.555555. If we were to increase the number in the seventh decimal position to 6, 7, 8, or 9 all would be right in the world and it will round up as expected.
1 2 3 |
const weridNumber = 5.5555555; parseFloat(weirdNumber).toFixed(); // 5.555555 <<totally weird!!! |
The Solution
How do we resolve this?
I found a really good solution shared by George Birbilis in StackOverflow. It does, however, warrant some explanation. Here is my version of the code:
+(Math.round(Number(weirdNum + "e+6")) + "e-6")
The ‘e’ here represents the exponent value. You will often see this when you are logging a huge number in JavaScript. It’s a kind of short-hand version.
For example, 5e6 would be:
= 5 * 10^6 (or to be more granular 5 * 10^+6)
= 5 * 10 * 10 * 10 * 10 * 10
= 5 * 1,000,000
= 5,000,000
So when we convert our weirdNumber
variable plus “e+6” with the Number constructor we are moving it left 6 decimal places.
= Number(weirdNum + “e+6”)
= Number(5.5555555 + “e+6”)
= Number(“5.5555555e+6”)
= 5555555.5
Now we can use the JavaScript Math.round()
method to round the last decimal place to the correct value.
= Math.round(5555555.5)
= 5,555,556
Next, we need to convert the number back to the correct decimal value by reversing the exponent value we set:
= +(5,555,556 + “e-6”)
Note the plus symbol at the start of the braces, this is a sneaky way of conversing an exponent number text string to a number.
Video 4
Video released 19 Oct 2022.
The Code
SlashCommands.gs
attemptConversion(text)
function replace:return xe text = "${(typeof conversion === "string")? conversion: JSON.stringify(conversion)}"
return currencyConversion(conversion);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * Calls the currency converter API and returns string containing the conversion * and the single dollar conversion, from > to and to > from. * * @param {Object} conversion - Curren conversion request. * @param {String} conversion.source - 3-letter currency code for source. * @param {String} conversion.destination - 3-letter currency code for desination. * @param {Number} conversion.amount - Amount to convert. * @returns {String} Either 'Error: ...' or String containing converstion to return to chat. */ function currencyConversion(conversion){ const result = CurrencyAPI().convertCurrency(conversion.source, conversion.destination, conversion.amount); // Return user error message if something happened when connecting with the API. if(typeof result === "String") return result; // Round destination to source rate to 6 decimal places. const destToSrcRate = +(Math.round((1/result.info.rate) + "e+6") + "e-6"); const message = `${conversion.amount} ${conversion.source} = ${result.result} ${conversion.destination} _1 ${conversion.source} = ${result.info.rate} ${conversion.destination}_ _1 ${conversion.destination} = ${destToSrcRate} ${conversion.source}_ _(Last updated: ${result.date})_ ` return message; } |
If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.
5. A Google Chat App Card for the list of currency codes
There are hundreds of currency codes that the users may wish to draw from and we can’t expect them to memorise them all. The easiest approach we have to support our users here is to provide them with a /xe-list slash command in their Google Chat or Space.
When the user returns the slash command, they will get a stylised card response:
This looks a lot nicer than listing all the currency codes in a message.
This time around instead of returning a text object property we will be returning a card version 2 property.
Cards version 2 JSON
We will need to create a JSON to send to the Chat API to construct our card.
The card contains a header property and a section property.
In our project, we style our header with a title, subtitle and image. Also, note that cards can contain stylable headers as well if you choose to use them.
The section sub-object contains an array of all of the sections that you want to add. Sections provide visual separation in the card and are useful for us to separate our currency codes by letters of the alphabet for ease of reading.
Inside each section, you can add a number of widgets set as an array. There are heaps of widgets to choose from that we noted in our Google Chat App for Developers tutorial.
From the widgets list, we used the Decorated Text widget. It has a wide range of uses from button clicks and better styling to adding icons and even switch controls.
We only needed to use the top label property to add our letter and then generate our list of currency codes and their descriptions for that letter using the text property.
Image link
If you want to use the same image in your project you can find it here:
https://images.unsplash.com/photo-1599930113854-d6d7fd521f10?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=880&q=80
The image is by globe maker, Gaël Gaborel.
Video 5
Video released 22 Oct 2022.
The Code
Code.gs – onMessage()
Add a third switch option for /xe-list
1 2 3 4 |
onMessage()" >... case 3: // /xe-list return createListCardResponse(); ... |
Don’t forget that you will need to go to your GCP console for your project. Select APIs and Services. Then scroll down to find your Chat API.
In the Chat API, select configuration. Scroll down to the Slash commands select and add a new slash command.
The details will be:
- Name: /xe-list
- Command ID: 3
- Description: List of all currency conversion codes.
Select ‘Done’ and then save the configuration.
CurrencyCardList.gs
Create a new file called CurrencyCardList.gs. Here you will add the following function.
createListCardResponse()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
/** * Creates a card-formatted response containing a list of all * currencies sectioned by letter of alphabet. * @return {object} JSON-formatted response */ function createListCardResponse() { // Header const header = { "title": "List of all Available currencies.", "subtitle": "In order of currency code.", "imageUrl": "https://images.unsplash.com/photo-1599930113854-d6d7fd521f10?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=880&q=80", "imageType": "CIRCLE" }; // Store sections let sections = []; // Working variables let letter = "A"; // A is the first letter set to create as a section top label. let listByLetter = ""; // e.g. "AFN | Afghan Afghani\nALL: Albanian Lek\n..." let counter = 1; const size = Object.keys(CurrencyCodes).length; // Separate each section by starting letter. for(var cur in CurrencyCodes){ // THIS IS THE NEW SECTION CONDITION // If the first letter of the key does not match the current 'letter' create a new section. // Of if we are the last item in the object. (Will grab "z" and add the section header and add the listByLetter items.) if(cur[0] !== letter || size === counter){ const widget = { "decoratedText":{ "topLabel": letter, "text": listByLetter, "wrapText": true } } sections.push({"widgets":[widget]}) letter = cur[0]; // Set the new letter. listByLetter = ""; // Refresh the list by letter. } // If there is not change to the letter keep adding to list listByLetter += `${cur} | ${CurrencyCodes[cur]}\n` counter ++; }; // Returns the basic outer structure with varialbes to the header and sections return { cards_v2: [{ cardId: "currencyList", card: { header, sections } }] }; }; |
Conclusion
That wraps up our Currency Converter Google Chat App built-in Google Apps Script.
There are a bunch of further directions we could go with the chatbot. We could add an API key input dialogue for each user to add their own API key for the Exchange Rage API.
Alternatively, we could create a customisation dialogue that will allow the user to create a custom display format and input type for their specific region. After all, not all currencies are written the same in different countries.
Another thing we could do is to create a dialogue when users just add /xe instead of appending an amount, source and destination code. Then we could rely on selection boxes for users to choose their currencies, and even add a date.
What else can you think of to improve this project? I would love to hear in the description below.
This was an enormous project to put together taking up several months of preparation, content writing and video creation. I hope you got something out of it.
If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.
What to take things further? Check out my tutorial on building webhooks for Google Chat:
~Yagi
What about debugging the code. Anyway to set breakpoints and see what’s happening to ensue the script logic is accurate?
It’s a bit of a challenge with this kind of frontend-backend setup, but breaking your backend into functional components for ease of testing will go some ways to help you here. Further, you can debug these backend processes, by checking breakpoint lines and running debug on test functions in the IDE.
The front-end component (parsing the chatapp object) is trickier to debug. You could develop using staging points, incrementally adding elements to your chatapp object. That’s generally how I do it. Along with a fair bit of manual testing.
Error messages here are not hugely helpful at the time of this reply unfortunately.