Laravel Envoyer Notifications in Zoho Cliq

I recently switched a project from a home-grown deployment script to Laravel Envoyer. While the homegrown script could maybe have been adapted it would have taken time, and had to be maintained, and Envoyer offers some useful extras like Slack, Discord, and Microsoft Teams integration, and heartbeat monitoring.

Except we don’t use Slack, Discord, or Teams, we use Zoho‘s Slack competitor, Cliq.

Comparing Slack’s web hooks with Cliq we see that a bot is needed in Cliq, but making one is super-easy, on the order of four or five clicks.

Once there is a bot it can receive web hooks. Figuring out the right way to provide an authentication token was weird, there is not clear documentation, but the reply from Eric Hirst in in the Zoho forum thread announcing Cliq bots has a solution that works.

Now we can receive web hooks, I tried putting the bot’s web hook URL in for both Slack notifications and Discord notifications in Envoyer, and I got notifications. Both systems expect a POSTed JSON object, so that’s what they get. The Discord one is simpler one of the properties is simply markdown text, which Cliq understands, so the Cliq incoming web hook handler needs to grab that markdown text and return it so the bot will post the message in Cliq.

// Configure the bot's incoming webhook URL in any third-party service to trigger this handler.
response = Map();
message = body.toMap();
if(message.containKey('content'))
{
	response.put("text",message.get('content'));

	// If you want the bot to post to one of your channels, and want it to appear
	// as the bot posting, add the bot info like this. Otherwise it the message 
	// will be "from" the person who owns the auth token, with a small "bot" flag next to it.
	response.put('bot',{"name":"Envoyer","image":"https://envoyer.io/img/favicons/apple-touch-icon-120x120.png"});

	// If you want to post to a channel this is how. If you don't do this the bot
	// will simply post the message to any chats it has open.
	zoho.cliq.postToChannel('general',response);
}
return response;

The beauty of simply dumping the markdown into the chat is that this simple code handles all notifications from Envoyer, including successful & failed deployments and heartbeat notifications. You can look for specific text and alter your notification if you want, (I have a big headline for successful deployments), but it’s not needed.

It would be easier to test this if there was a way to trigger a test notification in Envoyer, but Envoyer isn’t really something to tinker with, it’s supposed to just work. It would also be great if Envoyer could support Zoho Cliq directly, but I’m not sure many people in the Laravel community are using Zoho.

So now we have Envoyer notifications in Zoho Cliq in our organization. If you’re one of the few Laravel / Zoho unicorns out there like me you can have them too.

One-Click DevonThink Markdown Journal Entry

With COVID-19 running wild here in Quebec we are homeschooling this year. One of the ways that the government evaluates the progress of homeschool students is by asking parents to submit a portfolio. Since I’m primarily responsible for English classes I need to have records. I already use DevonThink as a data repository, so that’s where the homeschooling records are going.

English Journal

Every time we spend some time working on English I create a journal entry as a new Markdown in Devonthink. I thought about keeping one large document and continually appending to it but several small documents seems more searchable and gives me accurate timestamps and geolocations, (if we ever travel again). The journal entries are all kept in a Homeschool > English Journal group.

Since creating a new markdown document several times a week is kind of slow I made a template, and added a button to the menu bar in Devonthink to create a new English Journal entry with one click.

Creating a Devonthink Template

As a fairly new Devonthink user this was my first foray into using templates. I assumed there would be some sort of template editor in Devonthink but there isn’t. You can create an existing document as a template or copy an existing template in Finder. There are also two kinds of templates: normal, and “smart” templates. Normal templates aren’t completely dumb – they have some placeholders that can be replaced by dynamic values, (things like Date, Time, or the user’s name). Smart templates are a bundle of files, including an AppleScript file. The AppleScript file is the main file in the template and controls everything. There can be other files in the bundle, (like a template.md file), and the AppleScript file can refer to those files. I ended up with a Smart template, with some minimal smarts.

A screenshot of the directory structure of the English Journal Devonthink template.The script in my template opens my “Home” database, makes sure the Homeschool > English Journal group exists, then creates a new markdown document in the English Journal group based on the English Journal.md file. The script is relatively simple:


-- Import helper library
tell application "Finder" to set pathToAdditions to ((path to application id "DNtp" as string) & "Contents:Resources:Template Script Additions.scpt") as alias
set helperLibrary to load script pathToAdditions

-- Get the template file path.
set theTemplateFile to helperLibrary's pathToLocalizedResources() & "English Journal.md"

tell application id "DNtp"
	
	
	-- Open the database.
	set theDatabase to open database "/Users/John/Databases/Home.dtBase2"
	-- Get a reference to the group I want.
	set theLocation to create location "/Homeschool/English Journal" in theDatabase
	
	-- Create the document based on the template file.
	set entry to import theTemplateFile to theLocation placeholders {}
	
	-- Open the new document.
	open tab for record entry
	
end tell

The markdown file is pretty simple too. %time% and %longDate% are Devonthink placeholders to put the date & time into the journal entry.

# English Activity Record
## %time% %longDate%

### Activities
- Pages ### - ### in _Toute ma 3e année_.

### Parent Reading Aloud

I may adjust it to prompt for a title for each entry, and to add my current location to the text of each entry.

One-Click Template Use

With a working template bundle, (in ~/Library/Application Support/DEVONthink 3/Templates.noindex), it was time to put a button in the Devonthink menu bar:

How to put a template in the Devonthink toolbar:

  1. Move the template bundle into ~/Library/Application Support/DEVONthink 3/Templates.noindex/Toolbar
  2. Restart Devonthink
  3. Go to View > Customize Toolbar in Devonthink
  4. Drag the “English” button to the Toolbar.

While View > Customize Toolbar is open you can choose to show the Icon and Text in the toolbar if you want.

Set an icon for the toolbar button:

It is possible to set a custom icon for the toolbar button, (by default it’s a gear). Devonthink has a weird way of setting the icon, (weird in a good way): set the icon of the template bundle in Finder. Devonthink takes whatever icon Finder thinks the template should have and puts it in the toolbar.

How to customize a file’s icon on macOS:

  1. Open the image you want to use as a custom icon, (in Preview, or wherever).
  2. Copy the image, (Command-C, or Edit > Copy).
  3. Option-click the file that will get the custom icon.
  4. Select “Get Info” from the menu.
  5. Click the file icon in the “Get Info” window so it’s highlighted.
  6. Command-V to paste the image you copied in Step 2 as the custom icon.

If you ever want to remove the custom icon to back to the Get Info window and Command-X to remove it.

The new “English” button in my Devonthink.

Mobile Entry?

This system only works on my computer. I’d like to have a mobile option but in this moment it’s not a pressing need.

OmniFocus Progress Chart Part 1: Extract & Save Data

Measured progress motivates me. I want to see how many things are in my OmniFocus library and if it’s trending larger or smaller. My desktop has been home to a list of today’s completed OmniFocus tasks for a long time, and now it’s home to a pair of charts: remaining items over the last 30 days, and items completed per day for the last 30 days.

The charts are created in 3 steps, and I am breaking these up into two posts:

  1. Extract a snapshot of the status of the database every day, (this post).
  2. Use that snapshot to create a text-based chart, (next post).
  3. Show the chart on my desktop, (next post).

Steps 1 & 2 are handled by a Javascript for Automation application written in Script Editor. Step 3 is handled by GeekTool.

Extracting & Saving the Database Status

There are a few ways to build a program that interacts with other programs on a Mac. For the completed task list I used Automator, but I don’t like editing Java- or Apple- script in the tiny Automator windows, so I tried out Script Editor. Script Editor lets us create an app bundle with somewhat modularized code, but isn’t the greatest IDE and crashes more than it should. Save your work often.

Applescript is foreign to me so I opted for Javascript for Automation, aka JXA, Apple’s, (maybe abandoned?), attempt to get a Javascript version of Applescript running. It works ok, but documentation of how it actually works, and what parts of modern Javascript are supported, is hard to find.

To extract the data we can use OmniFocus’s flattenedTasks list, which gets a flattened list of all tasks in the database, then we can filter that list of tasks by status, creation date, completion date, and so on:

// Get the "document" that we need to work with.
const ofdoc = Application('OmniFocus').defaultDocument;

// Get a list of all tasks in the DB.
const tasks = ofdoc.flattenedTasks;
// now tasks.length is the total number of tasks in the DB
// (this will change a lot when you archive old tasks)

// Filter using the .whose method.
const remainingTasks = tasks.whose({
      effectivelyCompleted: false,
      effectivelyDropped: false
});

// ... etc. for completed tasks, and tasks completed/added/dropped in the past day.

Filtering with whose is pretty slow so this can’t be run every few seconds, (it takes several seconds to run), but for now I’m only updating daily so it’s fine if it’s slow.

I tried iterating over the tasks and checking the effectivelyCompleted property on each one, thinking it would be faster, but effectivelyCompleted has to send a message to OmniFocus to get a response, and doing that for my entire library is much slower than a single .whose() call.

Once all the tasks are filtered the current stats put into a Javascript object, which is .push()‘ed onto the end of a Javascript array of all the stats and stored on disk as a JSON file.

Modularizing the code was a hurdle.

I tried using module exports & imports but they don’t work. It seems like there is an import() function available but I can’t find what it does. It’s hard to see what Javascript/ECMAScript features are supported in JXA. Some documentation says that it uses the same Javascript engine as Safari, but if that was true when it was released it doesn’t appear to be true now.

Even without exports & imports the Release Notes say we can import libraries into our script like so:

// Imports from StatsProcessor.scpt - supposedly. Also, where is StatsProcessor.scpt?
const StatsProcessor = Library("StatsProcessor");

The 10.11 Release Notes say that we can put our library scripts into the ‘Contents/Library/Script Libraries’ directory of the App we’re making. I couldn’t find a way to do this, or even see the Script Libraries folder in Script Editor, but once I moved everything around with finder the library was recognized and the import worked. But this code crashed:

const StatsProcessor = Library("StatsProcessor");
StatsProcessor.anyPublicMethod();

Not only did it crash, but it made Script Editor crash as well. Calling any method on an imported library caused a crash. Apparently we need to use a “compiled” script – a .scptd file – for libraries. This isn’t documented anywhere that I can find, and there seems to be no way to change between a .scpt and .scptd file, at least not using a GUI.

There’s also no documentation for that a Library actually is and what’s available when it’s imported. From what I can see any global function declared in the imported file is available as a method on the imported object.

Once the modularization was working it was relatively straightforward to figure out where to store the data, (answer: In ~/Library/Application Support/OmniFocusStats), and save it as desired.

Next up: Using the data to make & display a chart.

Paste!

Once in a while I run into websites that block pasting in password, (or confirm password), fields. Like most bad ideas it doesn’t happen often but it happens just enough that I no longer want to manually type out the javascript to re-enable pasting.

So I made a bookmarklet. Here it is:

Paste!

Drag it to your bookmark toolbar to install it in your browser. When you find a page blocks paste click the bookmark and paste should be restored.

How it works

When clicked a bit of Javascript executes that adds an event listener to the capturing phase of the paste event and stops any further propagation of the event. Because we usually react to the bubble phase of DOM events, and the capture phase comes first, the bookmarklet’s event listener captures the event, lets the default action happen, and prevents all other JS from listening to it, preventing the web page from blocking your carefully-pasted text. The actual code is a shortened version of the function at How to Enable Pasting Text on Sites that Block It.

Gallery Captions for WooCommerce

Gallery Captions for WooCommerce is a WooCommerce Extension that adds captions to the product images on WooCommerce’s Single-Product page. It is my first commercial WooCommerce extension, and I’m happy to say it’s available for purchase on WooCommerce.com.

Why Captions?

On the surface this seems like a question with an obvious answer: To tell potential customers what they’re seeing in an image.

But when researching I found even more compelling reasons: Captions may be the most-read text on a page, (even more than the page title!). If a caption isn’t the most-read text it’s still very well-read, and the combination of an image and caption can help customers understand what they’re looking at and make a well-informed buying decision.

Super Simple

Gallery Captions is super simple to use. There is documentation, (of course), but it’s barely needed. There are no settings, just install & activate the plugin and it will pull the information set as the Title and Caption in your Media Library. I may add options in the future to choose to show the title, caption, or description, but for now it’s super-simple.

Add Captions to Your Store

If you want to add captions to your WooCommerce product galleries, (and you should want to), then go ahead and use Gallery Captions for WooCommerce.