188 lines
6.2 KiB
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."
|