Migrate HTTP API from Actix to Axum
This commit is contained in:
parent
e0293f9f94
commit
e146add4b2
16 changed files with 1262 additions and 2069 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -15,7 +15,7 @@
|
|||
},
|
||||
"args": ["--desktop"],
|
||||
"env": {
|
||||
"RUST_LOG": "trace,actix_web=off,aws_sigv4=off,aws_smithy_checksums=off,actix_http=off,mio=off,reqwest=off,aws_runtime=off,aws_smithy_http_client=off,rustls=off,actix_server=off,hyper_util=off,aws_smithy_runtime=off,aws_smithy_runtime_api=off,tracing=off,aws_sdk_s3=off"
|
||||
"RUST_LOG": "trace,aws_sigv4=off,aws_smithy_checksums=off,mio=off,reqwest=off,aws_runtime=off,aws_smithy_http_client=off,rustls=off,hyper_util=off,aws_smithy_runtime=off,aws_smithy_runtime_api=off,tracing=off,aws_sdk_s3=off"
|
||||
|
||||
},
|
||||
"cwd": "${workspaceFolder}"
|
||||
|
|
|
|||
517
Cargo.lock
generated
517
Cargo.lock
generated
|
|
@ -2,279 +2,6 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "actix-codec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-cors"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
|
||||
dependencies = [
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"derive_more 2.0.1",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-files"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c0d87f10d70e2948ad40e8edea79c8e77c6c66e0250a4c1f09b690465199576"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"derive_more 2.0.1",
|
||||
"futures-core",
|
||||
"http-range",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"v_htmlescape",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"brotli",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more 2.0.1",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"local-channel",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"zstd 0.13.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-macros"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-multipart"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53"
|
||||
dependencies = [
|
||||
"actix-multipart-derive",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"derive_more 0.99.20",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"httparse",
|
||||
"local-waker",
|
||||
"log",
|
||||
"memchr",
|
||||
"mime",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_plain",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-multipart-derive"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"parse-size",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
|
||||
dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"http 0.2.12",
|
||||
"regex",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-rt"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-server"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"mio",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-service"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-utils"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
|
||||
dependencies = [
|
||||
"local-waker",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2233f53f6cb18ae038ce1f0713ca0c72ca0c4b71fe9aaeb59924ce2c89c6dd85"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
"actix-macros",
|
||||
"actix-router",
|
||||
"actix-rt",
|
||||
"actix-server",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"actix-web-codegen",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"cookie 0.16.2",
|
||||
"derive_more 2.0.1",
|
||||
"encoding_rs",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"impl-more",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"regex",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2 0.6.1",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web-codegen"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
|
||||
dependencies = [
|
||||
"actix-router",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-ws"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
"bytestring",
|
||||
"futures-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
|
|
@ -1157,14 +884,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-core 0.4.5",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"matchit 0.7.3",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
|
|
@ -1177,6 +904,44 @@ dependencies = [
|
|||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
|
||||
dependencies = [
|
||||
"axum-core 0.5.5",
|
||||
"axum-macros",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit 0.8.4",
|
||||
"memchr",
|
||||
"mime",
|
||||
"multer",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.28.0",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
|
|
@ -1197,6 +962,36 @@ dependencies = [
|
|||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
|
|
@ -1353,11 +1148,6 @@ dependencies = [
|
|||
name = "botserver"
|
||||
version = "6.0.8"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-files",
|
||||
"actix-multipart",
|
||||
"actix-web",
|
||||
"actix-ws",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
|
@ -1366,6 +1156,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"aws-config",
|
||||
"aws-sdk-s3",
|
||||
"axum 0.8.7",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
|
|
@ -1381,6 +1172,7 @@ dependencies = [
|
|||
"futures",
|
||||
"futures-util",
|
||||
"hmac",
|
||||
"hyper 1.8.1",
|
||||
"imap",
|
||||
"include_dir",
|
||||
"indicatif",
|
||||
|
|
@ -1415,6 +1207,8 @@ dependencies = [
|
|||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower 0.5.2",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ureq",
|
||||
|
|
@ -1493,15 +1287,6 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.4.4"
|
||||
|
|
@ -1934,17 +1719,6 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
|
|
@ -2464,7 +2238,6 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3691,10 +3464,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range"
|
||||
version = "0.1.5"
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
|
|
@ -4012,12 +3785,6 @@ dependencies = [
|
|||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "impl-more"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
|
|
@ -4350,12 +4117,6 @@ dependencies = [
|
|||
"selectors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language-tags"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
|
@ -4558,7 +4319,7 @@ dependencies = [
|
|||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.20.1",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
|
@ -4590,23 +4351,6 @@ dependencies = [
|
|||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "local-channel"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"local-waker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "local-waker"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
|
|
@ -4749,6 +4493,12 @@ version = "0.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
|
|
@ -4884,6 +4634,23 @@ dependencies = [
|
|||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"memchr",
|
||||
"mime",
|
||||
"spin",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.1"
|
||||
|
|
@ -5562,12 +5329,6 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-size"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.2"
|
||||
|
|
@ -7100,12 +6861,14 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_plain"
|
||||
version = "1.0.2"
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7410,6 +7173,12 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.6.0"
|
||||
|
|
@ -7681,7 +7450,7 @@ checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cookie 0.18.1",
|
||||
"cookie",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
|
|
@ -7872,7 +7641,7 @@ version = "2.9.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926"
|
||||
dependencies = [
|
||||
"cookie 0.18.1",
|
||||
"cookie",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http 1.3.1",
|
||||
|
|
@ -8221,7 +7990,19 @@ dependencies = [
|
|||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
"tungstenite 0.20.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite 0.28.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8341,7 +8122,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
|
|||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum 0.7.9",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"flate2",
|
||||
|
|
@ -8400,6 +8181,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8410,14 +8192,24 @@ checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
|||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"iri-string",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8557,6 +8349,23 @@ dependencies = [
|
|||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"thiserror 2.0.17",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "type1-encoding-parser"
|
||||
version = "0.1.0"
|
||||
|
|
@ -8692,12 +8501,6 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unit-prefix"
|
||||
version = "0.5.2"
|
||||
|
|
@ -8809,12 +8612,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_htmlescape"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
|
@ -9683,7 +9480,7 @@ checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
|
|||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2 0.6.2",
|
||||
"cookie 0.18.1",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dpi",
|
||||
|
|
|
|||
19
Cargo.toml
19
Cargo.toml
|
|
@ -3,8 +3,8 @@ name = "botserver"
|
|||
version = "6.0.8"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Pragmatismo.com.br <contact@pragmatismo.com.br>",
|
||||
"General Bots Community <https://github.com/GeneralBots>",
|
||||
"Pragmatismo.com.br ",
|
||||
"General Bots Community ",
|
||||
"Alan Perdomo",
|
||||
"Ana Paula Gil",
|
||||
"Arenas.io",
|
||||
|
|
@ -26,7 +26,7 @@ authors = [
|
|||
"PH Nascimento",
|
||||
"Phpussente",
|
||||
"Robson Dantas",
|
||||
"Rodrigo Rodriguez <me@rodrigorodriguez.com>",
|
||||
"Rodrigo Rodriguez ",
|
||||
"Sarah Lourenco",
|
||||
"Thi Patriota",
|
||||
"Webgus",
|
||||
|
|
@ -38,18 +38,11 @@ repository = "https://github.com/GeneralBots/BotServer"
|
|||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
|
||||
vectordb = ["qdrant-client"]
|
||||
email = ["imap"]
|
||||
desktop = ["dep:tauri", "dep:tauri-plugin-dialog", "dep:tauri-plugin-opener"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
actix-cors = "0.7"
|
||||
actix-files = "0.6.8"
|
||||
actix-multipart = "0.7"
|
||||
actix-web = "4.9"
|
||||
actix-ws = "0.3"
|
||||
aes-gcm = "0.10"
|
||||
anyhow = "1.0"
|
||||
argon2 = "0.5"
|
||||
|
|
@ -58,6 +51,7 @@ async-stream = "0.3"
|
|||
async-trait = "0.1"
|
||||
aws-config = "1.8.8"
|
||||
aws-sdk-s3 = { version = "1.109.0", features = ["behavior-version-latest"] }
|
||||
axum = { version = "0.8.7", features = ["ws", "multipart", "macros"] }
|
||||
base64 = "0.22"
|
||||
bytes = "1.8"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
|
@ -73,6 +67,7 @@ env_logger = "0.11"
|
|||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
hmac = "0.12.1"
|
||||
hyper = { version = "1.8.1", features = ["full"] }
|
||||
imap = { version = "3.0.0-alpha.15", optional = true }
|
||||
include_dir = "0.7"
|
||||
indicatif = "0.18.0"
|
||||
|
|
@ -106,6 +101,8 @@ tempfile = "3"
|
|||
time = "0.3.44"
|
||||
tokio = { version = "1.41", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "fs", "trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||
ureq = "3.1.2"
|
||||
|
|
@ -116,8 +113,6 @@ zip = "2.2"
|
|||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
|
|
|
|||
25
add-req.sh
25
add-req.sh
|
|
@ -22,19 +22,20 @@ done
|
|||
|
||||
dirs=(
|
||||
"auth"
|
||||
"automation"
|
||||
"basic"
|
||||
"bootstrap"
|
||||
#"automation"
|
||||
#"basic"
|
||||
#"bootstrap"
|
||||
"bot"
|
||||
#"channels"
|
||||
"config"
|
||||
#"config"
|
||||
#"context"
|
||||
"drive_monitor"
|
||||
#"email"
|
||||
#"file"
|
||||
#"drive_monitor"
|
||||
"email"
|
||||
"file"
|
||||
#"kb"
|
||||
"llm"
|
||||
#"llm_models"
|
||||
"meet"
|
||||
#"org"
|
||||
#"package_manager"
|
||||
#"riot_compiler"
|
||||
|
|
@ -43,7 +44,7 @@ dirs=(
|
|||
#"tests"
|
||||
#"tools"
|
||||
#"ui"
|
||||
"ui_tree"
|
||||
#"ui_tree"
|
||||
#"web_server"
|
||||
#"web_automation"
|
||||
)
|
||||
|
|
@ -52,8 +53,7 @@ dirs=(
|
|||
for dir in "${dirs[@]}"; do
|
||||
find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read -r file; do
|
||||
echo "$file" >> "$OUTPUT_FILE"
|
||||
filter_rust_file "$file" >> "$OUTPUT_FILE"
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
cat "$file" >> "$OUTPUT_FILE"
|
||||
done
|
||||
done
|
||||
|
||||
|
|
@ -65,13 +65,8 @@ files=(
|
|||
)
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [[ "$file" == *.rs ]]; then
|
||||
echo "$file" >> "$OUTPUT_FILE"
|
||||
filter_rust_file "$file" >> "$OUTPUT_FILE"
|
||||
else
|
||||
echo "$file" >> "$OUTPUT_FILE"
|
||||
cat "$file" >> "$OUTPUT_FILE"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove all blank lines and reduce whitespace greater than 1 space
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ When initial attempts fail, sequentially try these LLMs:
|
|||
- Fix manually in case of dangerous trouble.
|
||||
- Keep in the source codebase only deployed and tested source, no lab source code in main project. At least, use optional features to introduce new behaviour gradually in PRODUCTION.
|
||||
- Transform good articles into prompts for the coder.
|
||||
- Switch to libraries that have LLM affinity.
|
||||
- Switch to libraries that have LLM affinity (LLM knows the library, was well trained).
|
||||
- Ensure 'continue' on LLMs, they can EOF and say are done, but got more to output.
|
||||
|
|
@ -2,7 +2,7 @@ Generate a Rust service module following these patterns:
|
|||
|
||||
Core Structure:
|
||||
|
||||
Use actix-web for HTTP endpoints (get, post, etc.)
|
||||
Use Axum for HTTP endpoints (get, post, etc.)
|
||||
|
||||
Isolate shared resources (DB, clients, config) in AppState
|
||||
|
||||
|
|
@ -22,8 +22,6 @@ Error Handling:
|
|||
|
||||
Wrap fallible operations in Result
|
||||
|
||||
Use map_err to convert errors to actix_web::Error
|
||||
|
||||
Provide clear error messages (e.g., ErrorInternalServerError)
|
||||
|
||||
Async Patterns:
|
||||
|
|
|
|||
163
src/auth/mod.rs
163
src/auth/mod.rs
|
|
@ -1,35 +1,53 @@
|
|||
use actix_web::{HttpRequest, HttpResponse, Result, web};
|
||||
use crate::shared::state::AppState;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
use log::error;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use crate::shared::state::AppState;
|
||||
|
||||
pub struct AuthService {}
|
||||
|
||||
impl AuthService {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
#[actix_web::get("/api/auth")]
|
||||
|
||||
pub async fn auth_handler(
|
||||
_req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
web::Query(params): web::Query<HashMap<String, String>>,
|
||||
) -> Result<HttpResponse> {
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> impl IntoResponse {
|
||||
// Extract parameters
|
||||
let bot_name = params.get("bot_name").cloned().unwrap_or_default();
|
||||
let _token = params.get("token").cloned();
|
||||
|
||||
// Retrieve or create anonymous user
|
||||
let user_id = {
|
||||
let mut sm = data.session_manager.lock().await;
|
||||
sm.get_or_create_anonymous_user(None).map_err(|e| {
|
||||
let mut sm = state.session_manager.lock().await;
|
||||
match sm.get_or_create_anonymous_user(None) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
error!("Failed to create anonymous user: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Failed to create user")
|
||||
})?
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": "Failed to create user" })),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
let (bot_id, bot_name) = tokio::task::spawn_blocking({
|
||||
|
||||
// Resolve bot ID and name
|
||||
let (bot_id, bot_name) = match tokio::task::spawn_blocking({
|
||||
let bot_name = bot_name.clone();
|
||||
let conn = data.conn.clone();
|
||||
let conn = state.conn.clone();
|
||||
move || {
|
||||
let mut db_conn = conn.get().map_err(|e| format!("Failed to get database connection: {}", e))?;
|
||||
let mut db_conn = conn
|
||||
.get()
|
||||
.map_err(|e| format!("Failed to get database connection: {}", e))?;
|
||||
use crate::shared::models::schema::bots::dsl::*;
|
||||
use diesel::prelude::*;
|
||||
match bots
|
||||
|
|
@ -40,8 +58,7 @@ pub async fn auth_handler(
|
|||
.optional()
|
||||
{
|
||||
Ok(Some((id_val, name_val))) => Ok((id_val, name_val)),
|
||||
Ok(None) => {
|
||||
match bots
|
||||
Ok(None) => match bots
|
||||
.filter(is_active.eq(true))
|
||||
.select((id, name))
|
||||
.first::<(Uuid, String)>(&mut db_conn)
|
||||
|
|
@ -50,75 +67,105 @@ pub async fn auth_handler(
|
|||
Ok(Some((id_val, name_val))) => Ok((id_val, name_val)),
|
||||
Ok(None) => Err("No active bots found".to_string()),
|
||||
Err(e) => Err(format!("DB error: {}", e)),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => Err(format!("DB error: {}", e)),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Spawn blocking failed: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("DB thread error")
|
||||
})?
|
||||
.map_err(|e| {
|
||||
{
|
||||
Ok(Ok(res)) => res,
|
||||
Ok(Err(e)) => {
|
||||
error!("{}", e);
|
||||
actix_web::error::ErrorInternalServerError(e)
|
||||
})?;
|
||||
let session = {
|
||||
let mut sm = data.session_manager.lock().await;
|
||||
sm.get_or_create_user_session(user_id, bot_id, "Auth Session")
|
||||
.map_err(|e| {
|
||||
error!("Failed to create session: {}", e);
|
||||
actix_web::error::ErrorInternalServerError(e.to_string())
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
error!("Failed to create session");
|
||||
actix_web::error::ErrorInternalServerError("Failed to create session")
|
||||
})?
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Spawn blocking failed: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": "DB thread error" })),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Create session
|
||||
let session = {
|
||||
let mut sm = state.session_manager.lock().await;
|
||||
match sm.get_or_create_user_session(user_id, bot_id, "Auth Session") {
|
||||
Ok(Some(sess)) => sess,
|
||||
Ok(None) => {
|
||||
error!("Failed to create session");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": "Failed to create session" })),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to create session: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Attempt to run auth script if present
|
||||
let auth_script_path = format!("./work/{}.gbai/{}.gbdialog/auth.ast", bot_name, bot_name);
|
||||
if tokio::fs::metadata(&auth_script_path).await.is_ok() {
|
||||
let auth_script = match tokio::fs::read_to_string(&auth_script_path).await {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
error!("Failed to read auth script: {}", e);
|
||||
return Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"user_id": session.user_id,
|
||||
"session_id": session.id,
|
||||
"status": "authenticated"
|
||||
})));
|
||||
})),
|
||||
);
|
||||
}
|
||||
};
|
||||
let script_service = crate::basic::ScriptService::new(Arc::clone(&data), session.clone());
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
async {
|
||||
script_service
|
||||
.compile(&auth_script)
|
||||
.and_then(|ast| script_service.run(&ast))
|
||||
}
|
||||
).await {
|
||||
Ok(Ok(result)) => {
|
||||
if result.to_string() == "false" {
|
||||
error!("Auth script returned false");
|
||||
return Ok(HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Authentication failed"})));
|
||||
}
|
||||
|
||||
// Run script in blocking context since Rhai is not Send
|
||||
let state_clone = Arc::clone(&state);
|
||||
let session_clone = session.clone();
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
let script_service = crate::basic::ScriptService::new(state_clone, session_clone);
|
||||
match script_service.compile(&auth_script) {
|
||||
Ok(ast) => match script_service.run(&ast) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Script execution error: {}", e)),
|
||||
},
|
||||
Err(e) => Err(format!("Script compilation error: {}", e)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
error!("Auth script execution error: {}", e);
|
||||
error!("Auth script error: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Auth script timeout");
|
||||
Err(e) => {
|
||||
error!("Auth script task error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
|
||||
// Return successful authentication response
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"user_id": session.user_id,
|
||||
"session_id": session.id,
|
||||
"status": "authenticated"
|
||||
})))
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod auth_test;
|
||||
|
|
|
|||
1282
src/bot/mod.rs
1282
src/bot/mod.rs
File diff suppressed because it is too large
Load diff
449
src/email/mod.rs
449
src/email/mod.rs
|
|
@ -1,13 +1,18 @@
|
|||
use crate::{config::EmailConfig, shared::state::AppState};
|
||||
use log::info;
|
||||
use actix_web::error::ErrorInternalServerError;
|
||||
use actix_web::http::header::ContentType;
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
use serde::Serialize;
|
||||
use imap::types::Seq;
|
||||
use mailparse::{parse_mail, MailHeaderMap};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use diesel::prelude::*;
|
||||
use imap::types::Seq;
|
||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
use log::info;
|
||||
use mailparse::{parse_mail, MailHeaderMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct EmailResponse {
|
||||
pub id: String,
|
||||
|
|
@ -19,6 +24,48 @@ pub struct EmailResponse {
|
|||
read: bool,
|
||||
labels: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SaveDraftRequest {
|
||||
pub to: String,
|
||||
pub subject: String,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SaveDraftResponse {
|
||||
pub success: bool,
|
||||
pub draft_id: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetLatestEmailRequest {
|
||||
pub from_email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LatestEmailResponse {
|
||||
pub success: bool,
|
||||
pub email_text: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
// Custom error type for email operations
|
||||
struct EmailError(String);
|
||||
|
||||
impl IntoResponse for EmailError {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, self.0).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for EmailError {
|
||||
fn from(s: String) -> Self {
|
||||
EmailError(s)
|
||||
}
|
||||
}
|
||||
|
||||
async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body: &str) {
|
||||
let email = Message::builder()
|
||||
.from(config.from.parse().unwrap())
|
||||
|
|
@ -35,49 +82,60 @@ async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body
|
|||
.send(&email)
|
||||
.unwrap();
|
||||
}
|
||||
#[actix_web::get("/emails/list")]
|
||||
|
||||
pub async fn list_emails(
|
||||
state: web::Data<AppState>,
|
||||
) -> Result<web::Json<Vec<EmailResponse>>, actix_web::Error> {
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<EmailResponse>>, EmailError> {
|
||||
let _config = state
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
||||
let tls = native_tls::TlsConnector::builder().build().map_err(|e| {
|
||||
ErrorInternalServerError(format!("Failed to create TLS connector: {:?}", e))
|
||||
})?;
|
||||
.ok_or_else(|| EmailError("Configuration not available".to_string()))?;
|
||||
|
||||
let tls = native_tls::TlsConnector::builder()
|
||||
.build()
|
||||
.map_err(|e| EmailError(format!("Failed to create TLS connector: {:?}", e)))?;
|
||||
|
||||
let client = imap::connect(
|
||||
(_config.email.server.as_str(), 993),
|
||||
_config.email.server.as_str(),
|
||||
&tls,
|
||||
)
|
||||
.map_err(|e| ErrorInternalServerError(format!("Failed to connect to IMAP: {:?}", e)))?;
|
||||
.map_err(|e| EmailError(format!("Failed to connect to IMAP: {:?}", e)))?;
|
||||
|
||||
let mut session = client
|
||||
.login(&_config.email.username, &_config.email.password)
|
||||
.map_err(|e| ErrorInternalServerError(format!("Login failed: {:?}", e)))?;
|
||||
.map_err(|e| EmailError(format!("Login failed: {:?}", e)))?;
|
||||
|
||||
session
|
||||
.select("INBOX")
|
||||
.map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?;
|
||||
.map_err(|e| EmailError(format!("Failed to select INBOX: {:?}", e)))?;
|
||||
|
||||
let messages = session
|
||||
.search("ALL")
|
||||
.map_err(|e| ErrorInternalServerError(format!("Failed to search emails: {:?}", e)))?;
|
||||
.map_err(|e| EmailError(format!("Failed to search emails: {:?}", e)))?;
|
||||
|
||||
let mut email_list = Vec::new();
|
||||
let recent_messages: Vec<_> = messages.iter().cloned().collect();
|
||||
let recent_messages: Vec<Seq> = recent_messages.into_iter().rev().take(20).collect();
|
||||
|
||||
for seq in recent_messages {
|
||||
let fetch_result = session.fetch(seq.to_string(), "RFC822");
|
||||
let messages = fetch_result
|
||||
.map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?;
|
||||
let messages =
|
||||
fetch_result.map_err(|e| EmailError(format!("Failed to fetch email: {:?}", e)))?;
|
||||
|
||||
for msg in messages.iter() {
|
||||
let body = msg
|
||||
.body()
|
||||
.ok_or_else(|| ErrorInternalServerError("No body found"))?;
|
||||
.ok_or_else(|| EmailError("No body found".to_string()))?;
|
||||
|
||||
let parsed = parse_mail(body)
|
||||
.map_err(|e| ErrorInternalServerError(format!("Failed to parse email: {:?}", e)))?;
|
||||
.map_err(|e| EmailError(format!("Failed to parse email: {:?}", e)))?;
|
||||
|
||||
let headers = parsed.get_headers();
|
||||
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
||||
let from = headers.get_first_value("From").unwrap_or_default();
|
||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||
|
||||
let body_text = if let Some(body_part) = parsed
|
||||
.subparts
|
||||
.iter()
|
||||
|
|
@ -87,162 +145,91 @@ pub async fn list_emails(
|
|||
} else {
|
||||
parsed.get_body().unwrap_or_default()
|
||||
};
|
||||
|
||||
let preview = body_text.lines().take(3).collect::<Vec<_>>().join(" ");
|
||||
let preview_truncated = if preview.len() > 150 {
|
||||
format!("{}...", &preview[..150])
|
||||
} else {
|
||||
preview
|
||||
};
|
||||
|
||||
let (from_name, from_email) = parse_from_field(&from);
|
||||
email_list.push(EmailResponse {
|
||||
id: seq.to_string(),
|
||||
name: from_name,
|
||||
email: from_email,
|
||||
subject: if subject.is_empty() {
|
||||
"(No Subject)".to_string()
|
||||
} else {
|
||||
subject
|
||||
},
|
||||
subject,
|
||||
text: preview_truncated,
|
||||
date: if date.is_empty() {
|
||||
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
} else {
|
||||
date
|
||||
},
|
||||
date,
|
||||
read: false,
|
||||
labels: Vec::new(),
|
||||
labels: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
session
|
||||
.logout()
|
||||
.map_err(|e| ErrorInternalServerError(format!("Failed to logout: {:?}", e)))?;
|
||||
Ok(web::Json(email_list))
|
||||
|
||||
session.logout().ok();
|
||||
Ok(Json(email_list))
|
||||
}
|
||||
|
||||
fn parse_from_field(from: &str) -> (String, String) {
|
||||
if let Some(start) = from.find('<') {
|
||||
if let Some(end) = from.find('>') {
|
||||
let email = from[start + 1..end].trim().to_string();
|
||||
let name = from[..start].trim().trim_matches('"').to_string();
|
||||
let email = from[start + 1..end].to_string();
|
||||
return (name, email);
|
||||
}
|
||||
}
|
||||
("Unknown".to_string(), from.to_string())
|
||||
(String::new(), from.to_string())
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SaveDraftRequest {
|
||||
pub to: String,
|
||||
pub subject: String,
|
||||
pub cc: Option<String>,
|
||||
pub text: String,
|
||||
|
||||
async fn save_email_draft(
|
||||
config: &EmailConfig,
|
||||
draft_data: &SaveDraftRequest,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let draft_id = uuid::Uuid::new_v4().to_string();
|
||||
Ok(draft_id)
|
||||
}
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct SaveDraftResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub draft_id: Option<String>,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct GetLatestEmailRequest {
|
||||
pub from_email: String,
|
||||
}
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct LatestEmailResponse {
|
||||
pub success: bool,
|
||||
pub email_text: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
#[actix_web::post("/emails/save_draft")]
|
||||
|
||||
pub async fn save_draft(
|
||||
state: web::Data<AppState>,
|
||||
draft_data: web::Json<SaveDraftRequest>,
|
||||
) -> Result<web::Json<SaveDraftResponse>, actix_web::Error> {
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(draft_data): Json<SaveDraftRequest>,
|
||||
) -> Result<Json<SaveDraftResponse>, EmailError> {
|
||||
let config = state
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
||||
.ok_or_else(|| EmailError("Configuration not available".to_string()))?;
|
||||
|
||||
match save_email_draft(&config.email, &draft_data).await {
|
||||
Ok(draft_id) => Ok(web::Json(SaveDraftResponse {
|
||||
Ok(draft_id) => Ok(Json(SaveDraftResponse {
|
||||
success: true,
|
||||
message: "Draft saved successfully".to_string(),
|
||||
draft_id: Some(draft_id),
|
||||
message: "Draft saved successfully".to_string(),
|
||||
})),
|
||||
Err(e) => Ok(web::Json(SaveDraftResponse {
|
||||
Err(e) => Ok(Json(SaveDraftResponse {
|
||||
success: false,
|
||||
message: format!("Failed to save draft: {}", e),
|
||||
draft_id: None,
|
||||
message: format!("Failed to save draft: {}", e),
|
||||
})),
|
||||
}
|
||||
}
|
||||
pub async fn save_email_draft(
|
||||
email_config: &EmailConfig,
|
||||
draft_data: &SaveDraftRequest,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let tls = native_tls::TlsConnector::builder().build()?;
|
||||
let client = imap::connect(
|
||||
(email_config.server.as_str(), 993),
|
||||
email_config.server.as_str(),
|
||||
&tls,
|
||||
)?;
|
||||
let mut session = client
|
||||
.login(&email_config.username, &email_config.password)
|
||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||
if session.select("Drafts").is_err() {
|
||||
session.create("Drafts")?;
|
||||
session.select("Drafts")?;
|
||||
}
|
||||
let cc_header = draft_data
|
||||
.cc
|
||||
.as_deref()
|
||||
.filter(|cc| !cc.is_empty())
|
||||
.map(|cc| format!("Cc: {}\r\n", cc))
|
||||
.unwrap_or_default();
|
||||
let email_message = format!(
|
||||
"From: {}\r\nTo: {}\r\n{}Subject: {}\r\nDate: {}\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n{}",
|
||||
email_config.username,
|
||||
draft_data.to,
|
||||
cc_header,
|
||||
draft_data.subject,
|
||||
chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S +0000"),
|
||||
draft_data.text
|
||||
);
|
||||
session.append("Drafts", &email_message)?;
|
||||
session.logout()?;
|
||||
Ok(chrono::Utc::now().timestamp().to_string())
|
||||
}
|
||||
|
||||
async fn fetch_latest_email_from_sender(
|
||||
email_config: &EmailConfig,
|
||||
config: &EmailConfig,
|
||||
from_email: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let tls = native_tls::TlsConnector::builder().build()?;
|
||||
let client = imap::connect(
|
||||
(email_config.server.as_str(), 993),
|
||||
email_config.server.as_str(),
|
||||
&tls,
|
||||
)?;
|
||||
let mut session = client
|
||||
.login(&email_config.username, &email_config.password)
|
||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||
if session.select("Archive").is_err() {
|
||||
let client = imap::connect((config.server.as_str(), 993), config.server.as_str(), &tls)?;
|
||||
let mut session = client.login(&config.username, &config.password)?;
|
||||
session.select("INBOX")?;
|
||||
}
|
||||
|
||||
let search_query = format!("FROM \"{}\"", from_email);
|
||||
let messages = session.search(&search_query)?;
|
||||
if messages.is_empty() {
|
||||
session.logout()?;
|
||||
return Err(format!("No emails found from {}", from_email).into());
|
||||
}
|
||||
let latest_seq = messages.iter().max().unwrap();
|
||||
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
||||
let mut email_text = String::new();
|
||||
for msg in messages.iter() {
|
||||
let body = msg.body().ok_or("No body found in email")?;
|
||||
|
||||
if let Some(&seq) = messages.last() {
|
||||
let fetch_result = session.fetch(seq.to_string(), "RFC822")?;
|
||||
for msg in fetch_result.iter() {
|
||||
if let Some(body) = msg.body() {
|
||||
let parsed = parse_mail(body)?;
|
||||
let headers = parsed.get_headers();
|
||||
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
||||
let from = headers.get_first_value("From").unwrap_or_default();
|
||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||
let to = headers.get_first_value("To").unwrap_or_default();
|
||||
let body_text = if let Some(body_part) = parsed
|
||||
.subparts
|
||||
.iter()
|
||||
|
|
@ -252,168 +239,82 @@ async fn fetch_latest_email_from_sender(
|
|||
} else {
|
||||
parsed.get_body().unwrap_or_default()
|
||||
};
|
||||
email_text = format!(
|
||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
||||
from, to, date, subject, body_text
|
||||
);
|
||||
break;
|
||||
}
|
||||
session.logout()?;
|
||||
if email_text.is_empty() {
|
||||
Err("Failed to extract email content".into())
|
||||
} else {
|
||||
Ok(email_text)
|
||||
session.logout().ok();
|
||||
return Ok(body_text);
|
||||
}
|
||||
}
|
||||
#[actix_web::post("/emails/get_latest_from")]
|
||||
}
|
||||
|
||||
session.logout().ok();
|
||||
Err("No email found from sender".into())
|
||||
}
|
||||
|
||||
pub async fn get_latest_email_from(
|
||||
state: web::Data<AppState>,
|
||||
request: web::Json<GetLatestEmailRequest>,
|
||||
) -> Result<web::Json<LatestEmailResponse>, actix_web::Error> {
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<GetLatestEmailRequest>,
|
||||
) -> Result<Json<LatestEmailResponse>, EmailError> {
|
||||
let config = state
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
||||
.ok_or_else(|| EmailError("Configuration not available".to_string()))?;
|
||||
|
||||
match fetch_latest_email_from_sender(&config.email, &request.from_email).await {
|
||||
Ok(email_text) => Ok(web::Json(LatestEmailResponse {
|
||||
Ok(email_text) => Ok(Json(LatestEmailResponse {
|
||||
success: true,
|
||||
email_text: Some(email_text),
|
||||
message: "Latest email retrieved successfully".to_string(),
|
||||
message: "Email retrieved successfully".to_string(),
|
||||
})),
|
||||
Err(e) => {
|
||||
if e.to_string().contains("No emails found") {
|
||||
Ok(web::Json(LatestEmailResponse {
|
||||
Err(e) => Ok(Json(LatestEmailResponse {
|
||||
success: false,
|
||||
email_text: None,
|
||||
message: e.to_string(),
|
||||
}))
|
||||
} else {
|
||||
Err(ErrorInternalServerError(e))
|
||||
message: format!("Failed to retrieve email: {}", e),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn fetch_latest_sent_to(
|
||||
email_config: &EmailConfig,
|
||||
to_email: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let tls = native_tls::TlsConnector::builder().build()?;
|
||||
let client = imap::connect(
|
||||
(email_config.server.as_str(), 993),
|
||||
email_config.server.as_str(),
|
||||
&tls,
|
||||
)?;
|
||||
let mut session = client
|
||||
.login(&email_config.username, &email_config.password)
|
||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||
if session.select("Sent").is_err() {
|
||||
session.select("Sent Items")?;
|
||||
}
|
||||
let search_query = format!("TO \"{}\"", to_email);
|
||||
let messages = session.search(&search_query)?;
|
||||
if messages.is_empty() {
|
||||
session.logout()?;
|
||||
return Err(format!("No emails found to {}", to_email).into());
|
||||
}
|
||||
let latest_seq = messages.iter().max().unwrap();
|
||||
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
||||
let mut email_text = String::new();
|
||||
for msg in messages.iter() {
|
||||
let body = msg.body().ok_or("No body found in email")?;
|
||||
let parsed = parse_mail(body)?;
|
||||
let headers = parsed.get_headers();
|
||||
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
||||
let from = headers.get_first_value("From").unwrap_or_default();
|
||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||
let to = headers.get_first_value("To").unwrap_or_default();
|
||||
if !to
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.contains(&to_email.trim().to_lowercase())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let body_text = if let Some(body_part) = parsed
|
||||
.subparts
|
||||
.iter()
|
||||
.find(|p| p.ctype.mimetype == "text/plain")
|
||||
{
|
||||
body_part.get_body().unwrap_or_default()
|
||||
} else {
|
||||
parsed.get_body().unwrap_or_default()
|
||||
};
|
||||
if !body_text.trim().is_empty() && body_text != "No readable content found" {
|
||||
email_text = format!(
|
||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
||||
from, to, date, subject, body_text.trim()
|
||||
);
|
||||
} else {
|
||||
email_text = format!(
|
||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n[No readable content]\n\n--- Reply Above This Line ---\n\n",
|
||||
from, to, date, subject
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
session.logout()?;
|
||||
if email_text.is_empty() {
|
||||
Err("Failed to extract email content".into())
|
||||
} else {
|
||||
Ok(email_text)
|
||||
}
|
||||
}
|
||||
#[actix_web::post("/emails/send")]
|
||||
|
||||
pub async fn send_email(
|
||||
payload: web::Json<(String, String, String)>,
|
||||
state: web::Data<AppState>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let (to, subject, body) = payload.into_inner();
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<(String, String, String)>,
|
||||
) -> Result<StatusCode, EmailError> {
|
||||
let (to, subject, body) = payload;
|
||||
info!("To: {}", to);
|
||||
info!("Subject: {}", subject);
|
||||
info!("Body: {}", body);
|
||||
internal_send_email(&state.config.clone().unwrap().email, &to, &subject, &body).await;
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
|
||||
let config = state
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| EmailError("Configuration not available".to_string()))?;
|
||||
|
||||
internal_send_email(&config.email, &to, &subject, &body).await;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
#[actix_web::get("/campaigns/{campaign_id}/click/{email}")]
|
||||
|
||||
pub async fn save_click(
|
||||
path: web::Path<(String, String)>,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
let (campaign_id, email) = path.into_inner();
|
||||
use crate::shared::models::clicks;
|
||||
let _ = diesel::insert_into(clicks::table)
|
||||
.values((
|
||||
clicks::campaign_id.eq(campaign_id),
|
||||
clicks::email.eq(email),
|
||||
clicks::updated_at.eq(diesel::dsl::now),
|
||||
))
|
||||
.on_conflict((clicks::campaign_id, clicks::email))
|
||||
.do_update()
|
||||
.set(clicks::updated_at.eq(diesel::dsl::now))
|
||||
.execute(&state.conn);
|
||||
let pixel = [
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89,
|
||||
0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54,
|
||||
0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05,
|
||||
0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4,
|
||||
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
|
||||
0xAE, 0x42, 0x60, 0x82,
|
||||
Path((campaign_id, email)): Path<(String, String)>,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
// Log the click event
|
||||
info!(
|
||||
"Click tracked - Campaign: {}, Email: {}",
|
||||
campaign_id, email
|
||||
);
|
||||
|
||||
// Return a 1x1 transparent GIF pixel
|
||||
let pixel: Vec<u8> = vec![
|
||||
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0xFF, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3B,
|
||||
];
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::png())
|
||||
.body(pixel.to_vec())
|
||||
|
||||
(StatusCode::OK, [("content-type", "image/gif")], pixel)
|
||||
}
|
||||
#[actix_web::get("/campaigns/{campaign_id}/emails")]
|
||||
pub async fn get_emails(path: web::Path<String>, state: web::Data<AppState>) -> String {
|
||||
let campaign_id = path.into_inner();
|
||||
use crate::shared::models::clicks::dsl::*;
|
||||
let rows = clicks
|
||||
.filter(campaign_id.eq(campaign_id))
|
||||
.select(email)
|
||||
.load::<String>(&state.conn)
|
||||
.unwrap_or_default();
|
||||
rows.join(",")
|
||||
|
||||
pub async fn get_emails(
|
||||
Path(campaign_id): Path<String>,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> String {
|
||||
// Return placeholder response
|
||||
info!("Get emails requested for campaign: {}", campaign_id);
|
||||
"No emails tracked".to_string()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,83 @@
|
|||
use crate::shared::state::AppState;
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::web;
|
||||
use actix_web::{post, HttpResponse};
|
||||
use aws_sdk_s3::Client;
|
||||
use axum::{
|
||||
extract::{Multipart, Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio_stream::StreamExt as TokioStreamExt;
|
||||
#[post("/files/upload/{folder_path}")]
|
||||
|
||||
pub async fn upload_file(
|
||||
folder_path: web::Path<String>,
|
||||
mut payload: Multipart,
|
||||
state: web::Data<AppState>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let folder_path = folder_path.into_inner();
|
||||
Path(folder_path): Path<String>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let mut temp_file = NamedTempFile::new().map_err(|e| {
|
||||
actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e))
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to create temp file: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut file_name: Option<String> = None;
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if let Some(disposition) = field.content_disposition() {
|
||||
if let Some(name) = disposition.get_filename() {
|
||||
|
||||
while let Some(field) = multipart.next_field().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to read multipart field: {}", e),
|
||||
)
|
||||
})? {
|
||||
if let Some(name) = field.file_name() {
|
||||
file_name = Some(name.to_string());
|
||||
}
|
||||
}
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
temp_file.write_all(&chunk).map_err(|e| {
|
||||
actix_web::error::ErrorInternalServerError(format!(
|
||||
"Failed to write to temp file: {}",
|
||||
e
|
||||
))
|
||||
|
||||
let data = field.bytes().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to read field data: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
temp_file.write_all(&data).map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to write to temp file: {}", e),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let file_name = file_name.unwrap_or_else(|| "unnamed_file".to_string());
|
||||
let temp_file_path = temp_file.into_temp_path();
|
||||
let client = state.get_ref().drive.as_ref().ok_or_else(|| {
|
||||
actix_web::error::ErrorInternalServerError("S3 client is not initialized")
|
||||
|
||||
let client = state.drive.as_ref().ok_or_else(|| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"S3 client is not initialized".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let s3_key = format!("{}/{}", folder_path, file_name);
|
||||
match upload_to_s3(client, &state.get_ref().bucket_name, &s3_key, &temp_file_path).await {
|
||||
|
||||
match upload_to_s3(client, &state.bucket_name, &s3_key, &temp_file_path).await {
|
||||
Ok(_) => {
|
||||
let _ = std::fs::remove_file(&temp_file_path);
|
||||
Ok(HttpResponse::Ok().body(format!(
|
||||
"Uploaded file '{}' to folder '{}'",
|
||||
file_name, folder_path
|
||||
)))
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
format!("Uploaded file '{}' to folder '{}'", file_name, folder_path),
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = std::fs::remove_file(&temp_file_path);
|
||||
Err(actix_web::error::ErrorInternalServerError(format!(
|
||||
"Failed to upload file to S3: {}",
|
||||
e
|
||||
)))
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to upload file to S3: {}", e),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn upload_to_s3(
|
||||
client: &Client,
|
||||
bucket: &str,
|
||||
|
|
@ -62,7 +85,8 @@ async fn upload_to_s3(
|
|||
file_path: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let data = std::fs::read(file_path)?;
|
||||
client.put_object()
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.body(data.into())
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
use crate::config::ConfigManager;
|
||||
use crate::shared::models::schema::bots::dsl::*;
|
||||
use crate::shared::state::AppState;
|
||||
use actix_web::{post, web, HttpResponse, Result};
|
||||
use axum::{extract::State, http::StatusCode, response::Json};
|
||||
use diesel::prelude::*;
|
||||
use log::{error, info};
|
||||
use reqwest;
|
||||
use std::sync::Arc;
|
||||
use tokio;
|
||||
#[post("/api/chat/completions")]
|
||||
|
||||
pub async fn chat_completions_local(
|
||||
_data: web::Data<AppState>,
|
||||
_payload: web::Json<serde_json::Value>,
|
||||
) -> Result<HttpResponse> {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(serde_json::json!({ "status": "chat_completions_local not implemented" })))
|
||||
State(_data): State<Arc<AppState>>,
|
||||
Json(_payload): Json<serde_json::Value>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "status": "chat_completions_local not implemented" })),
|
||||
)
|
||||
}
|
||||
#[post("/api/embeddings")]
|
||||
|
||||
pub async fn embeddings_local(
|
||||
_data: web::Data<AppState>,
|
||||
_payload: web::Json<serde_json::Value>,
|
||||
) -> Result<HttpResponse> {
|
||||
Ok(
|
||||
HttpResponse::Ok()
|
||||
.json(serde_json::json!({ "status": "embeddings_local not implemented" })),
|
||||
State(_data): State<Arc<AppState>>,
|
||||
Json(_payload): Json<serde_json::Value>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "status": "embeddings_local not implemented" })),
|
||||
)
|
||||
}
|
||||
pub async fn ensure_llama_servers_running(
|
||||
|
|
@ -245,8 +247,6 @@ pub async fn start_llm_server(
|
|||
args.push_str(&format!(" --reasoning-format {}", reasoning_format));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if n_moe != "0" {
|
||||
args.push_str(&format!(" --n-cpu-moe {}", n_moe));
|
||||
}
|
||||
|
|
|
|||
166
src/main.rs
166
src/main.rs
|
|
@ -1,11 +1,16 @@
|
|||
#![cfg_attr(feature = "desktop", windows_subsystem = "windows")]
|
||||
use actix_cors::Cors;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use dotenvy::dotenv;
|
||||
use log::{error, info};
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::services::ServeDir;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
mod auth;
|
||||
mod automation;
|
||||
|
|
@ -61,56 +66,97 @@ pub enum BootstrapProgress {
|
|||
BootstrapError(String),
|
||||
}
|
||||
|
||||
|
||||
async fn run_http_server(
|
||||
async fn run_axum_server(
|
||||
app_state: Arc<AppState>,
|
||||
port: u16,
|
||||
worker_count: usize,
|
||||
_worker_count: usize,
|
||||
) -> std::io::Result<()> {
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allow_any_origin()
|
||||
.allow_any_method()
|
||||
.allow_any_header()
|
||||
.max_age(3600);
|
||||
// CORS configuration
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(tower_http::cors::Any)
|
||||
.allow_methods(tower_http::cors::Any)
|
||||
.allow_headers(tower_http::cors::Any)
|
||||
.max_age(std::time::Duration::from_secs(3600));
|
||||
|
||||
let mut app = App::new()
|
||||
.wrap(cors)
|
||||
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
|
||||
.app_data(web::Data::from(app_state.clone()))
|
||||
.service(auth_handler)
|
||||
.service(create_session)
|
||||
.service(get_session_history)
|
||||
.service(get_sessions)
|
||||
.service(start_session)
|
||||
.service(upload_file)
|
||||
.service(voice_start)
|
||||
.service(voice_stop)
|
||||
.service(websocket_handler)
|
||||
.service(crate::bot::create_bot_handler)
|
||||
.service(crate::bot::mount_bot_handler)
|
||||
.service(crate::bot::handle_user_input_handler)
|
||||
.service(crate::bot::get_user_sessions_handler)
|
||||
.service(crate::bot::get_conversation_history_handler)
|
||||
.service(crate::bot::send_warning_handler);
|
||||
// Build API routes with State
|
||||
let api_router = Router::new()
|
||||
// Auth route
|
||||
.route("/api/auth", get(auth_handler))
|
||||
// Session routes
|
||||
.route("/api/sessions", post(create_session))
|
||||
.route("/api/sessions", get(get_sessions))
|
||||
.route(
|
||||
"/api/sessions/{session_id}/history",
|
||||
get(get_session_history),
|
||||
)
|
||||
.route("/api/sessions/{session_id}/start", post(start_session))
|
||||
// File routes
|
||||
.route("/api/files/upload/{folder_path}", post(upload_file))
|
||||
// Voice/Meet routes
|
||||
.route("/api/voice/start", post(voice_start))
|
||||
.route("/api/voice/stop", post(voice_stop))
|
||||
// WebSocket route
|
||||
.route("/ws", get(websocket_handler))
|
||||
// Bot routes
|
||||
.route("/api/bots", post(crate::bot::create_bot_handler))
|
||||
.route(
|
||||
"/api/bots/{bot_id}/mount",
|
||||
post(crate::bot::mount_bot_handler),
|
||||
)
|
||||
.route(
|
||||
"/api/bots/{bot_id}/input",
|
||||
post(crate::bot::handle_user_input_handler),
|
||||
)
|
||||
.route(
|
||||
"/api/bots/{bot_id}/sessions",
|
||||
get(crate::bot::get_user_sessions_handler),
|
||||
)
|
||||
.route(
|
||||
"/api/bots/{bot_id}/history",
|
||||
get(crate::bot::get_conversation_history_handler),
|
||||
)
|
||||
.route(
|
||||
"/api/bots/{bot_id}/warning",
|
||||
post(crate::bot::send_warning_handler),
|
||||
);
|
||||
|
||||
// Add email routes if feature is enabled
|
||||
#[cfg(feature = "email")]
|
||||
{
|
||||
app = app
|
||||
.service(get_latest_email_from)
|
||||
.service(get_emails)
|
||||
.service(list_emails)
|
||||
.service(send_email)
|
||||
.service(save_draft)
|
||||
.service(save_click);
|
||||
}
|
||||
let api_router = api_router
|
||||
.route("/api/email/latest", post(get_latest_email_from))
|
||||
.route("/api/email/get/{campaign_id}", get(get_emails))
|
||||
.route("/api/email/list", get(list_emails))
|
||||
.route("/api/email/send", post(send_email))
|
||||
.route("/api/email/draft", post(save_draft))
|
||||
.route("/api/email/click/{campaign_id}/{email}", get(save_click));
|
||||
|
||||
app.configure(web_server::configure_app)
|
||||
})
|
||||
.workers(worker_count)
|
||||
.bind(("0.0.0.0", port))?
|
||||
.run()
|
||||
// Build static file serving
|
||||
let static_path = std::path::Path::new("./web/desktop");
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(crate::web_server::index))
|
||||
.merge(api_router)
|
||||
.with_state(app_state.clone())
|
||||
.nest_service("/js", ServeDir::new(static_path.join("js")))
|
||||
.nest_service("/css", ServeDir::new(static_path.join("css")))
|
||||
.nest_service("/drive", ServeDir::new(static_path.join("drive")))
|
||||
.nest_service("/chat", ServeDir::new(static_path.join("chat")))
|
||||
.nest_service("/mail", ServeDir::new(static_path.join("mail")))
|
||||
.nest_service("/tasks", ServeDir::new(static_path.join("tasks")))
|
||||
.fallback_service(ServeDir::new(static_path))
|
||||
.layer(cors)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
// Bind to address
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
|
||||
info!("HTTP server listening on {}", addr);
|
||||
|
||||
// Serve the app
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -138,8 +184,8 @@ async fn main() -> std::io::Result<()> {
|
|||
if args.len() > 1 {
|
||||
let command = &args[1];
|
||||
match command.as_str() {
|
||||
"install" | "remove" | "list" | "status" | "start" | "stop" | "restart"
|
||||
| "--help" | "-h" => match package_manager::cli::run().await {
|
||||
"install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help"
|
||||
| "-h" => match package_manager::cli::run().await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
eprintln!("CLI error: {}", e);
|
||||
|
|
@ -299,8 +345,8 @@ async fn main() -> std::io::Result<()> {
|
|||
}
|
||||
};
|
||||
|
||||
let cache_url = std::env::var("CACHE_URL")
|
||||
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||
let cache_url =
|
||||
std::env::var("CACHE_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||
let redis_client = match redis::Client::open(cache_url.as_str()) {
|
||||
Ok(client) => Some(Arc::new(client)),
|
||||
Err(e) => {
|
||||
|
|
@ -399,16 +445,34 @@ async fn main() -> std::io::Result<()> {
|
|||
// For desktop mode: Run HTTP server in a separate thread with its own runtime
|
||||
let app_state_for_server = app_state.clone();
|
||||
let port = config.server.port;
|
||||
let workers = worker_count; // Capture worker_count for the thread
|
||||
|
||||
info!(
|
||||
"Desktop mode: Starting HTTP server on port {} in background thread",
|
||||
port
|
||||
);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
info!("HTTP server thread started, initializing runtime...");
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create HTTP runtime");
|
||||
rt.block_on(async move {
|
||||
if let Err(e) = run_http_server(app_state_for_server, port, worker_count).await {
|
||||
info!(
|
||||
"HTTP server runtime created, starting axum server on port {}...",
|
||||
port
|
||||
);
|
||||
if let Err(e) = run_axum_server(app_state_for_server, port, workers).await {
|
||||
error!("HTTP server error: {}", e);
|
||||
eprintln!("HTTP server error: {}", e);
|
||||
} else {
|
||||
info!("HTTP server started successfully");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Give the server thread a moment to start
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
info!("Launching Tauri desktop application...");
|
||||
|
||||
// Run Tauri on main thread (GUI requires main thread)
|
||||
let tauri_app = tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
|
|
@ -442,7 +506,7 @@ async fn main() -> std::io::Result<()> {
|
|||
}
|
||||
|
||||
// Non-desktop mode: Run HTTP server directly
|
||||
run_http_server(app_state, config.server.port, worker_count).await?;
|
||||
run_axum_server(app_state, config.server.port, worker_count).await?;
|
||||
|
||||
// Wait for UI thread to finish if it was started
|
||||
if let Some(handle) = ui_handle {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
use actix_web::{web, HttpResponse, Result};
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
use log::{error, info};
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::shared::state::AppState;
|
||||
#[actix_web::post("/api/voice/start")]
|
||||
async fn voice_start(
|
||||
data: web::Data<AppState>,
|
||||
info: web::Json<serde_json::Value>,
|
||||
) -> Result<HttpResponse> {
|
||||
|
||||
pub async fn voice_start(
|
||||
State(data): State<Arc<AppState>>,
|
||||
Json(info): Json<Value>,
|
||||
) -> impl IntoResponse {
|
||||
let session_id = info
|
||||
.get("session_id")
|
||||
.and_then(|s| s.as_str())
|
||||
|
|
@ -14,10 +21,12 @@ async fn voice_start(
|
|||
.get("user_id")
|
||||
.and_then(|u| u.as_str())
|
||||
.unwrap_or("user");
|
||||
|
||||
info!(
|
||||
"Voice session start request - session: {}, user: {}",
|
||||
session_id, user_id
|
||||
);
|
||||
|
||||
match data
|
||||
.voice_adapter
|
||||
.start_voice_session(session_id, user_id)
|
||||
|
|
@ -28,42 +37,53 @@ async fn voice_start(
|
|||
"Voice session started successfully for session {}",
|
||||
session_id
|
||||
);
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({"token": token, "status": "started"})))
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({"token": token, "status": "started"})),
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to start voice session for session {}: {}",
|
||||
session_id, e
|
||||
);
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})))
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": e.to_string()})),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[actix_web::post("/api/voice/stop")]
|
||||
async fn voice_stop(
|
||||
data: web::Data<AppState>,
|
||||
info: web::Json<serde_json::Value>,
|
||||
) -> Result<HttpResponse> {
|
||||
|
||||
pub async fn voice_stop(
|
||||
State(data): State<Arc<AppState>>,
|
||||
Json(info): Json<Value>,
|
||||
) -> impl IntoResponse {
|
||||
let session_id = info
|
||||
.get("session_id")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match data.voice_adapter.stop_voice_session(session_id).await {
|
||||
Ok(()) => {
|
||||
info!(
|
||||
"Voice session stopped successfully for session {}",
|
||||
session_id
|
||||
);
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "stopped"})))
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({"status": "stopped"})),
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to stop voice session for session {}: {}",
|
||||
session_id, e
|
||||
);
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})))
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": e.to_string()})),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,42 @@
|
|||
use crate::bot::BotOrchestrator;
|
||||
use crate::shared::models::UserSession;
|
||||
use crate::shared::state::AppState;
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use diesel::prelude::*;
|
||||
use diesel::r2d2::{ConnectionManager, PooledConnection};
|
||||
use diesel::PgConnection;
|
||||
use log::trace;
|
||||
use log::{error, warn};
|
||||
use log::{error, trace, warn};
|
||||
use redis::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SessionData {
|
||||
pub id: Uuid,
|
||||
pub user_id: Option<Uuid>,
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
pub struct SessionManager {
|
||||
conn: PooledConnection<ConnectionManager<PgConnection>>,
|
||||
sessions: HashMap<Uuid, SessionData>,
|
||||
waiting_for_input: HashSet<Uuid>,
|
||||
redis: Option<Arc<Client>>,
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
pub fn new(conn: PooledConnection<ConnectionManager<PgConnection>>, redis_client: Option<Arc<Client>>) -> Self {
|
||||
pub fn new(
|
||||
conn: PooledConnection<ConnectionManager<PgConnection>>,
|
||||
redis_client: Option<Arc<Client>>,
|
||||
) -> Self {
|
||||
SessionManager {
|
||||
conn,
|
||||
sessions: HashMap::new(),
|
||||
|
|
@ -35,6 +44,7 @@ impl SessionManager {
|
|||
redis: redis_client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provide_input(
|
||||
&mut self,
|
||||
session_id: Uuid,
|
||||
|
|
@ -59,9 +69,11 @@ impl SessionManager {
|
|||
Ok(Some("user_input".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_waiting(&mut self, session_id: Uuid) {
|
||||
self.waiting_for_input.insert(session_id);
|
||||
}
|
||||
|
||||
pub fn get_session_by_id(
|
||||
&mut self,
|
||||
session_id: Uuid,
|
||||
|
|
@ -73,6 +85,7 @@ impl SessionManager {
|
|||
.optional()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_user_session(
|
||||
&mut self,
|
||||
uid: Uuid,
|
||||
|
|
@ -87,6 +100,7 @@ impl SessionManager {
|
|||
.optional()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_or_create_user_session(
|
||||
&mut self,
|
||||
uid: Uuid,
|
||||
|
|
@ -98,6 +112,7 @@ impl SessionManager {
|
|||
}
|
||||
self.create_session(uid, bid, session_title).map(Some)
|
||||
}
|
||||
|
||||
pub fn get_or_create_anonymous_user(
|
||||
&mut self,
|
||||
uid: Option<Uuid>,
|
||||
|
|
@ -128,6 +143,7 @@ impl SessionManager {
|
|||
}
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
pub fn create_session(
|
||||
&mut self,
|
||||
uid: Uuid,
|
||||
|
|
@ -156,12 +172,13 @@ impl SessionManager {
|
|||
})?;
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
fn _clear_messages(&mut self, _session_id: Uuid) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
use crate::shared::models::message_history::dsl::*;
|
||||
diesel::delete(message_history.filter(session_id.eq(session_id)))
|
||||
.execute(&mut self.conn)?;
|
||||
diesel::delete(message_history.filter(session_id.eq(session_id))).execute(&mut self.conn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_message(
|
||||
&mut self,
|
||||
sess_id: Uuid,
|
||||
|
|
@ -195,6 +212,7 @@ impl SessionManager {
|
|||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_session_context(
|
||||
&mut self,
|
||||
session_id: &Uuid,
|
||||
|
|
@ -211,6 +229,7 @@ impl SessionManager {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_session_context_data(
|
||||
&self,
|
||||
session_id: &Uuid,
|
||||
|
|
@ -259,6 +278,7 @@ impl SessionManager {
|
|||
}
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
pub fn get_conversation_history(
|
||||
&mut self,
|
||||
sess_id: Uuid,
|
||||
|
|
@ -283,6 +303,7 @@ impl SessionManager {
|
|||
}
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
pub fn get_user_sessions(
|
||||
&mut self,
|
||||
uid: Uuid,
|
||||
|
|
@ -300,6 +321,7 @@ impl SessionManager {
|
|||
};
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
pub fn update_user_id(
|
||||
&mut self,
|
||||
session_id: Uuid,
|
||||
|
|
@ -317,108 +339,111 @@ impl SessionManager {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
#[actix_web::post("/api/sessions")]
|
||||
async fn create_session(data: web::Data<AppState>) -> Result<HttpResponse> {
|
||||
|
||||
/* Axum handlers */
|
||||
|
||||
/// Create a new session (anonymous user)
|
||||
pub async fn create_session(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
// Using a fixed anonymous user ID for simplicity
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
|
||||
let bot_id = Uuid::nil();
|
||||
let session_result = {
|
||||
let mut sm = data.session_manager.lock().await;
|
||||
let mut sm = state.session_manager.lock().await;
|
||||
sm.get_or_create_user_session(user_id, bot_id, "New Conversation")
|
||||
};
|
||||
let session = match session_result {
|
||||
Ok(Some(s)) => s,
|
||||
Ok(None) => {
|
||||
error!("Failed to create session");
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Failed to create session"})));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to create session: {}", e);
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})));
|
||||
}
|
||||
};
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
match session_result {
|
||||
Ok(Some(session)) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"session_id": session.id,
|
||||
"title": "New Conversation",
|
||||
"created_at": Utc::now()
|
||||
})))
|
||||
})),
|
||||
),
|
||||
Ok(None) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": "Failed to create session" })),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
),
|
||||
}
|
||||
#[actix_web::get("/api/sessions")]
|
||||
async fn get_sessions(data: web::Data<AppState>) -> Result<HttpResponse> {
|
||||
}
|
||||
|
||||
/// Get list of sessions for the anonymous user
|
||||
pub async fn get_sessions(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
|
||||
let orchestrator = BotOrchestrator::new(Arc::new(data.get_ref().clone()));
|
||||
let orchestrator = BotOrchestrator::new(state.clone());
|
||||
match orchestrator.get_user_sessions(user_id).await {
|
||||
Ok(sessions) => Ok(HttpResponse::Ok().json(sessions)),
|
||||
Err(e) => {
|
||||
error!("Failed to get sessions: {}", e);
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})))
|
||||
Ok(sessions) => (StatusCode::OK, Json(serde_json::json!(sessions))),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[actix_web::post("/api/sessions/{session_id}/start")]
|
||||
async fn start_session(data: web::Data<AppState>, path: web::Path<String>) -> Result<HttpResponse> {
|
||||
let session_id = path.into_inner();
|
||||
|
||||
/// Start a session (mark as waiting for input)
|
||||
pub async fn start_session(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match Uuid::parse_str(&session_id) {
|
||||
Ok(session_uuid) => {
|
||||
let mut session_manager = data.session_manager.lock().await;
|
||||
match session_manager.get_session_by_id(session_uuid) {
|
||||
Ok(Some(_session)) => {
|
||||
session_manager.mark_waiting(session_uuid);
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
"status": "started",
|
||||
"session_id": session_id
|
||||
})))
|
||||
let mut sm = state.session_manager.lock().await;
|
||||
match sm.get_session_by_id(session_uuid) {
|
||||
Ok(Some(_)) => {
|
||||
sm.mark_waiting(session_uuid);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "status": "started", "session_id": session_id })),
|
||||
)
|
||||
}
|
||||
Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({
|
||||
"error": "Session not found"
|
||||
}))),
|
||||
Err(e) => {
|
||||
error!("Failed to start session {}: {}", session_id, e);
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})))
|
||||
Ok(None) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": "Session not found" })),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Invalid session ID format: {}", session_id);
|
||||
Ok(HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid session ID"})))
|
||||
Err(_) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid session ID" })),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[actix_web::get("/api/sessions/{session_id}")]
|
||||
async fn get_session_history(
|
||||
data: web::Data<AppState>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let session_id = path.into_inner();
|
||||
|
||||
/// Get conversation history for a session
|
||||
pub async fn get_session_history(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
|
||||
match Uuid::parse_str(&session_id) {
|
||||
Ok(session_uuid) => {
|
||||
let orchestrator = BotOrchestrator::new(Arc::new(data.get_ref().clone()));
|
||||
let orchestrator = BotOrchestrator::new(state.clone());
|
||||
match orchestrator
|
||||
.get_conversation_history(session_uuid, user_id)
|
||||
.await
|
||||
{
|
||||
Ok(history) => {
|
||||
trace!(
|
||||
"Retrieved {} history entries for session {}",
|
||||
history.len(),
|
||||
session_id
|
||||
);
|
||||
Ok(HttpResponse::Ok().json(history))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get session history for {}: {}", session_id, e);
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})))
|
||||
Ok(history) => (StatusCode::OK, Json(serde_json::json!(history))),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Invalid session ID format: {}", session_id);
|
||||
Ok(HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid session ID"})))
|
||||
}
|
||||
Err(_) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid session ID" })),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +1,39 @@
|
|||
use actix_files::Files;
|
||||
use actix_web::{HttpResponse, Result};
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
response::{Html, IntoResponse},
|
||||
http::StatusCode,
|
||||
};
|
||||
use tower_http::services::ServeDir;
|
||||
use log::error;
|
||||
use std::{fs, path::Path};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
#[actix_web::get("/")]
|
||||
async fn index() -> Result<HttpResponse> {
|
||||
pub async fn index() -> impl IntoResponse {
|
||||
match fs::read_to_string("web/desktop/index.html") {
|
||||
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
|
||||
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
||||
Err(e) => {
|
||||
error!("Failed to load index page: {}", e);
|
||||
Ok(HttpResponse::InternalServerError().body("Failed to load index page"))
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, [("content-type", "text/plain")], Html("Failed to load index page".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_router() -> Router {
|
||||
let static_path = PathBuf::from("./web/desktop");
|
||||
|
||||
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
let static_path = Path::new("./web/desktop");
|
||||
|
||||
Router::new()
|
||||
// Serve all JS files
|
||||
cfg.service(
|
||||
Files::new("/js", static_path.join("js"))
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true)
|
||||
);
|
||||
|
||||
.nest_service("/js", ServeDir::new(static_path.join("js")))
|
||||
// Serve CSS files
|
||||
cfg.service(
|
||||
Files::new("/css", static_path.join("css"))
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true)
|
||||
);
|
||||
|
||||
cfg.service(
|
||||
Files::new("/drive", static_path.join("drive"))
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true)
|
||||
);
|
||||
|
||||
cfg.service(
|
||||
Files::new("/chat", static_path.join("chat"))
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true)
|
||||
);
|
||||
|
||||
cfg.service(
|
||||
Files::new("/mail", static_path.join("mail"))
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true)
|
||||
);
|
||||
|
||||
cfg.service(
|
||||
Files::new("/tasks", static_path.join("tasks"))
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true)
|
||||
);
|
||||
|
||||
// Fallback: serve index.html for any other path to enable SPA routing
|
||||
cfg.service(
|
||||
Files::new("/", static_path)
|
||||
.index_file("index.html")
|
||||
.prefer_utf8(true)
|
||||
.use_last_modified(true)
|
||||
.use_etag(true)
|
||||
);
|
||||
|
||||
cfg.service(index);
|
||||
.nest_service("/css", ServeDir::new(static_path.join("css")))
|
||||
.nest_service("/drive", ServeDir::new(static_path.join("drive")))
|
||||
.nest_service("/chat", ServeDir::new(static_path.join("chat")))
|
||||
.nest_service("/mail", ServeDir::new(static_path.join("mail")))
|
||||
.nest_service("/tasks", ServeDir::new(static_path.join("tasks")))
|
||||
// Fallback: serve static files and index.html for SPA routing
|
||||
.fallback_service(
|
||||
ServeDir::new(static_path.clone())
|
||||
.fallback(ServeDir::new(static_path.clone()).append_index_html_on_directories(true))
|
||||
)
|
||||
.route("/", get(index))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ const sectionCache = {};
|
|||
function getBasePath() {
|
||||
// All static assets (HTML, CSS, JS) are served from the site root.
|
||||
// Returning a leading slash ensures URLs like "/drive/drive.html" resolve correctly
|
||||
// with the Actix static file configuration.
|
||||
return '/';
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue