1

I would like a user to be able to tap a button in a sheet to install a trigger for a standalone script that will manage a couple of mail functions. I need this because I have a user that cannot follow instructions, and thus I cannot ensure they will create a trigger manually, properly.

I would like them to just open a Google Sheet, tap the button, approve what needs approving, then be good to go.

So, they need

  1. The 'main' function of my "amazingLabels" standalone Google Apps Script to trigger every minute.
  2. That must be installed by the amLabsInstallSheet Google Sheet. They have read access to both.

I have used this answer as the basis for what I'm trying to do, I haven't found anything better, the only thing I've changed is some names.


So, I have created my own standalone script amazingLabels and inside it is the following:

function doGet(e) {
  createTrigger();
  return ContentService.createTextOutput('success!');
}

function createTimeTrigger() {
  UrlFetchApp.fetch('appUrlExec');
}

function createTrigger() {
  var tr = ScriptApp.newTrigger('main');
      tr.timeBased().everyMinutes(1).create();
}

function main() {
  mainOne();
  mainTwo();
};

Where appUrlExec is the /exec link for this amazingLabels standalone script.

Now, I should be able to create a spreadsheet, which we'll call amazingLabelsInstaller with amazingLabels as a library, and inside the bound script for the spreadsheet call:

function testLib() {
  amazingLabels.createTimeTrigger();
}

However I just cannot get it to install that trigger for the user to run every minute.

If I focus on any other function, it works. Ie. if I call 'main' directly with amazingLabels.main(); it will run that function.

What am I doing wrongly here? Is it the new trigger code?

I've tried every combination of deployment, from a Google Suite deployment, to a private account deployment. From running as myself, to running as the user. I've tried them as editors on both document or none (but it should work as viewers, right?), and with both script and sheet deployed or sheet not-deployed.

Any help would be appreciated, apologies if anything is unclear, here, happy to work through it in the comments.

Rubén
  • 34,714
  • 9
  • 70
  • 166
Eliot Cole
  • 141
  • 8
  • BTW, I've tried that original one pretty much verbatim, too ... it doesn't work. Is it possible that amazingLabels (the standalone script) even though they have access, isn't prompting them for approval for some reason, or something? – Eliot Cole May 31 '21 at 07:55
  • To re-confirm, in case it isn't clear, the sheet is _purely_ an installer, that's it. When they tap the (_drawing_) 'Install' button in the sheet it should install the minute based trigger for the standalone script. ------------------- They are fully aware of the intent of this, it is a private thing to prevent the 'difficult' process of setting up a trigger. I have also attempted with the linked Stack Answer code almost verbatim. Also, I'm getting "We're sorry, a server error occurred. Please wait a bit and try again." responses with verbatim code. – Eliot Cole May 31 '21 at 10:50
  • I wonder if the issue is that you can't put the current exec link INTO the script itself. Also, running the standalone script ... I'm wondering if that's essentially doing nothing as it's not running as *any* user, and therefore isn't installing the triggers anywhere. – Eliot Cole May 31 '21 at 11:02

3 Answers3

0

This will help to prevent creating multiple triggers for the same function.

function createTBT(funcname) {
  const ts = ScriptApp.getProjectTriggers().map(t => t.getHandlerFunction());
  if (!~ts.indexOf(funcname)) {
    ScriptApp.newTrigger(funcname).timeBased().everyMinutes(1).create();
  }
}
Cooper
  • 59,616
  • 6
  • 23
  • 54
  • Thanks, @Cooper, that'll be useful for when I can get any triggers working. Right now that's not going to do much, though, I'm afraid. Thanks, though! – Eliot Cole May 31 '21 at 07:52
  • OK, so ... if I'm reading you right, this will stop users installing this multiple times and in effect adding more and more triggers for the same thing. Is `funcname` just shorthand to tell me to put the function name in there, like `main`? Also, should it be `'main'`, or is it fine without the quotes? – Eliot Cole Jun 02 '21 at 00:52
  • funcname is a parameter so if you want to create a trigger for a function name foo() then you would call the functioin like this: `createTBT('foo');` – Cooper Jun 02 '21 at 01:27
  • Aha! Thanks, Cooper. Yeah, I'm going to try to integrate this into my Sites workaround, I should just be able to replace the trigger creation with this, right? Without any other requirements, for example; are there any different scopes required to *check* for triggers, rather than create them? – Eliot Cole Jun 03 '21 at 10:42
0

Make a clear separation between the 2 scripts you need and provide the necessary scopes

  • First script:

Is a script bound to the spreadsheet with the content:

function createTimeTrigger() {
  UrlFetchApp.fetch('appUrlExec');
}

Assign function createTimeTrigger to a custom button.

  • Second script:

Is your standalone script with the content

function doGet(e) {
  createTrigger();
  return ContentService.createTextOutput('success!');
}


function createTrigger() {
  var tr = ScriptApp.newTrigger('main');
  tr.timeBased().everyMinutes(1).create();
}

function main() {
  mainOne();
  mainTwo();
};

function mainOne() {
...
}

function mainTwo() {
...
}

Make sure to deploy this script as a WebApp and deploy a new version whenever you made any changes.

Also, the deployment options need to be:

"Execute as": "me", "Who has access to the WebApp": "Anyone, even anonymous".

(If you want to deploy as "The user accessing the WebApp": This is also possible, but you would need to manually pass and apply the OAuth2 token).

Very important:

Make sure that your scripts have the correct scopes

  • The bound script needs the https://www.googleapis.com/auth/drive.readonly scope to be able to execute a WebApp programmatically through a Urlfetch call. This scope is not automatically assigned -you need to add it to your manifest. Also, you need the https://www.googleapis.com/auth/script.external_request scope for using the UrlfetchApp in general.

  • the WebApp needs at least the scope "https://www.googleapis.com/auth/script.scriptapp" to create the trigger - possibly more scopes depending on the functionality

ziganotschka
  • 25,866
  • 2
  • 16
  • 33
  • Ahh! ... That's amazing, thanks you, @ziganotschka. I'll give this a go presently and let you know how I get along. I think the two things I hadn't done was: 1. *The `urlFetchApp` part.* I'd actually been reading up on that looking for the solution, but hadn't quite understood how to use it this way. What you've said makes total sense, and is brilliant, cheers. 2. *Assigning the scopes.* I had assumed it had done at least part of that work for me, but never thought to check it (_and would've had to find out where_), so MASSIVE thanks on that. ..... *OK! Off to test!* – Eliot Cole Jun 01 '21 at 22:00
  • Hmmm. I have a feeling I'm running in to the issue that I was musing about in my follow up comments when I said "_isn't prompting them for approval for some reason, or something_." I'd taken a brief look in to that, and I started to look at `getAuthorizationUrl()` and wondered if I needed to grab the URL for the standalone, and also present that to the user. Both the standalone, and the spreadsheet, were shared with the user (my personal account) as an editor. FYI, my only test so far has been from a G-Suite version, so I will test from a personal account to a personal account. – Eliot Cole Jun 01 '21 at 22:43
  • Both scripts have the scopes mentioned, and gmail (to add the labels), plus the gmail service in the standalone. I'd forgotten the service in the first run, so understandably nothing happened. So I published a new version and tried with the new exec URL, and it ran for a long, long, time, and did ... well ... nothing, actually (not said harshly). Could I need to find a way to authorise the second script's access to the user's triggers, and mail? – Eliot Cole Jun 01 '21 at 22:47
  • Dang, didn't work on private account either. Other than making a function to create the trigger separately, it seems OK ... the `doGet(e)` calls this function: `function labelsSetup() { var labelOne = LABEL_NAME; var labelTwo = DONE_LABEL_NAME; var labelThree = IMPORTANT_LABEL_NAME; var labelFour = DONE_IMPORTANT_LABEL_NAME; var tr = ScriptApp.newTrigger('main'); GmailApp.createLabel(labelOne); GmailApp.createLabel(labelTwo); GmailApp.createLabel(labelThree); GmailApp.createLabel(labelFour); tr.timeBased().everyMinutes(15).create();}` – Eliot Cole Jun 01 '21 at 23:11
  • I tried running the EXEC directly and got an error "_aseda_", and a search on that revealed this work around here - https://stackoverflow.com/a/47708897/15832984 ... however, when I ran the script without the domain on the G-Suite version, I got this error: " Exception: Request failed for https://script.google.com returned code 404. Truncated server response: – Eliot Cole Jun 01 '21 at 23:36
  • Private not working either, have tried removing "macros" from the fetch in the spreadsheet, and with the domain (on the G-Suite), too. – Eliot Cole Jun 01 '21 at 23:56
  • "aesda" was a mistake, I meant to paste the error there. – Eliot Cole Jun 02 '21 at 00:31
  • Hi Eliot! Yes, you need to copy the deployment URL from the standalone WebApp into the bound script manually (`getAuthorizationUrl()`) would only work if it's the same script. As for doing nothing, have you checked `View->Executions` for BOTH the bound and the standalone script? And how did you get the 404? Through pasting the WebApp URL into the browser? This means that there is something not right about the URL (or maybe you are signed in with multiple accounts)? Btw., when you publish the WebApp, it should be: "Execute as": "me", "Who has access to the WebApp": "Anyone, even anonymous". – ziganotschka Jun 02 '21 at 04:51
  • PS: If you want to deploy as "The user accessing the WebApp": This is also possible, but you would need to manually pass and apply the OAuth2 token – ziganotschka Jun 02 '21 at 06:13
  • Brilliant, mate, thank you! :-) ... FYI (maybe a question?) If the WebApp executes as me, then the trigger will be added to my account, no? The objective is that the script checks the *user's* Gmail, not mine for the Labels that have been chosen. I have basically made a label forwarder, and it works ;-). FWIW - Not signed in with multiple accounts, just one on any attempt made. Have tried G-Suite to Private, Private to Private, and Private to G-Suite, none work in Sheets, but it works perfectly (private-private) in Sites. Ideally I would have this work G-Suite to Private, though, somehow. – Eliot Cole Jun 03 '21 at 10:40
  • 1
    Yes, if you want the trigger to be set-up for the user, then you need to deploy the WebApp as the uer - passing the token. It sohuld not make any difference for the functionality either you deploy your scripts from a private or Google domain account (apart from for [Quota limits](https://developers.google.com/apps-script/guides/services/quotas)). – ziganotschka Jun 03 '21 at 11:32
  • It's the quota limits that are my biggest headache, to be honest. There is no definition as to whether or not if a user is running a script, as themselves (as we've discussed) if that's going to hit my limits or theirs. Heh. But that's a whole 'nother question ;-). – Eliot Cole Jun 07 '21 at 15:01
  • 1
    In short: It's gonna hit their limits. But you are right, for visibility reasons it is best to post it as a separate question. – ziganotschka Jun 07 '21 at 15:21
0

I have found a workaround (not a fix) to achieve my goals:

Don't use Sheets to do the dirty work, use Sites ... and even then, only do it from a private account.


If you try to do this from a G-Suite account, I'm relatively sure that having your G-Suite Admin allow anonymous sharing on Drive will allow you to publish the webapp so that it will run for external people. However this is a sweeping change that isn't granular enough to be specifically for scripts, and could be abused.


Anyway, essentially the solution is relatively simple:

  1. Publish the script as a web app on a private account with the doGet(e) pointing to the required function.
  2. Ensure the right scopes are set, as per ziganotschka's answer.
  3. Create a Google Sites site with a unique name.
  4. Click the '<> Embed' button on the right, and use the "By URL" method.
  5. Paste in the exec link from the deployed standalone script.
  6. Publish the page.

Now anyone that visits that link can click through the authorisation flow and it will have activated the required function!

It's not perfect, because it's not under the umbrella of the organisation which it should be. Plus it provides a possible issue with reduced script running rate limits for unpaid accounts.

Eliot Cole
  • 141
  • 8