Skip to content

Instantly share code, notes, and snippets.

@dorzepowski
Last active November 18, 2019 18:16
Show Gist options
  • Save dorzepowski/1232d0db8fa12bd8f25905b77b853e2d to your computer and use it in GitHub Desktop.
Save dorzepowski/1232d0db8fa12bd8f25905b77b853e2d to your computer and use it in GitHub Desktop.
Clockify simple CLI
#!/bin/bash
########################################################################################################################
########################################################################################################################
# README:
#
# This script is currently prepared to add work log entries (time entries) to clockify from command line.
# Currently supported parameters for time entry are:
# - Project Name (project will be searched in clockify by name case sensitive)
# - Tag Name (tag will be searched in clockify by name case sensitive [only one tag can be provided currently])
# - Date for which work should be logged in format yyyy-mm-dd
# - Number of hours to log (a floating point number of hours like `7.5`)
# - Description (unfortunately currently value with the spaces does NOT work)
#
# Also you can choose PROJECT and TAG as a default project/tag for work log entries
# (so the required values will be date, number of hours and description)
#
#
# During first use you will need to log in to the clockify so prepare your email and passowrd ;)
#
# Requirements to run:
# - internet connection (obviously)
# - clockify.me account (obviously)
# - httpie <https://httpie.org> `apt-get install httpie`
# - jq <https://stedolan.github.io/jq/> `apt-get install jq`
########################################################################################################################
########################################################################################################################
set -o pipefail
#Terminal colors and style
red=`tput setaf 1`
green=`tput setaf 2`
normal_color=`tput sgr0`
bold=$(tput bold)
normal=$(tput sgr0)
API_URL="https://api.clockify.me/api"
CONFIG_FILE="$HOME/.clockify"
userId=''
token=''
refreshToken=''
workspaceId=''
defaultProjectName=''
defaultProjectId=''
defaultTagName=''
defaultTagId=''
tokenRefreshed=false
WORK_LOG_USAGE="[-h] [--help] [PROJECT] [TAG] [DATE] HOURS DESCRIPTION"
CONFIG_MODE_USAGE="config [-s DEFAULT_PROJECT_AND_TAG][-p DEFAULT_PROJECT] [-t DEFAULT_TAG] [-h]"
function usage() {
echo \
"usage: $WORK_LOG_USAGE
or $CONFIG_MODE_USAGE
"
}
function worklogHelp() {
echo \
"WORK LOG ENTRY MODE:
usage: $WORK_LOG_USAGE
PROJECT
Name of the project you want to log your work to.
When omitted DEFAULT will be used (see: CONFIG MODE to configure default value)
TAG
Name of the tag you want to assign to work log entry.
When omitted DEFAULT will be used (see: CONFIG MODE to configure default value)
DATE
Date of the work log entry in format 'yyyy-mm-dd'
When omitted today date will be used
HOURS
Decimal number of hours of work you want to log. Accepts decimal value of hours ex. 7.5
DESCRIPTION
Description of the log entry you want to add
-h --help
Show help description
"
}
function configHelp() {
echo \
"CONFIG MODE:
usage: $CONFIG_MODE_USAGE
Returns saved configuration
-s DEFAULT_PROJECT_AND_TAG
Search and set default values for project and tag using the same name.
Alias for: 'config -p DEFAULT_PROJECT_AND_TAG -t DEFAULT_PROJECT_AND_TAG'
-p DEFAULT_PROJECT
Search and set default value for project.
The default project will be used during logging work, so the PROJECT argument can be omitted.
PROJECT argument from 'WORK LOG ENTRY MODE' has precedence over stored DEFAULT_PROJECT
-t DEFAULT_TAG
Search for and sets default value for tag.
The default project will be used during logging work, so the TAG argument can be omitted.
TAG argument from 'WORK LOG ENTRY MODE' has precedence over stored DEFAULT_TAG
-c
Clean saved defaults [DEFAULT PROJECT and DEFAULT TAG]
-h
Show help for config
"
}
function help() {
usage
worklogHelp
configHelp
}
function success(){
echo "$1 finished with ${green}${bold}SUCCESS${normal}${normal_color}"
}
function failure() {
(>&2 echo "$1 finished with ${red}${bold}FAILURE${normal}${normal_color}")
exit 1
}
function checkRequirements(){
local requirementsMiss=false
command -v http >/dev/null 2>&1 || {
requirementsMiss=true
echo >&2 \
"Httpie <https://httpie.org> is required to use this script.
use command below to install it:
sudo apt-get install httpie
"
}
command -v jq >/dev/null 2>&1 || {
requirementsMiss=true
echo >&2 \
"jq <https://stedolan.github.io/jq/> is required to use this script.
use command below to install it:
sudo apt-get install jq
"
}
if [[ "$requirementsMiss" = true ]]; then
failure "Requirements check"
fi
}
function refreshToken {
if [[ "$tokenRefreshed" = true ]]; then
return 4
fi
{
userjson=$(api POST "/auth/token/refresh" "refreshToken"="$refreshToken" -b) && \
token=$(echo $userjson | jq -r .token) && \
refreshToken=$(echo $userjson | jq -r .refreshToken) && \
saveConfig && \
tokenRefreshed=true
} || {
failure "Refreshing token"
exit 1
}
}
function api() {
{
http --check-status --ignore-stdin $1 $API_URL$2 "X-Auth-Token":"$token" ${@:3}
} || {
case $? in
2) echo 'Request timed out during checking connection!' ;;
3) echo 'Unexpected HTTP 3xx Redirection during checking connection!' ;;
4) refreshToken && api $@ && return 0 ;;
5) echo 'HTTP 5xx Server Error during checking connection!' ;;
6) echo 'Exceeded --max-redirects=<n> redirects!' ;;
*) echo 'Unknown error during checking connection' ;;
esac
return $?
}
}
function signIn() {
read -p 'Username [email]: ' email
read -sp 'Password: ' password
echo
{
userjson=$(http --check-status --ignore-stdin POST https://api.clockify.me/api/auth/token "email"="$email" "password"="$password" -p b ) && \
userId=$(echo $userjson | jq -r .id) && \
token=$(echo $userjson | jq -r .token) && \
refreshToken=$(echo $userjson | jq -r .refreshToken) && \
workspaceId=$(echo $userjson | jq -r '.membership |.[] | select(.membershipType == "WORKSPACE") | .targetId')
} && {
success "Sign in"
} || {
echo "$?"
failure "Sign in"
exit 1
}
}
function saveConfig {
echo "#Config file for clockify script. Avoid manual changes, use rather 'clockify config ...'" > $CONFIG_FILE
echo "userId=\"$userId\"" >> $CONFIG_FILE
echo "token=\"$token\"" >> $CONFIG_FILE
echo "refreshToken=\"$refreshToken\"" >> $CONFIG_FILE
echo "workspaceId=\"$workspaceId\"" >> $CONFIG_FILE
echo "defaultProjectName=\"$defaultProjectName\"" >> $CONFIG_FILE
echo "defaultProjectId=\"$defaultProjectId\"" >> $CONFIG_FILE
echo "defaultTagName=\"$defaultTagName\"" >> $CONFIG_FILE
echo "defaultTagId=\"$defaultTagId\"" >> $CONFIG_FILE
}
function bootstrap() {
if [ -f $CONFIG_FILE ]; then
{
source $CONFIG_FILE;
} || {
failure "Loading config file ($CONFIG_FILE)"
exit 1
}
fi
if [ -z "$userId" ] || [ -z "$token" ] || [ -z "$refreshToken" ]; then
echo "You need to sign in to clockify" &&
signIn &&
saveConfig
fi
}
############# API CALLS #################
function findProjectId(){
api GET "/workspaces/$workspaceId/projects/" | jq -r ".[] | select(.name == \"$1\") | .id"
}
function findTagId(){
api GET "/workspaces/$workspaceId/tags/" | jq -r ".[] | select(.name == \"$1\") | .id"
}
############# CONFIG MODE #####################
function cleanDefaults() {
defaultProjectName=''
defaultProjectId=''
defaultTagName=''
defaultTagId=''
{
saveConfig
} && {
success "Cleaning defaults"
} || {
failure "Cleaning defaults"
exit 1
}
}
function setupDefaultProject(){
local projectId=''
{
projectId=$(findProjectId $1)
} || {
failure "Setup default Project"
}
if [[ -z "$projectId" ]]; then
failure "Setup default Project"
fi
defaultProjectName=$1
defaultProjectId=${projectId}
{
saveConfig
} && {
success "Setup default Project"
} || {
failure "Setup default Project"
}
}
function setupDefaultTag(){
local tagId=''
{
tagId=$(findTagId $1)
} || {
failure "Setup default Tag"
}
if [[ -z "$tagId" ]]; then
failure "Setup default Tag"
fi
defaultTagName=$1
defaultTagId=${tagId}
{
saveConfig
} && {
success "Setup default Tag"
} || {
failure "Setup default Tag"
}
}
function config() {
local clean=false project='' tag=''
if [[ -z "$@" ]]; then
cat $CONFIG_FILE
exit 0
fi
while getopts "hcs:p:t:" opt $@; do
case $opt in
h) configHelp && exit 0;;
s)
project=$OPTARG
tag=$OPTARG
;;
p)
project=$OPTARG
;;
t)
tag=$OPTARG
;;
c)
clean=true
;;
esac
done
bootstrap
if [[ "$clean" = true ]]; then
cleanDefaults
fi
if [[ ! -z "$project" ]]; then
setupDefaultProject ${project}
echo "defaultProjectName=\"$defaultProjectName\""
echo "defaultProjectId=\"$defaultProjectId\""
fi
if [[ ! -z "$tag" ]]; then
setupDefaultTag ${tag}
echo "defaultTagName=\"$defaultTagName\""
echo "defaultTagId=\"$defaultTagId\""
fi
}
########## WORK LOG MODE #################
function worklog(){
local project='' projectId='' tag='' tagId='' date='' hours='' description=''
function parseInput(){
case $# in
2)
hours=$1
description=$2
;;
3)
date=$1
hours=$2
description=$3
;;
4)
project=$1
date=$2
hours=$3
description=$4
;;
5)
project=$1
tag=$2
date=$3
hours=$4
description=$5
;;
*) echo "Too many arguments (got $# and max is 5), don't know how to handle it."
echo "usage $WORK_LOG_USAGE"
failure "Logging work"
;;
esac
}
function retrieveProject() {
if [[ ! -z "$project" ]]; then
projectId=$(findProjectId ${project})
else
project=${defaultProjectName}
projectId=${defaultProjectId}
fi
}
function retrieveTag() {
if [[ ! -z "$tag" ]]; then
tagId=$(findTagId ${tag})
elif [[ ! -z "$defaultTagId" ]]; then
tagId=${defaultTagId}
tag=${defaultTagName}
elif [[ ! -z "$project" ]]; then
tag=${project}
tagId=$(findTagId ${tag})
fi
}
function validate(){
if (($# < 7 )); then
echo "Error: $((7 - $#)) parameters aren't set but are required
project | ${project:-${red}Error: Empty value ${normal_color}}
projectId | ${projectId:-${red}Error: Empty value ${normal_color}}
tag | ${tag:-${red}Error: Empty value ${normal_color}}
tagId | ${tagId:-${red}Error: Empty value ${normal_color}}
date | ${date:-${red}Error: Empty value ${normal_color}}
hours | ${hours:-${red}Error: Empty value ${normal_color}}
description | ${description:-${red}Error: Empty value ${normal_color}}
"
failure "Logging work [gathering values]"
fi
}
function saveTimeEntry(){
local delta=$(echo "scale=0; ${hours}*60/1" | bc)
local start=$(date -d "$date +8 hour" --utc +%FT%TZ )
local end=$(date -d "$start +$delta minute" --utc +%FT%TZ )
{
api POST "/workspaces/${workspaceId}/timeEntries/" \
"start"="$start" \
"end"="$end" \
"billable"="true" \
"description"="$description" \
"projectId"="$projectId" \
"tagIds":="[\"$tagId\"]" \
-p b > /dev/null
} && {
echo "Entry has been saved:
| date | project | tag | description | hours
| $date | $project | $tag | $description | $hours
"
success "Logging work (saving entry)"
} || {
failure "Logging work (saving entry)"
}
}
function printSummary(){
# user time entries
# http GET "https://api.clockify.me/api/workspaces/${workspaceId}/timeEntries/user/${userId}" X-Auth-Token:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1Yzg5MjIzOGIwNzk4NzcwMjg0MGFjNDUiLCJpc3MiOiJjbG9ja2lmeSIsIm5hbWUiOiJEYW1pYW4gT3J6ZXBvd3NraSIsImV4cCI6MTU1Mjk0ODI2NywiZW1haWwiOiJkYW1pYW4ub3J6ZXBvd3NraUBjaXJjbGVrZXVyb3BlLmNvbSJ9.qrju2a6c58Ta5iGgfoEXh_wzVmtMQxEx0MeNY5c54uE start=2019-03-01T00:00:00Z end=2019-03-31T00:00:00Z -b
# entries in range
# http POST "https://api.clockify.me/api/workspaces/${workspaceId}/timeEntries/user/${userId}/entriesInRange" X-Auth-Token:$token "start"="2019-03-01T00:00:00Z" "end"="2019-03-31T00:00:00Z" -b
local currentMonthYear=$(date "+%Y-%m")
local periodStart=$(date -d "${currentMonthYear}-01" --utc +%FT%TZ)
local periodEnd=$(date -d "${periodStart} +1 month" --utc +%FT%TZ)
local currentMonthName=$(date -d "$periodStart" +%Y' '%B)
local totalMilis=$(api PUT "/workspaces/$workspaceId/timeEntries/duration" \
"start"="$periodStart" \
"end"="$periodEnd" -b)
local total=$(echo "scale=2; $totalMilis/1000/60/60" | bc)
echo \
"Summary ${currentMonthName}:
Total: ${total}h"
}
########### MAIN LOGGING WORK #############
input=$@
parseInput $@
bootstrap
retrieveProject
retrieveTag
date=${date:-$(date -I)}
validate $project $projectId $tag $tagId $date $hours $description
saveTimeEntry
printSummary
}
########### MAIN ##################
checkRequirements
if [[ -z "$@" ]]; then
usage
elif [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
help
elif [[ "$1" == "config" ]]; then
config ${@:2}
else
worklog $@
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment