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}}").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."