Last active
November 18, 2019 18:16
-
-
Save dorzepowski/1232d0db8fa12bd8f25905b77b853e2d to your computer and use it in GitHub Desktop.
Clockify simple CLI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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