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"],
"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
View file

@ -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",

View file

@ -3,34 +3,34 @@ name = "botserver"
version = "6.0.8"
edition = "2021"
authors = [
"Pragmatismo.com.br <contact@pragmatismo.com.br>",
"General Bots Community <https://github.com/GeneralBots>",
"Alan Perdomo",
"Ana Paula Gil",
"Arenas.io",
"Atylla L",
"Christopher de Castilho",
"Dario Junior",
"David Lerner",
"Experimentation Garage",
"Flavio Andrade",
"Heraldo Almeida",
"Joao Parana",
"Jonathas C",
"J Ramos",
"Lucas Picanco",
"Marcos Velasco",
"Matheus 39x",
"Oerlabs Henrique",
"Othon Lima",
"PH Nascimento",
"Phpussente",
"Robson Dantas",
"Rodrigo Rodriguez <me@rodrigorodriguez.com>",
"Sarah Lourenco",
"Thi Patriota",
"Webgus",
"Zuilho Se",
"Pragmatismo.com.br ",
"General Bots Community ",
"Alan Perdomo",
"Ana Paula Gil",
"Arenas.io",
"Atylla L",
"Christopher de Castilho",
"Dario Junior",
"David Lerner",
"Experimentation Garage",
"Flavio Andrade",
"Heraldo Almeida",
"Joao Parana",
"Jonathas C",
"J Ramos",
"Lucas Picanco",
"Marcos Velasco",
"Matheus 39x",
"Oerlabs Henrique",
"Othon Lima",
"PH Nascimento",
"Phpussente",
"Robson Dantas",
"Rodrigo Rodriguez ",
"Sarah Lourenco",
"Thi Patriota",
"Webgus",
"Zuilho Se",
]
description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br"
license = "AGPL-3.0"
@ -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"

View file

@ -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

View file

@ -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.

View file

@ -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:

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 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 {}
}
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> {
let bot_name = params.get("bot_name").cloned().unwrap_or_default();
let _token = params.get("token").cloned();
let user_id = {
let mut sm = data.session_manager.lock().await;
sm.get_or_create_anonymous_user(None).map_err(|e| {
error!("Failed to create anonymous user: {}", e);
actix_web::error::ErrorInternalServerError("Failed to create user")
})?
};
let (bot_id, bot_name) = tokio::task::spawn_blocking({
let bot_name = bot_name.clone();
let conn = data.conn.clone();
move || {
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))
.select((id, name))
.first::<(Uuid, String)>(&mut db_conn)
.optional()
{
Ok(Some((id_val, name_val))) => Ok((id_val, name_val)),
Ok(None) => {
match bots
.filter(is_active.eq(true))
.select((id, name))
.first::<(Uuid, String)>(&mut db_conn)
.optional()
{
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| {
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")
})?
};
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!({
"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"})));
}
}
Ok(Err(e)) => {
error!("Auth script execution error: {}", e);
}
Err(_) => {
error!("Auth script timeout");
}
}
}
Ok(HttpResponse::Ok().json(serde_json::json!({
"user_id": session.user_id,
"session_id": session.id,
"status": "authenticated"
})))
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 = 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);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": "Failed to create user" })),
);
}
}
};
// Resolve bot ID and name
let (bot_id, bot_name) = match tokio::task::spawn_blocking({
let bot_name = bot_name.clone();
let conn = state.conn.clone();
move || {
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))
.select((id, name))
.first::<(Uuid, String)>(&mut db_conn)
.optional()
{
Ok(Some((id_val, name_val))) => Ok((id_val, name_val)),
Ok(None) => match bots
.filter(is_active.eq(true))
.select((id, name))
.first::<(Uuid, String)>(&mut db_conn)
.optional()
{
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
{
Ok(Ok(res)) => res,
Ok(Err(e)) => {
error!("{}", e);
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 (
StatusCode::OK,
Json(serde_json::json!({
"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)]
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 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,333 +145,176 @@ 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() {
session.select("INBOX")?;
}
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")?;
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()
.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))
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 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()
};
session.logout().ok();
return Ok(body_text);
}
}
}
session.logout().ok();
Err("No email found from sender".into())
}
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)
pub async fn get_latest_email_from(
State(state): State<Arc<AppState>>,
Json(request): Json<GetLatestEmailRequest>,
) -> Result<Json<LatestEmailResponse>, EmailError> {
let config = state
.config
.as_ref()
.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(Json(LatestEmailResponse {
success: true,
email_text: Some(email_text),
message: "Email retrieved successfully".to_string(),
})),
Err(e) => Ok(Json(LatestEmailResponse {
success: false,
email_text: None,
message: format!("Failed to retrieve email: {}", e),
})),
}
}
#[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()
}

View file

@ -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() {
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
))
})?;
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());
}
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())

View file

@ -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(
@ -68,7 +70,7 @@ pub async fn ensure_llama_servers_running(
info!(" Embedding Model: {}", embedding_model);
info!(" LLM Server Path: {}", llm_server_path);
info!("Restarting any existing llama-server processes...");
if let Err(e) = tokio::process::Command::new("sh")
.arg("-c")
.arg("pkill llama-server -9 || true")
@ -161,7 +163,7 @@ pub async fn ensure_llama_servers_running(
}
if llm_ready && embedding_ready {
info!("All llama.cpp servers are ready and responding!");
// Update LLM provider with new endpoints
let _llm_provider1 = Arc::new(crate::llm::OpenAIClient::new(
llm_model.clone(),
@ -229,15 +231,15 @@ pub async fn start_llm_server(
.get_config(&default_bot_id, "llm-server-n-predict", None)
.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)
.unwrap_or("4096".to_string());
// TODO: Move flash-attn, temp, top_p, repeat-penalty to config as well.
// TODO: Create --jinja.
// --jinja --flash-attn on
let mut args = format!(
// TODO: Move flash-attn, temp, top_p, repeat-penalty to config as well.
// TODO: Create --jinja.
// --jinja --flash-attn on
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 {}",
model_path, port, gpu_layers
);
@ -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));
}
@ -265,8 +265,8 @@ pub async fn start_llm_server(
if n_predict != "0" {
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) {
let mut cmd = tokio::process::Command::new("cmd");
cmd.arg("/C").arg(format!(

View file

@ -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),
);
#[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);
}
// Add email routes if feature is enabled
#[cfg(feature = "email")]
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 {

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 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()})),
)
}
}
}

View file

@ -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!({
"session_id": session.id,
"title": "New Conversation",
"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()})))
}
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::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();
/// 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(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) {
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
})))
}
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()})))
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) => (
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" })),
),
}
}

View file

@ -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");
// Serve all JS files
cfg.service(
Files::new("/js", static_path.join("js"))
.prefer_utf8(true)
.use_last_modified(true)
.use_etag(true)
);
// 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);
Router::new()
// Serve all JS files
.nest_service("/js", ServeDir::new(static_path.join("js")))
// Serve CSS files
.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))
}

View file

@ -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 '/';
}