bk/src/bk.nim

188 lines
6.2 KiB
Nim

import htmlparser, json, net, options, os, strscans, times, unicode, uri, xmltree
from strutils import ffDecimal, formatFloat, replace
import cligen, colorize, elvis
from bakalari as baka import Grade, newBakalari
type
Config = object
website: string
username: string
refreshToken: string
const
defaultConfigFile = getConfigDir() / "bk.json"
dayNames = [
1: "Mon",
2: "Tue",
3: "Wed",
4: "Thu",
5: "Fri",
6: "Sat",
7: "Sun",
]
proc error(message: string) {.noReturn.} =
stderr.writeLine("Error: " & message)
quit QuitFailure
proc loadConfig(configFile = defaultConfigFile): Config =
try:
configFile.readFile.parseJson.to(Config)
except IOError:
error "Can't read the config file. Are you signed in?"
except JsonParsingError, JsonKindError:
error "Invalid config file. Make sure it's in JSON format."
except KeyError:
error "The config file doesn't contain all necessary fields."
proc saveConfig(configFile = defaultConfigFile, config: Config) =
try:
configFile.writeFile((%*config).pretty)
except IOError:
error "Can't write the config file."
template withBakalari(configFile: string, body: untyped): untyped =
var config = configFile.loadConfig
let bakalari {.inject.} = newBakalari(config.website.parseUri, config.refreshToken)
try:
body
finally:
config.refreshToken = bakalari.refreshToken
configFile.saveConfig(config)
proc signin(
website: string,
username: string,
password: string,
configFile = defaultConfigFile,
) =
## sign in to Bakaláři and save the credentials
let
bakalari = newBakalari(website.parseUri, username, password)
config = Config(
website: website,
username: username,
refreshToken: bakalari.refreshToken,
)
configFile.saveConfig(config)
proc average(grades: seq[Grade]): float =
var sum, weightSum: float
for grade in grades:
var value: int
if scanf(grade.text, "$i-", value):
sum += (value.float + 0.5) * grade.weight.float
weightSum += grade.weight.float
elif scanf(grade.text, "$i", value):
sum += value.float * grade.weight.float
weightSum += grade.weight.float
return weightSum ? (sum / weightSum) ! 0.0
proc grades(
configFile = defaultConfigFile,
) =
## display the list of grades and calculate averages
withBakalari(configFile):
for subject in baka.grades(bakalari):
stdout.writeLine subject.name.fgLightYellow & " " & subject.grades.average.formatFloat(ffDecimal, 2)
for grade in subject.grades:
var line = ""
line &= grade.text.align(2).fgLightMagenta
line &= " "
line &= ($grade.weight).align(2).fgLightCyan
line &= " "
line &= grade.caption
stdout.writeLine line
proc homework(
configFile = defaultConfigFile,
) =
## display the list of homework
withBakalari(configFile):
for homework in baka.homework(bakalari):
stdout.writeLine "----------------------------------------------------------------".fgLightGray
stdout.writeLine (homework.startTime.format("yyyy-MM-dd") & " / " & homework.endTime.format("yyyy-MM-dd")).fgLightCyan
stdout.writeLine homework.subject.fgLightYellow
stdout.writeLine homework.teacher.fgLightMagenta
stdout.writeLine homework.content
proc messages(
configFile = defaultConfigFile,
) =
withBakalari(defaultConfigFile):
for message in baka.messages(bakalari):
stdout.writeLine "----------------------------------------------------------------".fgLightGray
stdout.writeLine message.sentTime.format("yyyy-MM-dd") .fgLightCyan
stdout.writeLine message.title.fgLightYellow
stdout.writeLine message.sender.fgLightMagenta
stdout.writeLine message.text.replace("<br />", "{{br}}").parseHtml.innerText.replace("{{br}}", "\n")
proc timetable(
configFile = defaultConfigFile,
date = "",
nextWeek = false,
oneDay = false,
permanent = false,
) =
## display the timetable (for the current week by default)
let date = if ?date:
try:
date.parse("yyyyMMdd")
except TimeParseError:
error "Can't parse the date. Make sure it's in YYYYMMDD format."
else:
nextWeek ? (now() + 1.weeks) ! now()
withBakalari(configFile):
let timetable = baka.timetable(bakalari, permanent, some(date))
for day in timetable.days:
if oneDay:
if date.format("yyyyMMdd") != day.date.format("yyyyMMdd"):
continue
stdout.writeLine dayNames[day.dayOfWeek] & " " & day.date.format("yyyy-MM-dd")
for lesson in day.lessons:
var line = ""
line &= lesson.hour.number
line &= ". "
line &= lesson.hour.beginTime.align(5, '0'.Rune).fgLightCyan
line &= "-".fgLightCyan
line &= lesson.hour.endTime.align(5, '0'.Rune).fgLightCyan
line &= " "
line &= lesson.subject.abbrev.align(4).fgLightYellow
line &= " "
line &= lesson.teacher.abbrev.align(4).fgLightMagenta
line &= " "
line &= lesson.room.abbrev.align(4).fgLightGreen
stdout.writeLine line
if lesson.change.isSome:
stdout.writeLine ("^ " & lesson.change.unsafeGet.fgLightRed).fgLightRed
try:
dispatchMulti(
[signin, help = {
"website": "the URL for Bakaláři (e.g. https://bakalari.myschool.cz)",
"username": "your username",
"password": "your password (will not be stored anywhere)",
"config-file": "where to store the credentials",
}],
[grades, help = {
"config-file": "where the credentials are stored",
}],
[homework, help = {
"config-file": "where the credentials are stored",
}],
[messages, help = {
"config-file": "where the credentials are stored",
}],
[timetable, help = {
"config-file": "where the credentials are stored",
"date": "any date inside the week you want to display, in YYYYMMDD format (defaults to today)",
"nextWeek": "display the next week rather that the current week (overriden by --date)",
"one-day": "display only the specified day",
"permanent": "display the permanent timetable",
}],
)
except OSError:
error "Generic OS error. Check your internet connection."
except SslError:
error "Connection interrupted. Check your internet connection."