diff --git a/.env.example b/.env.example
index 9831085a..9bc26567 100644
--- a/.env.example
+++ b/.env.example
@@ -153,6 +153,8 @@
# Retention (days) for the whitecircle_checks audit log; 0 disables the cleanup worker.
# WHITECIRCLE_CHECKS_RETENTION_DAYS=30
# Privacy-preserving ads (server-side only; disabled by default).
+# Advertising is restricted to the initiating user's private chat with the bot.
+# These switches never enable advertising in groups or channels.
# GRADIUS_ENABLED=false
# GRADIUS_UTILITY_IMAGE_ENABLED=false
# GRADIUS_UTILITY_RATES_ENABLED=false
diff --git a/Cargo.lock b/Cargo.lock
index a54cc5d8..74ae443e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -497,9 +497,6 @@ name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
-dependencies = [
- "serde_core",
-]
[[package]]
name = "block-buffer"
@@ -1131,17 +1128,6 @@ dependencies = [
"miniz_oxide",
]
-[[package]]
-name = "flume"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
-dependencies = [
- "futures-core",
- "futures-sink",
- "spin",
-]
-
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1191,17 +1177,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
-[[package]]
-name = "futures-executor"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
[[package]]
name = "futures-intrusive"
version = "0.5.0"
@@ -1832,16 +1807,6 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
-[[package]]
-name = "libsqlite3-sys"
-version = "0.35.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
-dependencies = [
- "pkg-config",
- "vcpkg",
-]
-
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -2134,9 +2099,8 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
- "sha1 0.10.6",
+ "sha1",
"sha2 0.10.9",
- "sqlx",
"thiserror",
"time",
"tokio",
@@ -2328,7 +2292,8 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
- "sqlx",
+ "sqlx-core",
+ "sqlx-postgres",
"thiserror",
"time",
"tokio",
@@ -2359,7 +2324,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
- "sha1 0.10.6",
+ "sha1",
"thiserror",
"time",
"tokio",
@@ -3200,17 +3165,6 @@ dependencies = [
"digest 0.10.7",
]
-[[package]]
-name = "sha1"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "digest 0.11.3",
-]
-
[[package]]
name = "sha1_smol"
version = "1.0.1"
@@ -3332,22 +3286,6 @@ name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
-dependencies = [
- "lock_api",
-]
-
-[[package]]
-name = "sqlx"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71"
-dependencies = [
- "sqlx-core",
- "sqlx-macros",
- "sqlx-mysql",
- "sqlx-postgres",
- "sqlx-sqlite",
-]
[[package]]
name = "sqlx-core"
@@ -3384,72 +3322,6 @@ dependencies = [
"url",
]
-[[package]]
-name = "sqlx-macros"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf"
-dependencies = [
- "proc-macro2",
- "quote",
- "sqlx-core",
- "sqlx-macros-core",
- "syn 2.0.117",
-]
-
-[[package]]
-name = "sqlx-macros-core"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3"
-dependencies = [
- "cfg-if",
- "dotenvy",
- "either",
- "heck",
- "hex",
- "proc-macro2",
- "quote",
- "serde",
- "serde_json",
- "sha2 0.10.9",
- "sqlx-core",
- "sqlx-mysql",
- "sqlx-postgres",
- "sqlx-sqlite",
- "syn 2.0.117",
- "thiserror",
- "tokio",
- "url",
-]
-
-[[package]]
-name = "sqlx-mysql"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27"
-dependencies = [
- "bitflags",
- "byteorder",
- "bytes",
- "crc",
- "digest 0.11.3",
- "dotenvy",
- "either",
- "futures-core",
- "futures-util",
- "generic-array",
- "log",
- "percent-encoding",
- "serde",
- "sha1 0.11.0",
- "sha2 0.11.0",
- "sqlx-core",
- "thiserror",
- "time",
- "tracing",
-]
-
[[package]]
name = "sqlx-postgres"
version = "0.9.0"
@@ -3486,31 +3358,6 @@ dependencies = [
"whoami",
]
-[[package]]
-name = "sqlx-sqlite"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c"
-dependencies = [
- "atoi",
- "flume",
- "form_urlencoded",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-intrusive",
- "futures-util",
- "libsqlite3-sys",
- "log",
- "percent-encoding",
- "serde",
- "sqlx-core",
- "thiserror",
- "time",
- "tracing",
- "url",
-]
-
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -4090,12 +3937,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
-[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
[[package]]
name = "version_check"
version = "0.9.5"
diff --git a/Cargo.toml b/Cargo.toml
index c2f4bc36..6f681d50 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,7 +63,8 @@ serde = "1.0.228"
serde_json = "1.0"
sha1 = "0.10.6"
sha2 = "0.10.9"
-sqlx = { version = "0.9.0", default-features = false }
+sqlx-core = { version = "0.9.0", default-features = false }
+sqlx-postgres = { version = "0.9.0", default-features = false }
thiserror = "2.0.18"
time = "0.3"
tokio = "1.52.3"
diff --git a/crates/openplotva-app/Cargo.toml b/crates/openplotva-app/Cargo.toml
index a20446f5..9c31e7f3 100644
--- a/crates/openplotva-app/Cargo.toml
+++ b/crates/openplotva-app/Cargo.toml
@@ -41,7 +41,6 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sha1.workspace = true
sha2.workspace = true
-sqlx = { workspace = true, features = ["json", "postgres", "runtime-tokio", "time"] }
thiserror.workspace = true
time = { workspace = true, features = ["formatting", "parsing"] }
tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
diff --git a/crates/openplotva-app/src/checkin.rs b/crates/openplotva-app/src/checkin.rs
index aad467f2..f5cacea2 100644
--- a/crates/openplotva-app/src/checkin.rs
+++ b/crates/openplotva-app/src/checkin.rs
@@ -2766,6 +2766,7 @@ mod tests {
TelegramFileMetadataStoreFuture, UpdateHandler, UpdateStateStore, UpdateStateStoreFuture,
process_update_with_state_store_at,
};
+ use openplotva_storage::database as sqlx;
use openplotva_taskman::{JobPayload, JobType, control_job_params_from_stateless_job};
use openplotva_telegram::{
CallbackHandlerKind, DispatcherConfig, DispatcherQueue, TelegramOutboundMethodKind,
diff --git a/crates/openplotva-app/src/dialog_messages.rs b/crates/openplotva-app/src/dialog_messages.rs
index 5908aef7..3635b399 100644
--- a/crates/openplotva-app/src/dialog_messages.rs
+++ b/crates/openplotva-app/src/dialog_messages.rs
@@ -2536,6 +2536,7 @@ const OBSCENIFY_VOWELS: &str = "ауоыиэяюёе";
#[cfg(test)]
mod tests {
use crate::dialog_tools::DrawImageScheduleRequest;
+ use openplotva_storage::database as sqlx;
use openplotva_telegram::TELEGRAM_PARSE_MODE_HTML;
use std::{
collections::VecDeque,
diff --git a/crates/openplotva-app/src/dialog_turn/ledger.rs b/crates/openplotva-app/src/dialog_turn/ledger.rs
index 567a390b..832fd6f1 100644
--- a/crates/openplotva-app/src/dialog_turn/ledger.rs
+++ b/crates/openplotva-app/src/dialog_turn/ledger.rs
@@ -2,6 +2,7 @@
//! `RuntimeLlmObserver` pattern in `runtime_llm.rs`. One record per dialog
//! worker tick that dequeued a job.
+use openplotva_storage::database as sqlx;
use std::sync::{
Arc, Mutex,
atomic::{AtomicI64, Ordering},
@@ -443,6 +444,7 @@ pub async fn delete_old_turn_outcomes_batch(
#[cfg(test)]
mod tests {
use super::*;
+ use openplotva_storage::database as sqlx;
use sqlx::Execute;
fn sample_record(job_id: i64, outcome: &str) -> DialogTurnOutcomeRecord {
diff --git a/crates/openplotva-app/src/gradius_ads.rs b/crates/openplotva-app/src/gradius_ads.rs
index e91122b3..a5967d22 100644
--- a/crates/openplotva-app/src/gradius_ads.rs
+++ b/crates/openplotva-app/src/gradius_ads.rs
@@ -1,3 +1,4 @@
+use openplotva_storage::database as sqlx;
use std::{future::Future, pin::Pin, sync::Arc};
use openplotva_llm::gradius::vip_hint_for_impression;
@@ -542,6 +543,10 @@ pub struct GradiusUtilityAdRequest {
pub completed_at: OffsetDateTime,
}
+pub(crate) const fn is_private_ad_chat(chat_id: i64, user_id: i64) -> bool {
+ chat_id > 0 && chat_id == user_id
+}
+
pub(crate) fn initialize_placement_service(
config: &openplotva_config::AppConfig,
api_key: &str,
@@ -595,17 +600,13 @@ impl GradiusUtilityAdService {
&self,
request: GradiusUtilityAdRequest,
) -> Result