311 lines
9.5 KiB
Bash
Executable File
311 lines
9.5 KiB
Bash
Executable File
#!/bin/bash
|
||
# The epsi2ics project produces an ICS file from an EPSI school-year calendar.
|
||
# Copyright © 2019 Y. Gablin, under the GPL-3.0-or-later license.
|
||
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
|
||
#
|
||
# This script only works when executed in the timezone of the calendar, and only if this timezone offset
|
||
# is low enough to keep all events in the day they belong to in the web calendar (usually −6h ≤ TZ ≤ +8h).
|
||
#
|
||
# From EPSI “edtmobilityeng” to ICS, for the current school year. Usage:
|
||
#
|
||
# epsiEDTtoICS.sh -u <user> [ -c [ <path> | - ] ] [ -j ]
|
||
# epsiEDTtoICS.sh -u <user> [ -p [ <path> | - ] [ -w <count> ] ] [ -j ]
|
||
# <user>: EPSI Login (firstname.lastname)
|
||
# <path>: Location where the ICS file is created (-c) or patched (-p)
|
||
# <count>: Number of weeks to update (including the current one), if -p is specified
|
||
#
|
||
# If no path is given, “-” is assumed, which is a placeholder for the standard output.
|
||
# If neither -c nor -p is given, “-c -” is assumed.
|
||
# If -p is used with “-” as a path, then only the updated weeks are sent to the standard output.
|
||
# If -p is given without -w, the default count of updated weeks is 5.
|
||
# If -j is used, logs are sent to systemd-journald instead of the standard error (needs systemd-cat).
|
||
|
||
LANG8BIT=fr_FR@euro
|
||
|
||
LOGIN=
|
||
PATCH=
|
||
OUTPUT=-
|
||
MAXOFFSET=
|
||
JOURNAL=
|
||
readonly -a LEVELS=( emerg alert crit err warning notice info debug )
|
||
|
||
while getopts u:c:p:w:j arg; do case "$arg" in
|
||
u) LOGIN="$OPTARG" ;;
|
||
c) OUTPUT="${OPTARG:--}"; PATCH= ;;
|
||
p) OUTPUT="${OPTARG:--}"; PATCH=true ;;
|
||
w) MAXOFFSET="${OPTARG//[^0-9]}" ;;
|
||
j) JOURNAL=true ;;
|
||
esac; done
|
||
|
||
# $1: log level ∈ [0 … 7]
|
||
# $2: log text (“-” for STDIN)
|
||
function log() {
|
||
local l="$1"
|
||
shift
|
||
{
|
||
[ "$1" == - ] && cat || echo "$*"
|
||
} | {
|
||
[ -n "$JOURNAL" ] && systemd-cat -t epsi2ical -p ${LEVELS[$l]} || sed "s/^/[${LEVELS[$l]}] /" >&2
|
||
}
|
||
}
|
||
|
||
function check_parameters_and_set_defaults() {
|
||
if [ -z "$LOGIN" ]; then
|
||
log 2 'No user provided (option -u)'
|
||
exit 1
|
||
fi
|
||
if [ "$OUTPUT" != - ] && ! [ \
|
||
-n "$OUTPUT" -a \
|
||
-d "$(dirname "$OUTPUT")" -a \
|
||
-w "$(dirname "$OUTPUT")" -a \
|
||
"$(basename "$OUTPUT")" != . -a \
|
||
"$(basename "$OUTPUT")" != .. \
|
||
]; then
|
||
log 2 "The directory (option -c or -p) “$(dirname "$OUTPUT")” is not writeable, or the filename is invalid"
|
||
exit 2
|
||
fi
|
||
if [ -n "$PATCH" -a "$OUTPUT" != - ] && ! [ -f "$OUTPUT" -a -r "$OUTPUT" -a -w "$OUTPUT" ]; then
|
||
log 2 "Patching (option -p) needs an existing r/w file; this one is not: $OUTPUT"
|
||
exit 3
|
||
fi
|
||
if [ -n "$JOURNAL" ] && ! which systemd-cat >/dev/null; then
|
||
JOURNAL=
|
||
log 4 "Command “systemd-cat” not found"
|
||
fi
|
||
MAXOFFSET=$((${MAXOFFSET:-5}*7))
|
||
}
|
||
|
||
function fold_long_lines() {
|
||
env LANG="$LANG8BIT" sed -r 's/(.{73})(.)/\1\n \2/g'
|
||
}
|
||
|
||
function lf_to_crlf() {
|
||
sed 's/$/\r/'
|
||
}
|
||
|
||
function write_output() {
|
||
[ "$OUTPUT" == - ] && cat || {
|
||
cat >"${OUTPUT}.tmp" && mv -f "${OUTPUT}.tmp" "$OUTPUT" || {
|
||
log 3 "Failed to write to: $OUTPUT"
|
||
exit 20
|
||
}
|
||
}
|
||
}
|
||
|
||
# $1: string to encode
|
||
function urlencode() {
|
||
local saveLANG="$LANG"
|
||
export LANG="$LANG8BIT"
|
||
local index char
|
||
|
||
for (( index=0; index<${#1}; index++ )); do
|
||
char=${1:$index:1}
|
||
case "$char" in
|
||
[-A-Za-z0-9._~]) printf '%s' "$char" ;;
|
||
*) printf '%%%02x' "'$char" ;;
|
||
esac
|
||
done
|
||
|
||
export LANG="$saveLANG"
|
||
}
|
||
|
||
# $1: date in format %m/%d/%Y
|
||
function fetch_html_week() {
|
||
local url="https://edtmobiliteng.wigorservices.net/WebPsDyn.aspx?action=posEDTBEECOME&serverid=C&Tel=$(urlencode "$LOGIN")&date=$1"
|
||
for ((i=1; i<5; i=i*2)); do
|
||
week="$(curl -s "$url")" && { echo "$week"; sleep 0.3s; return; } || log 7 "Request “${url}” failed"
|
||
sleep ${i}s
|
||
done
|
||
log 3 "Cannot get week calendar from upstream for Month/Day/Year: $1"
|
||
exit 10
|
||
}
|
||
|
||
# &0: html week (see: fetch_html_week)
|
||
# $1: offset (±) from today, to locate the week
|
||
# $2: timestamp for this particular ical generation
|
||
function html_week_to_ical_week() {
|
||
local offsetDayOfWeek=$(date +%u -d "${1}days") # ∈ [1=monday … 5=friday]
|
||
local mon=$(date +%Y%m%dT%z -d "$((${1}+1-$offsetDayOfWeek))days")
|
||
local tue=$(date +%Y%m%dT%z -d "$((${1}+2-$offsetDayOfWeek))days")
|
||
local wed=$(date +%Y%m%dT%z -d "$((${1}+3-$offsetDayOfWeek))days")
|
||
local thu=$(date +%Y%m%dT%z -d "$((${1}+4-$offsetDayOfWeek))days")
|
||
local fri=$(date +%Y%m%dT%z -d "$((${1}+5-$offsetDayOfWeek))days")
|
||
|
||
tr '\r\n' ' ' \
|
||
| sed -r 's/[[:blank:]]+/ /g' \
|
||
| grep -oE '<DIV [^>]*class="Case" [^>]*style="[^"]*left *:[^"]*"|DIV [^>]*style="[^"]*left *:[^"]*" [^>]*class="Case"|<td [^>]*class="TC(ase|Prof|hdeb|Salle)"([^<]|<[^/]|</[^t]|</t[^d])*</td>' \
|
||
| awk -vmon=$mon -vtue=$tue -vwed=$wed -vthu=$thu -vfri=$fri -vid="epsi2ics/${LOGIN}@${HOSTNAME}" -vdtstamp=$2 -F$'\t' '
|
||
function out() {
|
||
if (from!="" && to!="" && ase!="") printf( \
|
||
"BEGIN:VEVENT\nUID:%s/%s/%s\nDTSTAMP:%s\nDTSTART:%s\nDTEND:%s\nSUMMARY:%s\nLOCATION:%s\nDESCRIPTION:🗣 %s 👥 %s\nEND:VEVENT\n", \
|
||
from, id, prof, dtstamp, from, to, ase, where, prof, who)
|
||
from=""
|
||
to=""
|
||
ase=""
|
||
where=""
|
||
prof=""
|
||
who=""
|
||
day=""
|
||
zoneH=0
|
||
zoneM=0
|
||
}
|
||
function toZtime(localH, localM) {
|
||
localM-=zoneM
|
||
if (localM<0) {
|
||
localM+=60
|
||
localH-=1
|
||
} else if (localM>59) {
|
||
localM-=60
|
||
localH+=1
|
||
}
|
||
localH-=zoneH
|
||
return sprintf("%s%02d%02d00Z", day, localH, localM)
|
||
}
|
||
/class="Case"/ {
|
||
out()
|
||
pc=gensub(".*left *: *([0-9]+)[%.].*", "\\1", 1)
|
||
if (pc < 115) tmpd=mon
|
||
else if (pc < 135) tmpd=tue
|
||
else if (pc < 155) tmpd=wed
|
||
else if (pc < 175) tmpd=thu
|
||
else tmpd=fri
|
||
day=substr(tmpd, 1, 9)
|
||
zoneH=substr(tmpd, 10, 3)+0
|
||
zoneM=(substr(tmpd, 10, 1) substr(tmpd, 13, 2))+0
|
||
}
|
||
/class="TCase"/ {
|
||
ase=gensub(".*</div>(.*)</.*", "\\1", 1)
|
||
}
|
||
/class="TCProf"/ {
|
||
split(gensub(".*>(.*)<br/?>(.*)</.*", "\\1\t\\2", 1), tmp)
|
||
prof=tmp[1]
|
||
who=tmp[2]
|
||
}
|
||
/class="TChdeb"/ {
|
||
split(gensub(".*> *0?([0-9]+):0?([0-9]+) *- *0?([0-9]+):0?([0-9]+) *</.*", "\\1\t\\2\t\\3\t\\4", 1), tmp)
|
||
from=toZtime(tmp[1], tmp[2])
|
||
to=toZtime(tmp[3], tmp[4])
|
||
}
|
||
/class="TCSalle"/ {
|
||
where=gensub(".*>(Salle:)?(.*)</.*", "\\2", 1)
|
||
}
|
||
END {
|
||
out()
|
||
}'
|
||
}
|
||
|
||
function output_ical_header() {
|
||
cat <<-ENDOFTEXT
|
||
BEGIN:VCALENDAR
|
||
VERSION:2.0
|
||
PRODID:yalis.fr/epsi2ical v1.1
|
||
ENDOFTEXT
|
||
}
|
||
|
||
function output_ical_footer() {
|
||
cat <<-ENDOFTEXT
|
||
END:VCALENDAR
|
||
ENDOFTEXT
|
||
}
|
||
|
||
# $1: offset (±) from today, to locate the week
|
||
# $2: timestamp for this particular ical generation
|
||
function output_ical_week {
|
||
fetch_html_week $(date +%m/%d/%Y -d "${1}days") \
|
||
| html_week_to_ical_week "$1" "$2"
|
||
}
|
||
|
||
# $1: offset (±) from today, to locate the week
|
||
function dump_known_ical_week {
|
||
local offsetDayOfWeek=$(date +%u -d "${1}days") # ∈ [1=monday … 5=friday]
|
||
local mon=$(date +%Y%m%d -d "$((${1}+1-$offsetDayOfWeek))days")
|
||
local tue=$(date +%Y%m%d -d "$((${1}+2-$offsetDayOfWeek))days")
|
||
local wed=$(date +%Y%m%d -d "$((${1}+3-$offsetDayOfWeek))days")
|
||
local thu=$(date +%Y%m%d -d "$((${1}+4-$offsetDayOfWeek))days")
|
||
local fri=$(date +%Y%m%d -d "$((${1}+5-$offsetDayOfWeek))days")
|
||
|
||
tr -d '\r' <"$OUTPUT" \
|
||
| awk -v week="^DTSTART:($mon|$tue|$wed|$thu|$fri)" '
|
||
function out() {
|
||
if (inWeek==1) {
|
||
printf("%s", day)
|
||
inWeek=0
|
||
}
|
||
}
|
||
/BEGIN:VEVENT/ { day=$0 "\n"; next }
|
||
{ day=day $0 "\n" }
|
||
$0 ~ week { inWeek=1 }
|
||
/END:VEVENT/ { out() }
|
||
'
|
||
}
|
||
|
||
# &1: first offset of the school-year, and max offset of the same school-year, space-separated
|
||
function get_first_and_max_offset_of_year() {
|
||
local currentYear=$(date +%Y)
|
||
local splitDay=$(date +%j -d "${currentYear}-08-01" | sed 's/^0*//') # XXXX-08-01 ∈ [1 … 366]
|
||
local dayOfYear=$(date +%j | sed 's/^0*//') # this day ∈ [1 … 366]
|
||
local offset= offsetMax=
|
||
|
||
if [ $dayOfYear -lt $splitDay ]; then
|
||
offsetMax=$(( splitDay-dayOfYear ))
|
||
offset=$(( offsetMax-365 ))
|
||
else
|
||
offset=$(( splitDay-dayOfYear ))
|
||
offsetMax=$(( 365+offset ))
|
||
fi
|
||
|
||
printf '%s %s\n' $offset $offsetMax
|
||
}
|
||
|
||
# $1: offset (±) from today, to locate the week
|
||
# exit status: 0 if $1 is in the current week
|
||
function is_current_week() {
|
||
local offsetWeek=$(date +%G%V -d "${1}days")
|
||
local currentWeek=$(date +%G%V)
|
||
[ $offsetWeek -eq $currentWeek ]
|
||
}
|
||
|
||
function main() {
|
||
check_parameters_and_set_defaults
|
||
{
|
||
local dtstamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||
local offset= offsetMax=
|
||
local step=-1 # -1: before patching; 0: patching time-span; 1: after patching
|
||
|
||
read -r offset offsetMax < <(get_first_and_max_offset_of_year)
|
||
|
||
output_ical_header
|
||
while [ $offset -lt $offsetMax ]; do
|
||
if [ -n "$PATCH" ]; then
|
||
case $step in
|
||
-1)
|
||
if is_current_week $offset; then
|
||
MAXOFFSET=$((MAXOFFSET+offset))
|
||
step=0
|
||
continue
|
||
fi
|
||
dump_known_ical_week $offset ;;
|
||
0)
|
||
if [ $offset -ge $MAXOFFSET ]; then
|
||
step=1
|
||
continue
|
||
fi
|
||
output_ical_week $offset "$dtstamp" ;;
|
||
1)
|
||
dump_known_ical_week $offset ;;
|
||
esac
|
||
else
|
||
output_ical_week $offset "$dtstamp"
|
||
fi
|
||
offset=$(($offset+7))
|
||
done
|
||
output_ical_footer
|
||
|
||
} \
|
||
| fold_long_lines \
|
||
| lf_to_crlf \
|
||
| write_output
|
||
}
|
||
|
||
main
|