feat: get courses
This commit is contained in:
parent
3cbc26e11c
commit
9d247c76d5
21 changed files with 2604 additions and 255 deletions
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[submodule "crates/tui-confirm-dialog"]
|
||||||
|
path = crates/tui-confirm-dialog
|
||||||
|
url = https://git.newty.dev/newt/tui-confirm-dialog-tokio
|
||||||
|
[submodule "crates/reqwest_cookie_store_tokio"]
|
||||||
|
path = crates/reqwest_cookie_store_tokio
|
||||||
|
url = https://git.newty.dev/newt/reqwest_cookie_store_tokio
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -1,3 +1,5 @@
|
||||||
{
|
{
|
||||||
"rust-analyzer.cargo.features": "all"
|
"files.exclude": {
|
||||||
|
"target": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
134
Cargo.lock
generated
134
Cargo.lock
generated
|
@ -28,9 +28,31 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.18"
|
version = "0.2.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
|
@ -117,9 +139,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.1.31"
|
version = "1.1.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
|
checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
@ -241,6 +263,16 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "4.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"num_cpus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
@ -279,6 +311,7 @@ dependencies = [
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"email_address",
|
"email_address",
|
||||||
|
"ijson",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"paste",
|
"paste",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
@ -345,9 +378,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
|
@ -506,9 +539,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.0"
|
version = "0.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
"equivalent",
|
"equivalent",
|
||||||
|
@ -788,6 +821,18 @@ dependencies = [
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ijson"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b96214564d1f12875bd9661b183d8494dd10e373cb693629536fe2f3125e254b"
|
||||||
|
dependencies = [
|
||||||
|
"dashmap",
|
||||||
|
"lazy_static",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indenter"
|
name = "indenter"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -858,9 +903,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.161"
|
version = "0.2.162"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
|
@ -972,6 +1017,16 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_threads"
|
name = "num_threads"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
@ -1223,9 +1278,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1287,13 +1342,15 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest_cookie_store"
|
name = "reqwest_cookie_store"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
source = "git+https://git.newty.dev/newt/reqwest_cookie_store_tokio.git#aad0c697e209c6e79b15ff49b3bf0d05663492c3"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
"futures",
|
"futures",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1320,9 +1377,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.38"
|
version = "0.38.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
|
checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
|
@ -1412,9 +1469,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework-sys"
|
name = "security-framework-sys"
|
||||||
version = "2.12.0"
|
version = "2.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
|
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1592,9 +1649,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.86"
|
version = "2.0.87"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c"
|
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1644,9 +1701,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.13.0"
|
version = "3.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
|
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
@ -1657,18 +1714,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.66"
|
version = "1.0.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.66"
|
version = "1.0.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5"
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1791,6 +1848,30 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-test"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.12"
|
version = "0.7.12"
|
||||||
|
@ -1871,13 +1952,12 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tui_confirm_dialog"
|
name = "tui_confirm_dialog"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "117929c8e294f8ceea4f56f4d09a6a4e694f057d175f2cf61433309fb01e9a94"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"rand",
|
"rand",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"regex",
|
"regex",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
28
Cargo.toml
28
Cargo.toml
|
@ -1,24 +1,4 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "echoed"
|
resolver = "2"
|
||||||
publish = false
|
default-members = ["crates/echoed"]
|
||||||
version = "0.1.0"
|
members = ["crates/*"]
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-trait = "0.1.83"
|
|
||||||
color-eyre = "0.6.3"
|
|
||||||
cookie_store = "0.21.1"
|
|
||||||
crossterm = "0.28.1"
|
|
||||||
email_address = "0.2.9"
|
|
||||||
lazy_static = "1.5.0"
|
|
||||||
paste = "1.0.15"
|
|
||||||
ratatui = "0.29.0"
|
|
||||||
regex = "1.11.1"
|
|
||||||
reqwest = { version = "0.12.9", features = ["cookies", "multipart"] }
|
|
||||||
reqwest_cookie_store = { git = "https://git.newty.dev/newt/reqwest_cookie_store_tokio.git" }
|
|
||||||
serde_cbor = "0.11.2"
|
|
||||||
thiserror = "1.0.66"
|
|
||||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }
|
|
||||||
tui-textarea = "0.7.0"
|
|
||||||
tui_confirm_dialog = "0.2.4"
|
|
||||||
url = "2.5.3"
|
|
||||||
|
|
2349
crates/echoed/Cargo.lock
generated
Normal file
2349
crates/echoed/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
crates/echoed/Cargo.toml
Normal file
25
crates/echoed/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "echoed"
|
||||||
|
publish = false
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.83"
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
cookie_store = "0.21.1"
|
||||||
|
crossterm = "0.28.1"
|
||||||
|
email_address = "0.2.9"
|
||||||
|
ijson = "0.1.3"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
paste = "1.0.15"
|
||||||
|
ratatui = "0.29.0"
|
||||||
|
regex = "1.11.1"
|
||||||
|
reqwest = { version = "0.12.9", features = ["cookies", "json", "multipart"] }
|
||||||
|
reqwest_cookie_store = { path = "../reqwest_cookie_store_tokio" }
|
||||||
|
serde_cbor = "0.11.2"
|
||||||
|
thiserror = "1.0.66"
|
||||||
|
tokio = { version = "1.41.1", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||||
|
tui-textarea = "0.7.0"
|
||||||
|
tui_confirm_dialog = { path = "../tui-confirm-dialog" }
|
||||||
|
url = "2.5.3"
|
|
@ -2,7 +2,6 @@ use color_eyre::Result;
|
||||||
use echoed::{UserData, DEFAULT_ECHO360};
|
use echoed::{UserData, DEFAULT_ECHO360};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::multipart::Form;
|
use reqwest::multipart::Form;
|
||||||
use reqwest_cookie_store::CookieStoreMutex;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::State;
|
use crate::State;
|
|
@ -2,6 +2,7 @@ use color_eyre::Result;
|
||||||
use cookie_store::{Cookie, CookieDomain, CookieStore};
|
use cookie_store::{Cookie, CookieDomain, CookieStore};
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use echoed::{UserData, DEFAULT_ECHO360};
|
use echoed::{UserData, DEFAULT_ECHO360};
|
||||||
|
use helper::echo;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
widgets::{BorderType, Borders},
|
widgets::{BorderType, Borders},
|
||||||
|
@ -9,11 +10,12 @@ use ratatui::{
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use reqwest_cookie_store::CookieStoreMutex;
|
use reqwest_cookie_store::CookieStoreMutex;
|
||||||
use std::{
|
use std::{
|
||||||
|
env::var,
|
||||||
io::{Stdout, Write},
|
io::{Stdout, Write},
|
||||||
sync::{mpsc as std_mpsc, Arc},
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tui_confirm_dialog::{ConfirmDialog, ConfirmDialogState, Listener};
|
use tui_confirm_dialog::{ConfirmDialog, ConfirmDialogState, Listener};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use views::ViewContainer;
|
use views::ViewContainer;
|
||||||
|
@ -34,7 +36,7 @@ pub enum Command {
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
exit: ConfirmDialogState,
|
exit: ConfirmDialogState,
|
||||||
exit_rx: std_mpsc::Receiver<Listener>,
|
exit_rx: mpsc::Receiver<Listener>,
|
||||||
client: Client,
|
client: Client,
|
||||||
cookies: Arc<CookieStoreMutex>,
|
cookies: Arc<CookieStoreMutex>,
|
||||||
echo_user: Option<UserData>,
|
echo_user: Option<UserData>,
|
||||||
|
@ -42,7 +44,7 @@ pub struct State {
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
fn new(jar: CookieStoreMutex) -> Result<Self> {
|
fn new(jar: CookieStoreMutex) -> Result<Self> {
|
||||||
let (exit_tx, exit_rx) = std_mpsc::channel::<Listener>();
|
let (exit_tx, exit_rx) = mpsc::channel::<Listener>(1);
|
||||||
let jar = Arc::new(jar);
|
let jar = Arc::new(jar);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -83,16 +85,27 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||||
|
|
||||||
// check if the cookies provide authentication
|
// check if the cookies provide authentication
|
||||||
let mut state = State::new(jar)?;
|
let mut state = State::new(jar)?;
|
||||||
let authenticated = helper::echo::check_auth(&mut state).await?;
|
let mut authenticated = echo::check_auth(&mut state).await?;
|
||||||
|
|
||||||
|
// authenticate if env variables are provided
|
||||||
|
if !authenticated {
|
||||||
|
if let Ok((email, password)) = var("ECHO360_EMAIL")
|
||||||
|
.and_then(|email| var("ECHO360_PASSWORD").map(|password| (email, password)))
|
||||||
|
{
|
||||||
|
authenticated = echo::authenticate(&email, &password, &mut state)
|
||||||
|
.await
|
||||||
|
.is_ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut view: ViewContainer = if authenticated {
|
let mut view: ViewContainer = if authenticated {
|
||||||
Arc::new(Mutex::new(views::Home::default())) as ViewContainer
|
Arc::new(Mutex::new(views::Courses::default())) as ViewContainer
|
||||||
} else {
|
} else {
|
||||||
Arc::new(Mutex::new(views::Auth::default())) as ViewContainer
|
Arc::new(Mutex::new(views::Auth::default())) as ViewContainer
|
||||||
};
|
};
|
||||||
|
|
||||||
view.lock().await.setup();
|
view.lock().await.setup(&state).await?;
|
||||||
let (view_tx, view_rx) = std_mpsc::channel::<Command>();
|
let (view_tx, mut view_rx) = mpsc::channel::<Command>(1);
|
||||||
let mut exit_triggered = Instant::now();
|
let mut exit_triggered = Instant::now();
|
||||||
exit_triggered -= EXIT_TIMEOUT;
|
exit_triggered -= EXIT_TIMEOUT;
|
||||||
|
|
||||||
|
@ -100,6 +113,7 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
// check if the app should exit
|
// check if the app should exit
|
||||||
if let Ok((_, exit)) = state.exit_rx.try_recv() {
|
if let Ok((_, exit)) = state.exit_rx.try_recv() {
|
||||||
|
println!("{:?}", exit);
|
||||||
if exit.unwrap_or_default() {
|
if exit.unwrap_or_default() {
|
||||||
// save cookies
|
// save cookies
|
||||||
// todo: decide on a place to store this
|
// todo: decide on a place to store this
|
||||||
|
@ -116,7 +130,7 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||||
match view_rx.try_recv() {
|
match view_rx.try_recv() {
|
||||||
Ok(Command::ChangeView(new_view)) => {
|
Ok(Command::ChangeView(new_view)) => {
|
||||||
view = new_view;
|
view = new_view;
|
||||||
view.lock().await.setup();
|
view.lock().await.setup(&state).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -149,7 +163,7 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||||
if key.code == KeyCode::Esc && exit_triggered.elapsed() < EXIT_TIMEOUT {
|
if key.code == KeyCode::Esc && exit_triggered.elapsed() < EXIT_TIMEOUT {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
state.exit.handle(key);
|
state.exit.handle(key).await;
|
||||||
exit_triggered = Instant::now();
|
exit_triggered = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,4 @@ pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
pub use ratatui::prelude::*;
|
pub use ratatui::prelude::*;
|
||||||
pub use std::any::Any;
|
pub use std::any::Any;
|
||||||
|
|
||||||
pub type ViewSender = std::sync::mpsc::Sender<Command>;
|
pub type ViewSender = tokio::sync::mpsc::Sender<Command>;
|
|
@ -13,17 +13,19 @@ macro_rules! import_view {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
import_view!(Auth, Home);
|
import_view!(Auth, Courses);
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait View {
|
pub trait View {
|
||||||
fn setup(&mut self) {}
|
async fn setup(&mut self, _state: &State) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
fn draw(&self, frame: &mut Frame, state: &State);
|
fn draw(&self, frame: &mut Frame, state: &State);
|
||||||
async fn keypress(
|
async fn keypress(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: KeyEvent,
|
_key: KeyEvent,
|
||||||
state: &mut State,
|
_state: &mut State,
|
||||||
command_tx: &ViewSender,
|
_command_tx: &ViewSender,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -35,7 +37,8 @@ pub type ViewContainer = Arc<Mutex<dyn View + Send + Sync>>;
|
||||||
macro_rules! change_view {
|
macro_rules! change_view {
|
||||||
($tx:expr, $view:ident) => {
|
($tx:expr, $view:ident) => {
|
||||||
$tx.send(Command::ChangeView(std::sync::Arc::new(
|
$tx.send(Command::ChangeView(std::sync::Arc::new(
|
||||||
tokio::sync::Mutex::new(super::$view),
|
tokio::sync::Mutex::new(super::$view::default()),
|
||||||
)))?
|
)))
|
||||||
|
.await?
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -18,9 +18,10 @@ pub struct Auth<'t> {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<'t> View for Auth<'t> {
|
impl<'t> View for Auth<'t> {
|
||||||
fn setup(&mut self) {
|
async fn setup(&mut self, _: &State) -> Result<()> {
|
||||||
self.password.set_mask_char('\u{2022}');
|
self.password.set_mask_char('\u{2022}');
|
||||||
self.password.set_cursor_style(HIDE_STYLE);
|
self.password.set_cursor_style(HIDE_STYLE);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame, _: &State) {
|
fn draw(&self, frame: &mut Frame, _: &State) {
|
||||||
|
@ -120,7 +121,7 @@ impl<'t> View for Auth<'t> {
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
change_view!(tx, Home);
|
change_view!(tx, Courses);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.error = Some(e.to_string().into());
|
self.error = Some(e.to_string().into());
|
70
crates/echoed/src/views/courses.rs
Normal file
70
crates/echoed/src/views/courses.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use color_eyre::eyre::eyre;
|
||||||
|
use echoed::DEFAULT_ECHO360;
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Course {
|
||||||
|
name: Cow<'static, str>,
|
||||||
|
code: Cow<'static, str>,
|
||||||
|
section_id: Cow<'static, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Courses {
|
||||||
|
courses: Vec<Course>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl View for Courses {
|
||||||
|
async fn setup(&mut self, state: &State) -> Result<()> {
|
||||||
|
let res = state
|
||||||
|
.client
|
||||||
|
.get(format!("{}user/enrollments", DEFAULT_ECHO360.clone()))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<ijson::IValue>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let enrollments = res["data"]
|
||||||
|
.as_array()
|
||||||
|
.map(|data| data[0]["userSections"].as_array())
|
||||||
|
.flatten()
|
||||||
|
.ok_or(eyre!("No courses found"))?
|
||||||
|
.iter()
|
||||||
|
.map(|course| {
|
||||||
|
course["sectionId"].as_string().and_then(|section_id| {
|
||||||
|
course["sectionName"].as_string().and_then(|code| {
|
||||||
|
course["courseName"].as_string().map(|name| Course {
|
||||||
|
name: name.to_string().into(),
|
||||||
|
code: code.to_string().into(),
|
||||||
|
section_id: section_id.to_string().into(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Option<Vec<_>>>()
|
||||||
|
.ok_or(eyre!("No courses found"))?;
|
||||||
|
|
||||||
|
self.courses = enrollments;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut Frame, state: &State) {
|
||||||
|
// let user = state.echo_user.as_ref().unwrap();
|
||||||
|
if let Some(x) = self.courses.first() {
|
||||||
|
frame.render_widget(Line::raw(x.name.to_string()), frame.area());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn keypress(
|
||||||
|
&mut self,
|
||||||
|
key: KeyEvent,
|
||||||
|
state: &mut State,
|
||||||
|
_command_tx: &ViewSender,
|
||||||
|
) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
1
crates/reqwest_cookie_store_tokio
Submodule
1
crates/reqwest_cookie_store_tokio
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit aad0c697e209c6e79b15ff49b3bf0d05663492c3
|
1
crates/tui-confirm-dialog
Submodule
1
crates/tui-confirm-dialog
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 5c1156b1cd6ed765d4d06be82f8c027043aa0258
|
99
old.rs
99
old.rs
|
@ -1,99 +0,0 @@
|
||||||
use color_eyre::eyre::eyre;
|
|
||||||
use color_eyre::Result;
|
|
||||||
use regex::Regex;
|
|
||||||
use reqwest::multipart::Form;
|
|
||||||
use reqwest::{Client, Url};
|
|
||||||
use reqwest_cookie_store::CookieStoreMutex;
|
|
||||||
use std::env;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
const DEFAULT_BASE_URL: &str = "https://echo360.org.uk";
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
color_eyre::install()?;
|
|
||||||
#[cfg(feature = "dotenvy")]
|
|
||||||
dotenvy::dotenv()?;
|
|
||||||
|
|
||||||
let jar = Arc::new(CookieStoreMutex::default());
|
|
||||||
let client = Client::builder().cookie_provider(jar.clone()).build()?;
|
|
||||||
let base_url = Url::parse(DEFAULT_BASE_URL)?;
|
|
||||||
let domain = base_url.domain().unwrap();
|
|
||||||
|
|
||||||
// get the csrf token
|
|
||||||
client.get(base_url.clone()).send().await?;
|
|
||||||
|
|
||||||
let csrf_token = jar
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.get(domain, "/", "PLAY_SESSION")
|
|
||||||
.map(|cookie| {
|
|
||||||
let cookie_value = cookie.value().to_string();
|
|
||||||
cookie_value
|
|
||||||
.split("&")
|
|
||||||
.nth(1)
|
|
||||||
.map(|part| part.split("=").last())
|
|
||||||
.flatten()
|
|
||||||
.map(|x| x.to_string())
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.ok_or(eyre!("No csrf token found"))?;
|
|
||||||
|
|
||||||
// authenticate
|
|
||||||
client
|
|
||||||
.post(format!("{base_url}login"))
|
|
||||||
.query(&[("csrfToken", csrf_token)])
|
|
||||||
.multipart(
|
|
||||||
Form::new()
|
|
||||||
.text("email", env::var("ECHO360_EMAIL")?)
|
|
||||||
.text("password", env::var("ECHO360_PASSWORD")?),
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// get institution id
|
|
||||||
let insitution_id = jar
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.get(domain, "/", "PLAY_SESSION")
|
|
||||||
.map(|cookie| {
|
|
||||||
let cookie_value = cookie.value().to_string();
|
|
||||||
cookie_value
|
|
||||||
.split("&")
|
|
||||||
.nth(1)
|
|
||||||
.map(|part| part.split("=").last())
|
|
||||||
.flatten()
|
|
||||||
.map(|x| x.to_string())
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.ok_or(eyre!("No institution id found"))?;
|
|
||||||
|
|
||||||
println!("Institution ID: {}", insitution_id);
|
|
||||||
|
|
||||||
// get authenticated user's name
|
|
||||||
let name = {
|
|
||||||
let html = client
|
|
||||||
.get(format!("{base_url}courses"))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
let first_name = Regex::new(r#"\\"firstName\\":\\"(\w+)\\""#)?
|
|
||||||
.captures(&html)
|
|
||||||
.unwrap()
|
|
||||||
.get(1)
|
|
||||||
.unwrap()
|
|
||||||
.as_str();
|
|
||||||
let last_name = Regex::new(r#"\\"lastName\\":\\"(\w+)\\""#)?
|
|
||||||
.captures(&html)
|
|
||||||
.unwrap()
|
|
||||||
.get(1)
|
|
||||||
.unwrap()
|
|
||||||
.as_str();
|
|
||||||
format!("{} {}", first_name, last_name)
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("Name: {}", name);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Home;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl View for Home {
|
|
||||||
fn draw(&self, frame: &mut Frame, state: &State) {
|
|
||||||
let user = state.echo_user.as_ref().unwrap();
|
|
||||||
|
|
||||||
frame.render_widget(Line::raw(user.name.to_string()), frame.area());
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn keypress(
|
|
||||||
&mut self,
|
|
||||||
key: KeyEvent,
|
|
||||||
state: &mut State,
|
|
||||||
_command_tx: &ViewSender,
|
|
||||||
) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
use super::{View, ViewContainer};
|
|
||||||
use crate::prelude::*;
|
|
||||||
use ratatui::widgets::{Block, Borders};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Quit {
|
|
||||||
previous: ViewContainer,
|
|
||||||
yes: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Quit {
|
|
||||||
pub fn new(previous: ViewContainer) -> Self {
|
|
||||||
Self {
|
|
||||||
previous,
|
|
||||||
yes: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl View for Quit {
|
|
||||||
fn draw(&self, frame: &mut Frame) {
|
|
||||||
let area = helper::border(frame, None);
|
|
||||||
|
|
||||||
// split vertically
|
|
||||||
let [title_area, buttons_area] =
|
|
||||||
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(area);
|
|
||||||
|
|
||||||
// render title
|
|
||||||
frame.render_widget(
|
|
||||||
Text::raw("Are you sure you would like to quit?").centered(),
|
|
||||||
title_area,
|
|
||||||
);
|
|
||||||
|
|
||||||
// render buttons
|
|
||||||
let [yes_btn_area, no_btn_area] =
|
|
||||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
||||||
.areas(buttons_area);
|
|
||||||
|
|
||||||
let yes_btn = Text::raw("Yes");
|
|
||||||
let no_btn = Text::raw("No");
|
|
||||||
|
|
||||||
frame.render_widget(yes_btn, yes_btn_area);
|
|
||||||
frame.render_widget(no_btn, no_btn_area);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn keypress(&mut self, key: KeyEvent, tx: &ViewSender) -> Result<()> {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Esc => {
|
|
||||||
tx.send(ViewCommand::Change(self.previous.clone())).await?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue