Keul Blog

Automate your Slack A.F.K. status on MacOS

April 02, 2023

Introduction

Although I’ve been working remotely for a pretty long time, B-Open unofficially became a remote company (or at least “remote friendly™”) only with the global pandemic.

When working from home, one important duty is to notify colleagues when you are available or not. We use more or less the same timetable, but we have a high level of flexibility so we can’t be sure if a colleague is in front of the monitor or not. Communication in that case is even more important.

If you are Italian, you recognized this movie. If not, I don't pretend you get the joke

In one word: the AFK (Away From Keyboard) status.

Our primary communication channel is Slack and, for ages, we used the #general channel to notify every ”I’m going to lunch”, ”I’ll be off this afternoon” or ”I hate my job, I need a 10 minutes break from you all“.
To be honest I have never been too good at this: for example, sometimes I come back from lunch without typing ”I’m back”, but this protocol was unofficial and never agreed company wide.

Recently B-Open increased in number of employees (a rough +7 in a few months) and we all noticed that the #general channel became just a source of noise.

Some colleagues (you wont believe me, but this request did not come from the management) asked for changes: a more formal rule on how we should notify our AFK status.

Welcome to the Great AFK Status Protocol™:

  • when an employ goes AFK, he’s invited to change his Slack status
  • when an employ goes AFK, he can (optionally) write something on a dedicated #goodmorning-logoff channel (easy to be silenced in case you don’t care)
  • when an employ come BFK, he can (optionally) write a back-to-keyboard again on #goodmorning-logoff
  • having our #general channel back, less noisy
  • …profit

Sauron's Eye - from The Two Towers

You can say that Slack already handles some of these needs. For example: why use the Slack status when it automatically puts you “away” after a while?
This is only partially true. I’m old enough to have used HipChat and it was a lot better at this, more configurable on handling/notifying your hidle status.

Now that this TGAFKSP is official, I’m feeling more compelled to play my part. At the same time: I’m still not good at this and I’m also quite lazy.

To fulfill this new protocol, I’ve to:

  • click on personal Slack menu
  • click on “Update your status” (OK, I think there’s a shortcut for this)
  • type something, or select a recent ones (this is super cool, because it’s easy to set status you use often)
  • use a status icon maybe?
  • go to #goodmorning-logoff
  • type something (most of times, the same message)
  • remember to clear my status when back (!!!)
  • maybe: type something again on #goodmorning-logoff

Long Story Short

When I move away from my keyboard, I always lock my screen (although I’m completely alone at home. I trust nobody, neither my cat, and I don’t have one). And if I’m taking a break, or going to lunch, it means I’ll lock my screen.
So…

Can I automate my AFK Slack interactions, connecting them to a lock screen attempt?

What I need:

  • A piece of code that interacts with Slack
  • A way to intercept when I lock my screen, sending to Slack a status change and a message
  • A way to intercept when I unlock my screen, clearing my Slack status and send a message
  • Everything working on MacOS (operative system running on my work laptop)
  • Everything in Python or JavaScript (or just using external tools, they are always welcome)
  • Everything automatically launched at login

Let see how I solved this.

Disclaimer!
This post if highly focused on MacOS! If you don’t care about it, you are probably not going to like this.
You are warned.

Interacting with Slack

This is the easy part and I won’t spend time on this: Slack provides a way to define new apps that can interact with your account on a specific workspace.

Now… the concept of “app” is a bit confusing to me (nowadays everything is “an app”) but in the end the documentation is quite good; there’s also an official Python API 🙌.
After having installed this app in your workspace, and finding which scopes to enable, the Python code I need to write is trivial.

So, 70% of the job is done, isn’t it?

Intercept my lock/unlock screen actions

No.

The problem is: I’m on a Mac.
While intercepting system actions from Linux seems quite easy, on MacOS is a totally different matter. To be honest, is not so complex if you use Apple languages like Cocoa, Objective-C or whatever (see below), but not with Python or other languages I know.

I also looked for plugins, and there are some interesting ones, like this sleepwatcher but it seems an abandoned project not working anymore on recent MacOS.

MacOS Notification Center

After Googling and Stackoverflowing a while (like if it’s still 2022 and ChatGPT was not there) I found that Apple provides a notification center, notifying events for many actions like the lock/unlock screen.
It’s only a matter of subscribing to an event. There’s an API for this.

So the problem now is: how can I use this system API from Python?

Accessing the notification center from Python

Looking here and there, I found there’s a Python package able to interact with primitives provided by the OS (and many other powerful features), a Python bridge that exposes every API I need: welcome to PyObjC.
To be more precise, a subframework called Quartz.

To be honest, this fragmentation is a bit confusing (in the end I solved my dependencies with just pip install pyobjc-framework-notificationcenter) and I was not so interested in understanding this better.

Yet another problem: this package is not easy to use.
There are examples but not much API documentation. Please note: this because the documentation would probably be the same already provided by the Apple developer portal, but I was able to understand a bit there! Probably everything becomes automagically clear if you subscribe to the Apple Developer Program… who knows?

After finding a Stack Overflow answer that pointed me in the right direction, I was able to have “something working”.

I want to be honest: GitHub Copilot has been a real copilot in this.
It wrote bits of code I didn’t find anywhere and helped me understand better how libs above work. In the end, the core of the activity can be resumed in the following code:

# Everything is there because of pip installed pyobjc-framework-notificationcenter
import Foundation # WTF is Foundation? Something from Asimov maybe?
from PyObjCTools import AppHelper

nc = Foundation.NSDistributedNotificationCenter.defaultCenter()
nc.addObserver_selector_name_object_( screenLockHandler, "getScreenIsLocked:", "com.apple.screenIsLocked", None)
nc.addObserver_selector_name_object_( screenLockHandler, "getScreenIsUnlocked:", "com.apple.screenIsUnlocked", None)

AppHelper.runConsoleEventLoop()

Roughly speaking: I’m now able to call a method when the screen is locked and another when unlocked (screenLockHandler is an object of type NSObject which is 🤷‍♂️).
The final AppHelper.runConsoleEventLoop() makes this Python code to run indefinitely waiting for events, which is perfect as I want to make this a background daemon.

Running as a daemon

How can it be complex to run something as a daemon on startup in my MacOS? I’m sure I just need to put a .sh file somewhere, isn’t it?

No way.

The official way is launchd, which requires you build an XML file and register it with the tool.
I spent some time on this, but I was not able to make it work.
Disclaimer: I was tired, not happy to spent time on the less interesting activity.

I found another approach, which is a bit hacky but easier: Automator app, preinstalled in every MacOS.

Automator Icon

One of the many tasks an Automator application can do is ”Execute a shell script“.

Automator - execute shell script screenshot

So it’s just a matter of:

  • Create a new Automator application (I called it “AFK”)
  • Add a ”Execute a shell script” task pointing to my afk_agent Python entry point

That’s all, right?

Not exactly.
It seems that Automator is making a copy of the script, embedding it somewhere (and generating a 4Mb file). This is not a big problem until you don’t have to change the script, but during the development this was very annoying.

A better approach:

  • Create a new Automator application (I called it “AFK”)
  • Create a afk.sh file internally just calls the afk_agent Python entry point
  • Add a ”Execute a shell script” task pointing to afk.sh

In this way the embedded code is just the shell script and I can change the agent when I need.

Running at login

Now I need to run my Automator task at login.

This is done with the standard MacOS user interface: “System Preferences” “Users and groups” “Login elements” just add the Automator stuff here.

A screeenshot of the Automator app in the "Login elements" system user interface

Disclaimer

As I said: this is hacky. Someday I will drop this and learn how to make launchd.

But it is not this day.

Also, there’s another advantage in this approach: the Automator icon continues to spin until the agent terminates (so: never).
This is not wasting CPU and I can see that I’ve the agent running.

Automator icon spinning

You can call this a poor-man-system-tray, for free! 🙌

Advanced: custom commands

80% of my AFK needs are solved, but I have exceptions:

  • I want to warn colleagues when I log-off
  • I’d like to notify differently when I go to lunch
  • Sometimes my lunch to is “special” (when I go for some jogging 🏃‍♂️)

I’ve my agent running, but it only intercepts my lock/unlock screen and displays standard messages.
How can I send it custom messages?

The solution is to write an AFK client.

I live with open terminals, so it’s very convenient for me to write something like:

afk lunch

What I expect from this:

  • the afk client connects to my afk_agent
  • to the custom command lunch, I assigned a special configuration for the Slack away status and message.

This is easy, but to make it worth it, my command should also lock my screen for me.
Even better: maybe, if my custom command is something like logoff, I want to send my Macbook to sleep directly.

How difficult can be to explicitly control the lock screen or suspend status on MacOS?

Sending system commands with osascript

You already know the answer: it seems that nothing is trivial at this level on a Mac.

After many search, I found:

  • there’s an osascript command you can use to send AppleScript commands
  • there’s a command for sending the MacOS to sleep: tell application "Finder" to sleep
  • but there’s no command to lock the screen! 🤦‍♂️
    Luckily you can solve this by sending the same keys combination you use for this task: tell application "System Events" to keystroke "q" using {control down, command down}

So, in Python everything is just something like this:

import subprocess
def sleep():
    subprocess.Popen("""osascript -e 'tell application "Finder" to sleep'""")

MacOS Privacy settings

When running this kind of commands, you are required to allow the program to take control of the machine.

This is done by configuring “System Preferences” ➡️ “Privacy and Security” ➡️ “Privacy”, but you don’t have to do it manually: the OS automatically asks you for permission and will open the panel for you.

Conclusion

I’ve the code, a very hacky code full of global (I love using global when doing stuff for myself, like a big mid-finger to my software engineering study) but I’m quite happy with the result.

I mean: it has been used in production for a couple of months and nobody complained.

Everything is available on GitHub as a Python package: afk_slack_agent.

👋

Skeletor "until we meet again meme"


I'm Luca Fabbri, I'm a (Web?) (Full-stack?) (Front-end?) developer at B-Open.