~/.eth0/ 

This is my code. There is a lot like it, but this is mine.

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

If you’re booking a parking slot

Optional fields

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…

View license file

#!/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 ))