0

Explanation:

I have a Google Apps Script project that automatically scans a bunch of rows and invites the email addresses contained in each row. It knows which set of invites to send the email addresses to row by row depending on the label in each row. Essentially the row will be invited to a specific set of invites. See example below:

type/label status email address 1 email address 2
apple 1 ... 1@external 1@business
apple 1 ... 2@external 2@business
apple 1 ... 3@external 3@business
apple 1 ... 4@external 4@business
apple 2 ... 5@external 5@business
apple 2 ... 6@external 6@business

The type/label in the first column ("apple 1" and "apple 2" for example) is matched in another sheet called Settings where a range of invites are listed below each label. If in the main sheet a row says "apple 1" - then it goes to Settings, finds "apple 1" and gets range below the label, gets event ID from each link listed below it, and invites the row's email addresses to each invite. This results in a lot of back and forth inviting, row by row.

Example Settings sheet:

apple 1 apple 2 apple 3
google calendar invite link 1 google calendar invite link 2
google calendar invite link 3 google calendar invite link 4

As I understand it, the script currently scans row #1, gets the email addresses in columns 3 and 4, gets the label for that row ("apple 1"), matches the label ("apple 1") in the settings, gets the invites listed below "apple 1" in the settings, gets their event IDs, and invites the email addresses for that row to those set of invites. If the invites were successful for that row, it writes "invited" in the status column and continues on with the next rows.

As you can see this is really inefficient. It works seamlessly (though slowly, taking about 5-8 minutes) for 100 rows of email addresses where the labels are pointing to 3-5 unique invite links each label. So if I assigned "apple 1" and "apple 2" to 50 rows each, and in the settings "apple 1" and "apple 2" had 4 unique invites listed below each of them, it invites perfectly fine, but very slowly.

Solutions:

I want for the script to run more efficiently. Perhaps it should organize "apple 1" rows and "apple 2" and store their respective email addresses in separate arrays, then invite them all in one go to each invite?

Hypothetical flow:

  • 50 rows with "apple 1"
  • "apple 1" in settings has 10 unique invites linked below it
  • run program
  • gather all email addresses for rows labeled "apple 1"
  • list e.g. [1@external.com 1@business.com 2@external.com 2@business.com 3@external.com 3@business.com 4@external.com 4@business.com]
  • go through all "apple 1" invites and invite that list in one action (is that possible?) to each invite; the same way you would edit an invite manually and paste that list then save the event
  • continue doing this for invites 2-10
  • mark "invited!" in the status column of each row that was successfully invited
  • do the same with "apple 2"

Is this the best way / the solution and how could I rewrite the current set up to be as efficient as possible?

And/or perhaps we can use timers and an empty cell/variable to store values so that if execution is terminated, it runs it again, continuing with the stored data?

Errors:

I test on a business Gmail account, meaning execution time is 30 minutes long and the amount of invites I can send out is 10,000 (or potentially more).

First, I do not send out more than 200+ invites when the script runs. Second, half those emails being invited are primary domain (within the business) so half don't even count in the "10,000 invites" limitation which are specific to external domains per the document linked HERE.

I am not exceeding these limits, so I believe the amount of calls to the API is what's causing issues. The errors I receive are different on a personal account and business Google Suite account:

Personal:

Error: GoogleJsonResponseException: API call to calendar.events.patch failed with error: Calendar usage limits exceeded.

Business:

GoogleJsonResponseException: API call to calendar.events.patch failed with error: The service is currently unavailable.

Each pull/call may be overloading.

I will include the main script and the html for the sidebar (open from menu):

code.gs

function onInstall(e) {
    onOpen(e);
}

function onOpen() {
    var ui = SpreadsheetApp.getUi();

    ui.createMenu('► Invite Automation ◄')
        .addItem('Open Auto-Invite Program', 'openSidebar')
        .addToUi();
}

function openSidebar() {
    var html = doGet();
    SpreadsheetApp.getUi()
        .showSidebar(html);
}

function doGet(request) {
    return HtmlService.createTemplateFromFile('Campaign') // campaign.html
        .evaluate()
        .setSandboxMode(HtmlService.SandboxMode.IFRAME)
        .setTitle('Auto-Invite Program') // Header of the side bar
        .setWidth(300);
}

function include(filename) {
    return HtmlService.createHtmlOutputFromFile(filename)
        .getContent();
}


function getSettingsSheet() {
    const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
    for (let i = 0; i < sheets.length; i++) {
        if (~sheets[i].getName().indexOf('Settings')) {
            return sheets[i];
        }
    }
}

function getEventIdFromLink(link) {
    const m = link.match(/eventedit\/(\w+)/);
    const sp = Utilities.newBlob(Utilities.base64Decode(m[1])).getDataAsString().split(" ");
    console.log(sp);
    return sp[0];
}

function getListColumn(list) {
    const settingsSheet = getSettingsSheet();
    const lastCol = settingsSheet.getLastColumn();
    const labels = settingsSheet.getRange(1, 1, 1, lastCol).getValues()[0]; // LABELS STARTING ROW IN THE SETTINGS -- labels must be written row one, everything below it is invite links
    console.log(labels);
    return (labels.indexOf(list)) + 1;
}

function getListEventIds(list) {
    const columnNum = getListColumn(list);
    const settingsSheet = getSettingsSheet();
    const events = settingsSheet.getRange(2, columnNum, settingsSheet.getLastRow()).getValues(); // EVENT STARTING ROW IN THE SETTINGS -- these are invite links that start after the labels/header in the settings
    eventIds = events.map((r) => r[0]).map((e) => e.trim()).filter((e) => e != '').map(getEventIdFromLink);
    return eventIds;
}

function getEvent(eventId) {
    // return CalendarApp.getDefaultCalendar().getEventById(eventId);
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    // var calendarId = ss.getSheetByName('Settings').getRange(2, 10).getValue(); // CALENDAR ID 1 OF 2
    var calendarId = CalendarApp.getDefaultCalendar().getId(); // CALENDAR ID 1 OF 2 -- i usually point to a specific calendar id, but for public display I will revert to default id

    console.log(calendarId);
    return CalendarApp.getCalendarById(calendarId).getEventById(eventId);
}

function getEmails(emailcolumn) {
    var leadSheet = SpreadsheetApp.getActiveSheet();
    return leadSheet.getRange(2, emailcolumn, leadSheet.getLastRow(), 2)
        .getValues()
        .filter(r => r[1] != 'Invited')
        .map((r) => r[0])
        .filter((e) => ~e.indexOf('@'));
}

function existsInAll(arr, search) {
    for (var i = 0; i < arr.length; i++) {
        if (!~arr[i].indexOf(search)) {
            return false
        }
    }
    return true;
}

function inviteGuests(statuscolumn, emailcolumns) {

    emailcolumns = emailcolumns.split(',').map(col => parseInt(col));

    var leadSheet = SpreadsheetApp.getActiveSheet();
    const lastRow = leadSheet.getLastRow();
    const statusValues = leadSheet.getRange(1, statuscolumn, leadSheet.getLastRow(), 1).getValues();
    // const inviteValues = leadSheet.getRange(1, 4, leadSheet.getLastRow(), 1).getValues();

    var lock = LockService.getScriptLock();

    if (lock.tryLock(1000)) { // Wait for 1s

        for (var row = 2; row <= lastRow; row++) {

            let list = statusValues[row - 1][0];
            // let inviteList = inviteValues[row - 1][0];

            if (~list.indexOf('Invited')) continue;
            // if (~inviteList.indexOf('Invited')) continue;

            list = list.replace(/\s*\(.*\)\s*/, ''); //remove parentheses

            if (list == '') continue;
            console.log(list);

            const listCol = getListColumn(list);

            if (listCol == 0) continue;

            const statusRange = leadSheet.getRange(row, statuscolumn);
            const inviteRange = leadSheet.getRange(row, 2); // COLUMN 2 IS WHERE IT NOTES "INVITED" OR "FAILED" FOR EACH RESPECTIVE ROW
            const eventIds = getListEventIds(list);


            if (eventIds.length == 0) {
                SpreadsheetApp.getActiveSpreadsheet().toast('A row failed because the label/assignment exists but had no invites linked below it in Settings.', 'Failed!', 30);
                // statusRange.setValue(`${list}`);
                inviteRange.setValue(`❌  Failed`);
                continue;
            }

            const events = eventIds.map(getEvent);

            let emails = emailcolumns.map(col => leadSheet.getRange(row, col).getValue().trim()).filter(e => e != '');

            // if (emails.length == 0) continue;

            if (emails.length == 0) {
                SpreadsheetApp.getActiveSpreadsheet().toast("A row failed because no email address was listed.", "Failed!", 30);
                inviteRange.setValue(`❌  Failed`);
                continue;
            }

            // Logger.log(emails);

            let allAttendees = []

            events.map((event) => {

                const eventId = event.getId().split('@')[0];
                // Logger.log(eventId);

                // const calendarId = CalendarApp.getDefaultCalendar().getId();
                // const calendarId = leadSheet.getSheetByName('Settings').getRange(1, 2).getValue();
                // Logger.log(calendarId);

                var ss = SpreadsheetApp.getActiveSpreadsheet();
                // var calendarId = ss.getSheetByName('Settings').getRange(2, 10).getValue(); // CALENDAR ID 2 OF 2
                var calendarId = CalendarApp.getDefaultCalendar().getId(); // CALENDAR ID 1 OF 2 -- i usually point to a specific calendar id, but for public display I will revert to default id
                const resource = Calendar.Events.get(calendarId, eventId);
                const newattendees = emails.map((e) => ({
                    email: e
                }));

                if (resource.attendees) {
                    resource.attendees.push(...newattendees);
                } else {
                    resource.attendees = newattendees;
                }

                Calendar.Events.patch(resource, calendarId, eventId, {
                    sendUpdates: 'all',
                    sendNotifications: true
                });

                const updatedResource = Calendar.Events.get(calendarId, eventId);
                allAttendees.push((updatedResource.attendees || []).map((a) => a.email));
            })

            if (emails.every(email => existsInAll(allAttendees, email))) {
                // statusRange.setValue(`${list}`);
                inviteRange.setValue(`✅  Invited`);
            }
        }

        lock.releaseLock();
    } else {

        SpreadsheetApp.getUi().alert("Warning! The program is already running. Running two or more simultaneous sessions is not allowed. Do not click the button again until invitations are completed.");

    }

}

Campaign.html

<?!= HtmlService.createHtmlOutputFromFile('Style').getContent(); ?>
<div class="sidebar">
   <div>
      <h1 class="splash">Invite Guests</h1>
      <p>
         Guest List:
         <select class="guestlist" name="guestlist">
            <option value="Invite All">Scan Entire List</option>
         </select>
      </p>
      <p>
         Invite Type:
         <select class="statuscolumn" name="statuscolumn">
            <option value="1" selected>Column A</option>
         </select>
      </p>
      <p>
         Email Address List(s):
         <select class="inviteemailcolumns" name="inviteemailcolumns">
            <option value="3,4" selected>Column C & D</option>
         </select>
      </p>
      <p>
         <input class="button-apple" name="button-apple" value="Invite Guests" type="button-apple" onclick="inviteGuests($('.statuscolumn').val(), $('.inviteemailcolumns').val()); this.form.submit(); this.disabled=true;" onsubmit="disableButton()">
      </p>
      <p class="invite-result"><u>Do not click the button more than once or while the operation is running.</u><br><br>Inviting guests will scan each row and invite email addresses according to their label in Column C.<br><br>The label must exist in <i>Settings</i> and have invite links listed below it.<br><br><u>Remove labels for rows you do not want invited.</u><br><br>If a row fails, fix the issue and run the program again.</p>
      <!-- <p class="invite-message"><u></u></p> -->
   </div>
</div>

    
    
    <script src="//ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js">
    </script>
    
    <script>
    
    function inviteSuccess() {
      $(".invite-result").html('<font color="#008000"> Invitations Sent!</font>');
    }
    
    function inviteFailure(e) {
      $(".invite-result").html('<font color="#FF0000">⚠️ Error: ' + e.message + '</font>');
    }
    
    function inviteGuests(statuscolumn, emailcolumns){
      $(".invite-result").html('<font color="#000000">⏳ Inviting...<br><br>Please do not click the button again until completed.</font>');
      google.script.run.withSuccessHandler(inviteSuccess).withFailureHandler(inviteFailure).inviteGuests(statuscolumn, emailcolumns);
    }
    </script>

Style.html

<!-- This CSS package applies Google styling; it should always be included. -->
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>

<style>

body {
    font-family: 'Roboto';
}

.subject,
.load,
.action {
    width: 250px;
}

.button-apple {
    font-family: 'Roboto';
    background-color: #1d9bd9 !important;
    border: 1px solid #1d9bd9 !important;
    color: #ffffff !important;
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: normal;
    height: 29px;
    line-height: 27px;
    margin: 0;
    min-width: 232px;
    outline: 0;
    padding: 0 8px;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
    caret-color: transparent;
}

.button-apple:hover {
    background: -webkit-gradient(linear,left top,left bottom,from(#49b4e9),to(#0293dc)) !important;
    background: -moz-linear-gradient(top,#49b4e9,#0293dc) !important;
    background: linear-gradient(top,#49b4e9,#0293dc) !important;
}

.button-apple:focus {
    border: 1px solid #1d9bd9 !important;
}


.button-stop {
    font-family: 'Roboto';
    background-color: #ff0000 !important;
    border: 1px solid #ff0000 !important;
    color: #ffffff !important;
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: normal;
    height: 29px;
    line-height: 27px;
    margin: 0;
    min-width: 232px;
    outline: 0;
    padding: 0 8px;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
    caret-color: transparent;
}

.button-stop:hover {
    background: -webkit-gradient(linear,left top,left bottom,from(#49b4e9),to(#0293dc)) !important;
    background: -moz-linear-gradient(top,#49b4e9,#0293dc) !important;
    background: linear-gradient(top,#49b4e9,#0293dc) !important;
}

.button-stop:focus {
    border: 1px solid #ff0000 !important;
}



input[type="text"] {
  margin-top: 5px !important;
}

.result,
.invite-result {
    background-color: #FFFFFF;
    border-radius: 2px;
    padding: 10px;
    font-size: 13px;
    line-height: 17px;
    text-align: center;
    border: 1px #cecece dashed;
}

.invite-message {
    background-color: #FFFFFF;
    border-radius: 2px;
    padding: 10px;
    font-size: 13px;
    line-height: 17px;
    text-align: center;
    border: 1px #cecece dashed;
}

}
hr.style-four {
    margin: 15px 0px;
    height: 15px;
    border: 0;
    border-top: 2px #e0e0e0 dotted;
}

.sidebar {
padding: 10px 17px;
    position: relative !important;
    width: auto !important;
}

 h1.splash {
    font-size: 22px;
    font-weight: bold;
    letter-spacing: 0px;
    color: #1d9bd9;
    padding: 5px 0px 8px 5px;
    margin-bottom: 18px;
    border-bottom: 2px #e8e8e8 dotted;
    text-align: left;
 }
 
</style>

  • Can I ask you about the length of `events` of `const events = eventIds.map(getEvent);`? – Tanaike May 28 '21 at 07:36
  • As you can see that is the eventIds for any given label in the settings. When I have 4 invites listed below “apple 1” (so there are 4 event ids for “apple 1”) the program runs completely fine. When I link 10 unique invites below “apple 1” I assume that is the root cause of the error! (I usually have 10 labels with 10 unique invite links listed below each, making it 100 unique event IDs, 10 event IDs per label. Each row has a different label / is invited to 10 different invites usually). This is when the error happens. Summary: length is 10 when error happens. When length is ~4 it works – usernametaken May 28 '21 at 07:40
  • Thank you for replying. From your situation, the length of `events` is 4. Even when the number of loops is 4, such error occurs. Is my understanding correct? – Tanaike May 28 '21 at 07:55
  • When I test with 4 events under “apple 1” there is no error but it is slow since it invites row by row. When I ran the script for my main use and “apple 1” has 10 and “apple 2” has 10 and “apple 3” has 10 and there were 200 rows with 2 emails each row, it gave me that error. When that happens it runs for 70-90 seconds on the business Gmail and invites a few rows then stops with error. 1-5 = OK. 10 or so = error. – usernametaken May 28 '21 at 07:57
  • Thank you for replying. I understood that when the length of `events` is 4, no error occurs. About `When I ran the script for my main use and “apple 1” has 10 and “apple 2” has 10 and “apple 3” has 10 and there were 200 rows with 2 emails each row, it gave me that error.`, in this case, the length of `events` is 200? When the length of `events` is 200, such error occurs. Is my understanding correct? I apologize for my poor English skill. By the way, I cannot understand about `1-5 = OK. 10 or so = error.`. – Tanaike May 28 '21 at 08:00
  • Sorry for confusing. When I list 10 invites (10 event IDs) I think this is what gives me error. But when I try with 1-5 invites it seems ok but slow. – usernametaken May 28 '21 at 08:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232986/discussion-between-usernametaken-and-tanaike). – usernametaken May 28 '21 at 08:09
  • Thank you for replying. About `When I list 10 invites (10 event IDs) I think this is what gives me error. But when I try with 1-5 invites it seems ok but slow.`, in this case, are you saying about the length of `emails` in your script? If my understanding is correct, your error doesn't depend on the length of `events`? For example, when the length of `events` is 4 and the length of `emials` is 10, such error occurs. Is my understanding correct? – Tanaike May 28 '21 at 08:10
  • Sorry no. I don’t think the length of emails is the issue. Because row 1 will have 2 emails, row 2 will have 2 emails, etc. you can imagine 100 rows. When the script runs they are all invited to 10 invites. This happens to every row... 2 emails —> invited to event 1, then event 2... all the way down to event 10. Then row 2 does the same all the way down to row 100. I don’t think the emails are the problem. It is the way the script invites emails every row one by one to 10 different invites, looping through the range. – usernametaken May 28 '21 at 08:17
  • If I add 4 invites, it seems to not have a problem... when I have 10 invites, it seems to have a problem. Because the amount of rows and emails it's inviting to the 10 invites, 10 times per row is very inefficient (or I think 20 times per row if there are two emails, because one email is invited at a time?) – usernametaken May 28 '21 at 08:23
  • Thank you for replying. I have to apologize for my poor English skill again. In your situation, your error doesn't depend on both the length of `events` and the length of `emails` in your script. Even when the lengths of `events` and `emails` are large, no error occurs. ​Is my understanding correct? – Tanaike May 28 '21 at 08:25
  • There is an error when the events length is large, like when there are 10 invites. It is inviting inefficiently and causing rate limit error because of the way it is inviting. First row (2 emails) invited to 10. Then second row (2 emails) invited to 10. Then third row (2 emails) invited to 10. And it goes on and then execution terminates because of rate limit even though I did not invite 10,000 and execution only lasted 70 seconds. – usernametaken May 28 '21 at 08:34
  • I need to make it invite all "apple 1" at once. If there are 100 rows with "apple 1" it needs to invite all those emails to the 10 invites. Please see my example spreadsheet. You can copy it and add the script to it: https://docs.google.com/spreadsheets/d/1LnIDm83eAr0c_x3lZKG0KUZZZcKlBGmIvouo4GJpMUc/edit?usp=sharing you will see how it works – usernametaken May 28 '21 at 08:35
  • Thank you for replying. I have to apologize for my poor English skill again. I thought that when I could know whether the lengths of `events` and `emails` are related to your current issue, it might lead to the solution. But unfortunately, from your replying, I cannot still understand about it. This is due to my poor English skill. I deeply apologize for this. But I would like to try to understand it. When I could correctly understand it, I would like to think of the solution. I deeply apologize I cannot resolve your issue soon. – Tanaike May 28 '21 at 08:36
  • @Tanaike Please see my updated post, I added screenshots. Also feel free to duplicate the spreadsheet I linked you. It should help explain how the script works and how it is inefficient since it goes row by row. It is inviting many rows (emails) to 10 invites one by one and it is hitting rate limit/usage limit and causing error even for business Gmail account. Events length being large causes error, but when it is small it works slow. – usernametaken May 28 '21 at 08:42
  • Thank you for your additional information. When I could correctly understand about your situation and find the solution, I would like to propose an answer. If I cannot understand it, I apologize again. – Tanaike May 28 '21 at 08:46
  • To pinpoint the execution error, are you doing an invitation to each user through apps script to the same event over and over? If so, why not using the Calendar API https://developers.google.com/calendar/v3/reference/events/update to put all the users as `attendees[]` and `sendUpdates` to all? – Kessy May 28 '21 at 11:19
  • Hi @Kessy if you look at the script I provided in the thread it is doing `let allAttendees = []` then doing `Calendar.Events.patch(resource, calendarId, eventId, { sendUpdates: 'all',` the problem is it's doing this row by row and I need it more efficient and I don't know the root cause of the error – usernametaken May 28 '21 at 16:25
  • As I am understanding this you are doing an 'events.patch' for each email you need to send it? – Kessy Jun 03 '21 at 15:59

0 Answers0