Help

Re: ERROR: btoa is not defined

Solved
Jump to Solution
1356 4
cancel
Showing results for 
Search instead for 
Did you mean: 
Nicholas_Payne
4 - Data Explorer
4 - Data Explorer

Hi everyone,

I am trying to automate an SMS using a basic Twilio script that I found here:
Twilio Script

This script works when I run it within the script app, even with my changes made to disable to input text. When I run the same code in the automation run script action I get the error code ‘ReferenceError: btoa is not defined’.

My code is below:

* Twilio send SMS script
 *
 * Send a single SMS to a provided telephone number using the Twilio service
 *
 * The Airtable "Send SMS" Block (available to customers with a "Pro" account)
 * is capable of sending many messages in batches based on the contents of the
 * Airtable Base in which it is installed.
 *
 * **Notes on adapting this script.**
 *
 * The script prompts for input every time it is run. For some users, one or
 * more of these values may be the same with every execution. To streamline
 * their workflow, these users may modify this script by defining the constant
 * values in the first few lines. The values should be expressed as JavaScript
 * strings in the object named `hardCoded`.
 */
'use strict';

/**
 * Users may provide values for any of the properties in the following object
 * to streamline the script's startup.
 */
const hardCoded = {
    twilioAccountSid: 'XXX',
    twilioSendingNumber: 'XXX',
    // Note: the code in Airtable scripts is visible to all users of the
    // Airtable base. By entering the Twilio Auth Token here, all users will
    // have access to that sensitive information.
    receivingNumber: 'XXX',
    messageBody: 'XXX',
    twilioAuthToken: 'XXX'
};

const twilioAccountSid =
    hardCoded.twilioAccountSid || (await input.textAsync('Twilio Account SID'));
const receivingNumber = hardCoded.receivingNumber;
const messageBody = hardCoded.messageBody;
const twilioSendingNumber =
    hardCoded.twilioSendingNumber || (await input.textAsync('Twilio sending telephone number'));
const twilioAuthToken = hardCoded.twilioAuthToken || (await input.textAsync('Twilio Auth Token'));

const url = `https://api.twilio.com/2010-04-01/Accounts/${twilioAccountSid}/Messages.json`;
const headers = {
    // Format the "Authorization" header according to "HTTP Basic Auth"
    Authorization: `Basic ${  btoa(`${twilioAccountSid}:${twilioAuthToken}`)}`,
    // Twilio expects request data to be formatted as though it were submitted
    // via an HTML form
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
};
const body = new URLSearchParams({
    From: twilioSendingNumber,
    To: receivingNumber,
    Body: messageBody
});

let result;

try {
    let response = await fetch(url, {
        method: 'POST',
        headers,
        body
    });

    if (!response.ok) {
        result = `Error sending SMS: "${await response.text()}"`;
    } else {
        result = 'SMS sent successfully.';
    }
} catch (error) {
    result = `Error sending SMS: "${error}"`;
}

output.text(result);

Can anyone provide me with some help here?

Thank you

1 Solution

Accepted Solutions
kuovonne
18 - Pluto
18 - Pluto

Welcome to the Airtable community.

You have run into one of the differences between scripting app and automation action scripts. You can’t use btoa in an action script. No idea why.

You can run the btoa conversion in scripting app, output the resulting string, and substitute that in place of the btoa in this automation script.

See Solution in Thread

6 Replies 6
kuovonne
18 - Pluto
18 - Pluto

Welcome to the Airtable community.

You have run into one of the differences between scripting app and automation action scripts. You can’t use btoa in an action script. No idea why.

You can run the btoa conversion in scripting app, output the resulting string, and substitute that in place of the btoa in this automation script.

Welcome to the community, @Nicholas_Payne! :grinning_face_with_big_eyes: If @kuovonne’s answer solved your problem, please mark her comment as the solution to your question. This helps others who may be searching with similar questions. Thanks!

Rasmus
5 - Automation Enthusiast
5 - Automation Enthusiast

Hi Kuovonne,

Thanks for your help in this community. I was wondering if you could help elaborate on how we can output the resulting string? I am getting the same error and have posted my question in this thread: https://community.airtable.com/t5/automations/referenceerror-btoa-is-not-defined/m-p/168924

I used ChatGPT to fix my btoa error with TinyPNG. My use case was to upload images to a column called IMAGE in Airtable, then have TinyPNG auto compress them and then output the results to a different column called IMAGE COMPRESSION. ChatGPT actually worked. Every error Airtable gave me, I pasted it into ChatGPT, it rewrote the code and at one point it seemed as though it got frustrated and wrote a very long code! Pasted and it worked! This script was for automation purposes. If you want to see the script, let me know. I'm actually thinking to make a YouTube video about this.

Hi Aundre,

That sounds amazing. I'd love to see the script - and a video too!

Hey Rasmus,

I'll see what I can do. I've never done a public YouTube video, but hey! 😅

The Script:

 
// Replace 'xxx' with your actual TinyPNG API key
const tinyPngApiKey = 'xxx';
const airtableAttachmentSource = 'Image';
const airtableAttachmentDestination = 'Image Compression';

// Access the "Inventory - Factory" table and "Image Compression" view
const table = base.getTable('Inventory - Factory');
const view = table.getView('Image Compression');

// Retrieve records from the specified view
const queryResult = await view.selectRecordsAsync();

// Function to encode a string to Base64 without using btoa
function encodeToBase64(str) {
const base64Chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let result = '';

for (let i = 0; i < str.length; i += 3) {
const group = (str.charCodeAt(i) << 16) | (str.charCodeAt(i + 1) << 8) | str.charCodeAt(i + 2);

result +=
base64Chars.charAt((group >> 18) & 63) +
base64Chars.charAt((group >> 12) & 63) +
base64Chars.charAt((group >> 6) & 63) +
base64Chars.charAt(group & 63);
}

return result;
}

// Function to adjust orientation based on the original image
async function adjustOrientation(record, compressedImageUrls) {
const originalAttachments = record.getCellValue(airtableAttachmentSource);
const originalOrientation = originalAttachments && originalAttachments[0] ? originalAttachments[0].orientation : null;

if (originalOrientation) {
// Adjust the orientation of each compressed image URL
compressedImageUrls.forEach((compressedImage) => {
compressedImage.orientation = originalOrientation;
});
}
}

// Function to compress and save images for a record
async function compressAndSaveImages(record) {
const attachments = record.getCellValue(airtableAttachmentSource);
let compressedImageUrls = [];

if (attachments && attachments.length > 0) {
// Iterate through each attachment in the field
for (let [i, attachment] of attachments.entries()) {
const recordAttachmentUrl = attachment['url'];

console.log(`Compressing ${record.getCellValue(airtableAttachmentSource)} (Image ${i + 1})`);

const apiKey = 'api:' + tinyPngApiKey;
const encodedApiKey = encodeToBase64(apiKey);

const request = await fetch('https://api.tinify.com/shrink', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + encodedApiKey,
},
body: JSON.stringify({ source: { url: recordAttachmentUrl } }),
});

const json = await request.json();

// Checks that the API didn't fail
if (request.status == 201) {
const compressedImageUrl = json.output.url;
compressedImageUrls.push({ url: compressedImageUrl });
}
}

// Update the record with all the compressed image URLs
await table.updateRecordAsync(record.id, {
[airtableAttachmentDestination]: compressedImageUrls,
});

// Adjust the orientation of compressed images based on the original image
await adjustOrientation(record, compressedImageUrls);

console.log(`Images compressed and saved for record: ${record.id}`);
}
}

// Process each record in the view
for (const record of queryResult.records) {
await compressAndSaveImages(record);
}