~baka/miyagi

2180f38edf9d210e7bf31d0428e046b344084b84 — baka 2 months ago d87fa6c master
WASM, caching w/ Redis, pages, feels like something kinda usable now
M Cargo.lock => Cargo.lock +742 -642
@@ 9,57 9,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"

[[package]]
name = "aead"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
dependencies = [
 "generic-array",
]

[[package]]
name = "aes"
version = "0.6.0"
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
 "aes-soft",
 "aesni",
 "cipher",
]

[[package]]
name = "aes-gcm"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da"
dependencies = [
 "aead",
 "aes",
 "cipher",
 "ctr",
 "ghash",
 "subtle",
]

[[package]]
name = "aes-soft"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072"
dependencies = [
 "cipher",
 "opaque-debug",
]

[[package]]
name = "aesni"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
dependencies = [
 "cipher",
 "opaque-debug",
 "memchr",
]

[[package]]


@@ 72,112 27,10 @@ dependencies = [
]

[[package]]
name = "anyhow"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"

[[package]]
name = "async-channel"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
dependencies = [
 "concurrent-queue 1.2.4",
 "event-listener",
 "futures-core",
]

[[package]]
name = "async-executor"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b"
dependencies = [
 "async-lock",
 "async-task",
 "concurrent-queue 2.0.0",
 "fastrand",
 "futures-lite",
 "slab",
]

[[package]]
name = "async-global-executor"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
dependencies = [
 "async-channel",
 "async-executor",
 "async-io",
 "async-lock",
 "blocking",
 "futures-lite",
 "once_cell",
]

[[package]]
name = "async-io"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7"
dependencies = [
 "async-lock",
 "autocfg",
 "concurrent-queue 1.2.4",
 "futures-lite",
 "libc",
 "log",
 "parking",
 "polling",
 "slab",
 "socket2",
 "waker-fn",
 "winapi",
]

[[package]]
name = "async-lock"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685"
dependencies = [
 "event-listener",
 "futures-lite",
]

[[package]]
name = "async-std"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [
 "async-channel",
 "async-global-executor",
 "async-io",
 "async-lock",
 "crossbeam-utils",
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-lite",
 "gloo-timers",
 "kv-log-macro",
 "log",
 "memchr",
 "once_cell",
 "pin-project-lite",
 "pin-utils",
 "slab",
 "wasm-bindgen-futures",
]

[[package]]
name = "async-task"
version = "4.3.0"
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"

[[package]]
name = "async-trait"


@@ 191,12 44,6 @@ dependencies = [
]

[[package]]
name = "atomic-waker"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"

[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 216,7 63,7 @@ dependencies = [
 "http",
 "http-body",
 "hyper",
 "itoa",
 "itoa 1.0.4",
 "matchit",
 "memchr",
 "mime",


@@ 257,7 104,7 @@ checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
dependencies = [
 "axum",
 "bytes",
 "cookie 0.16.1",
 "cookie",
 "futures-util",
 "http",
 "mime",


@@ 271,30 118,36 @@ dependencies = [
]

[[package]]
name = "base-x"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"

[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"

[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
 "serde",
]

[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "block-buffer"
version = "0.9.0"
name = "bitvec"
version = "0.19.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
dependencies = [
 "generic-array",
 "funty",
 "radium",
 "tap",
 "wyz",
]

[[package]]


@@ 307,36 160,22 @@ dependencies = [
]

[[package]]
name = "blocking"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
dependencies = [
 "async-channel",
 "async-task",
 "atomic-waker",
 "fastrand",
 "futures-lite",
 "once_cell",
]

[[package]]
name = "bumpalo"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"

[[package]]
name = "bytes"
version = "1.2.1"
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"

[[package]]
name = "cache-padded"
version = "1.2.0"
name = "bytes"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"

[[package]]
name = "canvaslms"


@@ 350,9 189,15 @@ dependencies = [

[[package]]
name = "cc"
version = "1.0.76"
version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f"
checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"

[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"

[[package]]
name = "cfg-if"


@@ 367,10 212,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
 "iana-time-zone",
 "js-sys",
 "num-integer",
 "num-traits",
 "serde",
 "time 0.1.44",
 "wasm-bindgen",
 "winapi",
]



@@ 381,15 228,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"

[[package]]
name = "cipher"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
dependencies = [
 "generic-array",
]

[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 410,45 248,20 @@ dependencies = [
]

[[package]]
name = "concurrent-queue"
version = "1.2.4"
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
 "cache-padded",
]

[[package]]
name = "concurrent-queue"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
dependencies = [
 "crossbeam-utils",
 "cfg-if 1.0.0",
 "wasm-bindgen",
]

[[package]]
name = "const_fn"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"

[[package]]
name = "cookie"
version = "0.14.4"
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951"
dependencies = [
 "aes-gcm",
 "base64",
 "hkdf",
 "hmac",
 "percent-encoding",
 "rand 0.8.5",
 "sha2",
 "time 0.2.27",
 "version_check",
]
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"

[[package]]
name = "cookie"


@@ 457,7 270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917"
dependencies = [
 "percent-encoding",
 "time 0.3.15",
 "time 0.3.17",
 "version_check",
]



@@ 477,27 290,12 @@ dependencies = [
]

[[package]]
name = "cpuid-bool"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"

[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
 "cfg-if",
]

[[package]]
name = "crossbeam-utils"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
dependencies = [
 "cfg-if",
 "cfg-if 1.0.0",
]

[[package]]


@@ 511,39 309,48 @@ dependencies = [
]

[[package]]
name = "crypto-mac"
version = "0.10.1"
name = "css-minify"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"
checksum = "692b185e3b7c9af96b3195f3021f53a931d896968ed2ad3fb1cdb6558b30c9ab"
dependencies = [
 "generic-array",
 "subtle",
 "derive_more",
 "indexmap",
 "nom",
]

[[package]]
name = "ctor"
version = "0.1.26"
name = "cssparser"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
dependencies = [
 "cssparser-macros",
 "dtoa-short",
 "itoa 0.4.8",
 "matches",
 "phf 0.8.0",
 "proc-macro2",
 "quote",
 "smallvec",
 "syn",
]

[[package]]
name = "ctr"
name = "cssparser-macros"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f"
checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e"
dependencies = [
 "cipher",
 "quote",
 "syn",
]

[[package]]
name = "cxx"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888"
checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
dependencies = [
 "cc",
 "cxxbridge-flags",


@@ 553,9 360,9 @@ dependencies = [

[[package]]
name = "cxx-build"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3"
checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
dependencies = [
 "cc",
 "codespan-reporting",


@@ 568,15 375,15 @@ dependencies = [

[[package]]
name = "cxxbridge-flags"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f"
checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"

[[package]]
name = "cxxbridge-macro"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704"
checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
dependencies = [
 "proc-macro2",
 "quote",


@@ 584,12 391,16 @@ dependencies = [
]

[[package]]
name = "digest"
version = "0.9.0"
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
 "generic-array",
 "convert_case",
 "proc-macro2",
 "quote",
 "rustc_version",
 "syn",
]

[[package]]


@@ 598,36 409,57 @@ version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
 "block-buffer 0.10.3",
 "block-buffer",
 "crypto-common",
]

[[package]]
name = "discard"
version = "1.0.4"
name = "dtoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"

[[package]]
name = "event-listener"
version = "2.5.3"
name = "dtoa-short"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6"
dependencies = [
 "dtoa",
]

[[package]]
name = "fastrand"
version = "1.8.0"
name = "duct"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d"
dependencies = [
 "libc",
 "once_cell",
 "os_pipe",
 "shared_child",
]

[[package]]
name = "ego-tree"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591"

[[package]]
name = "encoding_rs"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
dependencies = [
 "instant",
 "cfg-if 1.0.0",
]

[[package]]
name = "flate2"
version = "1.0.24"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
 "crc32fast",
 "miniz_oxide",


@@ 649,18 481,19 @@ dependencies = [
]

[[package]]
name = "futures"
version = "0.3.25"
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"

[[package]]
name = "futf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-executor",
 "futures-io",
 "futures-sink",
 "futures-task",
 "futures-util",
 "mac",
 "new_debug_unreachable",
]

[[package]]


@@ 670,7 503,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
dependencies = [
 "futures-core",
 "futures-sink",
]

[[package]]


@@ 680,49 512,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"

[[package]]
name = "futures-executor"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
 "futures-core",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-io"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"

[[package]]
name = "futures-lite"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [
 "fastrand",
 "futures-core",
 "futures-io",
 "memchr",
 "parking",
 "pin-project-lite",
 "waker-fn",
]

[[package]]
name = "futures-macro"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "futures-sink"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 740,16 529,19 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-macro",
 "futures-sink",
 "futures-task",
 "memchr",
 "pin-project-lite",
 "pin-utils",
 "slab",
]

[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
 "byteorder",
]

[[package]]


@@ 763,12 555,21 @@ dependencies = [
]

[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
 "unicode-width",
]

[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
 "cfg-if",
 "cfg-if 1.0.0",
 "libc",
 "wasi 0.9.0+wasi-snapshot-preview1",
]


@@ 779,31 580,28 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
 "cfg-if",
 "cfg-if 1.0.0",
 "libc",
 "wasi 0.11.0+wasi-snapshot-preview1",
]

[[package]]
name = "ghash"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375"
dependencies = [
 "opaque-debug",
 "polyval",
]

[[package]]
name = "gloo-timers"
version = "0.2.4"
name = "h2"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [
 "futures-channel",
 "bytes",
 "fnv",
 "futures-core",
 "js-sys",
 "wasm-bindgen",
 "futures-sink",
 "futures-util",
 "http",
 "indexmap",
 "slab",
 "tokio",
 "tokio-util",
 "tracing",
]

[[package]]


@@ 822,6 620,12 @@ dependencies = [
]

[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"

[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 831,23 635,17 @@ dependencies = [
]

[[package]]
name = "hkdf"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
dependencies = [
 "digest 0.9.0",
 "hmac",
]

[[package]]
name = "hmac"
version = "0.10.1"
name = "html5ever"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
 "crypto-mac",
 "digest 0.9.0",
 "log",
 "mac",
 "markup5ever",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]


@@ 858,7 656,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
 "bytes",
 "fnv",
 "itoa",
 "itoa 1.0.4",
]

[[package]]


@@ 879,28 677,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"

[[package]]
name = "http-types"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
dependencies = [
 "anyhow",
 "async-channel",
 "async-std",
 "base64",
 "cookie 0.14.4",
 "futures-lite",
 "infer",
 "pin-project-lite",
 "rand 0.7.3",
 "serde",
 "serde_json",
 "serde_qs",
 "serde_urlencoded",
 "url",
]

[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 922,11 698,12 @@ dependencies = [
 "futures-channel",
 "futures-core",
 "futures-util",
 "h2",
 "http",
 "http-body",
 "httparse",
 "httpdate",
 "itoa",
 "itoa 1.0.4",
 "pin-project-lite",
 "socket2",
 "tokio",


@@ 970,21 747,47 @@ dependencies = [
]

[[package]]
name = "infer"
version = "0.2.3"
name = "include_dir"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
dependencies = [
 "include_dir_macros",
]

[[package]]
name = "include_dir_macros"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
dependencies = [
 "proc-macro2",
 "quote",
]

[[package]]
name = "instant"
version = "0.1.12"
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
 "cfg-if",
 "autocfg",
 "hashbrown",
]

[[package]]
name = "ipnet"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"

[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"

[[package]]
name = "itoa"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1000,12 803,22 @@ dependencies = [
]

[[package]]
name = "kv-log-macro"
version = "1.0.7"
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
 "log",
 "arrayvec",
 "bitflags",
 "cfg-if 1.0.0",
 "ryu",
 "static_assertions",
]

[[package]]


@@ 1024,16 837,51 @@ dependencies = [
]

[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
 "autocfg",
 "scopeguard",
]

[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
 "cfg-if",
 "value-bag",
 "cfg-if 1.0.0",
]

[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"

[[package]]
name = "markup5ever"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
 "log",
 "phf 0.10.1",
 "phf_codegen 0.10.0",
 "string_cache",
 "string_cache_codegen",
 "tendril",
]

[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"

[[package]]
name = "matchit"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1046,6 894,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"

[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"

[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1062,10 916,33 @@ dependencies = [
]

[[package]]
name = "minify-html"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f84854d62363972a73c3d8331b85a479366a0871a83f2a01ac11b9ba787c10"
dependencies = [
 "aho-corasick",
 "css-minify",
 "lazy_static",
 "memchr",
 "minify-js",
]

[[package]]
name = "minify-js"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe033709f5a1159736cf7e22748518ffb75af26f3a6264d52ecc8bb38c68c36"
dependencies = [
 "lazy_static",
 "parse-js",
]

[[package]]
name = "miniz_oxide"
version = "0.5.4"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
 "adler",
]


@@ 1086,18 963,79 @@ dependencies = [
name = "miyagi"
version = "0.1.0"
dependencies = [
 "serde",
]

[[package]]
name = "miyagi-build"
version = "0.1.0"
dependencies = [
 "duct",
 "minify-html",
]

[[package]]
name = "miyagi-server"
version = "0.1.0"
dependencies = [
 "axum",
 "axum-extra",
 "bincode",
 "canvaslms",
 "futures",
 "chrono",
 "handlebars",
 "http-types",
 "include_dir",
 "minify-html",
 "miyagi",
 "redis",
 "scraper",
 "serde",
 "serde_derive",
 "serde_json",
 "time 0.3.15",
 "tokio",
 "tower-http",
 "url",
]

[[package]]
name = "miyagi-wasm"
version = "0.1.0"
dependencies = [
 "bincode",
 "console_error_panic_hook",
 "handlebars",
 "include_dir",
 "miyagi",
 "reqwest",
 "serde",
 "url",
 "wasm-bindgen",
 "wasm-bindgen-futures",
 "web-sys",
 "wee_alloc",
]

[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"

[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"

[[package]]
name = "nom"
version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [
 "bitvec",
 "funty",
 "lexical-core",
 "memchr",
 "version_check",
]

[[package]]


@@ 1130,80 1068,195 @@ dependencies = [
]

[[package]]
name = "num_threads"
version = "0.1.6"
name = "once_cell"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"

[[package]]
name = "os_pipe"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
dependencies = [
 "libc",
 "winapi",
]

[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
 "lock_api",
 "parking_lot_core",
]

[[package]]
name = "parking_lot_core"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
dependencies = [
 "cfg-if 1.0.0",
 "libc",
 "redox_syscall",
 "smallvec",
 "windows-sys",
]

[[package]]
name = "parse-js"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66bb85ec60d22b9e6d4adac1e3dbdaf3903a4485f476c5f4dd7ed1285cbf4dad"
dependencies = [
 "aho-corasick",
 "lazy_static",
 "memchr",
]

[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"

[[package]]
name = "pest"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a"
dependencies = [
 "thiserror",
 "ucd-trie",
]

[[package]]
name = "pest_derive"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63"
dependencies = [
 "pest",
 "pest_generator",
]

[[package]]
name = "pest_generator"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1"
dependencies = [
 "pest",
 "pest_meta",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "pest_meta"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662"
dependencies = [
 "libc",
 "once_cell",
 "pest",
 "sha1 0.10.5",
]

[[package]]
name = "once_cell"
version = "1.16.0"
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
 "phf_macros",
 "phf_shared 0.8.0",
 "proc-macro-hack",
]

[[package]]
name = "opaque-debug"
version = "0.3.0"
name = "phf"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
 "phf_shared 0.10.0",
]

[[package]]
name = "parking"
version = "2.0.0"
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
 "phf_generator 0.8.0",
 "phf_shared 0.8.0",
]

[[package]]
name = "percent-encoding"
version = "2.2.0"
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
 "phf_generator 0.10.0",
 "phf_shared 0.10.0",
]

[[package]]
name = "pest"
version = "2.4.1"
name = "phf_generator"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
 "thiserror",
 "ucd-trie",
 "phf_shared 0.8.0",
 "rand 0.7.3",
]

[[package]]
name = "pest_derive"
version = "2.4.1"
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fd9bc6500181952d34bd0b2b0163a54d794227b498be0b7afa7698d0a7b18f"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
 "pest",
 "pest_generator",
 "phf_shared 0.10.0",
 "rand 0.8.5",
]

[[package]]
name = "pest_generator"
version = "2.4.1"
name = "phf_macros"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2610d5ac5156217b4ff8e46ddcef7cdf44b273da2ac5bca2ecbfa86a330e7c4"
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
dependencies = [
 "pest",
 "pest_meta",
 "phf_generator 0.8.0",
 "phf_shared 0.8.0",
 "proc-macro-hack",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "pest_meta"
version = "2.4.1"
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824749bf7e21dd66b36fbe26b3f45c713879cccd4a009a917ab8e045ca8246fe"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
 "once_cell",
 "pest",
 "sha1 0.10.5",
 "siphasher",
]

[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
 "siphasher",
]

[[package]]


@@ 1239,37 1292,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"

[[package]]
name = "polling"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2"
dependencies = [
 "autocfg",
 "cfg-if",
 "libc",
 "log",
 "wepoll-ffi",
 "winapi",
]

[[package]]
name = "polyval"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd"
dependencies = [
 "cpuid-bool",
 "opaque-debug",
 "universal-hash",
]

[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"

[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"

[[package]]
name = "proc-macro-hack"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1294,6 1328,12 @@ dependencies = [
]

[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"

[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1304,6 1344,7 @@ dependencies = [
 "rand_chacha 0.2.2",
 "rand_core 0.5.1",
 "rand_hc",
 "rand_pcg",
]

[[package]]


@@ 1365,6 1406,15 @@ dependencies = [
]

[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
 "rand_core 0.5.1",
]

[[package]]
name = "redis"
version = "0.21.6"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1372,7 1422,7 @@ checksum = "571c252c68d09a2ad3e49edd14e9ee48932f3e0f27b06b4ea4c9b2a706d31103"
dependencies = [
 "async-trait",
 "combine",
 "itoa",
 "itoa 1.0.4",
 "percent-encoding",
 "ryu",
 "sha1 0.6.1",


@@ 1380,6 1430,49 @@ dependencies = [
]

[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
 "bitflags",
]

[[package]]
name = "reqwest"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
dependencies = [
 "base64",
 "bytes",
 "encoding_rs",
 "futures-core",
 "futures-util",
 "h2",
 "http",
 "http-body",
 "hyper",
 "ipnet",
 "js-sys",
 "log",
 "mime",
 "once_cell",
 "percent-encoding",
 "pin-project-lite",
 "serde",
 "serde_json",
 "serde_urlencoded",
 "tokio",
 "tower-service",
 "url",
 "wasm-bindgen",
 "wasm-bindgen-futures",
 "web-sys",
 "winreg",
]

[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1396,9 1489,9 @@ dependencies = [

[[package]]
name = "rustc_version"
version = "0.2.3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
 "semver",
]


@@ 1431,6 1524,28 @@ dependencies = [
]

[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"

[[package]]
name = "scraper"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5684396b456f3eb69ceeb34d1b5cb1a2f6acf7ca4452131efa3ba0ee2c2d0a70"
dependencies = [
 "cssparser",
 "ego-tree",
 "getopts",
 "html5ever",
 "matches",
 "selectors",
 "smallvec",
 "tendril",
]

[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1447,19 1562,30 @@ dependencies = [
]

[[package]]
name = "semver"
version = "0.9.0"
name = "selectors"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
dependencies = [
 "semver-parser",
 "bitflags",
 "cssparser",
 "derive_more",
 "fxhash",
 "log",
 "matches",
 "phf 0.8.0",
 "phf_codegen 0.8.0",
 "precomputed-hash",
 "servo_arc",
 "smallvec",
 "thin-slice",
]

[[package]]
name = "semver-parser"
version = "0.7.0"
name = "semver"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"

[[package]]
name = "serde"


@@ 1483,39 1609,38 @@ dependencies = [

[[package]]
name = "serde_json"
version = "1.0.87"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
dependencies = [
 "itoa",
 "itoa 1.0.4",
 "ryu",
 "serde",
]

[[package]]
name = "serde_qs"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
dependencies = [
 "percent-encoding",
 "serde",
 "thiserror",
]

[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
 "form_urlencoded",
 "itoa",
 "itoa 1.0.4",
 "ryu",
 "serde",
]

[[package]]
name = "servo_arc"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
dependencies = [
 "nodrop",
 "stable_deref_trait",
]

[[package]]
name = "sha1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1530,9 1655,9 @@ version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [
 "cfg-if",
 "cfg-if 1.0.0",
 "cpufeatures",
 "digest 0.10.6",
 "digest",
]

[[package]]


@@ 1542,19 1667,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"

[[package]]
name = "sha2"
version = "0.9.9"
name = "shared_child"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a"
dependencies = [
 "block-buffer 0.9.0",
 "cfg-if",
 "cpufeatures",
 "digest 0.9.0",
 "opaque-debug",
 "libc",
 "winapi",
]

[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"

[[package]]
name = "slab"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1564,6 1692,12 @@ dependencies = [
]

[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"

[[package]]
name = "socket2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1580,70 1714,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"

[[package]]
name = "standback"
version = "0.2.17"
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
dependencies = [
 "version_check",
]
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"

[[package]]
name = "stdweb"
version = "0.4.20"
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
dependencies = [
 "discard",
 "rustc_version",
 "stdweb-derive",
 "stdweb-internal-macros",
 "stdweb-internal-runtime",
 "wasm-bindgen",
]
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"

[[package]]
name = "stdweb-derive"
version = "0.5.3"
name = "string_cache"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08"
dependencies = [
 "proc-macro2",
 "quote",
 "new_debug_unreachable",
 "once_cell",
 "parking_lot",
 "phf_shared 0.10.0",
 "precomputed-hash",
 "serde",
 "serde_derive",
 "syn",
]

[[package]]
name = "stdweb-internal-macros"
version = "0.2.9"
name = "string_cache_codegen"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
dependencies = [
 "base-x",
 "phf_generator 0.10.0",
 "phf_shared 0.10.0",
 "proc-macro2",
 "quote",
 "serde",
 "serde_derive",
 "serde_json",
 "sha1 0.6.1",
 "syn",
]

[[package]]
name = "stdweb-internal-runtime"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"

[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"

[[package]]
name = "syn"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1661,6 1769,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"

[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"

[[package]]
name = "tendril"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
dependencies = [
 "futf",
 "mac",
 "utf-8",
]

[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1670,6 1795,12 @@ dependencies = [
]

[[package]]
name = "thin-slice"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"

[[package]]
name = "thiserror"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1702,58 1833,29 @@ dependencies = [

[[package]]
name = "time"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
dependencies = [
 "const_fn",
 "libc",
 "standback",
 "stdweb",
 "time-macros 0.1.1",
 "version_check",
 "winapi",
]

[[package]]
name = "time"
version = "0.3.15"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [
 "itoa",
 "libc",
 "num_threads",
 "time-macros 0.2.4",
 "itoa 1.0.4",
 "serde",
 "time-core",
 "time-macros",
]

[[package]]
name = "time-macros"
version = "0.1.1"
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
dependencies = [
 "proc-macro-hack",
 "time-macros-impl",
]
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"

[[package]]
name = "time-macros"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"

[[package]]
name = "time-macros-impl"
version = "0.1.2"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
dependencies = [
 "proc-macro-hack",
 "proc-macro2",
 "quote",
 "standback",
 "syn",
 "time-core",
]

[[package]]


@@ 1773,9 1875,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"

[[package]]
name = "tokio"
version = "1.21.2"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
dependencies = [
 "autocfg",
 "bytes",


@@ 1811,6 1913,7 @@ dependencies = [
 "futures-sink",
 "pin-project-lite",
 "tokio",
 "tracing",
]

[[package]]


@@ 1872,7 1975,7 @@ version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
 "cfg-if",
 "cfg-if 1.0.0",
 "log",
 "pin-project-lite",
 "tracing-core",


@@ 1942,16 2045,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"

[[package]]
name = "universal-hash"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
dependencies = [
 "generic-array",
 "subtle",
]

[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1985,18 2078,13 @@ dependencies = [
 "form_urlencoded",
 "idna",
 "percent-encoding",
 "serde",
]

[[package]]
name = "value-bag"
version = "1.0.0-alpha.9"
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
dependencies = [
 "ctor",
 "version_check",
]
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"

[[package]]
name = "version_check"


@@ 2005,12 2093,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

[[package]]
name = "waker-fn"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"

[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 2055,7 2137,7 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
 "cfg-if",
 "cfg-if 1.0.0",
 "wasm-bindgen-macro",
]



@@ 2080,7 2162,7 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
 "cfg-if",
 "cfg-if 1.0.0",
 "js-sys",
 "wasm-bindgen",
 "web-sys",


@@ 2145,12 2227,15 @@ dependencies = [
]

[[package]]
name = "wepoll-ffi"
version = "0.1.2"
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
 "cc",
 "cfg-if 0.1.10",
 "libc",
 "memory_units",
 "winapi",
]

[[package]]


@@ 2240,3 2325,18 @@ name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"

[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
 "winapi",
]

[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"

M Cargo.toml => Cargo.toml +32 -16
@@ 3,20 3,36 @@ name = "miyagi"
version = "0.1.0"
edition = "2021"

[workspace]
members = ["src/server", "src/wasm", "src/build"]
default-members = ["src/server", "src/wasm", "src/build"]

    [workspace.package]
    description = "A minimalist web UI + caching server for Instructure's Canvas LMS"
    version = "0.1.0"
    edition = "2021"

    [workspace.dependencies]
    bincode = "1.3.3"
    handlebars = { version = "4.3.5" }
    serde = { version = "1.0.143", features = ["derive"] }
    url = "2.3.1"
    include_dir = "0.7.3"
    regex = "1.7.0"
    scraper = "0.13.0"
    argon2 = "0.4.1"

    axum = { version = "0.5.15" }
    axum-extra = { version = "0.3.7", features = ["serde", "cookie", "spa"] }
    canvaslms = { path = "canvaslms" }
    chrono = { version = "0.4.23", features = ["serde"] }
    redis = { version = "0.21.5" }
    tokio = { version = "1.20.1", features = ["macros", "rt", "rt-multi-thread"] }
    tower-http = { version = "0.3.4", features = ["fs", "metrics"] }

[dependencies]
#actix-web = "4.1.0"
axum = "0.5.15"
axum-extra = { version = "0.3.7", features = ["serde", "cookie", "spa"] }
canvaslms = { path = "./canvaslms" }
#cookie = "0.16.0"
futures = "0.3.23"
#diesel = { version = "1.4.8", features = ["sqlite"] }
handlebars = { version = "4.3.3", features = ["dir_source"] }
http-types = "2.12.0"
redis = "0.21.5"
serde = "1.0.143"
serde_derive = "1.0.143"
serde_json = "1.0.83"
#serde_qs = { version = "0.10.1", features = ["axum"] }
time = { version = "0.3.13", features = ["parsing", "formatting", "itoa"] }
tokio = { version = "1.20.1", features = ["macros", "rt", "rt-multi-thread"] }
serde = { version = "1.0.143", features = ["derive"] }

[lib]
name = "miyagi"
path = "src/common/lib.rs"

A Dockerfile => Dockerfile +19 -0
@@ 0,0 1,19 @@
FROM alpine:latest

RUN apk update && \
apk add rustup gcc musl-dev musl-utils openssl-dev

RUN rustup-init -y && \
~/.cargo/bin/rustup target add wasm32-unknown-unknown && \
~/.cargo/bin/rustup toolchain install nightly && \
~/.cargo/bin/cargo install wasm-bindgen-cli

WORKDIR /miyagi
COPY . .
RUN ~/.cargo/bin/cargo run -r -p miyagi-build

RUN ~/.cargo/bin/rustup toolchain uninstall nightly && \
~/.cargo/bin/rustup toolchain uninstall stable && \
apk del openssl openssl-dev

ENTRYPOINT [ "/miyagi/target/release/miyagi-server" ]

M canvaslms/Cargo.lock => canvaslms/Cargo.lock +162 -67
@@ 10,9 10,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"

[[package]]
name = "android_system_properties"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
 "libc",
]


@@ 25,15 25,15 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"

[[package]]
name = "base64"
version = "0.13.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"

[[package]]
name = "bumpalo"
version = "3.11.0"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"

[[package]]
name = "canvaslms"


@@ 47,9 47,9 @@ dependencies = [

[[package]]
name = "cc"
version = "1.0.73"
version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"

[[package]]
name = "cfg-if"


@@ 59,9 59,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "chrono"
version = "0.4.22"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
 "iana-time-zone",
 "num-integer",


@@ 78,6 78,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"

[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
 "termcolor",
 "unicode-width",
]

[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 93,6 103,50 @@ dependencies = [
]

[[package]]
name = "cxx"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
dependencies = [
 "cc",
 "cxxbridge-flags",
 "cxxbridge-macro",
 "link-cplusplus",
]

[[package]]
name = "cxx-build"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
dependencies = [
 "cc",
 "codespan-reporting",
 "once_cell",
 "proc-macro2",
 "quote",
 "scratch",
 "syn",
]

[[package]]
name = "cxxbridge-flags"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"

[[package]]
name = "cxxbridge-macro"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "flate2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 104,58 158,76 @@ dependencies = [

[[package]]
name = "form_urlencoded"
version = "1.0.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
 "matches",
 "percent-encoding",
]

[[package]]
name = "iana-time-zone"
version = "0.1.46"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
 "android_system_properties",
 "core-foundation-sys",
 "iana-time-zone-haiku",
 "js-sys",
 "wasm-bindgen",
 "winapi",
]

[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
 "cxx",
 "cxx-build",
]

[[package]]
name = "idna"
version = "0.2.3"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
 "matches",
 "unicode-bidi",
 "unicode-normalization",
]

[[package]]
name = "itoa"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"

[[package]]
name = "js-sys"
version = "0.3.59"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
dependencies = [
 "wasm-bindgen",
]

[[package]]
name = "libc"
version = "0.2.132"
version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"

[[package]]
name = "link-cplusplus"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
dependencies = [
 "cc",
]

[[package]]
name = "log"


@@ 167,12 239,6 @@ dependencies = [
]

[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"

[[package]]
name = "miniz_oxide"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 202,21 268,21 @@ dependencies = [

[[package]]
name = "once_cell"
version = "1.13.1"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"

[[package]]
name = "percent-encoding"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"

[[package]]
name = "proc-macro2"
version = "1.0.43"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
 "unicode-ident",
]


@@ 247,9 313,9 @@ dependencies = [

[[package]]
name = "rustls"
version = "0.20.6"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
dependencies = [
 "log",
 "ring",


@@ 264,6 330,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"

[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"

[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 275,18 347,18 @@ dependencies = [

[[package]]
name = "serde"
version = "1.0.144"
version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.144"
version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
dependencies = [
 "proc-macro2",
 "quote",


@@ 295,9 367,9 @@ dependencies = [

[[package]]
name = "serde_json"
version = "1.0.85"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7"
dependencies = [
 "itoa",
 "ryu",


@@ 312,9 384,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"

[[package]]
name = "syn"
version = "1.0.99"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
dependencies = [
 "proc-macro2",
 "quote",


@@ 322,6 394,15 @@ dependencies = [
]

[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
 "winapi-util",
]

[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 355,20 436,26 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"

[[package]]
name = "unicode-ident"
version = "1.0.3"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"

[[package]]
name = "unicode-normalization"
version = "0.1.21"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
 "tinyvec",
]

[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"

[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 395,13 482,12 @@ dependencies = [

[[package]]
name = "url"
version = "2.2.2"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
 "form_urlencoded",
 "idna",
 "matches",
 "percent-encoding",
]



@@ 413,9 499,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"

[[package]]
name = "wasm-bindgen"
version = "0.2.82"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
 "cfg-if",
 "wasm-bindgen-macro",


@@ 423,9 509,9 @@ dependencies = [

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.82"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
dependencies = [
 "bumpalo",
 "log",


@@ 438,9 524,9 @@ dependencies = [

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.82"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
 "quote",
 "wasm-bindgen-macro-support",


@@ 448,9 534,9 @@ dependencies = [

[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.82"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
 "proc-macro2",
 "quote",


@@ 461,15 547,15 @@ dependencies = [

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.82"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"

[[package]]
name = "web-sys"
version = "0.3.59"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1"
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
dependencies = [
 "js-sys",
 "wasm-bindgen",


@@ 487,9 573,9 @@ dependencies = [

[[package]]
name = "webpki-roots"
version = "0.22.4"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be"
dependencies = [
 "webpki",
]


@@ 511,6 597,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
 "winapi",
]

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

M canvaslms/Cargo.toml => canvaslms/Cargo.toml +4 -0
@@ 8,3 8,7 @@ chrono = { version = "0.4.22", features = ["alloc", "serde", "time", "clock"], d
serde = { version = "1.0.143", features = ["derive"] }
serde_json = "1.0.83"
ureq = { version = "2.5.0", features = ["serde", "serde_json", "json"] }

[features]
models-only = []
miyagi-custom = []

A canvaslms/LICENSE => canvaslms/LICENSE +7 -0
@@ 0,0 1,7 @@
Copyright (c) 2022 Lincoln Williams

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

M canvaslms/README.md => canvaslms/README.md +2 -21
@@ 3,28 3,9 @@

An easy-to-use client for the Canvas LMS REST API written in Rust.

```rust
let client = Client {
    url: "https://yourschool.instructure.com",
    token: "12345~dmeeDJWJNWjdjwkdjk77483dmKD"
};

for c in courses(client).unwrap() {
    println!("{}", c.clone().name.unwrap());

    for a in c.overdue_assignments(client).unwrap() {
        if a.due_at.is_some() {
            println!(" ! {} ({})", a.name, a.due_in());
        }
    }
    for a in c.upcoming_assignments(client).unwrap() {
        if a.due_at.is_some() {
            println!(" - {} ({})", a.name, a.due_in());
        }
    }
}
```
Licensed MIT.

## References

[Canvas LMS REST API Documentation](https://canvas.instructure.com/doc/api/)


D canvaslms/src/_models.rs => canvaslms/src/_models.rs +0 -4
@@ 1,4 0,0 @@

use crate::Client;
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};

M canvaslms/src/lib.rs => canvaslms/src/lib.rs +120 -69
@@ 1,100 1,151 @@
// Instructure devs: can you not make software?
//
// The API endpoints are quite obviously implemented and documented by
// completely different people. There's no streamlining whatsoever; it's just a
// free-for-all!!

#![doc = include_str!("../README.md")]

extern crate ureq;
extern crate chrono;
extern crate serde;

#[cfg(not(feature = "models-only"))]
extern crate serde_json;
#[cfg(not(feature = "models-only"))]
extern crate ureq;

mod models;
pub use std::io::Result;

pub use models::*;

use std::io::Read;
use serde_json::Result;
use chrono::prelude::*;
pub struct Ctx {
  pub course: u64,
}

fn _get_req(c: Client, path: &str, params: &[(&str, &str)]) -> Box<dyn Read + Send + Sync + 'static> {
#[cfg(not(feature = "models-only"))]
pub fn get_req<T: for<'de> serde::Deserialize<'de>>(
  c: Client,
  path: &str,
  params: &[(&str, &str)],
) -> Result<T> {
  let path = if path.starts_with("/") {
    format!("{}/api/v1{}", c.url, path)
  } else {
    path.to_string()
  };

  let mut r = ureq::get(path.as_str())
    .set("Authorization", format!("Bearer {}", c.token).as_str());
  let mut r = ureq::get(&path).set("Authorization", &format!("Bearer {}", c.token));

  for p in params {
    r = r.query(p.0, p.1);
  }

  r.call().unwrap().into_reader()
  let res = r.call();
  if res.is_ok() {
    let data = serde_json::from_reader::<_, T>(res.unwrap().into_reader());
    if data.is_ok() {
      Ok(data.unwrap())
    } else {
      Err(data.err().unwrap().into())
    }
  } else {
    panic!("could not send request (are you connected to the internet?)")
  }
}

#[macro_export]
macro_rules! get_req {
    ( $client:expr, $path:expr, $params:expr ) => {{
        Ok(
            serde_json::from_reader(
                crate::_get_req($client, $path, $params)
            ).unwrap()
        )
    }}
#[cfg(not(feature = "models-only"))]
pub fn post_req<B: serde::Serialize, T: for<'de> serde::Deserialize<'de>>(
  c: Client,
  path: &str,
  data: Option<B>,
) -> Result<T> {
  let path = if path.starts_with("/") {
    format!("{}/api/v1{}", c.url, path)
  } else {
    path.to_string()
  };

  let r = ureq::post(&path).set("Authorization", &format!("Bearer {}", c.token));

  let res = if data.is_some() {
    let body = serde_json::to_string(&data.unwrap()).unwrap();
    r.send_string(&body)
  } else {
    r.call()
  };

  if res.is_ok() {
    let data = serde_json::from_reader::<_, T>(res.unwrap().into_reader());
    if data.is_ok() {
      Ok(data.expect("failed to deserialize"))
    } else {
      Err(data.err().unwrap().into())
    }
  } else {
    panic!("could not send request (are you connected to the internet?)")
  }
}

/// List your [Course]s
#[cfg(not(feature = "models-only"))]
pub fn courses(c: Client) -> Result<Vec<Course>> {
    get_req!{c, "/courses", &[
        ("state[]", "available"),
        ("enrollment_state", "active"),
        ("include[]", "total_students"),
        ("include[]", "syllabus_body"),
        ("include[]", "course_image"),
        //("include[]", "total_scores")
    ]}
  get_req(
    c,
    "/courses",
    &[
      ("state[]", "available"),
      ("enrollment_state", "active"),
      ("include[]", "total_students"),
      ("include[]", "syllabus_body"),
      ("include[]", "course_image"),
      //("include[]", "total_scores")
    ],
  )
}

#[derive(Clone)]
#[derive(Clone, Default)]
pub struct Client {
    pub url: &'static str,
    pub token: String,
}
impl Default for Client {
    fn default() -> Self {
        Self {
            url: "",
            token: String::new(),
        }
    }
  pub url: String,
  pub token: String,
}

impl Assignment {
    /// Returns a string indicating the number of days/weeks until an [Assignment] is due.
    /// 
    /// If the [Assignment] is overdue, this instead returns the number of days since the [Assignment] was due.
    pub fn due_in(&self) -> String {
        let due = self.due_at.unwrap();
        let now: DateTime<Utc> = Utc::now();

        let days: i64 = <i64 as From<u32>>::from(due.day()) - <i64 as From<u32>>::from(now.day());
        let weeks = due.iso_week().week() - now.iso_week().week();

        if days <= -1 {
            if days < -1 {
                format!("{} days late", days.abs())
            } else {
                "a day late".to_string()
            }
        } else if days < 7 {
            if days > 1 {
                format!("due in {} days", days)
            } else {
                "due tomorrow".to_string()
            }
        } else {
            if days > 7 {
                format!("{} weeks", weeks)
            } else {
                "a week".to_string()
            }
        }
    }
}
// impl Assignment {
//   /// Returns a string indicating the number of days/weeks until an
// [Assignment]   /// is due.
//   ///
//   /// If the [Assignment] is overdue, this instead returns the number of days
//   /// since the [Assignment] was due.
//   pub fn due_in(&self) -> String {
//     let due = self.due_at.unwrap();
//     let now: DateTime<Utc> = Utc::now();

//     let days: i64 =
//       <i64 as From<u32>>::from(due.day()) - <i64 as
// From<u32>>::from(now.day());     let weeks = due.iso_week().week() -
// now.iso_week().week();

//     if days <= -1 {
//       if days < -1 {
//         format!("{} days late", days.abs())
//       } else {
//         "a day late".to_string()
//       }
//     } else if days < 7 {
//       if days > 1 {
//         format!("due in {} days", days)
//       } else {
//         "due tomorrow".to_string()
//       }
//     } else {
//       if days > 7 {
//         format!("{} weeks", weeks)
//       } else {
//         "a week".to_string()
//       }
//     }
//   }
// }

pub const ASSIGNMENTS_LIMIT: &'static str = "50";
#[cfg(not(feature = "models-only"))]
pub const PER_PAGE: &'static str = "100";

D canvaslms/src/main.rs => canvaslms/src/main.rs +0 -30
@@ 1,30 0,0 @@
use canvaslms::*;

fn main() {
  let cl = Client {
    url: "https://ccsdschools.instructure.com",
    token: "11531~OiE5rvyDjyz6nhVVeGN3MbuFchIaLKG3aB4DQbf7MoDyZj2VUHCPbJQytZRCo4wS"
  };

  for c in courses(cl).unwrap() {
    println!("{}", c.clone().name.unwrap());

    for m in c.modules(cl).unwrap() {
      println!(" - {}", m.name);
      for i in m.items(cl).unwrap() {
        println!("   - {:?}: {}", i.kind, i.title);
      }
    }

    // for a in c.overdue_assignments(client).unwrap() {
    //     if a.due_at.is_some() {
    //         println!(" ! {} ({})", a.name, a.due_in());
    //     }
    // }
    // for a in c.upcoming_assignments(client).unwrap() {
    //     if a.due_at.is_some() {
    //         println!(" - {} ({})", a.name, a.due_in());
    //     }
    // }
  }
}
\ No newline at end of file

M canvaslms/src/models/assignment.rs => canvaslms/src/models/assignment.rs +25 -8
@@ 1,11 1,28 @@
use super::*;

#[derive(Serialize, Deserialize, Clone)]
#[derive(Deserialize, Clone)]
pub struct Assignment {
    pub id:          i64,
    pub name:        String,
    pub description: Option<String>,
    pub due_at:      Option<DateTime<Utc>>,
    #[serde(rename = "html_url")]
    pub url:         Option<String>
}
\ No newline at end of file
  pub id: u64,
  pub name: String,
  pub description: Option<String>,
  #[cfg(feature = "miyagi-custom")]
  #[serde(skip_serializing)]
  pub due_at: Option<DateTime<Utc>>,
  pub submission_types: Option<Vec<SubmissionType>>,
  #[serde(rename = "html_url")]
  pub url: Option<String>,
  pub submission: Option<Submission>,
}

#[derive(Serialize, Clone)]
struct AssignmentSubmissionSubmission {
  submission_type: SubmissionType,
  #[serde(skip_serializing_if = "Option::is_none")]
  body: Option<String>,
}

#[derive(Serialize, Clone)]
struct AssignmentSubmissionComment {
  #[serde(skip_serializing_if = "Option::is_none")]
  text_comment: Option<String>,
}

M canvaslms/src/models/course.rs => canvaslms/src/models/course.rs +108 -120
@@ 2,136 2,124 @@ use super::*;

#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Course {
    pub id:             u64,
    pub name:           Option<String>,
    pub course_code:    Option<String>,
    pub original_name:  Option<String>,
    pub start_at:       Option<DateTime<Utc>>,
    pub end_at:         Option<DateTime<Utc>>,
    pub total_students: Option<i64>,
    pub calendar:       Option<Calendar>,
    #[serde(rename = "image_download_url")]
    pub image_url:      Option<String>,
    pub default_view:   Option<DefaultView>,
    pub syllabus_body:  Option<String>,
    #[serde(rename = "html_url")]
    pub url:            Option<String>
  pub id: u64,
  pub name: Option<String>,
  pub course_code: Option<String>,
  pub original_name: Option<String>,
  pub start_at: Option<DateTime<Utc>>,
  pub end_at: Option<DateTime<Utc>>,
  pub total_students: Option<i64>,
  pub calendar: Option<Calendar>,
  #[serde(rename = "image_download_url")]
  pub image_url: Option<String>,
  pub default_view: Option<DefaultView>,
  pub syllabus_body: Option<String>,
  #[serde(rename = "html_url")]
  pub url: Option<String>,
}
impl Course {
    pub fn new(id: u64) -> Self {
        Self {
            id,
            ..Default::default()
        }
    }
    pub fn peers(&self, c: Client) -> Result<Vec<User>> {
        let total_students = format!("{}", self.total_students.unwrap());
        get_req!{
            c,
            format!("/courses/{}/users", self.id).as_str(),
            &[
                ("include[]", "avatar_url"),
                ("include[]", "bio"),
                ("include[]", "custom_links"),
                ("enrollment_type[]", "student"),
                ("limit", total_students.as_str())
            ]
        }
    }
    pub fn modules(&self, c: Client) -> Result<Vec<Module>> {
      get_req!{
        c,
        format!("/courses/{}/modules", self.id).as_str(),
        &[]
      }
    }
    pub fn quizzes(&self, c: Client) -> Result<Vec<Quiz>> {
      get_req!{
        c,
        format!("/courses/{}/quizzes", self.id).as_str(),
        &[]
      }
    }
    /// Gets past [Assignment]s
    pub fn past_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "past")
            ]
        }
    }
    /// Gets unsubmitted [Assignment]s
    pub fn unsubmitted_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "unsubmitted")
            ]
        }
    }
    /// Gets overdue [Assignment]s
    pub fn overdue_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "overdue")
            ]
        }
    }
    /// Gets upcoming [Assignment]s
    pub fn upcoming_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "upcoming")
            ]
        }
    }
    /// Gets future [Assignment]s
    pub fn future_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "future")
            ]
        }
  pub fn new(id: u64) -> Self {
    Self {
      id,
      ..Default::default()
    }
  }
  pub fn peers(&self, c: Client) -> Result<Vec<User>> {
    let total_students = format!("{}", self.total_students.unwrap());
    get_req(
      c,
      format!("/courses/{}/users", self.id).as_str(),
      &[
        ("include[]", "avatar_url"),
        ("include[]", "bio"),
        ("include[]", "custom_links"),
        ("enrollment_type[]", "student"),
        ("limit", total_students.as_str()),
      ],
    )
  }
  pub fn modules(&self, c: Client) -> Result<Vec<Module>> {
    get_req(
      c,
      format!("/courses/{}/modules", self.id).as_str(),
      &[("per_page", "50")],
    )
  }
  pub fn pages(&self, c: Client) -> Result<Vec<Page>> {
    get_req(
      c,
      &format!("/courses/{}/pages", self.id),
      &[("per_page", PER_PAGE)],
    )
  }
  // pub fn quizzes(&self, c: Client) -> Result<Vec<Quiz>> {
  //   get_req(c, &format!("/courses/{}/quizzes", self.id), &[])
  // }
  /// Gets past [Assignment]s
  pub fn past_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
    get_req(
      c,
      format!("/courses/{}/assignments", self.id).as_str(),
      &[("per_page", PER_PAGE), ("bucket", "past")],
    )
  }
  /// Gets unsubmitted [Assignment]s
  pub fn unsubmitted_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
    get_req(
      c,
      format!("courses/{}/assignments", self.id).as_str(),
      &[("per_page", PER_PAGE), ("bucket", "unsubmitted")],
    )
  }
  /// Gets overdue [Assignment]s
  pub fn overdue_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
    get_req(
      c,
      format!("/courses/{}/assignments", self.id).as_str(),
      &[("per_page", PER_PAGE), ("bucket", "overdue")],
    )
  }
  /// Gets upcoming [Assignment]s
  pub fn upcoming_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
    get_req(
      c,
      format!("/courses/{}/assignments", self.id).as_str(),
      &[("per_page", PER_PAGE), ("bucket", "upcoming")],
    )
  }
  /// Gets future [Assignment]s
  pub fn future_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
    get_req(
      c,
      format!("/courses/{}/assignments", self.id).as_str(),
      &[("per_page", PER_PAGE), ("bucket", "future")],
    )
  }
}

#[derive(Serialize, Deserialize, Clone)]
/// An `.ics` calendar
pub struct Calendar {
    /// URL to the `.ics` file
    pub ics: String
  /// URL to the `.ics` file
  pub ics: String,
}

#[derive(Serialize, Deserialize, Clone)]
/// Teacher-selected `default_view` for a [Course]
pub enum DefaultView {
    #[serde(rename = "feed")]
    /// `feed`
    Feed,
    #[serde(rename = "wiki")]
    /// `wiki`
    Wiki,
    #[serde(rename = "modules")]
    /// `modules`
    Modules,
    #[serde(rename = "assignments")]
    /// `assignments`
    Assignments,
    #[serde(rename = "syllabus")]
    /// `syllabus`
    Syllabus
}
\ No newline at end of file
  #[serde(rename = "feed")]
  /// `feed`
  Feed,
  #[serde(rename = "wiki")]
  /// `wiki`
  Wiki,
  #[serde(rename = "modules")]
  /// `modules`
  Modules,
  #[serde(rename = "assignments")]
  /// `assignments`
  Assignments,
  #[serde(rename = "syllabus")]
  /// `syllabus`
  Syllabus,
}

M canvaslms/src/models/mod.rs => canvaslms/src/models/mod.rs +7 -4
@@ 1,8 1,7 @@
pub use chrono::{DateTime, Utc};
pub use serde::{Deserialize, Serialize};

pub use crate::{ Client, get_req, ASSIGNMENTS_LIMIT };
pub use serde::{ Deserialize, Serialize };
pub use serde_json::Result;
pub use chrono::{ DateTime, Utc };
pub use crate::{get_req, Client, Result, PER_PAGE};

mod course;
pub use course::*;


@@ 13,5 12,9 @@ mod quiz;
pub use quiz::*;
mod assignment;
pub use assignment::*;
mod submission;
pub use submission::*;
mod user;
pub use user::*;
mod page;
pub use page::*;

M canvaslms/src/models/module.rs => canvaslms/src/models/module.rs +36 -12
@@ 6,32 6,56 @@ pub struct Module {
  pub name: String,
  pub items_url: String,
}
#[cfg(not(feature = "models-only"))]
impl Module {
    pub fn items(&self, c: Client) -> Result<Vec<Item>> {
      get_req!{
        c,
        self.items_url.as_str(),
        &[]
      }
    }
  /// Get a [Module]'s [Item]s
  pub fn items(&self, c: Client) -> Result<Vec<Item>> {
    get_req(c, self.items_url.as_str(), &[("per_page", PER_PAGE)])
  }
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Item {
  pub id: u64,
  // pub id: u64,
  pub title: String,
  #[serde(rename = "type")]
  pub kind: ItemType,
  pub html_url: Option<String>,
  pub url: Option<String>,
  /// external url that the item points to
  /// 
  ///
  /// (only for [ItemType::ExternalUrl] and [ItemType::ExternalTool] types)
  pub external_url: Option<String>,
  /// whether the external tool opens in a new tab
  /// 
  ///
  /// (only for [ItemType::ExternalTool] type)
  pub new_tab: Option<bool>,

  pub page_url: Option<String>,
}
#[cfg(not(feature = "models-only"))]
impl Item {
  /// Attempt to convert an [Item] to an [Assignment]
  pub fn as_assignment(&self, c: Client) -> Result<Assignment> {
    if self.kind == ItemType::Assignment
      && self.url.clone().unwrap().contains("instructure.com")
    {
      get_req(
        c,
        self.url.clone().unwrap().as_str(),
        &[("include[]", "submission")],
      )
    } else {
      Err(std::io::Error::from_raw_os_error(1))
    }
  }
  pub fn as_page(&self, c: Client) -> Result<Page> {
    if self.kind == ItemType::Page {
      get_req(c, self.url.clone().unwrap().as_str(), &[])
    } else {
      Err(std::io::Error::from_raw_os_error(1))
    }
  }
}

#[derive(Serialize, Deserialize, Clone)]


@@ 42,7 66,7 @@ pub enum HideResults {
  UntilLastAttempt,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum ItemType {
  File,
  Page,


@@ 59,4 83,4 @@ pub enum ItemType {
pub enum ScoringPolicy {
  KeepHighest,
  KeepLatest,
}
\ No newline at end of file
}

A canvaslms/src/models/page.rs => canvaslms/src/models/page.rs +7 -0
@@ 0,0 1,7 @@
use super::*;

#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Page {
  pub page_id: u64,
  pub body: Option<String>,
}

D canvaslms/src/models/quiz.rs => canvaslms/src/models/quiz.rs +0 -48
@@ 1,48 0,0 @@
use super::*;

#[derive(Serialize, Deserialize, Clone)]
pub struct Quiz {
  pub id: u64,

  pub title: String,
  pub description: Option<String>,

  /// whether the quiz has a published or unpublished draft state.
  pub published: bool,
  /// quiz time limit in minutes
  pub time_limit: Option<u64>,
  /// shuffle answers for students?
  pub shuffle_answers: bool,
  /// let students see their quiz responses?
  /// 
  /// possible values: null, `always`, `until_after_last_attempt`
  pub hide_results: Option<HideResults>,
  /// show which answers were correct when results are shown?
  /// 
  /// only valid if `hide_results` is null
  pub show_correct_answers: bool,
  /// restrict the `show_correct_answers` option above to apply only to the last submitted attempt of a quiz that allows multiple attempts.
  /// 
  /// only valid if `show_correct_answers` is true and `allowed_attempts` > 1
  pub show_correct_answers_last_attempt: bool,
  /// prevent the students from seeing their results more than once (right after they submit the quiz)
  pub one_time_results: bool,
  /// which quiz score to keep (only if `allowed_attempts` is not 1)
  /// 
  /// possible values: `keep_highest`, `keep_latest`
  pub scoring_policy: Option<ScoringPolicy>,
  /// how many times a student can take the quiz
  /// 
  /// -1 = unlimited attempts
  pub allowed_attempts: u32,
  /// show one question at a time?
  pub one_question_at_a_time: bool,
  /// the number of questions in the quiz
  pub question_count: u32,
  /// The total point value given to the quiz
  pub points_possible: u32,
  /// lock questions after answering?
  /// 
  /// only valid if `one_question_at_a_time` is true
  pub cant_go_back: bool,
}
\ No newline at end of file

A canvaslms/src/models/quiz/mod.rs => canvaslms/src/models/quiz/mod.rs +65 -0
@@ 0,0 1,65 @@
// use super::*;
// use crate::post_req;

// mod submission;
// mod submission_question;

// #[derive(Serialize, Deserialize, Clone)]
// pub struct Quiz {
//   pub id: u64,

//   pub title: String,
//   pub description: Option<String>,

//   /// whether the quiz has a published or unpublished draft state.
//   pub published: bool,
//   /// quiz time limit in minutes
//   pub time_limit: Option<u64>,
//   /// shuffle answers for students?
//   pub shuffle_answers: bool,
//   /// let students see their quiz responses?
//   ///
//   /// possible values: null, `always`, `until_after_last_attempt`
//   pub hide_results: Option<HideResults>,
//   /// show which answers were correct when results are shown?
//   ///
//   /// only valid if `hide_results` is null
//   pub show_correct_answers: bool,
//   /// restrict the `show_correct_answers` option above to apply only to the
// last   /// submitted attempt of a quiz that allows multiple attempts.
//   ///
//   /// only valid if `show_correct_answers` is true and `allowed_attempts` > 1
//   pub show_correct_answers_last_attempt: bool,
//   /// prevent the students from seeing their results more than once (right
// after   /// they submit the quiz)
//   pub one_time_results: bool,
//   /// which quiz score to keep (only if `allowed_attempts` is not 1)
//   ///
//   /// possible values: `keep_highest`, `keep_latest`
//   pub scoring_policy: Option<ScoringPolicy>,
//   /// how many times a student can take the quiz
//   ///
//   /// -1 = unlimited attempts
//   pub allowed_attempts: u32,
//   /// show one question at a time?
//   pub one_question_at_a_time: bool,
//   /// the number of questions in the quiz
//   pub question_count: u32,
//   /// The total point value given to the quiz
//   pub points_possible: u32,
//   /// lock questions after answering?
//   ///
//   /// only valid if `one_question_at_a_time` is true
//   pub cant_go_back: bool,
// }

// impl Quiz {
//   // pub fn take(&self) -> Result<QuizSubmission> {}
//   // pub fn submit(&self) -> Result<()> {
//   //   post_req(
//   //     c,
//   //     format!("/courses/{}/quizzes/{}/submissions/{}/complete"),
//   //     None,
//   //   )
//   // }
// }

A canvaslms/src/models/quiz/question.rs => canvaslms/src/models/quiz/question.rs +46 -0
@@ 0,0 1,46 @@
use super::*;

pub struct QuizQuestion {
  /// The ID of the [QuizQuestion]
  pub id: u64,
  /// The ID of the [Quiz] this question belongs to
  pub quiz_id: u64,
  /// The order in which this question will be retrieved and displayed
  pub position: u32,
  /// The name of this question
  pub question_name: Option<String>,
  /// The type of this question
  pub question_type: QuestionType,
  /// The text of this question
  pub question_text: String,
  /// The maximum amount of points possible received for getting this question correct.
  pub points_possible: u32,
  /// The comments to display if the student answers this question correctly
  // Plural?? Appears to be singular, treating as such until proven otherwise
  pub correct_comments: String,
  /// The comments to display if the student answers incorrectly.
  // See correct_comments
  pub incorrect_comments: String,
  /// The comments to display regardless of how the student answered.
  // What is this even for?
  pub neutral_comments: String,
  /// An array of available answers to display to the student.
  pub answers: Vec<()>,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum QuestionType {
  CalculatedQuestion,
  EssayQuestion,
  FileUploadQuestion,
  FillInMultipleBlanksQuestion,
  MatchingQuestion,
  MultipleAnswersQuestion,
  MultipleChoiceQuestion,
  MultipleDropdownsQuestion,
  NumericalQuestion,
  ShortAnswerQuestion,
  TextOnlyQuestion,
  TrueFalseQuestion,
}

A canvaslms/src/models/quiz/submission.rs => canvaslms/src/models/quiz/submission.rs +30 -0
@@ 0,0 1,30 @@
use super::*;

pub struct QuizSubmission {}
#[cfg(not(feature = "models-only"))]
impl QuizSubmission {
  /// Submit ("complete") the [QuizSubmission]
  pub fn submit(&self) -> Result<()> {
    post_req(
      c,
      &format!("/courses/{}/assignments/{}/submissions/{}/complete"),
      None,
    )
  }
  // /// Get questions
  // pub fn questions(&self, c: Client) -> Result<Vec<SubmissionQuestion>> {
  //   get_req(
  //     c,
  //     &format!("/quiz_submissions/{}/questions", self.id.unwrap()),
  //     &[],
  //   )
  // }
}

#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub struct SubmissionQuestion {
  /// The ID of the QuizQuestion this answer is for.
  pub id: u64,
  /// Whether this question is flagged.
  pub flagged: bool,
}

A canvaslms/src/models/quiz/submission_question.rs => canvaslms/src/models/quiz/submission_question.rs +27 -0
@@ 0,0 1,27 @@
use super::*;

pub struct QuizQuestion {
  /// The ID of the quiz question.
  pub id: u64,
  // The ID of the Quiz the question belongs to.
  pub quiz_id: u64,
  // The order in which the question will be retrieved and displayed.
  "position": 1,
  // The name of the question.
  pub question_name: Option<String>,
  // The type of the question.
  pub question_type: QuestionType,
  // The text of the question
  pub question_text: String,
  // The maximum amount of points possible received for getting this question
  // correct.
  "points_possible": 5,
  // The comments to display if the student answers the question correctly.
  "correct_comments": "That's correct!",
  // The comments to display if the student answers incorrectly.
  "incorrect_comments": "Unfortunately, that IS a prime number.",
  // The comments to display regardless of how the student answered.
  "neutral_comments": "Goldbach's conjecture proposes that every even integer
greater than 2 can be expressed as the sum of two prime numbers.",   // An
array of available answers to display to the student.   "answers": null
}

A canvaslms/src/models/submission.rs => canvaslms/src/models/submission.rs +61 -0
@@ 0,0 1,61 @@
use super::*;

#[derive(Serialize, Deserialize, Clone)]
pub struct Submission {
  /// [Submission] ID
  // AFAIK `id` is only shown on [Assignment]s with `?include[]=submission`
  // No idea
  pub id: Option<u64>,
  /// The grade for the [Submission], translated in the [Assignment] grading
  /// scheme (so a letter grade, for example).
  pub grade: Option<String>,
  pub score: Option<f32>,
  pub attempt: Option<u32>,
  /// Extra [Submission] attempts allowed for the given [User] and [Assignment].
  ///
  /// Unlimited if `-1`
  pub extra_attempts: Option<i8>,

  /// The id of the [User] who created the [Submission]
  pub user_id: u64,
  /// The id of the [User] who graded the [Submission]
  ///
  /// This will be null for [Submission]s that haven't been graded yet. It will
  /// be a positive number if a real [User] has graded the [Submission] and a
  /// negative number if the [Submission] was graded by a process (e.g. [Quiz]
  /// autograder and autograding LTI tools). Specifically autograded [Quiz]zes
  /// set `grader_id` to the negative of the quiz id. [Submission]s autograded
  /// by LTI tools set `grader_id` to the negative of the tool id.
  pub grader_id: Option<i64>,

  /// Whether the submission was made after the applicable due date
  pub late: Option<bool>,
  /// Whether the [Assignment] is excused
  ///
  /// Excused [Assignment]s have no impact on a [User]'s grade.
  pub excused: Option<bool>,
  /// Whether the [Assignment] is missing
  pub missing: Option<bool>,
  /// The amount of points automatically deducted from the score by the
  /// missing/late policy for a late or missing [Assignment].
  pub points_deducted: Option<f32>,
  /// The amount of time, in seconds, that a [Submission] is late by.
  pub seconds_late: Option<u32>,
}

#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SubmissionType {
  #[serde(rename = "none")]
  None,
  OnPaper,

  OnlineQuiz,
  OnlineTextEntry,
  OnlineUrl,
  OnlineUpload,
  MediaRecording,
  StudentAnnotation,
  DiscussionTopic,
  ExternalTool,
}

M canvaslms/src/models/user.rs => canvaslms/src/models/user.rs +11 -13
@@ 1,17 1,15 @@
use super::*;

// The `/users/:id/profile` endpoint seems useless to me so I'm not going to
// implement it.

#[derive(Serialize, Deserialize, Clone)]
/// A [User] is a real person (or test student)
/// A student, teacher, or observer
pub struct User {
    pub id:         i64,
    pub name:       String,
    pub short_name: String,
    pub avatar_url: String,
    /// Only appears if the [User] has their pronouns set
    // Why are there only 3 options?
    pub pronouns:   Option<String>,
    pub email:      Option<String>,
    /// Only appears if the [User] has their bio set
    // Why do I not have the option to set this?
    pub bio:        Option<String>
}
\ No newline at end of file
  pub id: u64,
  pub name: String,
  pub short_name: String,
  pub avatar_url: String,
  /// Only appears if the [User] has their pronouns set
  pub pronouns: Option<String>,
}

A docker-compose.yml => docker-compose.yml +13 -0
@@ 0,0 1,13 @@
version: '3.0'

services:
  miyagi:
    build: ./Dockerfile
    environment:
      - VIRTUAL_HOST=lncn.dev
      - VIRTUAL_PORT=8080
      - LETSENCRYPT_HOST=lncn.dev
      - REDIS_URL=redis://redis/
      - PUBLIC_URL=https://myg.lncn.dev
  redis:
    image: redis:latest

A src/build/Cargo.toml => src/build/Cargo.toml +12 -0
@@ 0,0 1,12 @@
[package]
name = "miyagi-build"
version.workspace = true
edition.workspace = true

[[bin]]
name = "miyagi-build"
path = "main.rs"

[dependencies]
duct = "0.13.5"
minify-html = "0.10.3"

A src/build/main.rs => src/build/main.rs +91 -0
@@ 0,0 1,91 @@
extern crate duct;
extern crate minify_html;

use std::{
  fs::{create_dir, read, remove_dir_all, write},
  io::Result,
};

use duct::cmd;
use minify_html::{minify, Cfg};

const BUILD_FILES: &[&str] = &[
  #[cfg(debug_assertions)]
  "target/wasm32-unknown-unknown/debug/miyagi-wasm.wasm",
  #[cfg(not(debug_assertions))]
  "target/wasm32-unknown-unknown/release/miyagi-wasm.wasm",
  "src/web/index.html",
  "src/web/test.html",
  "src/web/style.css",
];

#[cfg(debug_assertions)]
const ARGS: &[&str] = &["build", "-vv"];
#[cfg(not(debug_assertions))]
const ARGS: &[&str] = &["build", "-r", "-vv"];

fn build_rs(p: &str) -> Result<()> {
  cmd("cargo", ARGS).dir(p).run()?;

  Ok(())
}

fn main() -> Result<()> {
  // Format the code
  cmd!("cargo", "+nightly", "fmt")
    .run()
    .expect("failed to format code (do you have Rust nightly?)");

  // Build WebAssembly
  build_rs("src/wasm")?;

  // Create build & dist dirs for the web assets
  create_dir("build").expect("couldn't create build dir");
  #[allow(unused_must_use)]
  {
    remove_dir_all("dist");
    create_dir("dist");
  }

  let mut mcfg = Cfg::spec_compliant();
  mcfg.keep_comments = true;
  mcfg.minify_css = true;
  mcfg.minify_js = true;

  // Copy build files into build dir
  for inf in BUILD_FILES {
    let n = inf.split('/').last().unwrap();

    let mut f = read(inf)?;

    if n.ends_with(".html") || n.ends_with(".css") {
      let min = minify(f.as_mut_slice(), &mcfg);
      write(format!("dist/{n}"), min)?;
    } else {
      write(format!("build/{n}"), f)?;
    }
  }

  cmd!(
    "wasm-bindgen",
    "--target",
    "web",
    "--no-typescript",
    "--out-dir",
    "dist",
    "build/miyagi-wasm.wasm"
  )
  .run()?;

  remove_dir_all("build")?;

  build_rs("src/server")?;

  Ok(())
}

// cargo +nightly fmt
// # trunk build --public-url /app src/wasm/index.html
// cd src/wasm
// cargo build -p miyagi-server
// target/debug/miyagi-server

A src/cache/mod.rs => src/cache/mod.rs +7 -0
@@ 0,0 1,7 @@
use chrono::{DateTime, Utc};

pub struct Cache {
  url: String,
  token: String,
  last_update: DateTime<Utc>,
}

A src/common/dump.rs => src/common/dump.rs +3 -0
@@ 0,0 1,3 @@
pub struct Dump {
  courses: Vec<u64>,
}
\ No newline at end of file

A src/common/lib.rs => src/common/lib.rs +79 -0
@@ 0,0 1,79 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum ItemType {
  File,
  Page,
  Discussion,
  Assignment,
  Quiz,
  SubHeader,
  ExternalUrl,
  ExternalTool,
}

#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Data {
  pub authenticated: bool,
  pub courses: Vec<Course>,
  pub modules: Vec<Module>,
}

#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Module {
  pub name: String,
  pub items: Vec<Item>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Item {
  pub title: String,
  pub kind: ItemType,
  pub url: String,

  pub assignment: Option<Assignment>,

  pub is_google: bool,
}

#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Assignment {
  pub id: u64,
  pub name: Option<String>,
  pub description: Option<String>,
  pub kind: Option<Vec<SubmissionType>>,
  pub late: bool,
  pub missing: bool,
  pub points: Option<f32>,
  pub max_points: Option<f32>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum SubmissionType {
  None,
  OnPaper,

  OnlineQuiz,
  OnlineTextEntry,
  OnlineUrl,
  OnlineUpload,
  MediaRecording,
  StudentAnnotation,
  DiscussionTopic,
  ExternalTool,
}

#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Course {
  pub id: u64,
  pub name: String,
}

#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Page {
  pub id: Option<u64>,
  pub content: String,
}

// pub fn miyagi_data_to_json(d: Data) -> String {
//   serde_json::to_string(&d).unwrap()
// }

D src/main.rs => src/main.rs +0 -281
@@ 1,281 0,0 @@
/* Web app */
extern crate axum;
extern crate axum_extra;
extern crate handlebars;

/* Talk to Canvas */
extern crate canvaslms;

/* Serde */
#[macro_use]
extern crate serde_derive;
//extern crate serde_qs;

/* Database */
extern crate redis;
//#[macro_use]
//extern crate diesel;

use axum::response::Response;
#[allow(unused_imports)]
use axum::{
  body::Body,
  extract::{self, Extension, Path, Query},
  http::{header, StatusCode},
  response::{self, Html, IntoResponse, Json},
  routing::{self, any, get, get_service, post},
  Router, Server,
};
use axum_extra::extract::cookie::{Cookie, CookieJar};
use handlebars::Handlebars;
use models::*;
use canvaslms::{ courses, Course, Client, ItemType };
//use serde_qs::axum::QsQuery;
use std::{
  net::SocketAddr,
  sync::{
    mpsc::{channel, sync_channel, Receiver, Sender, SyncSender},
    Arc,
  },
  thread,
};
use tokio::{runtime::Builder, task};

#[derive(Clone)]
struct Ctx {
  hb: Handlebars<'static>,
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut hb = Handlebars::new();
    hb.register_templates_directory(".hbs", "views/").unwrap();
    let ctx = Ctx{
        hb,
    };
  let rtr = Router::new()
    //.route("/courses", get(courses));
    .route("/", get(home))
    .route("/faq", get(faq))
    .route("/courses", get(list_courses))
    .route("/courses/:id", get(course_modules))
    .route("/auth", get(auth))
    .layer(Extension(ctx));

  axum::Server::bind(&"127.0.0.1:8080".parse().unwrap())
    .serve(rtr.into_make_service())
    .await.unwrap();
  Ok(())
}

// async fn process(tok: String) {
//   print!("Caching Canvas data...");
//   let mut ccourses: Vec<models::Course> = Vec::new();

//   let token: String = tok.clone();
//   let client = Client {
//     url: "https://ccsdschools.instructure.com",
//     token,
//   };
//   for ccourse in courses(client.clone()).unwrap() {
//     if ccourse.name.is_some() {
//       let mut c = models::Course {
//         name: ccourse.name.clone().unwrap(),
//         assignments: Vec::new(),
//       };
//       let cassignments = ccourse.overdue_assignments(client.clone()).unwrap();
//       for ca in cassignments {
//         let mut a = models::Assignment {
//           name: ca.clone().name,
//           due_date: String::new(),
//           url: String::new(),
//         };
//         if ca.due_at.is_some() {
//           a.due_date = ca.due_in();
//         }
//         c.assignments.push(a)
//       }
//       ccourses.push(c)
//     }
//   }

//   let mut data = models::Data {
//     authenticated: false,
//     courses: ccourses,
//     modules: Vec::new()
//   };

//   if data.clone().courses.len() > 0 {
//     data.authenticated = true;
//   }

//   let mut conn = redis::Client::open("redis://127.0.0.1/").unwrap();
//   let _: () = redis::cmd("SET")
//     .arg(tok)
//     .arg(serde_json::to_string(&data).unwrap())
//     .query(&mut conn)
//     .unwrap();

//   println!(" done!");
// }

// async fn auth(mut req: Request<()>) -> tide::Result {
//   Ok(Response::new())
// }

mod models;

#[derive(Deserialize, Default, Clone)]
#[serde(default)]
struct AuthReq {
  token: Option<String>,
}

async fn home() -> impl IntoResponse {
  let mut hb = Handlebars::new();
  hb.register_templates_directory(".hbs", "views/").unwrap();
  Html(hb.render("home", &()).unwrap())
}

async fn auth(Query(q): Query<AuthReq>, jar: CookieJar) -> impl IntoResponse {
  if q.token.is_some() {
    let mut c = Cookie::named("token");
    println!("{}", q.token.clone().unwrap());
    c.set_value(q.token.unwrap());
    return jar.add(c).into_response();
  } else {
    let mut hb = Handlebars::new();
    hb.register_templates_directory(".hbs", "views/").unwrap();
    Html(hb.render("auth", &()).unwrap()).into_response()
  }
  //res.set_content_type("text/html; charset=utf8");
}

async fn faq() -> impl IntoResponse {
  let mut hb = Handlebars::new();
  hb.register_templates_directory(".hbs", "views/").unwrap();
  Html(hb.render("faq", &()).unwrap())
}

async fn course_modules(Extension(ctx): Extension<Ctx>, Path(id): Path<u64>, jar: CookieJar) -> impl IntoResponse {
    use ItemType::*;

    let t = jar.get("token").unwrap().value();
    let client = Client {
      url: "https://ccsdschools.instructure.com",
      token: String::from(t),
    };

    let mut modules: Vec<Module> = Vec::new();
    for m in canvaslms::Course::new(id).modules(client.clone()).unwrap() {
        let mut items: Vec<Item> = Vec::new();
        for i in m.items(client.clone()).unwrap() {
            let kind = i.clone().kind;
            items.push(Item {
                is_assignment: kind == Assignment,
                is_discussion: kind == Discussion,
                is_external_tool: kind == ExternalTool,
                is_external_url: kind == ExternalUrl,
                is_file: kind == File,
                is_page: kind == Page,
                is_quiz: kind == Quiz,
                is_subheader: kind == SubHeader,
                item: i,
            });
        }

        modules.push(Module {
            name: m.name,
            items,
        })
    }
      let mut data = models::Data {
        authenticated: false,
        courses: Vec::new(),
        modules: modules,
      };
  
      if jar.get("token").is_some() {
        data.authenticated = true;
      }
  
    let mut conn = redis::Client::open("redis://127.0.0.1/").unwrap();
    redis::cmd("INCRBY").arg("reqcount").arg(1).execute(&mut conn);
    let reqc: u64 = redis::cmd("GET").arg("reqcount").query(&mut conn).unwrap();
    println!("{reqc}");
    // let raw_data: String = redis::cmd("GET").arg(t.clone().unwrap().value()).query(&mut conn).unwrap();
    // let data: models::Data = serde_json::from_str(raw_data.as_str()).unwrap();
    // for c in &data.courses {
    //   println!("{}", c.name);
    // }
  
    Html(ctx.hb.render("modules", &data).unwrap()).into_response()
  }

async fn list_courses(Extension(ctx): Extension<Ctx>, jar: CookieJar) -> impl IntoResponse {
  let t = jar.get("token").unwrap().value();
  let client = Client {
    url: "https://ccsdschools.instructure.com",
    token: String::from(t),
  };

  let mut ccourses: Vec<models::Course> = Vec::new();
    for ccourse in courses(client.clone()).unwrap() {
      if ccourse.name.is_some() {
        let mut c = models::Course {
            id: ccourse.id,
          name: ccourse.name.clone().unwrap(),
          assignments: Vec::new(),
        };

        // let overdue = ccourse.overdue_assignments(client.clone()).unwrap();
        // for ca in overdue {
        //   let mut a = models::Assignment {
        //     name: ca.clone().name,
        //     due_date: String::new(),
        //     url: ca.clone().url.unwrap(),
        //   };
        //   // if ca.due_at.is_some() {
        //   //   a.due_date = ca.due_in();
        //   // }
        //   c.assignments.push(a)
        // }

        // let upcoming = ccourse.upcoming_assignments(client.clone()).unwrap();
        // for ca in upcoming {
        //   let mut a = models::Assignment {
        //     name: ca.clone().name,
        //     due_date: String::new(),
        //     url: String::new(),
        //   };
        //   // if ca.due_at.is_some() {
        //   //   a.due_date = ca.due_in();
        //   // }
        //   c.assignments.push(a)
        // }

        ccourses.push(c);
      }
    }
    let mut data = models::Data {
      authenticated: false,
      courses: ccourses,
      modules: Vec::new(),
    };

    if data.clone().courses.len() > 0 {
      data.authenticated = true;
    }

  let mut conn = redis::Client::open("redis://127.0.0.1/").unwrap();
  redis::cmd("INCRBY").arg("reqcount").arg(1).execute(&mut conn);
  let reqc: u64 = redis::cmd("GET").arg("reqcount").query(&mut conn).unwrap();
  println!("{reqc}");
  // let raw_data: String = redis::cmd("GET").arg(t.clone().unwrap().value()).query(&mut conn).unwrap();
  // let data: models::Data = serde_json::from_str(raw_data.as_str()).unwrap();
  // for c in &data.courses {
  //   println!("{}", c.name);
  // }

  Html(ctx.hb.render("courses", &data).unwrap()).into_response()
}

D src/models.rs => src/models.rs +0 -43
@@ 1,43 0,0 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone)]
pub struct Data {
  pub authenticated: bool,
  pub courses: Vec<Course>,
  pub modules: Vec<Module>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Module {
  pub name: String,
  pub items: Vec<Item>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Item {
  pub is_assignment: bool,
  pub is_discussion: bool,
  pub is_external_tool: bool,
  pub is_external_url: bool,
  pub is_file: bool,
  pub is_page: bool,
  pub is_quiz: bool,
  pub is_subheader: bool,
  pub item: canvaslms::Item,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Assignment {
  pub name: String,
  pub due_date: String,
  pub url: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Course {
  pub id: u64,
  pub name: String,
  pub assignments: Vec<Assignment>,
}

pub fn miyagi_data_to_json(d: Data) -> String { serde_json::to_string(&d).unwrap() }

A src/server/Cargo.toml => src/server/Cargo.toml +27 -0
@@ 0,0 1,27 @@
[package]
name = "miyagi-server"
version.workspace = true
edition.workspace = true

[dependencies]
axum = { workspace = true }
axum-extra = { workspace = true }
canvaslms = { workspace = true }
chrono = { workspace = true }
handlebars = { workspace = true, features = ["dir_source"] }
redis = { workspace = true }
tokio = { workspace = true }
tower-http = { workspace = true }

scraper = { workspace = true }
minify-html = "0.10.3"

miyagi = { path = "../.." }
serde = { workspace = true }
bincode = { workspace = true }
url = { workspace = true }
include_dir = { workspace = true }

[[bin]]
name = "miyagi-server"
path = "main.rs"

A src/server/main.rs => src/server/main.rs +256 -0
@@ 0,0 1,256 @@
#![forbid(unsafe_code)]

// Web app
extern crate axum;
extern crate axum_extra;
extern crate handlebars;

// Talk to Canvas
extern crate canvaslms;

// Serde
#[macro_use]
extern crate serde;

// Database
extern crate redis;

use std::{
  io::Read,
  net::SocketAddr,
  sync::{
    mpsc::{channel, sync_channel, Receiver, Sender, SyncSender},
    Arc,
  },
  thread,
};

#[allow(unused_imports)]
use axum::{
  body::Body,
  extract::{self, Extension, Path, Query},
  http::{header, StatusCode},
  middleware::from_fn,
  response::{self, Html, IntoResponse, Json, Redirect},
  routing::{self, any, get, get_service, post},
  Router, Server,
};
use axum::{
  error_handling::{future::HandleErrorFuture, HandleError, HandleErrorLayer},
  extract::RequestParts,
  http::{header::LOCATION, HeaderValue, Method, Request, Uri},
  middleware::Next,
  response::Response,
  BoxError,
};
use axum_extra::{
  extract::cookie::{self, Cookie, CookieJar},
  routing::SpaRouter,
};
use canvaslms::{courses, Client, Course, ItemType};
use handlebars::Handlebars;
use include_dir::{include_dir, Dir};
use miyagi::*;
use redis::{Client as RdClient, Commands};
use tower_http::services::ServeDir;

pub(crate) mod proc;
pub(crate) mod refresh;
pub(crate) mod util;

// const WEB: Dir = include_dir!("dist");

#[derive(Clone)]
pub struct Ctx {
  tok: Option<String>,
  cc: Option<canvaslms::Client>,
  hb: Handlebars<'static>,
  rd: redis::Client,
}

/// Used by ahlpr
fn _ahlpr(mut ctx: &mut Ctx, jar: &CookieJar) -> bool {
  let c = jar.get("a");
  if c.is_none() {
    return false;
  }

  let mut parts = c.unwrap().value().split(';');

  let t = String::from(parts.next().unwrap());
  let i = String::from(parts.next().unwrap());

  ctx.tok = Some(t.clone());
  ctx.cc = Some(Client { url: i, token: t });

  true
}
/// Auth helper
///
/// ahlpr is a helper macro for internal use.
/// It handles authentication.
macro_rules! ahlpr {
  ($ctx:expr, $jar:expr) => {
    _ahlpr(&mut $ctx, &$jar)
  };
  ($req:ident $ctx:expr, $jar:expr) => {
    if stringify!($req) == "req" {
      if !_ahlpr(&mut $ctx, &$jar) {
        return Redirect::temporary("/auth").into_response();
      }
    }
  };
}

// impl Mistake
macro_rules! api_endpoints {
  ( data => { $( $name:ident, $field:expr $(, { $( $fname:ident => $extractor:ident : $ftype:ty ),* } )? );* } ) => {
    mod data_api {
      use super::*;
      $(
      pub async fn $name(
        $($( $extractor($fname): $extractor<$ftype>, )*)?
        Extension(mut ctx): Extension<Ctx>,
        jar: CookieJar,
      ) -> Response {
        ahlpr!(req ctx, jar);
        let data = ctx.rd.hget::<_, _, Vec<u8>>(ctx.tok.clone(), $field);
        if data.is_ok() {
          data.unwrap().into_response()
        } else {
          Redirect::temporary("/api/courses/refresh").into_response()
        }
      }
      )*
    }
  };
  ( refresh => { $( $name:ident, $fn_name:ident $(, { $( $fname:ident => $extractor:ident : $ftype:ty ),* } )? );* } ) => {
    mod refresh_api {
      use super::*;
      $(
      pub async fn $name(
        $($( $extractor($fname): $extractor<$ftype>, )*)?
        Extension(mut ctx): Extension<Ctx>,
        jar: CookieJar,
      ) -> Response {
        ahlpr!(req ctx, jar);
        use refresh::*;
        $fn_name(ctx.clone(), ctx.cc.unwrap()$(, $($fname),*)?);
        Redirect::temporary("/app/#courses").into_response()
      }
      )*
    }
  };
}

api_endpoints! {
    data => {
        api_courses, "courses";
        api_assignments, format!("{id}_assignments"), { id => Path : u64 };
        api_course,  format!("{id}_modules"), { id => Path : u64 };
        api_assignment,  format!("assignment_{id}"), { id => Path : u64 };
        api_page, format!("page_{id}"), { id => Path : u64 }
    }
}
api_endpoints! {
    refresh => {
        api_courses_refresh, courses;
        api_course_refresh, course_modules, { id => Path : u64 };
        api_assignments_refresh, assignments, { id => Path : u64 };
        api_pages_refresh, pages, { id => Path : u64 }
    }
}
use data_api::*;
use refresh_api::*;

#[tokio::main]
async fn main() -> std::io::Result<()> {
  let mut hb = Handlebars::new();
  hb.register_templates_directory(".hbs", "views/").unwrap();

  let rd = RdClient::open(std::env::var("REDIS_URL").unwrap()).unwrap();

  let ctx = Ctx {
    hb,
    rd,
    tok: None,
    cc: None,
  };
  let rtr = Router::new()
    .route("/", get(home))
    .route("/faq", get(faq))
    .route("/auth", get(auth))
    .route("/preferences", get(pref))
    .nest(
      "/api",
      Router::new()
        .route("/courses", get(api_courses))
        .route("/courses/refresh", get(api_courses_refresh))
        .route("/course/:id", get(api_course))
        .route("/course/:id/refresh", get(api_course_refresh))
        .route("/assignment/:id", get(api_assignment))
        .route("/:id/assignments", get(api_assignments))
        .route("/:id/assignments/refresh", get(api_assignments_refresh))
        .route("/page/:id", get(api_page))
        .route("/:id/pages/refresh", get(api_pages_refresh)),
    )
    .merge(SpaRouter::new("/app", "dist"))
    .layer(Extension(ctx));

  axum::Server::bind(&"0.0.0.0:8080".parse().unwrap())
    .serve(rtr.into_make_service())
    .await
    .unwrap();
  Ok(())
}

#[derive(Serialize)]
struct Err {
  err: String,
}

#[derive(Deserialize, Default, Clone)]
#[serde(default)]
struct AuthReq {
  t: Option<String>,
  i: Option<String>,
}

#[derive(Serialize)]
struct AuthStat {
  authed: bool,
}

async fn home(Extension(mut ctx): Extension<Ctx>, jar: CookieJar) -> impl IntoResponse {
  let authed = ahlpr!(ctx, jar);
  Html(ctx.hb.render("home", &AuthStat { authed }).unwrap())
}

async fn pref() -> impl IntoResponse {
  let mut hb = Handlebars::new();
  hb.register_templates_directory(".hbs", "views/").unwrap();
  Html(hb.render("preferences", &()).unwrap())
}

async fn auth(
  Extension(ctx): Extension<Ctx>,
  Query(q): Query<AuthReq>,
  jar: CookieJar,
) -> Response {
  if q.t.is_some() && q.i.is_some() {
    let mut c = Cookie::named("a");
    c.set_value(format!("{};{}", q.t.unwrap(), q.i.unwrap()));
    let mut res = jar.add(c).into_response();
    *res.status_mut() = StatusCode::TEMPORARY_REDIRECT;
    res
      .headers_mut()
      .insert(LOCATION, HeaderValue::from_static("/api/courses/refresh"));
    res
  } else {
    Html(ctx.hb.render("auth", &()).unwrap()).into_response()
  }
}

async fn faq(Extension(ctx): Extension<Ctx>) -> impl IntoResponse {
  Html(ctx.hb.render("faq", &()).unwrap())
}

A src/server/proc.rs => src/server/proc.rs +26 -0
@@ 0,0 1,26 @@
use minify_html::{minify, Cfg};
use scraper::{Html, Selector};

use super::util::*;

pub fn process(input: String) -> String {
  let s = Selector::parse("a").unwrap();
  let doc = Html::parse_document(input.as_str());
  let elems = doc.select(&s);

  let mut o = String::new();
  o = input.clone();

  for e in elems {
    let u = e.value().attr("href");
    if u.is_some() {
      let u = u.unwrap();
      if check_for_google(u) {
        o = o.replace(u, &gdrive_raw_file(u));
        o = o.replace(u, &gdocs_raw_file(u));
      }
    }
  }

  String::from_utf8(minify(o.as_bytes(), &Cfg::default())).unwrap()
}

A src/server/refresh.rs => src/server/refresh.rs +222 -0
@@ 0,0 1,222 @@
use bincode::serialize as bincodify;
use canvaslms::{Client, ItemType::*};
use miyagi::{ItemType, SubmissionType};
use redis::{cmd, Commands};

use crate::{proc::process, util, Ctx, Item, Module};

macro_rules! remap_type {
    ( $from:expr , $to:ty ; $( $it:ident ),* ) => {
      match $from {
        $($it => <$to>::$it,
        )*
      }
    };
}

pub fn course_modules(mut c: Ctx, cl: Client, id: u64) {
  let mut modules: Vec<Module> = Vec::new();
  for m in canvaslms::Course::new(id).modules(cl.clone()).unwrap() {
    let mut items: Vec<Item> = Vec::new();
    for i in m.items(cl.clone()).unwrap() {
      let kind = i.clone().kind;
      let url = i.clone().url;
      let extern_url = i.clone().external_url;
      let assignment = i.as_assignment(cl.clone());

      items.push(Item {
        title: i.clone().title,
        kind: remap_type! {
          kind, ItemType;
          Assignment,
          Discussion,
          ExternalTool,
          ExternalUrl,
          File,
          Page,
          Quiz,
          SubHeader
        },
        url: if matches!(kind, ExternalUrl | ExternalTool) {
          extern_url.clone().unwrap()
        } else if matches!(kind, Page) {
          format!("#page-{}", i.clone().as_page(cl.clone()).unwrap().page_id)
        } else {
          if url.is_some() {
            url.unwrap()
          } else {
            String::new()
          }
        },

        assignment: if assignment.is_ok() {
          let a = assignment.unwrap();
          let s = a.clone().submission.unwrap();
          {
            use canvaslms::SubmissionType::*;
            Some(miyagi::Assignment {
              id: a.id,
              name: Option::None,
              description: Option::None,
              kind: Some(
                a.submission_types
                  .unwrap()
                  .iter()
                  .map(|&s| {
                    remap_type! {
                      s, SubmissionType;
                      None,
                      OnPaper,

                      OnlineQuiz,
                      OnlineTextEntry,
                      OnlineUrl,
                      OnlineUpload,
                      MediaRecording,
                      StudentAnnotation,
                      DiscussionTopic,
                      ExternalTool
                    }
                  })
                  .collect(),
              ),
              late: s.late.unwrap(),
              missing: s.missing.unwrap(),
              points: s.score,
              max_points: Option::None,
            })
          }
        } else {
          println!("{}", assignment.err().unwrap());
          None
        },
        // assignment: None,
        is_google: if extern_url.is_some() {
          util::check_for_google(&extern_url.unwrap())
        } else {
          false
        },
      });
    }

    modules.push(Module {
      name: m.name,
      items,
    })
  }
  cmd("HSET")
    .arg(&[&cl.clone().token, &format!("{id}_modules")])
    .arg(bincodify(&modules).unwrap())
    .execute(&mut c.rd);
}

pub fn courses(mut c: Ctx, cl: Client) {
  let mut ccourses: Vec<miyagi::Course> = Vec::new();
  for ccourse in canvaslms::courses(c.cc.unwrap()).unwrap() {
    if ccourse.name.is_some() {
      let mut c = miyagi::Course {
        id: ccourse.id,
        name: ccourse.name.clone().unwrap(),
      };

      ccourses.push(c);
    }
  }
  cmd("HSET")
    .arg(&[&cl.clone().token, "courses"])
    .arg(bincodify(&ccourses).unwrap())
    .execute(&mut c.rd);
}

pub fn assignments(mut c: Ctx, cl: Client, id: u64) {
  use canvaslms::SubmissionType::*;

  let mut rd = c.rd;
  let mut all: Vec<miyagi::Assignment> = Vec::new();

  let cc = canvaslms::Course::new(id);

  // haha funny
  let mut ass: Vec<canvaslms::Assignment> = Vec::new();
  ass.append(&mut cc.past_assignments(cl.clone()).unwrap_or_default());
  ass.append(&mut cc.overdue_assignments(cl.clone()).unwrap_or_default());
  ass.append(&mut cc.upcoming_assignments(cl.clone()).unwrap_or_default());
  ass.append(&mut cc.future_assignments(cl.clone()).unwrap_or_default());

  for ca in ass {
    let mut a = miyagi::Assignment {
      name: Some(ca.name),
      description: if ca.description.is_some() {
        Some(process(ca.description.unwrap()))
      } else {
        Option::None
      },
      kind: Some(
        ca.submission_types
          .unwrap()
          .iter()
          .map(|&s| {
            remap_type! {
              s, SubmissionType;
              None,
              OnPaper,

              OnlineQuiz,
              OnlineTextEntry,
              OnlineUrl,
              OnlineUpload,
              MediaRecording,
              StudentAnnotation,
              DiscussionTopic,
              ExternalTool
            }
          })
          .collect(),
      ),
      ..Default::default()
    };
    let _: () = rd
      .hset(
        cl.clone().token,
        format!("assignment_{}", ca.id),
        bincodify(&a).unwrap(),
      )
      .unwrap();
    {
      a.name = Option::None;
      all.push(a);
    }
  }

  // cmd("HSET")
  //   .arg(&[cl.clone().token.as_str(), "assignments"])
  //   .arg(bincodify(&all).unwrap())
  //   .execute(&mut c.rd);
  let _: () = rd
    .hset(cl.clone().token, "assignments", bincodify(&all).unwrap())
    .unwrap();
}

pub fn pages(mut c: Ctx, cl: Client, id: u64) {
  use canvaslms::ItemType::*;
  let data: Vec<u8> = c.rd.hget(c.clone().tok, format!("{id}_modules")).unwrap();

  let mut mitems: Vec<canvaslms::Item> = Vec::new();

  let cc = canvaslms::Course::new(id);
  for m in cc.modules(cl.clone()).unwrap() {
    mitems.append(&mut m.items(cl.clone()).unwrap());
  }

  for i in mitems.into_iter().filter(|x| x.kind == Page) {
    let mut rd = c.rd.clone();
    let cp = i.as_page(cl.clone()).unwrap();
    let _: () = rd
      .hset(
        c.clone().tok,
        format!("page_{}", cp.page_id),
        bincodify(&process(cp.body.unwrap())).unwrap(),
      )
      .unwrap();
  }
}

A src/server/util.rs => src/server/util.rs +54 -0
@@ 0,0 1,54 @@
use url::Url;

/// Checks whether or not a URL is for a Google service
pub fn check_for_google(u: &str) -> bool {
  let url = Url::parse(u).unwrap();
  let host = url.host_str().unwrap();
  matches!(
    host,
    "google.com"
      | "www.google.com"
      | "drive.google.com"
      | "docs.google.com"
      | "slides.google.com"
      | "drawings.google.com"
      | "forms.google.com"
      | "forms.gle"
      | "youtube.com"
      | "www.youtube.com"
      | "youtu.be"
  )
}

/// Converts Google Drive URL to raw file URL
// https://stackoverflow.com/questions/67813895
pub fn gdrive_raw_file(u: &str) -> String {
  if u.starts_with("https://drive.google.com/file/d/") {
    let id = u
      .strip_prefix("https://drive.google.com/file/d/")
      .unwrap()
      .split('/')
      .next()
      .unwrap();
    format!("https://drive.google.com/uc?id={id}")
  } else {
    u.to_string()
  }
}

// https://docs.google.com/spreadsheets/d/1luVl-CqjoXS_LsvMEznfoucI-mQ-jtdhKC0glnoL0NM/export?format=pdf
// https://stackoverflow.com/questions/33713084
pub fn gdocs_raw_file(u: &str) -> String {
  if u.starts_with("https://docs.google.com/") {
    let mut p = u.split("/d/").into_iter();
    {
      let prefix = p.next().unwrap();
      let id = p.next().unwrap().split('/').next().unwrap();
      // Sticking with PDF only for now
      // Add user setting for PDF, ODF, CSV, Microsoft, etc.
      format!("{prefix}/d/{id}/export?format=pdf")
    }
  } else {
    u.to_string()
  }
}

A src/wasm/.cargo/config => src/wasm/.cargo/config +6 -0
@@ 0,0 1,6 @@
[build]
target = "wasm32-unknown-unknown"

[profile.release]
opt-level = "s"
lto = true
\ No newline at end of file

A src/wasm/Cargo.toml => src/wasm/Cargo.toml +26 -0
@@ 0,0 1,26 @@
[package]
name = "miyagi-wasm"
version.workspace = true
edition.workspace = true

[dependencies]
wasm-bindgen = { version = "0.2.83" }
wasm-bindgen-futures = { version = "0.4.33" }
web-sys = { version = "0.3.60", features = ["console", "Document", "Element", "Window", "HtmlDocument", "HtmlElement", "HtmlCollection", "Node"] }
wee_alloc = { version = "0.4.5" }
reqwest = { version = "0.11.13", default-features = false }

miyagi = { path = "../.." }
serde = { workspace = true }
bincode = { workspace = true }
url = { workspace = true }
include_dir = "0.7.3"
handlebars = "4.3.5"

[target."cfg(debug_assertions)".dependencies]
console_error_panic_hook = "0.1.5"

[[bin]]
name = "miyagi-wasm"
path = "main.rs"
#crate-type = ["cdylib"]

A src/wasm/main.rs => src/wasm/main.rs +212 -0
@@ 0,0 1,212 @@
use handlebars::Handlebars;
use include_dir::{include_dir, Dir};
use miyagi::*;
use serde::{Deserialize, Serialize};
use url::Url;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::{future_to_promise, spawn_local, JsFuture};
use web_sys::{console, window, Document, HtmlElement, Node};

pub const PUBLIC_URL: &'static str = "https://lncn.dev";

static TMPL: Dir = include_dir!("src/wasm/views");

mod util;
use util::api;

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

fn main() {}

#[derive(Serialize)]
struct Data<T> {
  data: T,
}

#[derive(Serialize)]
struct Items<T> {
  items: Vec<T>,
}

async fn courses(d: &Document, hb: Handlebars<'static>) {
  let req = reqwest::get(api("/courses"))
    .await
    .unwrap()
    .bytes()
    .await
    .unwrap();
  let ccs = bincode::deserialize::<Vec<Course>>(req.to_vec().as_slice()).unwrap();

  d.get_element_by_id("app").unwrap().set_inner_html(
    &hb
      .render("courses", &Items::<Course> { items: ccs })
      .unwrap(),
  );

  d.set_title("(courses)[miyagi]");
}

async fn course(d: &Document, hb: Handlebars<'static>, id: String) {
  let req = reqwest::get(api(&format!("/course/{id}")))
    .await
    .unwrap()
    .bytes()
    .await
    .unwrap();
  let modules = bincode::deserialize::<Vec<Module>>(req.to_vec().as_slice()).unwrap();

  d.get_element_by_id("app").unwrap().set_inner_html(
    &hb
      .render(
        "modules",
        &Items::<Module> {
          items: modules.clone(),
        },
      )
      .unwrap(),
  );

  d.set_title(&format!("{} (modules)[miyagi]", id));
}

async fn assignments(d: &Document, hb: Handlebars<'static>, id: String) {
  let req = reqwest::get(api(&format!("/{id}/assignments")))
    .await
    .unwrap()
    .bytes()
    .await
    .unwrap();
  let modules = bincode::deserialize::<Vec<Assignment>>(req.to_vec().as_slice()).unwrap();

  d.get_element_by_id("app").unwrap().set_inner_html(
    &hb
      .render(
        "assignments",
        &Items::<Assignment> {
          items: modules.clone(),
        },
      )
      .unwrap(),
  );
}

async fn assignment(d: &Document, hb: Handlebars<'static>, id: String) {
  let req = reqwest::get(api(&format!("/assignment/{id}")))
    .await
    .unwrap()
    .bytes()
    .await
    .unwrap();
  let a = bincode::deserialize::<Assignment>(req.to_vec().as_slice()).unwrap();

  d.get_element_by_id("app").unwrap().set_inner_html(
    &hb
      .render("assignment", &Data::<Assignment> { data: a.clone() })
      .unwrap(),
  );

  d.set_title(&format!("{} (assignment)[miyagi]", a.clone().name.unwrap()));
}

#[wasm_bindgen(start)]
pub async fn main_js() {
  let d = window().unwrap().document().unwrap();

  let a = d.get_element_by_id("app").unwrap();
  a.set_inner_html("<center><h2>loading...</h2></center>");

  let mut hb = Handlebars::new();
  for f in TMPL.files() {
    hb.register_template_string(
      f.path()
        .file_name()
        .unwrap()
        .to_str()
        .unwrap()
        .strip_suffix(".hbs")
        .unwrap(),
      f.contents_utf8().unwrap(),
    )
    .unwrap();
  }

  #[cfg(debug_assertions)]
  console_error_panic_hook::set_once();

  a.set_inner_html("");

  router(&d, hb).await;
}

async fn home(d: &Document) {
  d.get_element_by_id("app").unwrap().set_inner_html(r#"<div class="sm box">
  <center>
    <b>welcome to miyagi!</b>
  </center>
  miyagi is a minimalist web client for Canvas LMS. it's currently very early alpha-stage software, so please report any bugs you may find via <a href="https://todo.sr.ht/~baka/miyagi">the bug tracker</a>. you may also provide feedback, suggestions, words of encouragement, or <strike>free labor</strike> contributions via <a href="https://lists.sr.ht/~baka/miyagi-discuss">the mailing list</a>.
</div>"#);
}

async fn router(d: &Document, hb: Handlebars<'static>) {
  let uri = Url::parse(d.document_uri().unwrap().as_str()).unwrap();
  let frag = uri.fragment();
  if frag.is_none() {
    home(d).await;
  } else {
    let frag = frag.unwrap();

    let spl = frag.split_once('-');
    let (frag, id) = spl.unwrap_or((frag, ""));

    match frag {
      "course" => {
        course(d, hb, id.to_string()).await;
      }
      "assignments" => {
        if id == "" {
          assignments(d, hb, id.to_string()).await;
        } else {
          assignment(d, hb, id.to_string()).await;
        }
      }
      "courses" => {
        courses(d, hb).await;
      }
      &_ => unimplemented!(),
    }
  }

  let elems = d.get_elements_by_tag_name("a");
  for i in 0..elems.length() {
    let e = elems.item(i).unwrap();

    let href = e.get_attribute("href").unwrap();

    if util::check_for_google(&href) {
      let goog = d.create_element("span").unwrap();
      goog
        .set_attribute(
          "title",
          "This links to a Google service that may track you.",
        )
        .unwrap();
      goog.set_attribute("class", "uhoh").unwrap();
      goog.set_inner_html("<sup>&lt;0&gt;</sup>");
      e.after_with_node_1(&goog).unwrap();

      e.set_attribute("href", &util::gdrive_raw_file(href.clone()))
        .unwrap();
    }

    if e.has_attribute("target") {
      continue;
    }

    e.set_attribute(
      "onclick",
      &format!("window.location = '{}'; window.location.reload();", href),
    )
    .unwrap();
  }
}

A src/wasm/util.rs => src/wasm/util.rs +58 -0
@@ 0,0 1,58 @@
pub fn api(path: &str) -> String { format!("{}/api{}", crate::PUBLIC_URL, path) }

pub fn itemtype_to_str(it: miyagi::ItemType) -> &'static str {
  use miyagi::ItemType::*;

  match it {
    Assignment => "assignment",
    Discussion => "discussion",
    ExternalTool => "external tool",
    ExternalUrl => "external resource",
    File => "file",
    Quiz => "quiz",
    Page => "page",
    SubHeader => "subheader",
  }
}

use url::Url;

/// Checks whether or not a URL is for a Google service
pub fn check_for_google(u: &str) -> bool {
  if u.starts_with("#") || u.starts_with("/api/") {
    return false;
  }
  let url = Url::parse(u).unwrap();
  let host = url.host_str().unwrap();
  matches!(
    host,
    "google.com"
      | "www.google.com"
      | "drive.google.com"
      | "docs.google.com"
      | "slides.google.com"
      | "drawings.google.com"
      | "forms.google.com"
      | "forms.gle"
      | "youtube.com"
      | "www.youtube.com"
      | "youtu.be"
  )
}

/// Converts Google Drive URL to raw file URL
// https://stackoverflow.com/questions/67813895
pub fn gdrive_raw_file(u: String) -> String {
  if u.starts_with("https://drive.google.com/file/d/") {
    let id = u
      .as_str()
      .strip_prefix("https://drive.google.com/file/d/")
      .unwrap()
      .split('/')
      .next()
      .unwrap();
    format!("https://drive.google.com/uc?id={id}")
  } else {
    u
  }
}

A src/wasm/views/assignment.hbs => src/wasm/views/assignment.hbs +17 -0
@@ 0,0 1,17 @@
{{#with data}}
  <h2>{{name}}</h2>
  <div>
    {{{description}}}
  </div>
  <p>
    {{#each kind}}
      {{#if (eq this "OnlineTextEntry")}}
        <form action="/api/" method="post">
          <textarea></textarea>
        </form>
      {{else}}
        <span>{{this}}</span>
      {{/if}}
    {{/each}}
  </p>
{{/with}}
\ No newline at end of file

A src/wasm/views/assignments.hbs => src/wasm/views/assignments.hbs +4 -0
@@ 0,0 1,4 @@
{{#each items}}
  {{{description}}}
  <hr>
{{/each}}
\ No newline at end of file

A src/wasm/views/courses.hbs => src/wasm/views/courses.hbs +12 -0
@@ 0,0 1,12 @@
{{#each items}}
  <h3>
    <a href="#course-{{id}}">{{name}}</a>
    <span> </span>
    <sub>
      <span>r </span>
      <a title="refresh modules" href="/api/course/{{id}}/refresh" target="">m</a>
      <span> </span>
      <a title="refresh assignments" href="/api/{{id}}/assignments/refresh" target="">a</a>
    </sub>
  </h3>
{{/each}}
\ No newline at end of file

A src/wasm/views/modules.hbs => src/wasm/views/modules.hbs +25 -0
@@ 0,0 1,25 @@
{{#each items}}
  <h2>{{name}}</h2>
  <div>
    {{#each items}}
      {{#if (eq kind "SubHeader")}}
        <h3 class="sh">{{title}}</h3>
      {{else}}
        <p>
          <a href="{{#if assignment}}#assignments-{{assignment.id}}{{else}}{{url}}{{/if}}" target="_blank">{{title}}</a>
          <br>
          <span class="sm">{{kind}}</span>
          {{#if assignment}}
            {{#with assignment}}
              {{#if missing}}
                <span class="uhoh sm"><b>MISSING</b></span>
              {{else}}
                <span class="sm">{{points}}</span>
              {{/if}}
            {{/with}}
          {{/if}}
        </p>
      {{/if}}
    {{/each}}
  </div>
{{/each}}

A src/wasm/views/page.hbs => src/wasm/views/page.hbs +1 -0
@@ 0,0 1,1 @@
{{{data}}}
\ No newline at end of file

A src/web/index.html => src/web/index.html +37 -0
@@ 0,0 1,37 @@
<!DOCTYPE html>
<html>
  <head>
    <title>miyagi</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <script type="module">
      import init from './miyagi-wasm.js';
      try {
        await init();
      } catch (e) {
        document.getElementById("app").innerHTML = "<h2>welp something broke</h2>";
      };
    </script>
    <center>
      <h1>miyagi</h1>
      <p id="nav">
        <a href="#courses">courses</a>
      </p>
    </center>
    <hr><br>

    <div id="app">
      <center>
        <h2>loading...</h2>
        <p>if this takes too long, there may be something wrong with your internet connection or browser</p>
      </center>
    </div>

    <br><hr>
    <p class="sm">
      <span>miyagi is <a href="https://wikipedia.org/wiki/Free_Software" target="_blank">free software</a></span><br>
      <span><a href="https://git.sr.ht/~baka/miyagi" target="_blank">sauce</a> - <a href="https://sr.ht/~baka/miyagi" target="_blank">join in</a> - agplv3</span>
    </span>
  </body>
</html>
\ No newline at end of file

A src/web/index.js => src/web/index.js +1 -0
@@ 0,0 1,1 @@
aaa
\ No newline at end of file

A src/web/style.css => src/web/style.css +58 -0
@@ 0,0 1,58 @@
body {
  font-family: monospace;
  font-size: 18px;
  padding-left: 10%;
  padding-right: 10%;

  background-color: #242424;
  color: #db00db;
}
a {
  color: #db00db;
}
.sm a {
  color: #a800a8;
}
hr {
  color: #a800a8;
}
div {
  padding-left: 10px;
}
.sh {
  margin-left: -5px;
}
.uhoh {
  color: #c90000 !important;
}
.list {
  font-size: 24px;
}
.sm {
  font-size: 16px;
  color: #a800a8;
}
.box {
  display: block;
  color: #828282;
  padding: 5px 5px 5px 5px;
  border: #828282 solid 1px;
}
input[type=text] {
  border: transparent 0px;
  border-radius: 5px;
  font-size: 16px;
  color: #ff6dff;
  padding: 5px 5px 5px 5px;
  background-color: #363636;
}
textarea {
  border: transparent 0px;
  border-radius: 5px;
  font-size: 16px;
  color: #ff6dff;
  padding: 20px 20px 20px 20px;
  width: 100%;
  height: 30%;
  background-color: #363636;
}
\ No newline at end of file

A src/web/test.html => src/web/test.html +8 -0
@@ 0,0 1,8 @@
<html>
    <head>
        <link rel="stylesheet" href="style.css" />
    </head>
    <body>
        <textarea></textarea>
    </body>
</html>
\ No newline at end of file

A views/assignment.hbs => views/assignment.hbs +2 -0
@@ 0,0 1,2 @@
{{!-- {{#> abase}}
    {{#*inline}} --}}
\ No newline at end of file

M views/auth.hbs => views/auth.hbs +22 -15
@@ 1,19 1,26 @@
{{#> base}}
  {{#*inline "content"}}
    <h2>let's get started</h2>
    <ol>
      <li>open your canvas dashboard</li>
      <li>click your profile picture to open the popup menu</li>
      <li>click "Settings" (the third option) in the popout menu</li>
      <li>scroll down to "Approved Integrations" and click "New Access Token"</li>
      <li>click in the first textbox and type "Miyagi" and press "Generate Token"</li>
      <li>select the token and copy to your clipboard</li>
      <li>paste the token from your clipboard below</li>
    </ol>
    <form method="get" class="sm">
      <label for="token">canvas access token: </label>
      <input type="text" name="token" id="token" placeholder="12345~TH1siS8dEfaR9aLtOk7N" /><br>
      <input type="submit" value="Authenticate" />
    </form>
  <center>
    <div style="max-width:560px;text-align:left">
      <h2>let's get started</h2>
      <ol>
        <li>open your canvas dashboard</li>
        <li>click your profile picture (in the top left corner) to open the popup menu</li>
        <li>click "Settings" (the third option) in the popout menu</li>
        <li>scroll down to "Approved Integrations" and click "New Access Token"</li>
        <li>click in the first textbox, type <code>miyagi</code> and press "Generate Token"</li>
        <li>select the token and copy to your clipboard</li>
        <li>paste the token from your clipboard below</li>
      </ol>
      <!-- spellcheck="false" to keep Google from sending **ACCESS TOKENS** home in plaintext -->
      <form method="get" class="sm" spellcheck="false">
        <label for="i">canvas instance <i>(protocol + host only, no "/")</i>: </label>
        <input type="text" name="i" id="i" placeholder="https://yourschool.instructure.com" style="width:100%;min-width:200px;max-width:300px" /><br>
        <label for="t">canvas access token: </label>
        <input type="text" name="t" id="t" placeholder="12345~TH1siS8dEfaR9aLtOk7N" style="width:100%;min-width:200px;max-width:300px" /><br>
        <input type="submit" value="Authenticate" />
      </form>
    </div>
  </center>
  {{/inline}}
{{/base}}

M views/base.hbs => views/base.hbs +8 -26
@@ 6,40 6,22 @@
<html>
  <head>
    <title>miyagi</title>
    <style>
      body {
        font-family: monospace;
        font-size: 18px;
        padding-left: 20%;
        padding-right: 20%;
      }
      div {
        padding-left: 10px;
      }
      .sh { margin-left: -5px; }
      .list {
        font-size: 24px;
      }
      .sm {
        font-size: 16px;
      }
      .box {
        display: block;
        color: #828282;
        padding: 5px 5px 5px 5px;
        border: #828282 solid 1px;
      }
    </style>
    {{!-- {{#if prefs.light}}
      <link rel="stylesheet" href="/assets/light.css" />
    {{else}}
      <link rel="stylesheet" href="/assets/dark.css" />
    {{/if}} --}}
    <link rel="stylesheet" href="/app/style.css" />
  </head>
  <body>
    <center>
      <h1>miyagi</h1>
      <h4>
        <a href="/courses">courses</a> | <a href="/faq">faq</a>
        <a href="/courses" title="your current courses">courses</a> | <a href="/faq">faq</a> | <a href="?refresh=true" title="retrieve fresh data from canvas (slow)">refresh</a>
      </h4>
    </center>
    {{> content}}
    <br><hr>
    <p class="sm">miyagi is an open source project made by volunteers - <a href="https://gitlab.com/subsnotdubs/miyagi">source code</a> - agplv3<br>not affiliated with or endorsed by instructure</p>
    <p class="sm">miyagi is free software made by volunteer(s) - <a href="https://gitlab.com/subsnotdubs/miyagi">source code</a> - agplv3<br>not affiliated with or endorsed by instructure</p>
  </body>
</html>

A views/err.hbs => views/err.hbs +8 -0
@@ 0,0 1,8 @@
{{#> base}}
    {{#*inline "content"}}
        <center>
            <h1>uh oh!</h1>
            <pre>{{ err }}</pre>
        </center>
    {{/inline}}
{{/base}}
\ No newline at end of file

M views/faq.hbs => views/faq.hbs +5 -3
@@ 5,9 5,11 @@
        <h1>FAQ</h1>
      </center>
      
      <h3>Can you see my grades, quiz submissions, or any other personal/sensitive information?</h3>
      <p>I <i>could</i>. Canvas access tokens allow access to <b>everything</b> you can see through Canvas (plus some extra metadata). It isn't possible to tweak permissions without help from someone at the district.</p>
      <p>You can run the server yourself if you'd like to.</p>
      <h3>Can't you see my grades, quiz submissions, or any other personal/sensitive information!?!1?!11?!</h3>
      <p>I <i>can</i>. To put it simply: if you don't trust me, don't use this. Canvas access tokens allow access to <b>everything</b> you can see through Canvas (plus some extra metadata). It isn't possible to tweak permissions without help from someone at the district.</p>
      <p>You can run Miyagi on your own server if you'd like to. Or you could always just use Canvas.</p>

      <h3></h3>

      <h3>Why is this soooooo slow?</h3>
      <p>Technical limitations of Canvas. I'm planning on implementing server-side caching to make it faster, whenever I find the time...</p>

M views/home.hbs => views/home.hbs +8 -5
@@ 2,15 2,18 @@
  {{#*inline "content"}}
    <div class="sm box">
      <center>
        <b>Welcome to Miyagi!</b>
        <b>welcome to miyagi!</b>
      </center>
      Miyagi is a minimalist web client for Canvas LMS. It's currently alpha-stage software, so please report any bugs you may find via the bug tracker or mailing list.<br>
      miyagi is a minimalist web client for Canvas LMS. it's currently very early alpha-stage software, so please report any bugs you may find via <a href="https://todo.sr.ht/~baka/miyagi">the bug tracker</a>. you may also provide feedback, suggestions, words of encouragement, or <strike>free labor</strike> contributions via <a href="https://lists.sr.ht/~baka/miyagi-discuss">the mailing l