feat: authenticate
This commit is contained in:
parent
52d184626d
commit
9a88ae0012
15 changed files with 785 additions and 68 deletions
61
.vscode/launch.json
vendored
Normal file
61
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'echoed'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=echoed"
|
||||
],
|
||||
"filter": {
|
||||
"name": "echoed",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'echoed'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=echoed",
|
||||
"--package=echoed"
|
||||
],
|
||||
"filter": {
|
||||
"name": "echoed",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'echoed'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=echoed",
|
||||
"--package=echoed"
|
||||
],
|
||||
"filter": {
|
||||
"name": "echoed",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
361
Cargo.lock
generated
361
Cargo.lock
generated
|
@ -250,10 +250,15 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.17"
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "echoed"
|
||||
|
@ -261,14 +266,15 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"color-eyre",
|
||||
"cookie_store",
|
||||
"crossterm",
|
||||
"dyn-clone",
|
||||
"email_address",
|
||||
"lazy_static",
|
||||
"paste",
|
||||
"ratatui",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest_cookie_store",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tui-textarea",
|
||||
"tui_confirm_dialog",
|
||||
|
@ -281,6 +287,15 @@ version = "1.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
|
@ -358,6 +373,21 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
|
@ -365,6 +395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -373,6 +404,23 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
|
@ -391,10 +439,15 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -568,6 +621,124 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_locid_transform_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locid_transform",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_provider_macros",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_macros"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
|
@ -588,6 +759,27 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
|
@ -668,6 +860,12 @@ version = "0.4.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
|
@ -1075,12 +1273,13 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "reqwest_cookie_store"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0b36498c7452f11b1833900f31fbb01fc46be20992a50269c88cf59d79f54e9"
|
||||
source = "git+https://git.newty.dev/newt/reqwest_cookie_store_tokio.git#aad0c697e209c6e79b15ff49b3bf0d05663492c3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cookie_store",
|
||||
"futures",
|
||||
"reqwest",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
@ -1327,6 +1526,12 @@ version = "0.9.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
|
@ -1381,6 +1586,17 @@ dependencies = [
|
|||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
|
@ -1415,6 +1631,26 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
|
@ -1458,6 +1694,16 @@ dependencies = [
|
|||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.0"
|
||||
|
@ -1475,9 +1721,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.41.0"
|
||||
version = "1.41.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
|
||||
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
@ -1674,15 +1920,27 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna 0.5.0",
|
||||
"idna 1.0.3",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
@ -1927,6 +2185,42 @@ version = "0.52.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
|
@ -1948,8 +2242,51 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -7,15 +7,16 @@ edition = "2021"
|
|||
[dependencies]
|
||||
async-trait = "0.1.83"
|
||||
color-eyre = "0.6.3"
|
||||
cookie_store = "0.21.0"
|
||||
crossterm = "0.28.1"
|
||||
dyn-clone = "1.0.17"
|
||||
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 = "0.8.0"
|
||||
tokio = { version = "1.41.0", features = ["macros", "rt-multi-thread"] }
|
||||
reqwest_cookie_store = { git = "https://git.newty.dev/newt/reqwest_cookie_store_tokio.git" }
|
||||
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.2"
|
||||
url = "2.5.3"
|
||||
|
|
5
Untitled-1.txt
Normal file
5
Untitled-1.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
https://content.echo360.org.uk/0000.<instid>/<media_id>/1/s1_av.m3u8
|
||||
|
||||
https://content.echo360.org.uk/0000.89dd0669-23e4-4b1c-b183-3e572d3428e5/f9dab8de-54bb-4081-8ddb-85dfa4819d87/1/s1_v.m3u8
|
||||
|
||||
https://content.echo360.org.uk/0000.89dd0669-23e4-4b1c-b183-3e572d3428e5/e232784c-6d82-4d19-af69-28feea79b0a8/1/s1_av.m3u8?x-ui
|
8
readme.md
Normal file
8
readme.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# echoed
|
||||
|
||||
## todo
|
||||
|
||||
- cookie persistence
|
||||
- browsing
|
||||
- downloading lectures
|
||||
- support for other echo360 instances
|
|
@ -1 +1,2 @@
|
|||
pub mod echo;
|
||||
pub mod tui;
|
||||
|
|
109
src/helper/echo.rs
Normal file
109
src/helper/echo.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use color_eyre::{eyre::eyre, Result};
|
||||
use echoed::{UserData, DEFAULT_ECHO360};
|
||||
use regex::Regex;
|
||||
use reqwest::multipart::Form;
|
||||
use reqwest_cookie_store::CookieStore;
|
||||
use std::sync::{MutexGuard, PoisonError};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::State;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthError {
|
||||
#[error("Failed to connect to echo360. Do you have an internet connection?")]
|
||||
Request(#[from] reqwest::Error),
|
||||
#[error("Failed to find {0}.")]
|
||||
Find(&'static str),
|
||||
#[error("Failed to parse regex.")]
|
||||
Regex(#[from] regex::Error),
|
||||
#[error("Invalid details.")]
|
||||
InvalidDetails,
|
||||
}
|
||||
|
||||
pub async fn authenticate(email: &str, password: &str, state: &mut State) -> Result<(), AuthError> {
|
||||
let base_url = DEFAULT_ECHO360.clone();
|
||||
let domain = base_url.domain().unwrap();
|
||||
|
||||
// get the csrf token
|
||||
state.client.get(base_url.clone()).send().await?;
|
||||
|
||||
let csrf_token = state
|
||||
.cookies
|
||||
.lock()
|
||||
.await
|
||||
.get(domain, "/", "PLAY_SESSION")
|
||||
.map(|cookie| {
|
||||
let value = cookie.value().to_string();
|
||||
value
|
||||
.split("&")
|
||||
.nth(1)
|
||||
.map(|part| part.split("=").last())
|
||||
.flatten()
|
||||
.map(|x| x.to_string())
|
||||
})
|
||||
.flatten()
|
||||
.ok_or(AuthError::Find("CSRF token"))?;
|
||||
|
||||
// authenticate
|
||||
state
|
||||
.client
|
||||
.post(format!("{base_url}login"))
|
||||
.query(&[("csrfToken", csrf_token)])
|
||||
.multipart(
|
||||
Form::new()
|
||||
.text("email", email.to_string())
|
||||
.text("password", password.to_string()),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// get institution id
|
||||
let insitution_id = state
|
||||
.cookies
|
||||
.lock()
|
||||
.await
|
||||
.get(domain, "/", "PLAY_SESSION")
|
||||
.map(|cookie| {
|
||||
let value = cookie.value().to_string();
|
||||
value
|
||||
.split("&")
|
||||
.nth(1)
|
||||
.map(|part| part.split("=").last())
|
||||
.flatten()
|
||||
.map(|x| x.to_string())
|
||||
})
|
||||
.flatten()
|
||||
.ok_or(AuthError::Find("Institution id"))?;
|
||||
|
||||
// get authenticated user's name
|
||||
let name = {
|
||||
let html = state
|
||||
.client
|
||||
.get(format!("{base_url}courses"))
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
let first_name = Regex::new(r#"\\"firstName\\":\\"(\w+)\\""#)?
|
||||
.captures(&html)
|
||||
.ok_or(AuthError::InvalidDetails)?
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
let last_name = Regex::new(r#"\\"lastName\\":\\"(\w+)\\""#)?
|
||||
.captures(&html)
|
||||
.ok_or(AuthError::InvalidDetails)?
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
format!("{} {}", first_name, last_name)
|
||||
};
|
||||
|
||||
// save to state
|
||||
state.echo_user = Some(UserData {
|
||||
name: name.into(),
|
||||
institution_id: insitution_id.into(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
widgets::{Block, BorderType, Borders, Padding},
|
||||
};
|
||||
|
||||
// Create a block with
|
||||
|
@ -8,13 +8,13 @@ pub fn border(frame: &mut Frame, title: Option<&str>) -> Rect {
|
|||
let outer = frame.area();
|
||||
let mut block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded);
|
||||
.border_type(BorderType::Rounded)
|
||||
.padding(Padding::proportional(1));
|
||||
|
||||
if let Some(title) = title {
|
||||
block = block.title(title);
|
||||
}
|
||||
|
||||
frame.render_widget(block.clone(), outer);
|
||||
|
||||
block.inner(outer)
|
||||
}
|
||||
|
|
15
src/lib.rs
Normal file
15
src/lib.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DEFAULT_ECHO360: Url = Url::parse("https://echo360.org.uk").unwrap();
|
||||
}
|
||||
|
||||
pub struct UserData {
|
||||
pub name: Cow<'static, str>,
|
||||
pub institution_id: Cow<'static, str>,
|
||||
}
|
55
src/main.rs
55
src/main.rs
|
@ -1,17 +1,20 @@
|
|||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use echoed::UserData;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{BorderType, Borders},
|
||||
};
|
||||
use reqwest::Client;
|
||||
use reqwest_cookie_store::CookieStoreMutex;
|
||||
use std::{
|
||||
io::Stdout,
|
||||
sync::{mpsc, Arc},
|
||||
sync::{mpsc as std_mpsc, Arc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tui_confirm_dialog::{ConfirmDialog, ConfirmDialogState, Listener, PopupMessage};
|
||||
use views::{ViewCommand, ViewContainer};
|
||||
use tui_confirm_dialog::{ConfirmDialog, ConfirmDialogState, Listener};
|
||||
use views::ViewContainer;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
|
@ -23,34 +26,45 @@ mod views;
|
|||
const EXIT_TIMEOUT: Duration = Duration::from_millis(150);
|
||||
const POLL_TIMEOUT: Duration = Duration::from_millis(16);
|
||||
|
||||
pub struct State {
|
||||
exit: ConfirmDialogState,
|
||||
exit_rx: mpsc::Receiver<Listener>,
|
||||
pub enum Command {
|
||||
ChangeView(ViewContainer),
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
let (exit_tx, exit_rx) = mpsc::channel::<Listener>();
|
||||
pub struct State {
|
||||
exit: ConfirmDialogState,
|
||||
exit_rx: std_mpsc::Receiver<Listener>,
|
||||
client: Client,
|
||||
cookies: Arc<CookieStoreMutex>,
|
||||
echo_user: Option<UserData>,
|
||||
}
|
||||
|
||||
Self {
|
||||
impl State {
|
||||
fn new() -> Result<Self> {
|
||||
let (exit_tx, exit_rx) = std_mpsc::channel::<Listener>();
|
||||
let jar = Arc::new(CookieStoreMutex::default());
|
||||
|
||||
Ok(Self {
|
||||
exit: ConfirmDialogState::default()
|
||||
.with_title("Exit")
|
||||
.with_text(Text::raw("Are you sure you would like to exit?"))
|
||||
.with_listener(Some(exit_tx)),
|
||||
exit_rx,
|
||||
// ..Default::default()
|
||||
}
|
||||
client: Client::builder().cookie_provider(jar.clone()).build()?,
|
||||
cookies: jar,
|
||||
echo_user: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
let mut view: ViewContainer = Arc::new(Mutex::new(views::Auth::default()));
|
||||
view.lock().await.setup();
|
||||
let mut state = State::default();
|
||||
let (view_tx, view_rx) = mpsc::channel::<ViewCommand>();
|
||||
let mut state = State::new()?;
|
||||
let (view_tx, view_rx) = std_mpsc::channel::<Command>();
|
||||
let mut exit_triggered = Instant::now();
|
||||
exit_triggered -= EXIT_TIMEOUT;
|
||||
|
||||
// tui
|
||||
loop {
|
||||
// check if the app should exit
|
||||
if let Ok((_, exit)) = state.exit_rx.try_recv() {
|
||||
|
@ -59,21 +73,21 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
// check for new view commands
|
||||
// check for new commands
|
||||
match view_rx.try_recv() {
|
||||
Ok(ViewCommand::Change(new_view)) => {
|
||||
Ok(Command::ChangeView(new_view)) => {
|
||||
view = new_view;
|
||||
view.lock().await.setup();
|
||||
}
|
||||
|
||||
Err(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// draw the view
|
||||
let mut view = view.lock().await;
|
||||
|
||||
terminal.draw(|frame| {
|
||||
view.draw(frame);
|
||||
view.draw(frame, &state);
|
||||
|
||||
if state.exit.is_opened() {
|
||||
frame.render_stateful_widget(
|
||||
|
@ -104,7 +118,7 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
|||
}
|
||||
|
||||
// then handle view keypresses
|
||||
view.keypress(key, &view_tx).await?;
|
||||
view.keypress(key, &mut state, &view_tx).await?;
|
||||
|
||||
// then handle global keypresses
|
||||
|
||||
|
@ -115,7 +129,8 @@ async fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
|||
&& !state.exit.is_opened()
|
||||
&& exit_triggered.elapsed() >= EXIT_TIMEOUT
|
||||
{
|
||||
state.exit = state.exit.open();
|
||||
let exit = std::mem::take(&mut state.exit);
|
||||
state.exit = exit.open();
|
||||
exit_triggered = Instant::now();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
pub use crate::{helper::tui as helper, views::ViewCommand};
|
||||
pub use crate::helper::tui as helper;
|
||||
pub use crate::views::View;
|
||||
pub use crate::{change_view, Command, State};
|
||||
pub use color_eyre::Result;
|
||||
pub use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
pub use ratatui::prelude::*;
|
||||
pub use std::any::Any;
|
||||
|
||||
pub type ViewSender = std::sync::mpsc::Sender<ViewCommand>;
|
||||
pub type ViewSender = std::sync::mpsc::Sender<Command>;
|
||||
|
|
29
src/views.rs
29
src/views.rs
|
@ -1,5 +1,4 @@
|
|||
use crate::prelude::*;
|
||||
use dyn_clone::DynClone;
|
||||
use crate::{prelude::*, State};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
|
@ -14,17 +13,29 @@ macro_rules! import_view {
|
|||
};
|
||||
}
|
||||
|
||||
import_view!(Auth);
|
||||
import_view!(Auth, Home);
|
||||
|
||||
#[async_trait]
|
||||
pub trait View: DynClone {
|
||||
fn setup(&mut self);
|
||||
fn draw(&self, frame: &mut Frame);
|
||||
async fn keypress(&mut self, key: KeyEvent, command_tx: &ViewSender) -> Result<()>;
|
||||
pub trait View {
|
||||
fn setup(&mut self) {}
|
||||
fn draw(&self, frame: &mut Frame, state: &State);
|
||||
async fn keypress(
|
||||
&mut self,
|
||||
key: KeyEvent,
|
||||
state: &mut State,
|
||||
command_tx: &ViewSender,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type ViewContainer = Arc<Mutex<dyn View + Send + Sync>>;
|
||||
|
||||
pub enum ViewCommand {
|
||||
Change(ViewContainer),
|
||||
#[macro_export]
|
||||
macro_rules! change_view {
|
||||
($tx:expr, $view:ident) => {
|
||||
$tx.send(Command::ChangeView(std::sync::Arc::new(
|
||||
tokio::sync::Mutex::new(super::$view),
|
||||
)))?
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use super::View;
|
||||
use crate::prelude::*;
|
||||
use crate::{helper::echo, prelude::*};
|
||||
use echoed::DEFAULT_ECHO360;
|
||||
use email_address::EmailAddress;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use std::borrow::Cow;
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
const SHOW_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
|
||||
|
@ -10,6 +13,7 @@ pub struct Auth<'t> {
|
|||
email: TextArea<'t>,
|
||||
password: TextArea<'t>,
|
||||
pass_select: bool,
|
||||
error: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -19,26 +23,50 @@ impl<'t> View for Auth<'t> {
|
|||
self.password.set_cursor_style(HIDE_STYLE);
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let area = helper::border(frame, Some("Auth"));
|
||||
let field_areas =
|
||||
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area);
|
||||
fn draw(&self, frame: &mut Frame, _: &State) {
|
||||
let area = helper::border(frame, Some("Login"));
|
||||
let areas = Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
for ((mut field, label), area) in [(&self.email, "Email"), (&self.password, "Password")]
|
||||
.iter()
|
||||
.zip(field_areas.iter())
|
||||
{
|
||||
let [label_area, field_area] =
|
||||
Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.areas(*area);
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![
|
||||
Line::raw(format!(
|
||||
"Please enter your {} details",
|
||||
DEFAULT_ECHO360.domain().unwrap()
|
||||
))
|
||||
.bold(),
|
||||
Line::raw("Hit enter to submit."),
|
||||
]),
|
||||
areas[0],
|
||||
);
|
||||
|
||||
frame.render_widget(Text::raw(*label), label_area);
|
||||
frame.render_widget(field, field_area);
|
||||
// render email
|
||||
let [label_area, field_area] =
|
||||
Layout::horizontal([Constraint::Length(10), Constraint::Fill(1)]).areas(areas[1]);
|
||||
frame.render_widget(Text::raw("Email:"), label_area);
|
||||
frame.render_widget(&self.email, field_area);
|
||||
|
||||
// render password
|
||||
let [label_area, field_area] =
|
||||
Layout::horizontal([Constraint::Length(10), Constraint::Fill(1)]).areas(areas[2]);
|
||||
frame.render_widget(Text::raw("Password:"), label_area);
|
||||
frame.render_widget(&self.password, field_area);
|
||||
|
||||
// render error
|
||||
if let Some(error) = &self.error {
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![Line::raw(""), Line::raw(error.clone()).fg(Color::Red)]),
|
||||
areas[3],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn keypress(&mut self, key: KeyEvent, _command_tx: &ViewSender) -> Result<()> {
|
||||
async fn keypress(&mut self, key: KeyEvent, state: &mut State, tx: &ViewSender) -> Result<()> {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
self.pass_select = false;
|
||||
|
@ -57,8 +85,49 @@ impl<'t> View for Auth<'t> {
|
|||
&mut self.email
|
||||
};
|
||||
|
||||
self.error = None;
|
||||
field.input(key);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// validate email
|
||||
let email = self.email.lines().concat();
|
||||
|
||||
if email.is_empty() {
|
||||
self.error = Some("Email is required".into());
|
||||
return Ok(());
|
||||
} else if !EmailAddress::is_valid(&email) {
|
||||
self.error = Some("Invalid email address".into());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// validate password
|
||||
let password = self.password.lines().concat();
|
||||
|
||||
if password.is_empty() {
|
||||
self.error = Some("Password is required".into());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// attempt to authenticate
|
||||
let email = self.email.lines().concat();
|
||||
let password = self.password.lines().concat();
|
||||
|
||||
match echo::authenticate(
|
||||
Box::leak(email.into_boxed_str()),
|
||||
Box::leak(password.into_boxed_str()),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
change_view!(tx, Home);
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(e.to_string().into());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
|
|
22
src/views/home.rs
Normal file
22
src/views/home.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
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(())
|
||||
}
|
||||
}
|
61
src/views/quit.rs
Normal file
61
src/views/quit.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
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