Tale of two duct tapes – /dev/ops

Tale of two duct tapes


Scenario: we have two pieces of software; one of them is a desktop application, the other is a website (or „a web app”, as these things are called nowadays). Challenge: we’d like to import some data from the website to the desktop application. Problem: desktop application does not support this, it has no such functionality. Solution: use two scripting languages coming from two different worlds.

As you might remember, my personal computer is a Mac. I switched to macOS (called „OS X” back then) in 2012 and over the years I have collected a list of software that I just can’t live without. One of these applications is Devonthink.

An even longer relationship connects me with Pinboard. I started using it in 2010 and always liked its simple but efficient UI; apart from that, I loved the idea of supporting a one-man operation that is an anti-thesis of how such services operate. What’s more, the author of Pinboard is an all-around nice human who gives brilliant talks. I mean, just watch the guy ranting about web obesity crisis:

Moreover, we share a name, we even have the same initials. Coincidence?

Anyway, I have been using Pinboard for years on Windows and Linux but when I started working with Devonthink on Mac, I also started using it to collect bookmarks. Thing is, Devonthink is macOS/iOS only, and I was spending my working hours on Windows side of things, using a Windows laptop provided by my employer. After testing different workflows, it became painfully apparent that nothing really worked for me.

I needed a solution that would work silently in the background, without me even noticing that it’s there.

I needed automation.

When it comes to Macs, people either love them or hate them. You have probably heard all these stories about sheeple using an OS with a mouse glued to their hands (hell, Apple reinvented the damned thing, right?). Not many people know that each Mac comes with a scripting language that dates back to 1993. It’s different from most other such languages because it was not designed for the servers. It was created to automate GUI applications. Sounds weird?

AppleScript does just that. Theoretically, it can make any application talk to any other application, both applications just need to register so-called „scripting dictionaries” that expose objects and operations that they support. It’s almost as using an application through code. As it turns out, Devonthink has an excellent AppleScript support.

By happy coincidence, Pinboard has a REST API, so it’s possible to talk to it remotely and perform almost all operations without using a browser. I am pretty certain that the entire integration could be done in AppleScript alone, but I do not really know this language this well, nor do I really want to know it better, at least for now. What I really know is how to talk to APIs using PowerShell, so the only question is: can it cooperate with AppleScript?

As you probably have guessed it, the answer is resounding „hell yes!”

Let me introduce you to two duct tapes that make it all possible:

function Get-PinboardBookmarks {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $ApiKey,

        [Parameter(Mandatory = $true)]
        [string]
        $Tag
    )
    
    begin {
        $url = 'https://api.pinboard.in/v1/posts/all' # URL to stable Pinboard's API; v2 is still in beta
    }
    
    process {
        $query = $url + "?auth_token=$ApiKey&format=json&tag=$tag"
        $result = Invoke-WebRequest $query -Method Get
        $json = $result.Content | ConvertFrom-Json
        return $json
    }
    
    end {
    }
}

$bookmarks = Get-PinboardBookmarks -ApiKey (Get-Secret -Name pinboard -AsPlainText) -Tag tbc # get just bookmarks tagged 'tbc' (to be continued)

if ($bookmarks) {
    foreach ($row in $bookmarks) {
        Write-Output "$($row.Description),$($row.href)" # print a row with each bookmark's title and URL, separated by comma
    }
}
set input to do shell script "/usr/local/bin/pwsh --command ~/Pinboard.ps1" -- assign output of PowerShell script to variable 'input'
set the text item delimiters to "," -- from now on, comma shall be your delimiter, macOS
repeat with line_number from 1 to count of paragraphs in input -- let's loop through the data passed by PowerShell
	set line_text to paragraph line_number of input -- line by line
	set {myTitle, myURL} to {text item 1, text item 2} of the line_text -- extract fields from the row, assign values to variables
	tell application id "DNtp" -- just what it says, tell Devonthink that
		if not (exists record with URL myURL) then create record with {name:myTitle, type:bookmark, URL:myURL} -- it should first check whether such URL exists in the database; if not, tell it to create new bookmark in the Inbox
	end tell
end repeat

That’s it.

Well, almost.

I am using Get-Secret from Microsoft.PowerShell.SecretManagement module because I do not like storing API keys in clear text, so before the wheels started turning, I had to do

Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore

Secret store had to be initialized:

Get-SecretStoreConfiguration

Its configuration had to be adjusted:

Set-SecretStoreConfiguration -Authentication None

The vault had to be registered:

Register-SecretVault -ModuleName Microsoft.PowerShell.SecretStore

Finally, secret could be stored:

Set-Secret -Name pinboard -Secret <API key here>

Then, since I had to schedule this somehow and felt a bit adventurous, I decided to give launchd a go and saved this to ~/Library/LaunchAgents/com.mc.DevonFeeder.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>com.mc.DevonFeeder</string>
        <key>ProgramArguments</key>
        <array>
            <string>/usr/bin/osascript</string>
            <string>/Users/maciej/DevonFeeder.scpt</string>
        </array>
        <key>StartInterval</key>
        <integer>3600</integer>
    </dict>
</plist>

and, as a final touch, done this:

launchctl bootstrap gui/501 ~/Library/LaunchAgents/com.mc.DevonFeeder.plist
launchctl kickstart gui/501/com.mc.DevonFeeder

Oh, and of course, I forgot about this magic incantation:

chmod +x DevonFeeder.scpt Pinboard.ps1

See how user-friendly macOS is?

Steve jobs saying "Click, boom, amazing!"


There’s still room for improvement. You could, for example, delete imported bookmarks from Pinboard using another PowerShell script - just make sure that these were successfully consumed by Devonthink1. Building on that, since you spend 40 hours per week using Windows laptop, you can have another PowerShell script (this time on Windows) which checks once per day if you have some bookmarks tagged with ‘tbc’ in Pinboard. If you do, the script can display a system notification saying something along these lines:

System notification saying ""

Oh, wait, now you know that I actually did all that. Sorry for not sharing the complete solution, but I wanted to leave something for you to play with.

Go. Tinker. Be brave. Think different.

  1. try/catch in AppleScript is easy.