email wip

This commit is contained in:
Per Stark
2024-12-22 19:55:47 +01:00
parent 3d941d948d
commit 9f23005210
23 changed files with 674 additions and 189 deletions

2
.gitignore vendored
View File

@@ -3,7 +3,7 @@
.devenv
database
node_modules
config.yaml
flake.nix

203
Cargo.lock generated
View File

@@ -240,6 +240,12 @@ dependencies = [
"password-hash",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -899,6 +905,9 @@ name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
[[package]]
name = "bitvec"
@@ -1227,18 +1236,66 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d84f8d224ac58107d53d3ec2b9ad39fd8c8c4e285d3c9cb35485ffd2ca88cb3"
dependencies = [
"async-trait",
"convert_case",
"json5",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml",
"winnow",
"yaml-rust2",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.18.1"
@@ -1533,6 +1590,15 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "dmp"
version = "0.2.0"
@@ -2061,6 +2127,15 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heapless"
version = "0.8.0"
@@ -2550,6 +2625,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]]
name = "jsonwebtoken"
version = "9.3.0"
@@ -3238,6 +3324,16 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]]
name = "overload"
version = "0.1.1"
@@ -3318,6 +3414,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@@ -3366,6 +3468,40 @@ dependencies = [
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "pest_meta"
version = "2.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "petgraph"
version = "0.6.5"
@@ -4140,6 +4276,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30"
[[package]]
name = "ron"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64 0.21.7",
"bitflags 2.6.0",
"serde",
"serde_derive",
]
[[package]]
name = "rstar"
version = "0.12.0"
@@ -4151,6 +4299,17 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rust-ini"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
dependencies = [
"cfg-if",
"ordered-multimap",
"trim-in-place",
]
[[package]]
name = "rust-stemmers"
version = "1.2.0"
@@ -4496,6 +4655,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -5247,11 +5415,26 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@@ -5260,6 +5443,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.6.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
@@ -5405,6 +5590,12 @@ dependencies = [
"web-sys",
]
[[package]]
name = "trim-in-place"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -6065,6 +6256,17 @@ dependencies = [
"time",
]
[[package]]
name = "yaml-rust2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d"
dependencies = [
"arraydeque",
"encoding_rs",
"hashlink",
]
[[package]]
name = "yoke"
version = "0.7.5"
@@ -6171,6 +6373,7 @@ dependencies = [
"axum_session_auth",
"axum_session_surreal",
"axum_typed_multipart",
"config",
"futures",
"lapin",
"lettre",

View File

@@ -12,6 +12,7 @@ axum_session = "0.14.4"
axum_session_auth = "0.14.1"
axum_session_surreal = "0.2.1"
axum_typed_multipart = "0.12.1"
config = "0.15.4"
futures = "0.3.31"
lapin = { version = "2.5.0", features = ["serde_json"] }
lettre = { version = "0.11.11", features = ["rustls-tls"] }

1
assets/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

13
assets/manifest.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "minne",
"short_name": "minne",
"start_url": "/",
"display": "standalone",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}

View File

@@ -877,6 +877,57 @@ html {
content: var(--tw-content);
}
.card {
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--rounded-box, 1rem);
}
.card:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.card figure {
display: flex;
align-items: center;
justify-content: center;
}
.card.image-full {
display: grid;
}
.card.image-full:before {
position: relative;
content: "";
z-index: 10;
border-radius: var(--rounded-box, 1rem);
--tw-bg-opacity: 1;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
opacity: 0.75;
}
.card.image-full:before,
.card.image-full > * {
grid-column-start: 1;
grid-row-start: 1;
}
.card.image-full > figure img {
height: 100%;
-o-object-fit: cover;
object-fit: cover;
}
.card.image-full > .card-body {
position: relative;
z-index: 20;
--tw-text-opacity: 1;
color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
}
.chat {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1164,6 +1215,34 @@ html {
}
}
.footer {
display: grid;
width: 100%;
grid-auto-flow: row;
place-items: start;
-moz-column-gap: 1rem;
column-gap: 1rem;
row-gap: 2.5rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.footer > * {
display: grid;
place-items: start;
gap: 0.5rem;
}
@media (min-width: 48rem) {
.footer {
grid-auto-flow: column;
}
.footer-center {
grid-auto-flow: row dense;
}
}
.form-control {
display: flex;
flex-direction: column;
@@ -1182,20 +1261,6 @@ html {
padding-bottom: 0.5rem;
}
.indicator {
position: relative;
display: inline-flex;
width: -moz-max-content;
width: max-content;
}
.indicator :where(.indicator-item) {
z-index: 1;
position: absolute;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
white-space: nowrap;
}
.input {
flex-shrink: 1;
-webkit-appearance: none;
@@ -1514,6 +1579,44 @@ html {
}
}
.card :where(figure:first-child) {
overflow: hidden;
border-start-start-radius: inherit;
border-start-end-radius: inherit;
border-end-start-radius: unset;
border-end-end-radius: unset;
}
.card :where(figure:last-child) {
overflow: hidden;
border-start-start-radius: unset;
border-start-end-radius: unset;
border-end-start-radius: inherit;
border-end-end-radius: inherit;
}
.card:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.card.bordered {
border-width: 1px;
--tw-border-opacity: 1;
border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
}
.card.compact .card-body {
padding: 1rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.card.image-full :where(figure) {
overflow: hidden;
border-radius: inherit;
}
.checkbox:focus {
box-shadow: none;
}
@@ -1916,78 +2019,6 @@ html {
width: 1.25rem;
}
.indicator :where(.indicator-item) {
bottom: auto;
inset-inline-end: 0px;
inset-inline-start: auto;
top: 0px;
--tw-translate-y: -50%;
--tw-translate-x: 50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item):where([dir="rtl"], [dir="rtl"] *) {
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-start) {
inset-inline-end: auto;
inset-inline-start: 0px;
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-start):where([dir="rtl"], [dir="rtl"] *) {
--tw-translate-x: 50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-center) {
inset-inline-end: 50%;
inset-inline-start: 50%;
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-center):where([dir="rtl"], [dir="rtl"] *) {
--tw-translate-x: 50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-end) {
inset-inline-end: 0px;
inset-inline-start: auto;
--tw-translate-x: 50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-end):where([dir="rtl"], [dir="rtl"] *) {
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-bottom) {
bottom: 0px;
top: auto;
--tw-translate-y: 50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-middle) {
bottom: 50%;
top: 50%;
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.indicator :where(.indicator-item.indicator-top) {
bottom: auto;
top: 0px;
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.menu-horizontal {
display: inline-flex;
flex-direction: row;
@@ -2027,10 +2058,6 @@ html {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.absolute {
position: absolute;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
@@ -2068,10 +2095,6 @@ html {
display: grid;
}
.max-h-full {
max-height: 100%;
}
.min-h-\[80vh\] {
min-height: 80vh;
}
@@ -2080,10 +2103,6 @@ html {
min-height: 100vh;
}
.min-h-full {
min-height: 100%;
}
.w-full {
width: 100%;
}
@@ -2092,14 +2111,14 @@ html {
max-width: 42rem;
}
.max-w-md {
max-width: 28rem;
}
.max-w-lg {
max-width: 32rem;
}
.max-w-md {
max-width: 28rem;
}
.flex-1 {
flex: 1 1 0%;
}
@@ -2159,6 +2178,10 @@ html {
border-top-right-radius: 0px;
}
.border {
border-width: 1px;
}
.border-transparent {
border-color: transparent;
}

View File

@@ -32,7 +32,7 @@ use zettle_db::{
AppState,
},
storage::{db::SurrealDbClient, types::user::User},
utils::mailer::Mailer,
utils::{config::get_config, mailer::Mailer},
};
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
@@ -44,12 +44,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.try_init()
.ok();
let config = get_config()?;
info!("{:?}", config);
// Set up RabbitMQ
let config = RabbitMQConfig {
amqp_addr: "amqp://localhost".to_string(),
exchange: "my_exchange".to_string(),
queue: "my_queue".to_string(),
routing_key: "my_key".to_string(),
let rabbitmq_config = RabbitMQConfig {
amqp_addr: config.rabbitmq_address,
exchange: config.rabbitmq_exchange,
queue: config.rabbitmq_queue,
routing_key: config.rabbitmq_routing_key,
};
let reloader = AutoReloader::new(move |notifier| {
@@ -62,14 +66,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(env)
});
let mailer = Mailer::new();
let app_state = AppState {
rabbitmq_producer: Arc::new(RabbitMQProducer::new(&config).await?),
rabbitmq_consumer: Arc::new(RabbitMQConsumer::new(&config, false).await?),
surreal_db_client: Arc::new(SurrealDbClient::new().await?),
rabbitmq_producer: Arc::new(RabbitMQProducer::new(&rabbitmq_config).await?),
rabbitmq_consumer: Arc::new(RabbitMQConsumer::new(&rabbitmq_config, false).await?),
surreal_db_client: Arc::new(
SurrealDbClient::new(
&config.surrealdb_address,
&config.surrealdb_username,
&config.surrealdb_password,
&config.surrealdb_namespace,
&config.surrealdb_database,
)
.await?,
),
openai_client: Arc::new(async_openai::Client::new()),
templates: Arc::new(reloader),
mailer: Arc::new(Mailer::new(
config.smtp_username,
config.smtp_relayer,
config.smtp_password,
)?),
};
// setup_auth(&app_state.surreal_db_client).await?;

View File

@@ -1,6 +1,10 @@
use tracing::info;
use tracing::{error, info};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use zettle_db::rabbitmq::{consumer::RabbitMQConsumer, RabbitMQConfig};
use zettle_db::{
ingress::content_processor::ContentProcessor,
rabbitmq::{consumer::RabbitMQConsumer, RabbitMQConfig, RabbitMQError},
utils::config::get_config,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -13,19 +17,47 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("Starting RabbitMQ consumer");
let config = get_config()?;
// Set up RabbitMQ config
let config = RabbitMQConfig {
amqp_addr: "amqp://localhost".to_string(),
exchange: "my_exchange".to_string(),
queue: "my_queue".to_string(),
routing_key: "my_key".to_string(),
let rabbitmq_config = RabbitMQConfig {
amqp_addr: config.rabbitmq_address.clone(),
exchange: config.rabbitmq_exchange.clone(),
queue: config.rabbitmq_queue.clone(),
routing_key: config.rabbitmq_routing_key.clone(),
};
// Create a RabbitMQ consumer
let consumer = RabbitMQConsumer::new(&config, true).await?;
let consumer = RabbitMQConsumer::new(&rabbitmq_config, true).await?;
// Start consuming messages
consumer.process_messages().await?;
loop {
match consumer.consume().await {
Ok((ingress, delivery)) => {
info!("Received IngressObject: {:?}", ingress);
// Get the TextContent
let text_content = ingress.to_text_content().await?;
// Initialize ContentProcessor which handles LLM analysis and storage
let content_processor = ContentProcessor::new(&config).await?;
// Begin processing of TextContent
content_processor.process(&text_content).await?;
// Remove from queue
consumer.ack_delivery(delivery).await?;
}
Err(RabbitMQError::ConsumeError(e)) => {
error!("Error consuming message: {}", e);
// Optionally add a delay before trying again
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
Err(e) => {
error!("Unexpected error: {}", e);
break;
}
}
}
Ok(())
}

View File

@@ -6,7 +6,7 @@ use tokio::task::JoinError;
use crate::{
ingress::types::ingress_input::IngressContentError, rabbitmq::RabbitMQError,
storage::types::file_info::FileError,
storage::types::file_info::FileError, utils::mailer::EmailError,
};
#[derive(Error, Debug)]
@@ -70,6 +70,8 @@ pub enum ApiError {
AuthRequired,
#[error("Templating error: {0}")]
TemplatingError(#[from] minijinja::Error),
#[error("Mail error: {0}")]
EmailError(#[from] EmailError),
}
impl IntoResponse for ApiError {
@@ -90,6 +92,7 @@ impl IntoResponse for ApiError {
ApiError::RabbitMQError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::FileError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::TemplatingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::EmailError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
};
(

View File

@@ -12,7 +12,7 @@ use crate::{
text_chunk::TextChunk, text_content::TextContent,
},
},
utils::embedding::generate_embedding,
utils::{config::AppConfig, embedding::generate_embedding},
};
use super::analysis::{
@@ -25,9 +25,16 @@ pub struct ContentProcessor {
}
impl ContentProcessor {
pub async fn new() -> Result<Self, ProcessingError> {
pub async fn new(app_config: &AppConfig) -> Result<Self, ProcessingError> {
Ok(Self {
db_client: SurrealDbClient::new().await?,
db_client: SurrealDbClient::new(
&app_config.surrealdb_address,
&app_config.surrealdb_username,
&app_config.surrealdb_password,
&app_config.surrealdb_namespace,
&app_config.surrealdb_database,
)
.await?,
openai_client: async_openai::Client::new(),
})
}

View File

@@ -1,13 +1,9 @@
use futures::StreamExt;
use lapin::{message::Delivery, options::*, types::FieldTable, Channel, Consumer, Queue};
use crate::{
error::IngressConsumerError,
ingress::{content_processor::ContentProcessor, types::ingress_object::IngressObject},
};
use crate::ingress::types::ingress_object::IngressObject;
use super::{RabbitMQCommon, RabbitMQCommonTrait, RabbitMQConfig, RabbitMQError};
use tracing::{error, info};
/// Struct to consume messages from RabbitMQ.
pub struct RabbitMQConsumer {
@@ -193,38 +189,6 @@ impl RabbitMQConsumer {
.await
.map_err(|e| RabbitMQError::ConsumeError(e.to_string()))?;
Ok(())
}
/// Function to continually consume messages as they come in
pub async fn process_messages(&self) -> Result<(), IngressConsumerError> {
loop {
match self.consume().await {
Ok((ingress, delivery)) => {
info!("Received IngressObject: {:?}", ingress);
// Get the TextContent
let text_content = ingress.to_text_content().await?;
// Initialize ContentProcessor which handles LLM analysis and storage
let content_processor = ContentProcessor::new().await?;
// Begin processing of TextContent
content_processor.process(&text_content).await?;
// Remove from queue
self.ack_delivery(delivery).await?;
}
Err(RabbitMQError::ConsumeError(e)) => {
error!("Error consuming message: {}", e);
// Optionally add a delay before trying again
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
Err(e) => {
error!("Unexpected error: {}", e);
break;
}
}
}
Ok(())
}
}

View File

@@ -1,6 +1,7 @@
use crate::rabbitmq::consumer::RabbitMQConsumer;
use crate::rabbitmq::publisher::RabbitMQProducer;
use crate::storage::db::SurrealDbClient;
use crate::utils::mailer::Mailer;
use minijinja_autoreload::AutoReloader;
use std::sync::Arc;
@@ -14,4 +15,5 @@ pub struct AppState {
pub surreal_db_client: Arc<SurrealDbClient>,
pub openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
pub templates: Arc<AutoReloader>,
pub mailer: Arc<Mailer>,
}

View File

@@ -1,5 +1,6 @@
use axum::{extract::State, http::StatusCode, response::IntoResponse};
use tracing::info;
use minijinja::context;
use tracing::{info, Instrument};
use crate::{error::ApiError, server::AppState};
@@ -12,6 +13,10 @@ pub async fn queue_length_handler(
info!("Queue length: {}", queue_length);
state
.mailer
.send_email_verification("per@starks.cloud", "1001010", &state.templates)?;
// Return the queue length with a 200 OK status
Ok((StatusCode::OK, queue_length.to_string()))
}

View File

@@ -11,7 +11,7 @@ use crate::{
storage::types::user::User,
};
page_data!(IndexData, {
page_data!(IndexData, "index.html", {
queue_length: u32,
});
@@ -25,7 +25,9 @@ pub async fn index_handler(
let queue_length = state.rabbitmq_consumer.get_queue_length().await?;
let output = render_template("index.html", IndexData { queue_length }, state.templates)?;
let data = IndexData { queue_length };
let output = render_template(IndexData::template_name(), data, state.templates)?;
Ok(output)
}

View File

@@ -7,6 +7,10 @@ pub mod auth;
pub mod index;
pub mod search_result;
pub trait PageData {
fn template_name() -> &'static str;
}
pub fn render_template<T>(
template_name: &str,
context: T,
@@ -44,12 +48,19 @@ where
#[macro_export]
macro_rules! page_data {
($name:ident, {$($(#[$attr:meta])* $field:ident: $ty:ty),*$(,)?}) => {
($name:ident, $template_name:expr, {$($(#[$attr:meta])* $field:ident: $ty:ty),*$(,)?}) => {
use serde::{Serialize, Deserialize};
use $crate::server::routes::html::PageData;
#[derive(Debug, Deserialize, Serialize)]
pub struct $name {
$($(#[$attr])* pub $field: $ty),*
}
impl PageData for $name {
fn template_name() -> &'static str {
$template_name
}
}
};
}

View File

@@ -18,18 +18,20 @@ impl SurrealDbClient {
///
/// # Returns
/// * `SurrealDbClient` initialized
pub async fn new() -> Result<Self, Error> {
let db = connect("ws://127.0.0.1:8000").await?;
pub async fn new(
address: &str,
username: &str,
password: &str,
namespace: &str,
database: &str,
) -> Result<Self, Error> {
let db = connect(address).await?;
// Sign in to database
db.signin(Root {
username: "root_user",
password: "root_password",
})
.await?;
db.signin(Root { username, password }).await?;
// Set namespace
db.use_ns("test").use_db("test").await?;
db.use_ns(namespace).use_db(database).await?;
Ok(SurrealDbClient { client: db })
}

38
src/utils/config.rs Normal file
View File

@@ -0,0 +1,38 @@
use config::{Config, ConfigError, File};
#[derive(Clone, Debug)]
pub struct AppConfig {
pub smtp_username: String,
pub smtp_password: String,
pub smtp_relayer: String,
pub rabbitmq_address: String,
pub rabbitmq_exchange: String,
pub rabbitmq_queue: String,
pub rabbitmq_routing_key: String,
pub surrealdb_address: String,
pub surrealdb_username: String,
pub surrealdb_password: String,
pub surrealdb_namespace: String,
pub surrealdb_database: String,
}
pub fn get_config() -> Result<AppConfig, ConfigError> {
let config = Config::builder()
.add_source(File::with_name("config"))
.build()?;
Ok(AppConfig {
smtp_username: config.get_string("SMTP_USERNAME")?,
smtp_password: config.get_string("SMTP_PASSWORD")?,
smtp_relayer: config.get_string("SMTP_RELAYER")?,
rabbitmq_address: config.get_string("RABBITMQ_ADDRESS")?,
rabbitmq_exchange: config.get_string("RABBITMQ_EXCHANGE")?,
rabbitmq_queue: config.get_string("RABBITMQ_QUEUE")?,
rabbitmq_routing_key: config.get_string("RABBITMQ_ROUTING_KEY")?,
surrealdb_address: config.get_string("SURREALDB_ADDRESS")?,
surrealdb_username: config.get_string("SURREALDB_USERNAME")?,
surrealdb_password: config.get_string("SURREALDB_PASSWORD")?,
surrealdb_namespace: config.get_string("SURREALDB_NAMESPACE")?,
surrealdb_database: config.get_string("SURREALDB_DATABASE")?,
})
}

View File

@@ -1,23 +1,81 @@
use std::env;
use lettre::address::AddressError;
use lettre::message::MultiPart;
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
use lettre::{Message, Transport};
use minijinja::context;
use minijinja_autoreload::AutoReloader;
use thiserror::Error;
use tracing::info;
pub struct Mailer {
pub mailer: SmtpTransport,
}
#[derive(Error, Debug)]
pub enum EmailError {
#[error("Email construction error: {0}")]
EmailParsingError(#[from] AddressError),
#[error("Email sending error: {0}")]
SendingError(#[from] lettre::transport::smtp::Error),
#[error("Body constructing error: {0}")]
BodyError(#[from] lettre::error::Error),
#[error("Templating error: {0}")]
TemplatingError(#[from] minijinja::Error),
}
impl Mailer {
pub fn new() -> Self {
let creds = Credentials::new(
env::var("SMTP_USERNAME").unwrap().to_owned(),
env::var("SMTP_PASSWORD").unwrap().to_owned(),
);
pub fn new(
username: String,
relayer: String,
password: String,
) -> Result<Self, lettre::transport::smtp::Error> {
let creds = Credentials::new(username, password);
let mailer = SmtpTransport::relay(env::var("SMTP_RELAYER").unwrap().as_str())
.unwrap()
.credentials(creds)
.build();
let mailer = SmtpTransport::relay(&relayer)?.credentials(creds).build();
Mailer { mailer }
Ok(Mailer { mailer })
}
pub fn send_email_verification(
&self,
email_to: &str,
verification_code: &str,
templates: &AutoReloader,
) -> Result<(), EmailError> {
let name = email_to
.split('@')
.next()
.unwrap_or("User")
.chars()
.enumerate()
.map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
.collect::<String>();
let context = context! {
name => name,
verification_code => verification_code
};
let env = templates.acquire_env()?;
let html = env
.get_template("email/email_verification.html")?
.render(&context)?;
let plain = env
.get_template("email/email_verification.txt")?
.render(&context)?;
let email = Message::builder()
.from("Admin <minne@starks.cloud>".parse()?)
.reply_to("Admin <minne@starks.cloud>".parse()?)
.to(format!("{} <{}>", name, email_to).parse()?)
.subject("Verify Your Email Address")
.multipart(MultiPart::alternative_plain_html(plain, html))?;
info!("Sending email to: {}", email_to);
self.mailer.send(&email)?;
Ok(())
}
}

View File

@@ -1,2 +1,3 @@
pub mod config;
pub mod embedding;
pub mod mailer;

View File

@@ -0,0 +1,77 @@
{# email_verification.html #}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Verification</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333333;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
text-align: center;
}
.card {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 30px;
margin: 20px auto;
max-width: 400px;
}
.verification-code {
font-size: 32px;
font-weight: bold;
color: #2c3e50;
letter-spacing: 2px;
margin: 20px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
}
.footer {
margin-top: 30px;
font-size: 12px;
color: #666666;
}
.header {
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Email Verification</h1>
</div>
<div class="card">
<p>Hello {{ name }},</p>
<p>Please use the following verification code to complete your registration:</p>
<div class="verification-code">
{{ verification_code }}
</div>
<p>This code will expire in 30 minutes.</p>
</div>
<div class="footer">
<p><strong>Security Notice:</strong> If you didn't request this verification code, please ignore this email. Someone might have entered your email address by mistake.</p>
<p>For your security, never share this code with anyone, including those claiming to be from our support team.</p>
<p>This is an automated message, please do not reply to this email.</p>
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
<p>© 2024 Your Company Name. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
{# email_verification.txt #}
Hello {{ name }},
Thank you for registering. To complete your registration, please use the following verification code:
{{ verification_code }}
This code will expire in 30 minutes.
IMPORTANT SECURITY INFORMATION:
- If you didn't request this verification code, please ignore this email.
- Never share this code with anyone, including those claiming to be from our support team.
- This is an automated message, please do not reply to this email.
For your security, if you did not initiate this request, you can safely ignore this email. Someone might have entered your email address by mistake.
Best regards,
Your Company Name
© 2024 Your Company Name. All rights reserved.

View File

@@ -5,8 +5,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}minnet{% endblock %}</title>
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
<script src="assets/htmx.min.js"></script>
<link rel="stylesheet" href="assets/style.css">
<link rel="manifest" href="/assets/manifest.json">
<!-- Optional but recommended for iOS support -->
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
{% block head %}{% endblock %}
</head>
{% block body %}{% endblock %}

View File

@@ -4,3 +4,4 @@
\[x\] redirects
\[\] hx-redirect
\[\] macro for pagedata?
\[\] add more config structs for clarity