How to use CallbackURL?

url-schemes
#1

I’m trying to use x-callback-url with the 2Do app to add new project and task details from Scriptable. I’m stuck on the last part which is how to actually send the URL I’ve formed, and how to tell what the result was. I’ve read the documentation for CallbackURL but it’s sketchy and doesn’t provide a simple working demo. In the sample code, there’s “Import File to Bear” which uses this function, but it doesn’t check for success or return results.

Here’s the code:

// other things before here to populate the variables
let baseURL = "twodo://x-callback-url/add"
let cb = new CallbackURL(baseURL)
cb.addParameter("task", title)
cb.addParameter("type", "1")
cb.addParameter("forList", listName)
cb.addParameter("note", notes)
cb.addParameter("priority", "2")
cb.addParameter("due", dueDate)
cb.addParameter("start", startDate)
cb.addParameter("ignoreDefaults", "1")
console.log(cb.getURL())
var result = await cb.open()
console.log(result)

There’s talk of ‘promises’ in the documentation, but no worked example I could see, or how to bring the two ideas together. The console output I get shows [object Object] whether I show result or result[0].

Can anyone share a worked example of an x-callback-url and results, please?

#2

Type “callbackurl scriptable” into the search on this forum and you should see a handful of results that include examples.

#3

Naturally I have done that, @sylumer, which is why I have been able to get as far as I have.
The question remains, as there are no examples I can find that particularly show how to deal with what comes back from the opencall.

#4

Okay, maybe I was a little off on the level of detail you need. Sorry.

On the 2do app URL scheme info page, it notes the following for the task add action.

All parameters are optional. Supplied parameters must be URL-encoded. This callback will also return the internally used, unique identifier of the newly created task (for the key named add), which can be used in pace of the forParentTask parameter for future callbacks.

Presumably it is that UUID string you are expecting to be returned.

I took your script and populated the parameters that you are passing to 2do. This is the only change I made. It looked like this.

// other things before here to populate the variables
let baseURL = "twodo://x-callback-url/add"
let cb = new CallbackURL(baseURL)
cb.addParameter("task", "test 1")
cb.addParameter("type", "1")
cb.addParameter("forList", "Work")
cb.addParameter("note", "Some notes")
cb.addParameter("priority", "2")
cb.addParameter("due", "2020-12-31")
cb.addParameter("start", "2020-12-01")
cb.addParameter("ignoreDefaults", "1")
console.log(cb.getURL())
var result = await cb.open()
console.log(result)

When I ran this, I got the following console output.

2019-04-28 09:24:44: twodo://x-callback-url/add?task=test%201&type=1&forList=Work&note=Some%20notes&priority=2&due=2020-12-31&start=2020-12-01&ignoreDefaults=1&x-source=Scriptable&x-success=scriptable://x-callback-url/success&x-error=scriptable://x-callback-url/error&x-cancel=scriptable://x-callback-url/cancel
2019-04-28 09:24:47: {"add":"3eee68c4e4784dfbad7b288bc499e3bb"}

The task appeared in 2do, and presumably has the ID matching the one returned above.

That return (held in the result variable) is JSON (JavaScript Object Notation), so you should be able to process that in the standard way to access the ID.

console.log(result.add)

Hopefully that should clarify things.

#5

Many thanks, @sylumer, that’s very helpful, particularly the JSON tip. (And beyond the call of duty to get 2Do and the details of its x-callback-scheme. Shame its documentation doesn’t give similar hints about JSON.)

However, this leads to new questions!

  1. in your first example I wonder why yours was returning something renderable and mine wasn’t? I had exactly the same sort of string variables being set.

  2. when using console.log(result.add) I got the UUID the first few times I ran it. Without changing that code block it’s now giving me errors on the main call var result = await cb.open(), saying 'Unexpected identifier ‘result’. Expected ; after variable declaration. If I remove the await it doesn’t give this error, but then has problems with undefined result, no doubt because it hasn’t waited for the return value.

Unless you can think of a reason for these errors, I’m wondering if this is a bug? I’m using v1.3.2 (86), in case you’re on a later version. Thanks.

#6

That’s what the following on their description of the add task means:

This callback will also return the internally used, unique identifier of the newly created task (for the key named add),

The point to note is the bit in brackets at the end about a key called ‘add’. They are just making an expectation about the reader’s level of understanding of how these things can, but don’t always, work.

Without having the exact contents, it is impossible to tell.

Can you share any details of the error? It would be very strange to go from something working to it not. Not impossible, but unusual. Closing both apps can occasionally help with random glitches, but I suspect that there is something specific here.

Yeah, the await is essential. You can’t remove it and expect it will work. :man_shrugging:t2:

I’m on the same version. It is possible it is a bug, but I’ve personally had no issues with any of the inter-app interactions I use with Scriptable, and I’ve been involved in helping with the beta testing.

I ran the version I posted above 25 times manually, followed by another 75 times via a loop. I didn’t get a single failure as far as I can see.

I tried posting the results, but I broke the post size limit in doing so, so just a screenshot to give an indication :open_mouth:

It would be worth seeing if you can reproduce whatever issue is occurring with any consistency.

1 Like
#7

Continued thanks, @sylumer, for trying this out many times.

I’ve now produced a (nearly minimum) non-working example, along the lines of StackOverflow. It first tries to add a 2Do project, and then add 4 tasks to that project. Full code is:

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// always-run-in-app: true; icon-color: yellow;
// icon-glyph: magic;
// term-plan-parse
// JS Core for Scriptable app
// 

var projectTitle="5-May Test Talk project"
var notes="Series: something"
var startStr1="29/04/2019"
var startStr2 = "01/05/2019"
var dueStr = "04/05/2019"
var pid = addProject(projectTitle, notes, startStr1, dueStr, "Talks")
addTask("Decide talk point","", startStr1, dueStr,projectTitle,pid)
addTask("Write talk","", startStr1, dueStr,projectTitle,pid)
addTask("Prep slides & props","",startStr2, dueStr,projectTitle,pid)
addTask("Practice & print","",startStr2, dueStr,projectTitle,pid)


function dateToYYYYMMDD(dateIn, dayOffset) {
	let dt = new Date(Date.parse(dateIn) + dayOffset*60*60*24*1000)  // arithmetic in milliseconds
	let output = dt.toLocaleDateString("en-GB", {year:'numeric', month:'2-digit', day:'2-digit'})
	return output
}

function addProject(title, notes, startDate, dueDate, listName) {
	var baseURL = "twodo://x-callback-url/add"
	var cb = new CallbackURL(baseURL)
	cb.addParameter("task", title)
	cb.addParameter("type", "1")
	cb.addParameter("forList", listName)
	cb.addParameter("note", notes)
	cb.addParameter("priority", "2")
	cb.addParameter("due", dueDate)
	cb.addParameter("start", startDate)
	cb.addParameter("ignoreDefaults", "1")
	console.log("- addProject for " + cb.getURL())
	var result = await cb.open()
	var projectID = result.add
  	console.log("  - callback returns " + projectID)
 	if (projectID != "") {
 		return projectID
 	} else {
 		console.error("Problem creating project" + projectTitle)
		return ""
	}
}

function addTask(title, notes, startDate, dueDate, projectTitle, projectID) {
	let baseURL = "twodo://x-callback-url/add"
	let cb = new CallbackURL(baseURL)
	cb.addParameter("task", title)
	cb.addParameter("type", "0")
	cb.addParameter("forList", projectTitle)
	cb.addParameter("forParentName", projectTitle)
	cb.addParameter("forParentUID", projectID)
	cb.addParameter("note", notes)
	cb.addParameter("priority", "2")
	cb.addParameter("due", dueDate)
	cb.addParameter("start", startDate)
	cb.addParameter("ignoreDefaults", "1")
	console.log("- addTask for " + cb.getURL())
	let result = await cb.open()
	console.log("  - callback returns " + result.add)
    if (result.add != "") {
 	    return result.add
 	} else {
 		console.error("Problem creating task" + title)
 	}
 	return 0
}

(I don’t know how to make this have JS formatting like you manage, sorry.)

This always returns the same error as I quoted before on the first URL call: Error on line 35: Unexpected identifier ‘cb’. Expected ';' after variable declaration.
Please could you or others try and see if this is some error in my setup? I have re-started the Scriptable app a number of times, which doesn’t cure it.

#8

Your functions contain promises, so they should be defined as async function, not simply function.

1 Like
#9

Aha. this newbie to JS is learning lots … I’ve now read through https://javascript.info/async-await etc., and modified the two functions as seems fit. Here’s the first:

async function addProject(title, notes, startDate, dueDate, listName) {
	var baseURL = "twodo://x-callback-url/add"
	var cb = new CallbackURL(baseURL)
	cb.addParameter("task", title)
	cb.addParameter("type", "1")
	cb.addParameter("forList", listName)
	cb.addParameter("note", notes)
	cb.addParameter("priority", "2")
	cb.addParameter("due", dueDate)
	cb.addParameter("start", startDate)
	cb.addParameter("ignoreDefaults", "1")
	console.log("- addProject for " + cb.getURL())
	try {
		var result = await cb.open()
		var projectID = await result.add
	   	console.log("  - callback returns " + projectID)
	} catch(err) {
	   console.error(err)
	}
 	return projectID
}

For safety I’ve added an await to dig into the result JSON, as well as try/catch.

This returns a valid UUID in the function. Hooray.

However, from adding another console message just after the line calling the function, I see that it gets to that before the function logs the result. I.e. it seems the function returns before the result is known. From the other documentation, I don’t seem to need to add more to the function, however it doesn’t seem to be waiting correctly.

It also never gets to the addTask function calls, at least according to the lack of further console messages or 2Do items. But also no error messages. What do I need to learn now? Thanks.

#10

Did you ask the script to wait when it ran the function? Did you use await?

Take a look at the script below. I’ve changed the script to use let instead of var (which shouldn’t change the effect, but it is a good habit to get used to using), and note the additional use of await.

let projectTitle="5-May Test Talk project"
let notes="Series: something"
let startStr1="29/04/2019"
let startStr2 = "01/05/2019"
let dueStr = "04/05/2019"
let pid = await addProject(projectTitle, notes, startStr1, dueStr, "Talks")
await addTask("Decide talk point","", startStr1, dueStr,projectTitle,pid)
await addTask("Write talk","", startStr1, dueStr,projectTitle,pid)
await addTask("Prep slides & props","",startStr2, dueStr,projectTitle,pid)
await addTask("Practice & print","",startStr2, dueStr,projectTitle,pid)


function dateToYYYYMMDD(dateIn, dayOffset) {
	let dt = new Date(Date.parse(dateIn) + dayOffset*60*60*24*1000)  // arithmetic in milliseconds
	let output = dt.toLocaleDateString("en-GB", {year:'numeric', month:'2-digit', day:'2-digit'})
	return output
}

async function addProject(title, notes, startDate, dueDate, listName) {
	let baseURL = "twodo://x-callback-url/add"
	let cb = new CallbackURL(baseURL)
	cb.addParameter("task", title)
	cb.addParameter("type", "1")
	cb.addParameter("forList", listName)
	cb.addParameter("note", notes)
	cb.addParameter("priority", "2")
	cb.addParameter("due", dueDate)
	cb.addParameter("start", startDate)
	cb.addParameter("ignoreDefaults", "1")
	console.log("- addProject for " + cb.getURL())
	try {
		let result = await cb.open()
		let projectID = await result.add
	   	console.log("  - callback returns " + projectID)
	} catch(err) {
	   console.error(err)
	}
 	return projectID
}

async function addTask(title, notes, startDate, dueDate, projectTitle, projectID) {
	let baseURL = "twodo://x-callback-url/add"
	let cb = new CallbackURL(baseURL)
	cb.addParameter("task", title)
	cb.addParameter("type", "0")
	cb.addParameter("forList", projectTitle)
	cb.addParameter("forParentName", projectTitle)
	cb.addParameter("forParentUID", projectID)
	cb.addParameter("note", notes)
	cb.addParameter("priority", "2")
	cb.addParameter("due", dueDate)
	cb.addParameter("start", startDate)
	cb.addParameter("ignoreDefaults", "1")
	console.log("- addTask for " + cb.getURL())
	let result = await cb.open()
	console.log("  - callback returns " + result.add)
    if (result.add != "") {
 	    return result.add
 	} else {
 		console.error("Problem creating task" + title)
 	}
 	return 0
}

Now I don’t have “Talks” list, so for my test I switched it to “Works”, but here’s what the output then was.

I think this is probably what you would expect, and the result in 2Do looked like this.

Hope that helps.

#11

Hopefully my last post, as I have now got this working fully, thanks to @sylumer.
I’d not tried an await in the function call, because the help pages I linked above specifically said that an await can’t be used in the top-level of a script, but only in functions etc. That seems like a detail that needs sharing. Perhaps a new example script could be made out of this, by someone who knows more JS than I do?

#12

Await isn’t (typically) usually used at the top level of a script because it would hold everything up. On a web page for instance that could stop other top level code from running that you would want to run.

But your particular code is a single linear thread of logic, so I don’t think there’s any issue with applying at the top level in this instance.

If you wanted to be stricter, simply wrap your top level code into another async function, e.g. main() and at the top level then call your main() function.

It’s in fat arrow format, but here is what JavaScript developers refer to as a top level async function; it calls a function called main and handles errors.

(async () => {
    try {
        var text = await main();
        console.log(text);
    } catch (e) {
        // Deal with the fact the chain failed
    }
})();
1 Like