Migrate HTTP API from Actix to Axum

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-20 13:28:35 -03:00
parent e0293f9f94
commit e146add4b2
16 changed files with 1262 additions and 2069 deletions

2
.vscode/launch.json vendored
View file

@ -15,7 +15,7 @@
}, },
"args": ["--desktop"], "args": ["--desktop"],
"env": { "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}" "cwd": "${workspaceFolder}"

517
Cargo.lock generated
View file

@ -2,279 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.25.1" version = "0.25.1"
@ -1157,14 +884,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core 0.4.5",
"bytes", "bytes",
"futures-util", "futures-util",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"itoa", "itoa",
"matchit", "matchit 0.7.3",
"memchr", "memchr",
"mime", "mime",
"percent-encoding", "percent-encoding",
@ -1177,6 +904,44 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "axum-core" name = "axum-core"
version = "0.4.5" version = "0.4.5"
@ -1197,6 +962,36 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.76" version = "0.3.76"
@ -1353,11 +1148,6 @@ dependencies = [
name = "botserver" name = "botserver"
version = "6.0.8" version = "6.0.8"
dependencies = [ dependencies = [
"actix-cors",
"actix-files",
"actix-multipart",
"actix-web",
"actix-ws",
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
"argon2", "argon2",
@ -1366,6 +1156,7 @@ dependencies = [
"async-trait", "async-trait",
"aws-config", "aws-config",
"aws-sdk-s3", "aws-sdk-s3",
"axum 0.8.7",
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"chrono", "chrono",
@ -1381,6 +1172,7 @@ dependencies = [
"futures", "futures",
"futures-util", "futures-util",
"hmac", "hmac",
"hyper 1.8.1",
"imap", "imap",
"include_dir", "include_dir",
"indicatif", "indicatif",
@ -1415,6 +1207,8 @@ dependencies = [
"time", "time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tower 0.5.2",
"tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"ureq", "ureq",
@ -1493,15 +1287,6 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "bytestring"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
dependencies = [
"bytes",
]
[[package]] [[package]]
name = "bzip2" name = "bzip2"
version = "0.4.4" version = "0.4.4"
@ -1934,17 +1719,6 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "cookie" name = "cookie"
version = "0.18.1" version = "0.18.1"
@ -2464,7 +2238,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.110",
"unicode-xid",
] ]
[[package]] [[package]]
@ -3691,10 +3464,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "http-range" name = "http-range-header"
version = "0.1.5" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]] [[package]]
name = "httparse" name = "httparse"
@ -4012,12 +3785,6 @@ dependencies = [
"nom 7.1.3", "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]] [[package]]
name = "include_dir" name = "include_dir"
version = "0.7.4" version = "0.7.4"
@ -4350,12 +4117,6 @@ dependencies = [
"selectors", "selectors",
] ]
[[package]]
name = "language-tags"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -4558,7 +4319,7 @@ dependencies = [
"sha2", "sha2",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite 0.20.1",
"url", "url",
] ]
@ -4590,23 +4351,6 @@ dependencies = [
"tokio-stream", "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]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.14" version = "0.4.14"
@ -4749,6 +4493,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@ -4884,6 +4634,23 @@ dependencies = [
"windows-sys 0.60.2", "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]] [[package]]
name = "multimap" name = "multimap"
version = "0.10.1" version = "0.10.1"
@ -5562,12 +5329,6 @@ dependencies = [
"windows-link 0.2.1", "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]] [[package]]
name = "password-hash" name = "password-hash"
version = "0.4.2" version = "0.4.2"
@ -7100,12 +6861,14 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde_plain" name = "serde_path_to_error"
version = "1.0.2" version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [ dependencies = [
"itoa",
"serde", "serde",
"serde_core",
] ]
[[package]] [[package]]
@ -7410,6 +7173,12 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "spki" name = "spki"
version = "0.6.0" version = "0.6.0"
@ -7681,7 +7450,7 @@ checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"cookie 0.18.1", "cookie",
"dirs", "dirs",
"dunce", "dunce",
"embed_plist", "embed_plist",
@ -7872,7 +7641,7 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926"
dependencies = [ dependencies = [
"cookie 0.18.1", "cookie",
"dpi", "dpi",
"gtk", "gtk",
"http 1.3.1", "http 1.3.1",
@ -8221,7 +7990,19 @@ dependencies = [
"futures-util", "futures-util",
"log", "log",
"tokio", "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]] [[package]]
@ -8341,7 +8122,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"async-trait", "async-trait",
"axum", "axum 0.7.9",
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"flate2", "flate2",
@ -8400,6 +8181,7 @@ dependencies = [
"tokio", "tokio",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@ -8410,14 +8192,24 @@ checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytes", "bytes",
"futures-core",
"futures-util", "futures-util",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util",
"http-range-header",
"httpdate",
"iri-string", "iri-string",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"tokio",
"tokio-util",
"tower 0.5.2", "tower 0.5.2",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@ -8557,6 +8349,23 @@ dependencies = [
"utf-8", "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]] [[package]]
name = "type1-encoding-parser" name = "type1-encoding-parser"
version = "0.1.0" version = "0.1.0"
@ -8692,12 +8501,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "unit-prefix" name = "unit-prefix"
version = "0.5.2" version = "0.5.2"
@ -8809,12 +8612,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@ -9683,7 +9480,7 @@ checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2 0.6.2", "block2 0.6.2",
"cookie 0.18.1", "cookie",
"crossbeam-channel", "crossbeam-channel",
"dirs", "dirs",
"dpi", "dpi",

View file

@ -3,34 +3,34 @@ name = "botserver"
version = "6.0.8" version = "6.0.8"
edition = "2021" edition = "2021"
authors = [ authors = [
"Pragmatismo.com.br <contact@pragmatismo.com.br>", "Pragmatismo.com.br ",
"General Bots Community <https://github.com/GeneralBots>", "General Bots Community ",
"Alan Perdomo", "Alan Perdomo",
"Ana Paula Gil", "Ana Paula Gil",
"Arenas.io", "Arenas.io",
"Atylla L", "Atylla L",
"Christopher de Castilho", "Christopher de Castilho",
"Dario Junior", "Dario Junior",
"David Lerner", "David Lerner",
"Experimentation Garage", "Experimentation Garage",
"Flavio Andrade", "Flavio Andrade",
"Heraldo Almeida", "Heraldo Almeida",
"Joao Parana", "Joao Parana",
"Jonathas C", "Jonathas C",
"J Ramos", "J Ramos",
"Lucas Picanco", "Lucas Picanco",
"Marcos Velasco", "Marcos Velasco",
"Matheus 39x", "Matheus 39x",
"Oerlabs Henrique", "Oerlabs Henrique",
"Othon Lima", "Othon Lima",
"PH Nascimento", "PH Nascimento",
"Phpussente", "Phpussente",
"Robson Dantas", "Robson Dantas",
"Rodrigo Rodriguez <me@rodrigorodriguez.com>", "Rodrigo Rodriguez ",
"Sarah Lourenco", "Sarah Lourenco",
"Thi Patriota", "Thi Patriota",
"Webgus", "Webgus",
"Zuilho Se", "Zuilho Se",
] ]
description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br" description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -38,18 +38,11 @@ repository = "https://github.com/GeneralBots/BotServer"
[features] [features]
default = ["desktop"] default = ["desktop"]
vectordb = ["qdrant-client"] vectordb = ["qdrant-client"]
email = ["imap"] email = ["imap"]
desktop = ["dep:tauri", "dep:tauri-plugin-dialog", "dep:tauri-plugin-opener"] desktop = ["dep:tauri", "dep:tauri-plugin-dialog", "dep:tauri-plugin-opener"]
[dependencies] [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" aes-gcm = "0.10"
anyhow = "1.0" anyhow = "1.0"
argon2 = "0.5" argon2 = "0.5"
@ -58,6 +51,7 @@ async-stream = "0.3"
async-trait = "0.1" async-trait = "0.1"
aws-config = "1.8.8" aws-config = "1.8.8"
aws-sdk-s3 = { version = "1.109.0", features = ["behavior-version-latest"] } aws-sdk-s3 = { version = "1.109.0", features = ["behavior-version-latest"] }
axum = { version = "0.8.7", features = ["ws", "multipart", "macros"] }
base64 = "0.22" base64 = "0.22"
bytes = "1.8" bytes = "1.8"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
@ -73,6 +67,7 @@ env_logger = "0.11"
futures = "0.3" futures = "0.3"
futures-util = "0.3" futures-util = "0.3"
hmac = "0.12.1" hmac = "0.12.1"
hyper = { version = "1.8.1", features = ["full"] }
imap = { version = "3.0.0-alpha.15", optional = true } imap = { version = "3.0.0-alpha.15", optional = true }
include_dir = "0.7" include_dir = "0.7"
indicatif = "0.18.0" indicatif = "0.18.0"
@ -106,6 +101,8 @@ tempfile = "3"
time = "0.3.44" time = "0.3.44"
tokio = { version = "1.41", features = ["full"] } tokio = { version = "1.41", features = ["full"] }
tokio-stream = "0.1" tokio-stream = "0.1"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "fs", "trace"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] } tracing-subscriber = { version = "0.3", features = ["fmt"] }
ureq = "3.1.2" ureq = "3.1.2"
@ -116,8 +113,6 @@ zip = "2.2"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[profile.release] [profile.release]
lto = true lto = true
opt-level = "z" opt-level = "z"

View file

@ -22,19 +22,20 @@ done
dirs=( dirs=(
"auth" "auth"
"automation" #"automation"
"basic" #"basic"
"bootstrap" #"bootstrap"
"bot" "bot"
#"channels" #"channels"
"config" #"config"
#"context" #"context"
"drive_monitor" #"drive_monitor"
#"email" "email"
#"file" "file"
#"kb" #"kb"
"llm" "llm"
#"llm_models" #"llm_models"
"meet"
#"org" #"org"
#"package_manager" #"package_manager"
#"riot_compiler" #"riot_compiler"
@ -43,7 +44,7 @@ dirs=(
#"tests" #"tests"
#"tools" #"tools"
#"ui" #"ui"
"ui_tree" #"ui_tree"
#"web_server" #"web_server"
#"web_automation" #"web_automation"
) )
@ -52,8 +53,7 @@ dirs=(
for dir in "${dirs[@]}"; do for dir in "${dirs[@]}"; do
find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read -r file; do find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read -r file; do
echo "$file" >> "$OUTPUT_FILE" echo "$file" >> "$OUTPUT_FILE"
filter_rust_file "$file" >> "$OUTPUT_FILE" cat "$file" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
done done
done done
@ -65,13 +65,8 @@ files=(
) )
for file in "${files[@]}"; do for file in "${files[@]}"; do
if [[ "$file" == *.rs ]]; then
echo "$file" >> "$OUTPUT_FILE"
filter_rust_file "$file" >> "$OUTPUT_FILE"
else
echo "$file" >> "$OUTPUT_FILE" echo "$file" >> "$OUTPUT_FILE"
cat "$file" >> "$OUTPUT_FILE" cat "$file" >> "$OUTPUT_FILE"
fi
done done
# Remove all blank lines and reduce whitespace greater than 1 space # Remove all blank lines and reduce whitespace greater than 1 space

View file

@ -18,5 +18,5 @@ When initial attempts fail, sequentially try these LLMs:
- Fix manually in case of dangerous trouble. - 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. - 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. - 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. - Ensure 'continue' on LLMs, they can EOF and say are done, but got more to output.

View file

@ -2,7 +2,7 @@ Generate a Rust service module following these patterns:
Core Structure: 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 Isolate shared resources (DB, clients, config) in AppState
@ -22,8 +22,6 @@ Error Handling:
Wrap fallible operations in Result Wrap fallible operations in Result
Use map_err to convert errors to actix_web::Error
Provide clear error messages (e.g., ErrorInternalServerError) Provide clear error messages (e.g., ErrorInternalServerError)
Async Patterns: Async Patterns:

View file

@ -1,124 +1,171 @@
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 log::error;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
use crate::shared::state::AppState;
pub struct AuthService {} pub struct AuthService {}
impl AuthService { impl AuthService {
pub fn new() -> Self { pub fn new() -> Self {
Self {} Self {}
} }
} }
#[actix_web::get("/api/auth")]
pub async fn auth_handler( pub async fn auth_handler(
_req: HttpRequest, State(state): State<Arc<AppState>>,
data: web::Data<AppState>, Query(params): Query<HashMap<String, String>>,
web::Query(params): web::Query<HashMap<String, String>>, ) -> impl IntoResponse {
) -> Result<HttpResponse> { // Extract parameters
let bot_name = params.get("bot_name").cloned().unwrap_or_default(); let bot_name = params.get("bot_name").cloned().unwrap_or_default();
let _token = params.get("token").cloned(); let _token = params.get("token").cloned();
let user_id = {
let mut sm = data.session_manager.lock().await; // Retrieve or create anonymous user
sm.get_or_create_anonymous_user(None).map_err(|e| { let user_id = {
error!("Failed to create anonymous user: {}", e); let mut sm = state.session_manager.lock().await;
actix_web::error::ErrorInternalServerError("Failed to create user") match sm.get_or_create_anonymous_user(None) {
})? Ok(id) => id,
}; Err(e) => {
let (bot_id, bot_name) = tokio::task::spawn_blocking({ error!("Failed to create anonymous user: {}", e);
let bot_name = bot_name.clone(); return (
let conn = data.conn.clone(); StatusCode::INTERNAL_SERVER_ERROR,
move || { Json(serde_json::json!({ "error": "Failed to create user" })),
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 };
.filter(name.eq(&bot_name))
.filter(is_active.eq(true)) // Resolve bot ID and name
.select((id, name)) let (bot_id, bot_name) = match tokio::task::spawn_blocking({
.first::<(Uuid, String)>(&mut db_conn) let bot_name = bot_name.clone();
.optional() let conn = state.conn.clone();
{ move || {
Ok(Some((id_val, name_val))) => Ok((id_val, name_val)), let mut db_conn = conn
Ok(None) => { .get()
match bots .map_err(|e| format!("Failed to get database connection: {}", e))?;
.filter(is_active.eq(true)) use crate::shared::models::schema::bots::dsl::*;
.select((id, name)) use diesel::prelude::*;
.first::<(Uuid, String)>(&mut db_conn) match bots
.optional() .filter(name.eq(&bot_name))
{ .filter(is_active.eq(true))
Ok(Some((id_val, name_val))) => Ok((id_val, name_val)), .select((id, name))
Ok(None) => Err("No active bots found".to_string()), .first::<(Uuid, String)>(&mut db_conn)
Err(e) => Err(format!("DB error: {}", e)), .optional()
} {
} Ok(Some((id_val, name_val))) => Ok((id_val, name_val)),
Err(e) => Err(format!("DB error: {}", e)), Ok(None) => match bots
} .filter(is_active.eq(true))
} .select((id, name))
}) .first::<(Uuid, String)>(&mut db_conn)
.await .optional()
.map_err(|e| { {
error!("Spawn blocking failed: {}", e); Ok(Some((id_val, name_val))) => Ok((id_val, name_val)),
actix_web::error::ErrorInternalServerError("DB thread error") Ok(None) => Err("No active bots found".to_string()),
})? Err(e) => Err(format!("DB error: {}", e)),
.map_err(|e| { },
error!("{}", e); Err(e) => Err(format!("DB error: {}", e)),
actix_web::error::ErrorInternalServerError(e) }
})?; }
let session = { })
let mut sm = data.session_manager.lock().await; .await
sm.get_or_create_user_session(user_id, bot_id, "Auth Session") {
.map_err(|e| { Ok(Ok(res)) => res,
error!("Failed to create session: {}", e); Ok(Err(e)) => {
actix_web::error::ErrorInternalServerError(e.to_string()) error!("{}", e);
})? return (
.ok_or_else(|| { StatusCode::INTERNAL_SERVER_ERROR,
error!("Failed to create session"); Json(serde_json::json!({ "error": e })),
actix_web::error::ErrorInternalServerError("Failed to create session") );
})? }
}; Err(e) => {
let auth_script_path = format!("./work/{}.gbai/{}.gbdialog/auth.ast", bot_name, bot_name); error!("Spawn blocking failed: {}", e);
if tokio::fs::metadata(&auth_script_path).await.is_ok() { return (
let auth_script = match tokio::fs::read_to_string(&auth_script_path).await { StatusCode::INTERNAL_SERVER_ERROR,
Ok(content) => content, Json(serde_json::json!({ "error": "DB thread error" })),
Err(e) => { );
error!("Failed to read auth script: {}", e); }
return Ok(HttpResponse::Ok().json(serde_json::json!({ };
"user_id": session.user_id,
"session_id": session.id, // Create session
"status": "authenticated" 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,
let script_service = crate::basic::ScriptService::new(Arc::clone(&data), session.clone()); Ok(None) => {
match tokio::time::timeout( error!("Failed to create session");
std::time::Duration::from_secs(5), return (
async { StatusCode::INTERNAL_SERVER_ERROR,
script_service Json(serde_json::json!({ "error": "Failed to create session" })),
.compile(&auth_script) );
.and_then(|ast| script_service.run(&ast)) }
} Err(e) => {
).await { error!("Failed to create session: {}", e);
Ok(Ok(result)) => { return (
if result.to_string() == "false" { StatusCode::INTERNAL_SERVER_ERROR,
error!("Auth script returned false"); Json(serde_json::json!({ "error": e.to_string() })),
return Ok(HttpResponse::Unauthorized() );
.json(serde_json::json!({"error": "Authentication failed"}))); }
} }
} };
Ok(Err(e)) => {
error!("Auth script execution error: {}", e); // Attempt to run auth script if present
} let auth_script_path = format!("./work/{}.gbai/{}.gbdialog/auth.ast", bot_name, bot_name);
Err(_) => { if tokio::fs::metadata(&auth_script_path).await.is_ok() {
error!("Auth script timeout"); 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);
Ok(HttpResponse::Ok().json(serde_json::json!({ return (
"user_id": session.user_id, StatusCode::OK,
"session_id": session.id, Json(serde_json::json!({
"status": "authenticated" "user_id": session.user_id,
}))) "session_id": session.id,
"status": "authenticated"
})),
);
}
};
// 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 error: {}", e);
}
Err(e) => {
error!("Auth script task error: {}", e);
}
}
}
// Return successful authentication response
(
StatusCode::OK,
Json(serde_json::json!({
"user_id": session.user_id,
"session_id": session.id,
"status": "authenticated"
})),
)
} }
#[cfg(test)] #[cfg(test)]
pub mod auth_test; pub mod auth_test;

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,18 @@
use crate::{config::EmailConfig, shared::state::AppState}; use crate::{config::EmailConfig, shared::state::AppState};
use log::info; use axum::{
use actix_web::error::ErrorInternalServerError; extract::{Path, State},
use actix_web::http::header::ContentType; http::StatusCode,
use actix_web::{web, HttpResponse, Result}; response::{IntoResponse, Response},
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; Json,
use serde::Serialize; };
use imap::types::Seq;
use mailparse::{parse_mail, MailHeaderMap};
use diesel::prelude::*; 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)] #[derive(Debug, Serialize)]
pub struct EmailResponse { pub struct EmailResponse {
pub id: String, pub id: String,
@ -19,6 +24,48 @@ pub struct EmailResponse {
read: bool, read: bool,
labels: Vec<String>, 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) { async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body: &str) {
let email = Message::builder() let email = Message::builder()
.from(config.from.parse().unwrap()) .from(config.from.parse().unwrap())
@ -35,49 +82,60 @@ async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body
.send(&email) .send(&email)
.unwrap(); .unwrap();
} }
#[actix_web::get("/emails/list")]
pub async fn list_emails( pub async fn list_emails(
state: web::Data<AppState>, State(state): State<Arc<AppState>>,
) -> Result<web::Json<Vec<EmailResponse>>, actix_web::Error> { ) -> Result<Json<Vec<EmailResponse>>, EmailError> {
let _config = state let _config = state
.config .config
.as_ref() .as_ref()
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; .ok_or_else(|| EmailError("Configuration not available".to_string()))?;
let tls = native_tls::TlsConnector::builder().build().map_err(|e| {
ErrorInternalServerError(format!("Failed to create TLS connector: {:?}", e)) let tls = native_tls::TlsConnector::builder()
})?; .build()
.map_err(|e| EmailError(format!("Failed to create TLS connector: {:?}", e)))?;
let client = imap::connect( let client = imap::connect(
(_config.email.server.as_str(), 993), (_config.email.server.as_str(), 993),
_config.email.server.as_str(), _config.email.server.as_str(),
&tls, &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 let mut session = client
.login(&_config.email.username, &_config.email.password) .login(&_config.email.username, &_config.email.password)
.map_err(|e| ErrorInternalServerError(format!("Login failed: {:?}", e)))?; .map_err(|e| EmailError(format!("Login failed: {:?}", e)))?;
session session
.select("INBOX") .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 let messages = session
.search("ALL") .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 mut email_list = Vec::new();
let recent_messages: Vec<_> = messages.iter().cloned().collect(); let recent_messages: Vec<_> = messages.iter().cloned().collect();
let recent_messages: Vec<Seq> = recent_messages.into_iter().rev().take(20).collect(); let recent_messages: Vec<Seq> = recent_messages.into_iter().rev().take(20).collect();
for seq in recent_messages { for seq in recent_messages {
let fetch_result = session.fetch(seq.to_string(), "RFC822"); let fetch_result = session.fetch(seq.to_string(), "RFC822");
let messages = fetch_result let messages =
.map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?; fetch_result.map_err(|e| EmailError(format!("Failed to fetch email: {:?}", e)))?;
for msg in messages.iter() { for msg in messages.iter() {
let body = msg let body = msg
.body() .body()
.ok_or_else(|| ErrorInternalServerError("No body found"))?; .ok_or_else(|| EmailError("No body found".to_string()))?;
let parsed = parse_mail(body) 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 headers = parsed.get_headers();
let subject = headers.get_first_value("Subject").unwrap_or_default(); let subject = headers.get_first_value("Subject").unwrap_or_default();
let from = headers.get_first_value("From").unwrap_or_default(); let from = headers.get_first_value("From").unwrap_or_default();
let date = headers.get_first_value("Date").unwrap_or_default(); let date = headers.get_first_value("Date").unwrap_or_default();
let body_text = if let Some(body_part) = parsed let body_text = if let Some(body_part) = parsed
.subparts .subparts
.iter() .iter()
@ -87,333 +145,176 @@ pub async fn list_emails(
} else { } else {
parsed.get_body().unwrap_or_default() parsed.get_body().unwrap_or_default()
}; };
let preview = body_text.lines().take(3).collect::<Vec<_>>().join(" "); let preview = body_text.lines().take(3).collect::<Vec<_>>().join(" ");
let preview_truncated = if preview.len() > 150 { let preview_truncated = if preview.len() > 150 {
format!("{}...", &preview[..150]) format!("{}...", &preview[..150])
} else { } else {
preview preview
}; };
let (from_name, from_email) = parse_from_field(&from); let (from_name, from_email) = parse_from_field(&from);
email_list.push(EmailResponse { email_list.push(EmailResponse {
id: seq.to_string(), id: seq.to_string(),
name: from_name, name: from_name,
email: from_email, email: from_email,
subject: if subject.is_empty() { subject,
"(No Subject)".to_string()
} else {
subject
},
text: preview_truncated, text: preview_truncated,
date: if date.is_empty() { date,
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()
} else {
date
},
read: false, read: false,
labels: Vec::new(), labels: vec![],
}); });
} }
} }
session
.logout() session.logout().ok();
.map_err(|e| ErrorInternalServerError(format!("Failed to logout: {:?}", e)))?; Ok(Json(email_list))
Ok(web::Json(email_list))
} }
fn parse_from_field(from: &str) -> (String, String) { fn parse_from_field(from: &str) -> (String, String) {
if let Some(start) = from.find('<') { if let Some(start) = from.find('<') {
if let Some(end) = 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 name = from[..start].trim().trim_matches('"').to_string();
let email = from[start + 1..end].to_string();
return (name, email); return (name, email);
} }
} }
("Unknown".to_string(), from.to_string()) (String::new(), from.to_string())
} }
#[derive(serde::Deserialize)]
pub struct SaveDraftRequest { async fn save_email_draft(
pub to: String, config: &EmailConfig,
pub subject: String, draft_data: &SaveDraftRequest,
pub cc: Option<String>, ) -> Result<String, Box<dyn std::error::Error>> {
pub text: String, 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( pub async fn save_draft(
state: web::Data<AppState>, State(state): State<Arc<AppState>>,
draft_data: web::Json<SaveDraftRequest>, Json(draft_data): Json<SaveDraftRequest>,
) -> Result<web::Json<SaveDraftResponse>, actix_web::Error> { ) -> Result<Json<SaveDraftResponse>, EmailError> {
let config = state let config = state
.config .config
.as_ref() .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 { match save_email_draft(&config.email, &draft_data).await {
Ok(draft_id) => Ok(web::Json(SaveDraftResponse { Ok(draft_id) => Ok(Json(SaveDraftResponse {
success: true, success: true,
message: "Draft saved successfully".to_string(),
draft_id: Some(draft_id), draft_id: Some(draft_id),
message: "Draft saved successfully".to_string(),
})), })),
Err(e) => Ok(web::Json(SaveDraftResponse { Err(e) => Ok(Json(SaveDraftResponse {
success: false, success: false,
message: format!("Failed to save draft: {}", e),
draft_id: None, 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( async fn fetch_latest_email_from_sender(
email_config: &EmailConfig, config: &EmailConfig,
from_email: &str, from_email: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let tls = native_tls::TlsConnector::builder().build()?; let tls = native_tls::TlsConnector::builder().build()?;
let client = imap::connect( let client = imap::connect((config.server.as_str(), 993), config.server.as_str(), &tls)?;
(email_config.server.as_str(), 993), let mut session = client.login(&config.username, &config.password)?;
email_config.server.as_str(), session.select("INBOX")?;
&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() {
session.select("INBOX")?;
}
let search_query = format!("FROM \"{}\"", from_email); let search_query = format!("FROM \"{}\"", from_email);
let messages = session.search(&search_query)?; let messages = session.search(&search_query)?;
if messages.is_empty() {
session.logout()?; if let Some(&seq) = messages.last() {
return Err(format!("No emails found from {}", from_email).into()); let fetch_result = session.fetch(seq.to_string(), "RFC822")?;
} for msg in fetch_result.iter() {
let latest_seq = messages.iter().max().unwrap(); if let Some(body) = msg.body() {
let messages = session.fetch(latest_seq.to_string(), "RFC822")?; let parsed = parse_mail(body)?;
let mut email_text = String::new(); let body_text = if let Some(body_part) = parsed
for msg in messages.iter() { .subparts
let body = msg.body().ok_or("No body found in email")?; .iter()
let parsed = parse_mail(body)?; .find(|p| p.ctype.mimetype == "text/plain")
let headers = parsed.get_headers(); {
let subject = headers.get_first_value("Subject").unwrap_or_default(); body_part.get_body().unwrap_or_default()
let from = headers.get_first_value("From").unwrap_or_default(); } else {
let date = headers.get_first_value("Date").unwrap_or_default(); parsed.get_body().unwrap_or_default()
let to = headers.get_first_value("To").unwrap_or_default(); };
let body_text = if let Some(body_part) = parsed session.logout().ok();
.subparts return Ok(body_text);
.iter()
.find(|p| p.ctype.mimetype == "text/plain")
{
body_part.get_body().unwrap_or_default()
} 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)
}
}
#[actix_web::post("/emails/get_latest_from")]
pub async fn get_latest_email_from(
state: web::Data<AppState>,
request: web::Json<GetLatestEmailRequest>,
) -> Result<web::Json<LatestEmailResponse>, actix_web::Error> {
let config = state
.config
.as_ref()
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
match fetch_latest_email_from_sender(&config.email, &request.from_email).await {
Ok(email_text) => Ok(web::Json(LatestEmailResponse {
success: true,
email_text: Some(email_text),
message: "Latest email retrieved successfully".to_string(),
})),
Err(e) => {
if e.to_string().contains("No emails found") {
Ok(web::Json(LatestEmailResponse {
success: false,
email_text: None,
message: e.to_string(),
}))
} else {
Err(ErrorInternalServerError(e))
} }
} }
} }
session.logout().ok();
Err("No email found from sender".into())
} }
pub async fn fetch_latest_sent_to(
email_config: &EmailConfig, pub async fn get_latest_email_from(
to_email: &str, State(state): State<Arc<AppState>>,
) -> Result<String, Box<dyn std::error::Error>> { Json(request): Json<GetLatestEmailRequest>,
let tls = native_tls::TlsConnector::builder().build()?; ) -> Result<Json<LatestEmailResponse>, EmailError> {
let client = imap::connect( let config = state
(email_config.server.as_str(), 993), .config
email_config.server.as_str(), .as_ref()
&tls, .ok_or_else(|| EmailError("Configuration not available".to_string()))?;
)?;
let mut session = client match fetch_latest_email_from_sender(&config.email, &request.from_email).await {
.login(&email_config.username, &email_config.password) Ok(email_text) => Ok(Json(LatestEmailResponse {
.map_err(|e| format!("Login failed: {:?}", e))?; success: true,
if session.select("Sent").is_err() { email_text: Some(email_text),
session.select("Sent Items")?; message: "Email retrieved successfully".to_string(),
} })),
let search_query = format!("TO \"{}\"", to_email); Err(e) => Ok(Json(LatestEmailResponse {
let messages = session.search(&search_query)?; success: false,
if messages.is_empty() { email_text: None,
session.logout()?; message: format!("Failed to retrieve email: {}", e),
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( pub async fn send_email(
payload: web::Json<(String, String, String)>, State(state): State<Arc<AppState>>,
state: web::Data<AppState>, Json(payload): Json<(String, String, String)>,
) -> Result<HttpResponse, actix_web::Error> { ) -> Result<StatusCode, EmailError> {
let (to, subject, body) = payload.into_inner(); let (to, subject, body) = payload;
info!("To: {}", to); info!("To: {}", to);
info!("Subject: {}", subject); info!("Subject: {}", subject);
info!("Body: {}", body); 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( pub async fn save_click(
path: web::Path<(String, String)>, Path((campaign_id, email)): Path<(String, String)>,
state: web::Data<AppState>, State(_state): State<Arc<AppState>>,
) -> HttpResponse { ) -> impl IntoResponse {
let (campaign_id, email) = path.into_inner(); // Log the click event
use crate::shared::models::clicks; info!(
let _ = diesel::insert_into(clicks::table) "Click tracked - Campaign: {}, Email: {}",
.values(( campaign_id, email
clicks::campaign_id.eq(campaign_id), );
clicks::email.eq(email),
clicks::updated_at.eq(diesel::dsl::now), // Return a 1x1 transparent GIF pixel
)) let pixel: Vec<u8> = vec![
.on_conflict((clicks::campaign_id, clicks::email)) 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xFF, 0xFF,
.do_update() 0xFF, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00,
.set(clicks::updated_at.eq(diesel::dsl::now)) 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3B,
.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,
]; ];
HttpResponse::Ok()
.content_type(ContentType::png()) (StatusCode::OK, [("content-type", "image/gif")], pixel)
.body(pixel.to_vec())
} }
#[actix_web::get("/campaigns/{campaign_id}/emails")]
pub async fn get_emails(path: web::Path<String>, state: web::Data<AppState>) -> String { pub async fn get_emails(
let campaign_id = path.into_inner(); Path(campaign_id): Path<String>,
use crate::shared::models::clicks::dsl::*; State(_state): State<Arc<AppState>>,
let rows = clicks ) -> String {
.filter(campaign_id.eq(campaign_id)) // Return placeholder response
.select(email) info!("Get emails requested for campaign: {}", campaign_id);
.load::<String>(&state.conn) "No emails tracked".to_string()
.unwrap_or_default();
rows.join(",")
} }

View file

@ -1,60 +1,83 @@
use crate::shared::state::AppState; use crate::shared::state::AppState;
use actix_multipart::Multipart;
use actix_web::web;
use actix_web::{post, HttpResponse};
use aws_sdk_s3::Client; use aws_sdk_s3::Client;
use axum::{
extract::{Multipart, Path, State},
http::StatusCode,
response::IntoResponse,
};
use std::io::Write; use std::io::Write;
use std::sync::Arc;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tokio_stream::StreamExt as TokioStreamExt;
#[post("/files/upload/{folder_path}")]
pub async fn upload_file( pub async fn upload_file(
folder_path: web::Path<String>, Path(folder_path): Path<String>,
mut payload: Multipart, State(state): State<Arc<AppState>>,
state: web::Data<AppState>, mut multipart: Multipart,
) -> Result<HttpResponse, actix_web::Error> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let folder_path = folder_path.into_inner();
let mut temp_file = NamedTempFile::new().map_err(|e| { 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; let mut file_name: Option<String> = None;
while let Some(mut field) = payload.try_next().await? {
if let Some(disposition) = field.content_disposition() { while let Some(field) = multipart.next_field().await.map_err(|e| {
if let Some(name) = disposition.get_filename() { (
file_name = Some(name.to_string()); StatusCode::BAD_REQUEST,
} format!("Failed to read multipart field: {}", e),
} )
while let Some(chunk) = field.try_next().await? { })? {
temp_file.write_all(&chunk).map_err(|e| { if let Some(name) = field.file_name() {
actix_web::error::ErrorInternalServerError(format!( file_name = Some(name.to_string());
"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 file_name = file_name.unwrap_or_else(|| "unnamed_file".to_string());
let temp_file_path = temp_file.into_temp_path(); 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); 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(_) => { Ok(_) => {
let _ = std::fs::remove_file(&temp_file_path); let _ = std::fs::remove_file(&temp_file_path);
Ok(HttpResponse::Ok().body(format!( Ok((
"Uploaded file '{}' to folder '{}'", StatusCode::OK,
file_name, folder_path format!("Uploaded file '{}' to folder '{}'", file_name, folder_path),
))) ))
} }
Err(e) => { Err(e) => {
let _ = std::fs::remove_file(&temp_file_path); let _ = std::fs::remove_file(&temp_file_path);
Err(actix_web::error::ErrorInternalServerError(format!( Err((
"Failed to upload file to S3: {}", StatusCode::INTERNAL_SERVER_ERROR,
e format!("Failed to upload file to S3: {}", e),
))) ))
} }
} }
} }
async fn upload_to_s3( async fn upload_to_s3(
client: &Client, client: &Client,
bucket: &str, bucket: &str,
@ -62,7 +85,8 @@ async fn upload_to_s3(
file_path: &std::path::Path, file_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let data = std::fs::read(file_path)?; let data = std::fs::read(file_path)?;
client.put_object() client
.put_object()
.bucket(bucket) .bucket(bucket)
.key(key) .key(key)
.body(data.into()) .body(data.into())

View file

@ -1,28 +1,30 @@
use crate::config::ConfigManager; use crate::config::ConfigManager;
use crate::shared::models::schema::bots::dsl::*; use crate::shared::models::schema::bots::dsl::*;
use crate::shared::state::AppState; use crate::shared::state::AppState;
use actix_web::{post, web, HttpResponse, Result}; use axum::{extract::State, http::StatusCode, response::Json};
use diesel::prelude::*; use diesel::prelude::*;
use log::{error, info}; use log::{error, info};
use reqwest; use reqwest;
use std::sync::Arc; use std::sync::Arc;
use tokio; use tokio;
#[post("/api/chat/completions")]
pub async fn chat_completions_local( pub async fn chat_completions_local(
_data: web::Data<AppState>, State(_data): State<Arc<AppState>>,
_payload: web::Json<serde_json::Value>, Json(_payload): Json<serde_json::Value>,
) -> Result<HttpResponse> { ) -> (StatusCode, Json<serde_json::Value>) {
Ok(HttpResponse::Ok() (
.json(serde_json::json!({ "status": "chat_completions_local not implemented" }))) StatusCode::OK,
Json(serde_json::json!({ "status": "chat_completions_local not implemented" })),
)
} }
#[post("/api/embeddings")]
pub async fn embeddings_local( pub async fn embeddings_local(
_data: web::Data<AppState>, State(_data): State<Arc<AppState>>,
_payload: web::Json<serde_json::Value>, Json(_payload): Json<serde_json::Value>,
) -> Result<HttpResponse> { ) -> (StatusCode, Json<serde_json::Value>) {
Ok( (
HttpResponse::Ok() StatusCode::OK,
.json(serde_json::json!({ "status": "embeddings_local not implemented" })), Json(serde_json::json!({ "status": "embeddings_local not implemented" })),
) )
} }
pub async fn ensure_llama_servers_running( pub async fn ensure_llama_servers_running(
@ -68,7 +70,7 @@ pub async fn ensure_llama_servers_running(
info!(" Embedding Model: {}", embedding_model); info!(" Embedding Model: {}", embedding_model);
info!(" LLM Server Path: {}", llm_server_path); info!(" LLM Server Path: {}", llm_server_path);
info!("Restarting any existing llama-server processes..."); info!("Restarting any existing llama-server processes...");
if let Err(e) = tokio::process::Command::new("sh") if let Err(e) = tokio::process::Command::new("sh")
.arg("-c") .arg("-c")
.arg("pkill llama-server -9 || true") .arg("pkill llama-server -9 || true")
@ -161,7 +163,7 @@ pub async fn ensure_llama_servers_running(
} }
if llm_ready && embedding_ready { if llm_ready && embedding_ready {
info!("All llama.cpp servers are ready and responding!"); info!("All llama.cpp servers are ready and responding!");
// Update LLM provider with new endpoints // Update LLM provider with new endpoints
let _llm_provider1 = Arc::new(crate::llm::OpenAIClient::new( let _llm_provider1 = Arc::new(crate::llm::OpenAIClient::new(
llm_model.clone(), llm_model.clone(),
@ -229,15 +231,15 @@ pub async fn start_llm_server(
.get_config(&default_bot_id, "llm-server-n-predict", None) .get_config(&default_bot_id, "llm-server-n-predict", None)
.unwrap_or("50".to_string()); .unwrap_or("50".to_string());
let n_ctx_size = config_manager let n_ctx_size = config_manager
.get_config(&default_bot_id, "llm-server-ctx-size", None) .get_config(&default_bot_id, "llm-server-ctx-size", None)
.unwrap_or("4096".to_string()); .unwrap_or("4096".to_string());
// TODO: Move flash-attn, temp, top_p, repeat-penalty to config as well. // TODO: Move flash-attn, temp, top_p, repeat-penalty to config as well.
// TODO: Create --jinja. // TODO: Create --jinja.
// --jinja --flash-attn on // --jinja --flash-attn on
let mut args = format!( let mut args = format!(
"-m {} --host 0.0.0.0 --port {} --top_p 0.95 --temp 0.6 --repeat-penalty 1.2 --n-gpu-layers {}", "-m {} --host 0.0.0.0 --port {} --top_p 0.95 --temp 0.6 --repeat-penalty 1.2 --n-gpu-layers {}",
model_path, port, gpu_layers model_path, port, gpu_layers
); );
@ -245,8 +247,6 @@ pub async fn start_llm_server(
args.push_str(&format!(" --reasoning-format {}", reasoning_format)); args.push_str(&format!(" --reasoning-format {}", reasoning_format));
} }
if n_moe != "0" { if n_moe != "0" {
args.push_str(&format!(" --n-cpu-moe {}", n_moe)); args.push_str(&format!(" --n-cpu-moe {}", n_moe));
} }
@ -265,8 +265,8 @@ pub async fn start_llm_server(
if n_predict != "0" { if n_predict != "0" {
args.push_str(&format!(" --n-predict {}", n_predict)); args.push_str(&format!(" --n-predict {}", n_predict));
} }
args.push_str(&format!(" --ctx-size {}", n_ctx_size)); args.push_str(&format!(" --ctx-size {}", n_ctx_size));
if cfg!(windows) { if cfg!(windows) {
let mut cmd = tokio::process::Command::new("cmd"); let mut cmd = tokio::process::Command::new("cmd");
cmd.arg("/C").arg(format!( cmd.arg("/C").arg(format!(

View file

@ -1,11 +1,16 @@
#![cfg_attr(feature = "desktop", windows_subsystem = "windows")] #![cfg_attr(feature = "desktop", windows_subsystem = "windows")]
use actix_cors::Cors; use axum::{
use actix_web::middleware::Logger; routing::{get, post},
use actix_web::{web, App, HttpServer}; Router,
};
use dotenvy::dotenv; use dotenvy::dotenv;
use log::{error, info}; use log::{error, info};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
mod auth; mod auth;
mod automation; mod automation;
@ -61,56 +66,97 @@ pub enum BootstrapProgress {
BootstrapError(String), BootstrapError(String),
} }
async fn run_axum_server(
async fn run_http_server(
app_state: Arc<AppState>, app_state: Arc<AppState>,
port: u16, port: u16,
worker_count: usize, _worker_count: usize,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
HttpServer::new(move || { // CORS configuration
let cors = Cors::default() let cors = CorsLayer::new()
.allow_any_origin() .allow_origin(tower_http::cors::Any)
.allow_any_method() .allow_methods(tower_http::cors::Any)
.allow_any_header() .allow_headers(tower_http::cors::Any)
.max_age(3600); .max_age(std::time::Duration::from_secs(3600));
let mut app = App::new() // Build API routes with State
.wrap(cors) let api_router = Router::new()
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i")) // Auth route
.app_data(web::Data::from(app_state.clone())) .route("/api/auth", get(auth_handler))
.service(auth_handler) // Session routes
.service(create_session) .route("/api/sessions", post(create_session))
.service(get_session_history) .route("/api/sessions", get(get_sessions))
.service(get_sessions) .route(
.service(start_session) "/api/sessions/{session_id}/history",
.service(upload_file) get(get_session_history),
.service(voice_start) )
.service(voice_stop) .route("/api/sessions/{session_id}/start", post(start_session))
.service(websocket_handler) // File routes
.service(crate::bot::create_bot_handler) .route("/api/files/upload/{folder_path}", post(upload_file))
.service(crate::bot::mount_bot_handler) // Voice/Meet routes
.service(crate::bot::handle_user_input_handler) .route("/api/voice/start", post(voice_start))
.service(crate::bot::get_user_sessions_handler) .route("/api/voice/stop", post(voice_stop))
.service(crate::bot::get_conversation_history_handler) // WebSocket route
.service(crate::bot::send_warning_handler); .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),
);
#[cfg(feature = "email")] // Add email routes if feature is enabled
{ #[cfg(feature = "email")]
app = app let api_router = api_router
.service(get_latest_email_from) .route("/api/email/latest", post(get_latest_email_from))
.service(get_emails) .route("/api/email/get/{campaign_id}", get(get_emails))
.service(list_emails) .route("/api/email/list", get(list_emails))
.service(send_email) .route("/api/email/send", post(send_email))
.service(save_draft) .route("/api/email/draft", post(save_draft))
.service(save_click); .route("/api/email/click/{campaign_id}/{email}", get(save_click));
}
app.configure(web_server::configure_app) // Build static file serving
}) let static_path = std::path::Path::new("./web/desktop");
.workers(worker_count)
.bind(("0.0.0.0", port))? let app = Router::new()
.run() .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 .await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
} }
#[tokio::main] #[tokio::main]
@ -138,8 +184,8 @@ async fn main() -> std::io::Result<()> {
if args.len() > 1 { if args.len() > 1 {
let command = &args[1]; let command = &args[1];
match command.as_str() { match command.as_str() {
"install" | "remove" | "list" | "status" | "start" | "stop" | "restart" "install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help"
| "--help" | "-h" => match package_manager::cli::run().await { | "-h" => match package_manager::cli::run().await {
Ok(_) => return Ok(()), Ok(_) => return Ok(()),
Err(e) => { Err(e) => {
eprintln!("CLI error: {}", e); eprintln!("CLI error: {}", e);
@ -299,8 +345,8 @@ async fn main() -> std::io::Result<()> {
} }
}; };
let cache_url = std::env::var("CACHE_URL") let cache_url =
.unwrap_or_else(|_| "redis://localhost:6379".to_string()); std::env::var("CACHE_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
let redis_client = match redis::Client::open(cache_url.as_str()) { let redis_client = match redis::Client::open(cache_url.as_str()) {
Ok(client) => Some(Arc::new(client)), Ok(client) => Some(Arc::new(client)),
Err(e) => { 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 // For desktop mode: Run HTTP server in a separate thread with its own runtime
let app_state_for_server = app_state.clone(); let app_state_for_server = app_state.clone();
let port = config.server.port; 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 || { std::thread::spawn(move || {
info!("HTTP server thread started, initializing runtime...");
let rt = tokio::runtime::Runtime::new().expect("Failed to create HTTP runtime"); let rt = tokio::runtime::Runtime::new().expect("Failed to create HTTP runtime");
rt.block_on(async move { 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); 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) // Run Tauri on main thread (GUI requires main thread)
let tauri_app = tauri::Builder::default() let tauri_app = tauri::Builder::default()
.setup(|app| { .setup(|app| {
@ -442,7 +506,7 @@ async fn main() -> std::io::Result<()> {
} }
// Non-desktop mode: Run HTTP server directly // 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 // Wait for UI thread to finish if it was started
if let Some(handle) = ui_handle { if let Some(handle) = ui_handle {

View file

@ -1,11 +1,18 @@
use actix_web::{web, HttpResponse, Result}; use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Json},
};
use log::{error, info}; use log::{error, info};
use serde_json::Value;
use std::sync::Arc;
use crate::shared::state::AppState; use crate::shared::state::AppState;
#[actix_web::post("/api/voice/start")]
async fn voice_start( pub async fn voice_start(
data: web::Data<AppState>, State(data): State<Arc<AppState>>,
info: web::Json<serde_json::Value>, Json(info): Json<Value>,
) -> Result<HttpResponse> { ) -> impl IntoResponse {
let session_id = info let session_id = info
.get("session_id") .get("session_id")
.and_then(|s| s.as_str()) .and_then(|s| s.as_str())
@ -14,10 +21,12 @@ async fn voice_start(
.get("user_id") .get("user_id")
.and_then(|u| u.as_str()) .and_then(|u| u.as_str())
.unwrap_or("user"); .unwrap_or("user");
info!( info!(
"Voice session start request - session: {}, user: {}", "Voice session start request - session: {}, user: {}",
session_id, user_id session_id, user_id
); );
match data match data
.voice_adapter .voice_adapter
.start_voice_session(session_id, user_id) .start_voice_session(session_id, user_id)
@ -28,42 +37,53 @@ async fn voice_start(
"Voice session started successfully for session {}", "Voice session started successfully for session {}",
session_id 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) => { Err(e) => {
error!( error!(
"Failed to start voice session for session {}: {}", "Failed to start voice session for session {}: {}",
session_id, e 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( pub async fn voice_stop(
data: web::Data<AppState>, State(data): State<Arc<AppState>>,
info: web::Json<serde_json::Value>, Json(info): Json<Value>,
) -> Result<HttpResponse> { ) -> impl IntoResponse {
let session_id = info let session_id = info
.get("session_id") .get("session_id")
.and_then(|s| s.as_str()) .and_then(|s| s.as_str())
.unwrap_or(""); .unwrap_or("");
match data.voice_adapter.stop_voice_session(session_id).await { match data.voice_adapter.stop_voice_session(session_id).await {
Ok(()) => { Ok(()) => {
info!( info!(
"Voice session stopped successfully for session {}", "Voice session stopped successfully for session {}",
session_id session_id
); );
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "stopped"}))) (
StatusCode::OK,
Json(serde_json::json!({"status": "stopped"})),
)
} }
Err(e) => { Err(e) => {
error!( error!(
"Failed to stop voice session for session {}: {}", "Failed to stop voice session for session {}: {}",
session_id, e 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()})),
)
} }
} }
} }

View file

@ -1,33 +1,42 @@
use crate::bot::BotOrchestrator; use crate::bot::BotOrchestrator;
use crate::shared::models::UserSession; use crate::shared::models::UserSession;
use crate::shared::state::AppState; 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 chrono::Utc;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, PooledConnection}; use diesel::r2d2::{ConnectionManager, PooledConnection};
use diesel::PgConnection; use diesel::PgConnection;
use log::trace; use log::{error, trace, warn};
use log::{error, warn};
use redis::Client; use redis::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::error::Error; use std::error::Error;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct SessionData { pub struct SessionData {
pub id: Uuid, pub id: Uuid,
pub user_id: Option<Uuid>, pub user_id: Option<Uuid>,
pub data: String, pub data: String,
} }
pub struct SessionManager { pub struct SessionManager {
conn: PooledConnection<ConnectionManager<PgConnection>>, conn: PooledConnection<ConnectionManager<PgConnection>>,
sessions: HashMap<Uuid, SessionData>, sessions: HashMap<Uuid, SessionData>,
waiting_for_input: HashSet<Uuid>, waiting_for_input: HashSet<Uuid>,
redis: Option<Arc<Client>>, redis: Option<Arc<Client>>,
} }
impl SessionManager { 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 { SessionManager {
conn, conn,
sessions: HashMap::new(), sessions: HashMap::new(),
@ -35,6 +44,7 @@ impl SessionManager {
redis: redis_client, redis: redis_client,
} }
} }
pub fn provide_input( pub fn provide_input(
&mut self, &mut self,
session_id: Uuid, session_id: Uuid,
@ -59,9 +69,11 @@ impl SessionManager {
Ok(Some("user_input".to_string())) Ok(Some("user_input".to_string()))
} }
} }
pub fn mark_waiting(&mut self, session_id: Uuid) { pub fn mark_waiting(&mut self, session_id: Uuid) {
self.waiting_for_input.insert(session_id); self.waiting_for_input.insert(session_id);
} }
pub fn get_session_by_id( pub fn get_session_by_id(
&mut self, &mut self,
session_id: Uuid, session_id: Uuid,
@ -73,6 +85,7 @@ impl SessionManager {
.optional()?; .optional()?;
Ok(result) Ok(result)
} }
pub fn get_user_session( pub fn get_user_session(
&mut self, &mut self,
uid: Uuid, uid: Uuid,
@ -87,6 +100,7 @@ impl SessionManager {
.optional()?; .optional()?;
Ok(result) Ok(result)
} }
pub fn get_or_create_user_session( pub fn get_or_create_user_session(
&mut self, &mut self,
uid: Uuid, uid: Uuid,
@ -98,6 +112,7 @@ impl SessionManager {
} }
self.create_session(uid, bid, session_title).map(Some) self.create_session(uid, bid, session_title).map(Some)
} }
pub fn get_or_create_anonymous_user( pub fn get_or_create_anonymous_user(
&mut self, &mut self,
uid: Option<Uuid>, uid: Option<Uuid>,
@ -128,6 +143,7 @@ impl SessionManager {
} }
Ok(user_id) Ok(user_id)
} }
pub fn create_session( pub fn create_session(
&mut self, &mut self,
uid: Uuid, uid: Uuid,
@ -156,12 +172,13 @@ impl SessionManager {
})?; })?;
Ok(inserted) Ok(inserted)
} }
fn _clear_messages(&mut self, _session_id: Uuid) -> Result<(), Box<dyn Error + Send + Sync>> { fn _clear_messages(&mut self, _session_id: Uuid) -> Result<(), Box<dyn Error + Send + Sync>> {
use crate::shared::models::message_history::dsl::*; use crate::shared::models::message_history::dsl::*;
diesel::delete(message_history.filter(session_id.eq(session_id))) diesel::delete(message_history.filter(session_id.eq(session_id))).execute(&mut self.conn)?;
.execute(&mut self.conn)?;
Ok(()) Ok(())
} }
pub fn save_message( pub fn save_message(
&mut self, &mut self,
sess_id: Uuid, sess_id: Uuid,
@ -195,6 +212,7 @@ impl SessionManager {
); );
Ok(()) Ok(())
} }
pub async fn update_session_context( pub async fn update_session_context(
&mut self, &mut self,
session_id: &Uuid, session_id: &Uuid,
@ -211,6 +229,7 @@ impl SessionManager {
} }
Ok(()) Ok(())
} }
pub async fn get_session_context_data( pub async fn get_session_context_data(
&self, &self,
session_id: &Uuid, session_id: &Uuid,
@ -259,6 +278,7 @@ impl SessionManager {
} }
Ok(String::new()) Ok(String::new())
} }
pub fn get_conversation_history( pub fn get_conversation_history(
&mut self, &mut self,
sess_id: Uuid, sess_id: Uuid,
@ -283,6 +303,7 @@ impl SessionManager {
} }
Ok(history) Ok(history)
} }
pub fn get_user_sessions( pub fn get_user_sessions(
&mut self, &mut self,
uid: Uuid, uid: Uuid,
@ -300,6 +321,7 @@ impl SessionManager {
}; };
Ok(sessions) Ok(sessions)
} }
pub fn update_user_id( pub fn update_user_id(
&mut self, &mut self,
session_id: Uuid, session_id: Uuid,
@ -317,108 +339,111 @@ impl SessionManager {
Ok(()) 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 user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
let bot_id = Uuid::nil(); let bot_id = Uuid::nil();
let session_result = { 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") sm.get_or_create_user_session(user_id, bot_id, "New Conversation")
}; };
let session = match session_result { match session_result {
Ok(Some(s)) => s, Ok(Some(session)) => (
Ok(None) => { StatusCode::OK,
error!("Failed to create session"); Json(serde_json::json!({
return Ok(HttpResponse::InternalServerError() "session_id": session.id,
.json(serde_json::json!({"error": "Failed to create session"}))); "title": "New Conversation",
} "created_at": Utc::now()
Err(e) => { })),
error!("Failed to create session: {}", e); ),
return Ok(HttpResponse::InternalServerError() Ok(None) => (
.json(serde_json::json!({"error": e.to_string()}))); StatusCode::INTERNAL_SERVER_ERROR,
} Json(serde_json::json!({ "error": "Failed to create session" })),
}; ),
Ok(HttpResponse::Ok().json(serde_json::json!({ Err(e) => (
"session_id": session.id, StatusCode::INTERNAL_SERVER_ERROR,
"title": "New Conversation", Json(serde_json::json!({ "error": e.to_string() })),
"created_at": Utc::now() ),
})))
}
#[actix_web::get("/api/sessions")]
async fn get_sessions(data: web::Data<AppState>) -> Result<HttpResponse> {
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
let orchestrator = BotOrchestrator::new(Arc::new(data.get_ref().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()})))
}
} }
} }
#[actix_web::post("/api/sessions/{session_id}/start")]
async fn start_session(data: web::Data<AppState>, path: web::Path<String>) -> Result<HttpResponse> { /// Get list of sessions for the anonymous user
let session_id = path.into_inner(); 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(state.clone());
match orchestrator.get_user_sessions(user_id).await {
Ok(sessions) => (StatusCode::OK, Json(serde_json::json!(sessions))),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
),
}
}
/// 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) { match Uuid::parse_str(&session_id) {
Ok(session_uuid) => { Ok(session_uuid) => {
let mut session_manager = data.session_manager.lock().await; let mut sm = state.session_manager.lock().await;
match session_manager.get_session_by_id(session_uuid) { match sm.get_session_by_id(session_uuid) {
Ok(Some(_session)) => { Ok(Some(_)) => {
session_manager.mark_waiting(session_uuid); sm.mark_waiting(session_uuid);
Ok(HttpResponse::Ok().json(serde_json::json!({ (
"status": "started", StatusCode::OK,
"session_id": session_id 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(_) => { Err(_) => (
warn!("Invalid session ID format: {}", session_id); StatusCode::BAD_REQUEST,
Ok(HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid session ID"}))) Json(serde_json::json!({ "error": "Invalid session ID" })),
} ),
} }
} }
#[actix_web::get("/api/sessions/{session_id}")]
async fn get_session_history( /// Get conversation history for a session
data: web::Data<AppState>, pub async fn get_session_history(
path: web::Path<String>, Extension(state): Extension<Arc<AppState>>,
) -> Result<HttpResponse> { Path(session_id): Path<String>,
let session_id = path.into_inner(); ) -> impl IntoResponse {
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
match Uuid::parse_str(&session_id) { match Uuid::parse_str(&session_id) {
Ok(session_uuid) => { Ok(session_uuid) => {
let orchestrator = BotOrchestrator::new(Arc::new(data.get_ref().clone())); let orchestrator = BotOrchestrator::new(state.clone());
match orchestrator match orchestrator
.get_conversation_history(session_uuid, user_id) .get_conversation_history(session_uuid, user_id)
.await .await
{ {
Ok(history) => { Ok(history) => (StatusCode::OK, Json(serde_json::json!(history))),
trace!( Err(e) => (
"Retrieved {} history entries for session {}", StatusCode::INTERNAL_SERVER_ERROR,
history.len(), Json(serde_json::json!({ "error": e.to_string() })),
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()})))
}
} }
} }
Err(_) => { Err(_) => (
warn!("Invalid session ID format: {}", session_id); StatusCode::BAD_REQUEST,
Ok(HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid session ID"}))) Json(serde_json::json!({ "error": "Invalid session ID" })),
} ),
} }
} }

View file

@ -1,75 +1,39 @@
use actix_files::Files; use axum::{
use actix_web::{HttpResponse, Result}; Router,
routing::get,
response::{Html, IntoResponse},
http::StatusCode,
};
use tower_http::services::ServeDir;
use log::error; use log::error;
use std::{fs, path::Path}; use std::{fs, path::PathBuf};
#[actix_web::get("/")] pub async fn index() -> impl IntoResponse {
async fn index() -> Result<HttpResponse> {
match fs::read_to_string("web/desktop/index.html") { 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) => { Err(e) => {
error!("Failed to load index page: {}", 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) { Router::new()
let static_path = Path::new("./web/desktop"); // Serve all JS files
.nest_service("/js", ServeDir::new(static_path.join("js")))
// Serve all JS files // Serve CSS files
cfg.service( .nest_service("/css", ServeDir::new(static_path.join("css")))
Files::new("/js", static_path.join("js")) .nest_service("/drive", ServeDir::new(static_path.join("drive")))
.prefer_utf8(true) .nest_service("/chat", ServeDir::new(static_path.join("chat")))
.use_last_modified(true) .nest_service("/mail", ServeDir::new(static_path.join("mail")))
.use_etag(true) .nest_service("/tasks", ServeDir::new(static_path.join("tasks")))
); // Fallback: serve static files and index.html for SPA routing
.fallback_service(
// Serve CSS files ServeDir::new(static_path.clone())
cfg.service( .fallback(ServeDir::new(static_path.clone()).append_index_html_on_directories(true))
Files::new("/css", static_path.join("css")) )
.prefer_utf8(true) .route("/", get(index))
.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);
} }

View file

@ -9,7 +9,6 @@ const sectionCache = {};
function getBasePath() { function getBasePath() {
// All static assets (HTML, CSS, JS) are served from the site root. // All static assets (HTML, CSS, JS) are served from the site root.
// Returning a leading slash ensures URLs like "/drive/drive.html" resolve correctly // Returning a leading slash ensures URLs like "/drive/drive.html" resolve correctly
// with the Actix static file configuration.
return '/'; return '/';
} }