Matrix Booking parking spot & desk booker
/matrix-booking/matrix-booker.sh
This script saves time when trying to add a booking in Matrix Booking. If your company uses it to book desks or parking spots, you can use this.
The script needs your Matrix Booking credentials in your ~/.netrc
file for the
hostname app.matrixbooking.com
. Apart from that, you only need to provide the
script with the necessary fields for the kind of booking you want to make:
If you’re booking a desk
office
: The name of the building where your desired desk is.desk_name
: The name of the desk you want to book.
If you’re booking a parking slot
parking_area
: The name of the building in Matrix Booking where the parking spot you want to book is.spot_name
: The name of the parking spot you want to book.license_plate
: Your car’s license plate to put in the booking title.
Optional fields
booking_date
: The date you want to book the parking spot. By default it’s set to book your parking spot two weeks in advance. This option supports any valid expression parseable by GNUstrtotime
, so anything you can pass to GNUdate
is valid, like'today'
,'+2 weeks'
or'next tuesday'
.start
: The starting hour of your booking. The default starting hour is 7am UTC for parking spots and 6am UTC for desks. Specify just the hour part in 24-hour format, e.g.start=9
.end
: The ending hour of your booking. The default ending hour is 6pm UTC for parking spots and 5pm UTC for desks. Specify just the hour part in 24-hour format, e.g.end=20
.
All those options can be specified in the command line like --parking-area
,
--spot-name
, --license-plate
, --office
, --desk-name
and --booking-date
or you can create a configuration file at ~/.config/matrix-booker.conf
and set
them there like Bash variables.
You can also maintain a list of ignored dates so the script doesn’t try to book
anything on those dates; you can use it for bank holidays or for your days off.
Just store one date per line in ISO format (YYYY-MM-DD
) in a file at
~/.config/matrix-booker-ignores.conf
and the script will end normally before
trying to book anything if it finds the requested date in the list.
You can run the script by hand or as part of a cronjob. In that case, you can run it everyday and it will ignore dates on weekends automatically.
The script tries hard to use the available date
command in your system. If you
use macOS, it’ll try to use gdate
if you installed coreutils
with Homebrew,
or if you don’t have it installed but Homebrew is present it’ll try to install
coreutils
itself. If you use any other flavor of BSD or you have a Mac but you
don’t have Homebrew installed, it’ll still try to use the BSD date
in your
system. In that case, you’ll probably have to provide the booking_date
option
with an adjustment value supported by BSD date
, although if you let it use the
default value it’ll try the equivalent adjustment value '+2w'
.
The script will honor the user’s timezone and the $TZ
environment variable can
be set to any valid timezone name.
Download
this script
Secondary click/Save as…
#!/usr/bin/env bash
#
# Copyright 2018-2022 eth0 <ethernet.zero@gmail.com>
#
# This work is free. You can redistribute it and/or modify it under the terms of
# the ISC License. See the COPYING file for more details.
# This script doesn't need any input from stdin, so just close it
exec 0<&-
# Join an array using a separator
join()
{
local sep="$1"
shift
local ret
printf -v ret "%s$sep" "$@"
echo "${ret::-${#sep}}"
}
# Call the Matrix Booking API
api()
{
local api='https://app.matrixbooking.com/api/v1'
local endpoint="$1"
shift
# Join query parameters
local params="$(join '&' "$@")"
local curl_args=(-snL)
[[ "$endpoint" != /* ]] && endpoint="/$endpoint"
# If you send something through stdin, then it's a POST request
if read -t0 _; then
curl_args+=(
-X POST
-H 'Content-Type: application/json;charset=utf-8'
-H "X-Time-Zone: ${TZ:-Europe/Madrid}"
-d@-
)
fi
curl "${curl_args[@]}" "$api$endpoint${params:+?}${params}" 2>/dev/null
}
# Shortcut function for jq'ing
json()
{
local query="$1"
shift
jq -rc "$@" "$query" 2>/dev/null
}
# Make sure all necessary options are provided
ensure_opts()
{
for arg; do
if [[ -z "${!arg}" ]]; then
err "You need to set the $arg option in $cfgfile or provide the --${arg//_/-}=<${arg^^}> option in the commandline"
return 1
fi
done
}
force_opts()
{
ensure_opts "$@" || exit 1
}
# Override variables from long options passed as arguments
# For example, --variable-name=value would set variable_name='value'
parse_args()
{
local opt
local val
local arg
for arg; do
# Ignore any arguments that don't start with a double dash
# So let's say there's a --variable-name='value' argument
if [[ "$arg" == --* ]]; then
# Remove the leading double dash
# Result: --variable-name=value -> variable-name=value
opt="${arg#--}"
# Set an empty value by default just in case the argument doesn't
# have a value, e.g. --variable-name would be the same as
# --variable-name=''
val=''
# If the argument has an equals sign, take everything after it as
# the value
# Result: variable-name=value -> value
[[ "$opt" == *=* ]] && val="${opt#*=}"
# Then take everything before it as the variable name and replace
# all dashes with underscores
# Result: variable-name=value -> variable_name
opt="${opt%%=*}"
opt="${opt//-/_}"
# Use the resulting option name as a variable nameref
declare -n var="$opt"
# So now, setting the nameref's value will set the variable named
# before the option name
var="$val"
# Then undefine the nameref and move on
unset -n var
fi
done
}
# Non-fatal error
err()
{
echo "${0##*/}: $*" >&2
}
# Fatal error
fail()
{
err "$*"
exit 1
}
# Check if a command is a file on disk and not an alias or a function
is_cmd()
{
local command="$1"
[[ "$(type -p "$command" 2>/dev/null)" ]]
}
# Portable-ish GNU+BSD date command
_date()
{
local datespec="$1"
local datefmt="${2:-%F}"
local date
local retval
if [[ "$OSTYPE" == *darwin* ]]; then
# If it's a Mac and Homebrew is installed, install coreutils to get
# GNU date as gdate
if ! is_cmd gdate && is_cmd brew; then
echo 'Ah, let me install GNU date for you.'
brew install coreutils || fail "Couldn't install GNU date in your system"
fi
date="$(gdate -d "$datespec" "+$datefmt" 2>/dev/null)"
retval=$?
elif ! is_cmd date; then
# No date command in your system? How odd!
fail "There's no date command in your system"
else
# Try a GNU date calculation
date="$(date -d "$datespec" "+$datefmt" 2>/dev/null)"
retval=$?
# If it doesn't generate anything, date is probably a BSD date
# It can be a macOS without Homebrew or any other *BSD
if [[ -z "$date" ]]; then
# If the booking date is the default, try the BSD date equivalent
# Unfortunately we can't translate GNU strtotime() values automatically
# because those date specs can be complex
[[ "$datespec" == '+2 weeks' ]] && datespec='+2w'
# Now try a BSD date command line
date="$(date -v"$datespec" "+$datefmt" 2>/dev/null)"
retval=$?
fi
# Nothing generated with a GNU nor a BSD date command line, so whatever
# date command is in the system, we don't know how to use it
[[ "$date" ]] || retval=1
fi
echo "$date"
return "$retval"
}
_time()
{
local date="$1"
local hour="$2"
printf '%sT%02d:00:00.000' "$date" $hour
}
# Get an hour in the user's timezone from an hour in UTC
_tzhour()
{
local hour="$1"
local tzhour
# First try GNU date, else BSD date
tzhour="$(date -d "$hour:00 UTC" +%k 2>/dev/null)"
[[ "$tzhour" ]] || tzhour="$(date -j -f '%k:%M %Z' "$hour:00 UTC" +%k 2>/dev/null)"
echo "$tzhour"
[[ "$tzhour" ]]
}
building_id()
{
api /location kind=BUILDING | \
json '.[] | select(.name == $name) | .id' \
--arg name "$*"
}
item_id()
{
local kind="$1"
local location_id="$2"
shift 2
api /location kind="${kind^^}" l="$location_id" | \
json '.[] | select(.name == $name) | .id' \
--arg name "$*"
}
book_item()
{
local item_id="$1"
shift
local extra_fields=()
# Turn all remaining arguments in the form key=value into JSON key/value
# pairs, e.g. key=value -> "key":"value"
for arg; do
extra_fields+=("\"${arg/=/\":\"}\"")
done
local extras="$(join ',' "${extra_fields[@]}")"
api /booking notifyScope=ALL_ATTENDEES <<-JSON
{
"locationId": $item_id,
${extras}${extras:+,}
"attendees": [],
"extraRequests": [],
"bookingGroup": {},
"owner": $owner,
"ownerIsAttendee": true
}
JSON
}
# Print any error messages returned by the server in the JSON response
process_response()
{
local response="$*"
if [[ -z "$response" || "$response" == *'doctype html'* ]]; then
return 1
elif [[ "$(json '.messageCode' <<< "$response")" == error* ]]; then
echo "$(json '.message' <<< "$response")"
return 1
fi
}
book()
{
local area="$1"
local area_name="$2"
local item="$3"
local item_name="$4"
local date="$5"
shift 5
# Default values are for parking spots
local kind=EQUIPMENT
local startHour=${start:-$(_tzhour $default_start)}
local endHour=${end:-$(_tzhour $default_end)}
if [[ "${item,,}" == desk ]]; then
kind=DESK
# Adjust the start and end hours if different from the default
if [[ -z "$start" ]]; then
(( startHour-- ))
fi
if [[ -z "$end" ]]; then
(( endHour-- ))
fi
fi
# Get building ID
area_id="$(building_id "$area_name")"
if [[ -z "$area_id" ]]; then
err "${area^} area '$area_name' not found"
return 1
fi
# Get item ID
item_id="$(item_id "$kind" "$area_id" "$item_name")"
if [[ -z "$item_id" ]]; then
err "${item^} '$item_name' not found"
return 1
fi
echo "Booking ${item,,} '$item_name' on $date..."
response="$(book_item "$item_id"\
timeFrom="$(_time "$date" $startHour)" \
timeTo="$(_time "$date" $endHour)" \
"$@"
)"
if ! errormsg="$(process_response "$response")"; then
err "${errormsg:-Error when performing the API request to book your ${item,,}}"
return 1
else
echo "Your booking for ${item,,} '$item_name' on $date is $(json '.status' <<< "$response")."
fi
}
booking_date='+2 weeks'
# Parking spots can be booked from 7am to 6pm UTC, desks from 6am to 5pm UTC
default_start=8
default_end=18
cfgfile="${XDG_CONFIG_HOME:-$HOME/.config}/matrix-booker.conf"
ignorefile="${XDG_CONFIG_HOME:-$HOME/.config}/matrix-booker-ignores.conf"
# Import all variable definitions in the config file, which means you can have
# all other sorts of comments and lines in it and they will be ignored
[[ -r "$cfgfile" ]] && source <(grep -E '^[[:alnum:]_]+=' "$cfgfile")
# Then override any variables passed as long options in the command line
(( $# )) && parse_args "$@"
# After that, make sure none of the necessary variables have been assigned an
# empty value
force_opts booking_date
# Calculate the booking date
date="$(_date "$booking_date")"
if [[ -z "$date" ]]; then
fail "Invalid booking date '$booking_date'"
fi
# Check if the booking date is bookable: not in weekends or in a list of ignored dates
if (( $(_date "$booking_date" %u) > 5 )) || grep -qE "^${date}\$" "$ignorefile"; then
exit 0
fi
# Get user data from Matrix Booking
userdata="$(api /user/current)"
if [[ "$userdata" == *'doctype html'* ]]; then
fail "Login failed, please make sure to set your credentials in your ~/.netrc for app.matrixbooking.com"
fi
owner="$(json '{id: .personId, email: .email, name: .name}' <<< "$userdata")"
failed=0
if [[ "$desk_name" ]] && ensure_opts office; then
book office "$office" desk "$desk_name" "$date"
(( failed += $? ))
fi
if [[ "$spot_name" ]] && ensure_opts parking_area license_plate; then
book parking "$parking_area" 'parking spot' "$spot_name" "$date" label="$license_plate"
(( failed += $? ))
fi
(( ! failed ))