Compare commits

..

No commits in common. "0bb0c5657a1c4ad86115498da20fac4ac33cd87f" and "86687a7b96519d61d2b815f697324a16ada85eef" have entirely different histories.

49 changed files with 913 additions and 2580 deletions

35
Cargo.lock generated
View File

@ -600,28 +600,6 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gpodder"
version = "0.1.0"
dependencies = [
"argon2",
"chrono",
"rand",
]
[[package]]
name = "gpodder_sqlite"
version = "0.1.0"
dependencies = [
"chrono",
"diesel",
"diesel_migrations",
"gpodder",
"libsqlite3-sys",
"rand",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
@ -681,12 +659,12 @@ dependencies = [
[[package]]
name = "http-body-util"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"pin-project-lite",
@ -948,15 +926,16 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
name = "otter"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"axum-extra",
"chrono",
"clap",
"cookie",
"diesel",
"diesel_migrations",
"figment",
"gpodder",
"gpodder_sqlite",
"http-body-util",
"libsqlite3-sys",
"rand",
"serde",
"tokio",

View File

@ -1,25 +1,19 @@
[workspace]
members = [
'gpodder',
'gpodder_sqlite'
]
[package]
name = "otter"
version = "0.1.0"
edition = "2021"
[dependencies]
gpodder = { path = "./gpodder" }
gpodder_sqlite = { path = "./gpodder_sqlite" }
argon2 = "0.5.3"
axum = { version = "0.8.1", features = ["macros"] }
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.30", features = ["derive", "env"] }
cookie = "0.18.1"
diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
figment = { version = "0.10.19", features = ["env", "toml"] }
http-body-util = "0.1.3"
libsqlite3-sys = { version = "0.31.0", features = ["bundled"] }
rand = "0.8.5"
serde = { version = "1.0.218", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] }

520
gpodder/Cargo.lock generated
View File

@ -1,520 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64ct"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gpodder"
version = "0.1.0"
dependencies = [
"argon2",
"chrono",
"rand",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "log"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -1,9 +0,0 @@
[package]
name = "gpodder"
version = "0.1.0"
edition = "2021"
[dependencies]
chrono = { version = "0.4.39", features = ["serde"] }
argon2 = "0.5.3"
rand = "0.8.5"

View File

@ -1,9 +0,0 @@
pub mod models;
mod repository;
mod store;
pub use models::*;
pub use repository::GpodderRepository;
pub use store::{
AuthErr, AuthStore, DeviceRepository, EpisodeActionRepository, Store, SubscriptionRepository,
};

View File

@ -1,954 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64ct"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "diesel"
version = "2.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a"
dependencies = [
"diesel_derives",
"libsqlite3-sys",
"r2d2",
"time",
]
[[package]]
name = "diesel_derives"
version = "2.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93958254b70bea63b4187ff73d10180599d9d8d177071b7f91e6da4e0c0ad55"
dependencies = [
"diesel_table_macro_syntax",
"dsl_auto_type",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "diesel_migrations"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6"
dependencies = [
"diesel",
"migrations_internals",
"migrations_macros",
]
[[package]]
name = "diesel_table_macro_syntax"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25"
dependencies = [
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "dsl_auto_type"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b"
dependencies = [
"darling",
"either",
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gpodder"
version = "0.1.0"
dependencies = [
"argon2",
"chrono",
"rand",
]
[[package]]
name = "gpodder_sqlite"
version = "0.1.0"
dependencies = [
"chrono",
"diesel",
"diesel_migrations",
"gpodder",
"rand",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libsqlite3-sys"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "migrations_internals"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff"
dependencies = [
"serde",
"toml",
]
[[package]]
name = "migrations_macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd"
dependencies = [
"migrations_internals",
"proc-macro2",
"quote",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r2d2"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
dependencies = [
"bitflags",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "scheduled-thread-pool"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
dependencies = [
"parking_lot",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time"
version = "0.3.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
[[package]]
name = "time-macros"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
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"
version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -1,13 +0,0 @@
[package]
name = "gpodder_sqlite"
version = "0.1.0"
edition = "2021"
[dependencies]
gpodder = { path = "../gpodder" }
diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
tracing = "0.1.41"
chrono = { version = "0.4.39", features = ["serde"] }
rand = "0.8.5"
libsqlite3-sys = { version = "0.31.0", features = ["bundled"] }

View File

@ -1,60 +0,0 @@
use diesel::prelude::*;
use crate::schema::*;
#[derive(Clone, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(super::user::User))]
#[diesel(table_name = sessions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Session {
pub id: i64,
pub user_id: i64,
pub last_seen: i64,
}
impl Session {
// pub fn new_for_user(pool: &DbPool, user_id: i64, last_seen: i64) -> DbResult<Self> {
// let id: i64 = rand::thread_rng().gen();
// Ok(Self {
// id,
// user_id,
// last_seen,
// }
// .insert_into(sessions::table)
// .returning(Self::as_returning())
// .get_result(&mut pool.get()?)?)
// }
// pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult<Option<super::user::User>> {
// Ok(sessions::dsl::sessions
// .inner_join(users::table)
// .filter(sessions::id.eq(id))
// .select(User::as_select())
// .get_result(&mut pool.get()?)
// .optional()?)
// }
// pub fn user(&self, pool: &DbPool) -> DbResult<Option<super::user::User>> {
// Self::user_from_id(pool, self.id)
// }
// pub fn by_id(pool: &DbPool, id: i64) -> DbResult<Option<Self>> {
// Ok(sessions::dsl::sessions
// .find(id)
// .get_result(&mut pool.get()?)
// .optional()?)
// }
// pub fn remove(self, pool: &DbPool) -> DbResult<bool> {
// Self::remove_by_id(pool, self.id)
// }
// pub fn remove_by_id(pool: &DbPool, id: i64) -> DbResult<bool> {
// Ok(
// diesel::delete(sessions::dsl::sessions.filter(sessions::id.eq(id)))
// .execute(&mut pool.get()?)?
// > 0,
// )
// }
}

View File

@ -1,47 +0,0 @@
use diesel::prelude::*;
use crate::schema::*;
#[derive(Clone, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct User {
pub id: i64,
pub username: String,
pub password_hash: String,
}
#[derive(Insertable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewUser<'a> {
pub username: &'a str,
pub password_hash: &'a str,
}
// impl NewUser {
// pub fn new(username: String, password: String) -> Self {
// Self {
// username,
// password_hash: hash_password(&password),
// }
// }
// }
// impl User {
// pub fn by_username(pool: &DbPool, username: impl AsRef<str>) -> DbResult<Option<Self>> {
// Ok(users::dsl::users
// .select(User::as_select())
// .filter(users::username.eq(username.as_ref()))
// .first(&mut pool.get()?)
// .optional()?)
// }
// pub fn verify_password(&self, password: impl AsRef<str>) -> bool {
// let password_hash = PasswordHash::new(&self.password_hash).unwrap();
// Argon2::default()
// .verify_password(password.as_ref().as_bytes(), &password_hash)
// .is_ok()
// }
// }

View File

@ -1,298 +0,0 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use diesel::{alias, dsl::not, prelude::*};
use gpodder::AuthErr;
use super::SqliteRepository;
use crate::{
models::{
device::{Device, DeviceType, NewDevice},
sync_group::SyncGroup,
},
schema::*,
DbError,
};
impl From<DeviceType> for gpodder::DeviceType {
fn from(value: DeviceType) -> Self {
match value {
DeviceType::Desktop => Self::Desktop,
DeviceType::Laptop => Self::Laptop,
DeviceType::Mobile => Self::Mobile,
DeviceType::Server => Self::Server,
DeviceType::Other => Self::Other,
}
}
}
impl From<gpodder::DeviceType> for DeviceType {
fn from(value: gpodder::DeviceType) -> Self {
match value {
gpodder::DeviceType::Desktop => Self::Desktop,
gpodder::DeviceType::Laptop => Self::Laptop,
gpodder::DeviceType::Mobile => Self::Mobile,
gpodder::DeviceType::Server => Self::Server,
gpodder::DeviceType::Other => Self::Other,
}
}
}
impl gpodder::DeviceRepository for SqliteRepository {
fn devices_for_user(
&self,
user: &gpodder::User,
) -> Result<Vec<gpodder::Device>, gpodder::AuthErr> {
(|| {
Ok::<_, DbError>(
devices::table
.select(Device::as_select())
.filter(devices::user_id.eq(user.id))
.get_results(&mut self.pool.get()?)?
.into_iter()
.map(|d| gpodder::Device {
id: d.device_id,
caption: d.caption,
r#type: d.type_.into(),
// TODO implement subscription count
subscriptions: 0,
})
.collect(),
)
})()
.map_err(AuthErr::from)
}
fn update_device_info(
&self,
user: &gpodder::User,
device_id: &str,
patch: gpodder::DevicePatch,
) -> Result<(), gpodder::AuthErr> {
(|| {
if let Some(mut device) = devices::table
.select(Device::as_select())
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
.get_result(&mut self.pool.get()?)
.optional()?
{
if let Some(caption) = patch.caption {
device.caption = caption;
}
if let Some(type_) = patch.r#type {
device.type_ = type_.into();
}
diesel::update(devices::table.filter(devices::id.eq(device.id)))
.set((
devices::caption.eq(&device.caption),
devices::type_.eq(&device.type_),
))
.execute(&mut self.pool.get()?)?;
} else {
let device = NewDevice {
device_id: device_id.to_string(),
user_id: user.id,
caption: patch.caption.unwrap_or(String::new()),
type_: patch.r#type.unwrap_or(gpodder::DeviceType::Other).into(),
};
diesel::insert_into(devices::table)
.values(device)
.execute(&mut self.pool.get()?)?;
}
Ok::<_, DbError>(())
})()
.map_err(AuthErr::from)
}
fn merge_sync_groups(
&self,
user: &gpodder::User,
device_ids: Vec<&str>,
) -> Result<i64, gpodder::AuthErr> {
(|| {
let conn = &mut self.pool.get()?;
conn.transaction(|conn| {
let devices: Vec<(i64, Option<i64>)> = devices::table
.select((devices::id, devices::sync_group_id))
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq_any(device_ids)),
)
.get_results(conn)?;
let mut sync_group_ids: Vec<i64> = devices
.iter()
.filter_map(|(_, group_id)| *group_id)
.collect();
// Remove any duplicates, giving us each sync group ID once
sync_group_ids.sort();
sync_group_ids.dedup();
// If any of the devices are already in a sync group, we reuse the first one we find.
// Otherwise, we generate a new one.
let sync_group_id = if let Some(id) = sync_group_ids.pop() {
id
} else {
SyncGroup::new(conn)?.id
};
// Move all devices in the other sync groups into the new sync group
diesel::update(
devices::table.filter(devices::sync_group_id.eq_any(sync_group_ids.iter())),
)
.set(devices::sync_group_id.eq(sync_group_id))
.execute(conn)?;
// Add the non-synchronized devices into the new sync group
let unsynced_device_ids =
devices.iter().filter_map(
|(id, group_id)| if group_id.is_none() { Some(id) } else { None },
);
diesel::update(devices::table.filter(devices::id.eq_any(unsynced_device_ids)))
.set(devices::sync_group_id.eq(sync_group_id))
.execute(conn)?;
// Remove the other now unused sync groups
diesel::delete(sync_groups::table.filter(sync_groups::id.eq_any(sync_group_ids)))
.execute(conn)?;
Ok::<_, DbError>(sync_group_id)
})
})()
.map_err(AuthErr::from)
}
fn remove_from_sync_group(
&self,
user: &gpodder::User,
device_ids: Vec<&str>,
) -> Result<(), gpodder::AuthErr> {
(|| {
let conn = &mut self.pool.get()?;
diesel::update(
devices::table.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq_any(device_ids)),
),
)
.set(devices::sync_group_id.eq(None::<i64>))
.execute(conn)?;
// This is in a different transaction on purpose, as the success of this removal shouldn't
// fail the entire query
SyncGroup::remove_unused(conn)?;
Ok::<_, DbError>(())
})()
.map_err(AuthErr::from)
}
fn synchronize_sync_group(
&self,
group_id: i64,
time_changed: DateTime<Utc>,
) -> Result<(), gpodder::AuthErr> {
(|| {
let time_changed = time_changed.timestamp();
let conn = &mut self.pool.get()?;
conn.transaction(|conn| {
let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id))
.select(devices::id)
.get_results(conn)?;
// For each device in the group, we get the list of subscriptions not yet in its own
// non-deleted list, and add it to the database
for device_id in device_ids.iter().copied() {
let d1 = alias!(device_subscriptions as d1);
let own_subscriptions = d1
.filter(
d1.field(device_subscriptions::device_id)
.eq(device_id)
.and(d1.field(device_subscriptions::deleted).eq(false)),
)
.select(d1.field(device_subscriptions::podcast_url));
let urls_to_add = device_subscriptions::table
.select(device_subscriptions::podcast_url)
.filter(
device_subscriptions::device_id
.eq_any(device_ids.iter())
.and(device_subscriptions::deleted.eq(false))
.and(not(
device_subscriptions::podcast_url.eq_any(own_subscriptions)
)),
)
.distinct()
.load_iter(conn)?
.collect::<Result<HashSet<String>, _>>()?;
super::subscription::insert_subscriptions_for_single_device(
conn,
device_id,
urls_to_add.iter(),
time_changed,
)?;
}
Ok::<_, DbError>(())
})
})()
.map_err(AuthErr::from)
}
fn devices_by_sync_group(
&self,
user: &gpodder::User,
) -> Result<(Vec<String>, Vec<Vec<String>>), gpodder::AuthErr> {
(|| {
let mut not_synchronized = Vec::new();
let mut synchronized = Vec::new();
let conn = &mut self.pool.get()?;
let mut devices = devices::table
.select((devices::device_id, devices::sync_group_id))
.filter(devices::user_id.eq(user.id))
.order(devices::sync_group_id)
.load_iter::<(String, Option<i64>), _>(conn)?;
let mut cur_group = &mut not_synchronized;
let mut cur_group_id: Option<i64> = None;
while let Some((device_id, group_id)) = devices.next().transpose()? {
if group_id != cur_group_id {
if group_id.is_none() {
cur_group = &mut not_synchronized;
} else {
synchronized.push(Vec::new());
let index = synchronized.len() - 1;
cur_group = &mut synchronized[index];
}
cur_group_id = group_id;
}
cur_group.push(device_id);
}
Ok::<_, DbError>((not_synchronized, synchronized))
})()
.map_err(AuthErr::from)
}
}

View File

@ -1,176 +0,0 @@
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use gpodder::AuthErr;
use super::SqliteRepository;
use crate::{
models::{
device::Device,
episode_action::{ActionType, EpisodeAction, NewEpisodeAction},
},
schema::*,
DbError,
};
impl From<gpodder::EpisodeAction> for NewEpisodeAction {
fn from(value: gpodder::EpisodeAction) -> Self {
let (action, started, position, total) = match value.action {
gpodder::EpisodeActionType::New => (ActionType::New, None, None, None),
gpodder::EpisodeActionType::Delete => (ActionType::Delete, None, None, None),
gpodder::EpisodeActionType::Download => (ActionType::Download, None, None, None),
gpodder::EpisodeActionType::Play {
started,
position,
total,
} => (ActionType::Play, started, Some(position), total),
};
NewEpisodeAction {
user_id: 0,
device_id: None,
podcast_url: value.podcast,
episode_url: value.episode,
time_changed: 0,
timestamp: value.timestamp.map(|t| t.timestamp()),
action,
started,
position,
total,
}
}
}
fn to_gpodder_action(
(device_id, db_action): (Option<String>, EpisodeAction),
) -> gpodder::EpisodeAction {
let action = match db_action.action {
ActionType::Play => gpodder::EpisodeActionType::Play {
started: db_action.started,
// SAFETY: the condition that this isn't null if the action type is "play" is
// explicitely enforced by the database using a CHECK constraint.
position: db_action.position.unwrap(),
total: db_action.total,
},
ActionType::New => gpodder::EpisodeActionType::New,
ActionType::Delete => gpodder::EpisodeActionType::Delete,
ActionType::Download => gpodder::EpisodeActionType::Download,
};
gpodder::EpisodeAction {
podcast: db_action.podcast_url,
episode: db_action.episode_url,
timestamp: db_action
.timestamp
// SAFETY the input to the from_timestamp function is always the result of a
// previous timestamp() function call, which is guaranteed to be each other's
// reverse
.map(|ts| DateTime::from_timestamp(ts, 0).unwrap()),
time_changed: DateTime::from_timestamp(db_action.time_changed, 0).unwrap(),
device: device_id,
action,
}
}
impl gpodder::EpisodeActionRepository for SqliteRepository {
fn add_episode_actions(
&self,
user: &gpodder::User,
actions: Vec<gpodder::EpisodeAction>,
time_changed: DateTime<Utc>,
) -> Result<(), gpodder::AuthErr> {
(|| {
let time_changed = time_changed.timestamp();
// TODO optimize this query
// 1. The lookup for a device could be replaced with a subquery, although Diesel seems to
// have a problem using an Option<String> to match equality with a String
// 2. Ideally the for loop would be replaced with a single query inserting multiple values,
// although each value would need its own subquery
//
// NOTE this function usually gets called from the same device, so optimizing the
// amount of device lookups required would be useful.
self.pool.get()?.transaction(|conn| {
for action in actions {
let device_id = if let Some(device) = &action.device {
Some(Device::device_id_to_id(conn, user.id, device)?)
} else {
None
};
let mut new_action: NewEpisodeAction = action.into();
new_action.user_id = user.id;
new_action.device_id = device_id;
new_action.time_changed = time_changed;
diesel::insert_into(episode_actions::table)
.values(&new_action)
.execute(conn)?;
}
Ok::<_, DbError>(())
})
})()
.map_err(AuthErr::from)
}
fn episode_actions_for_user(
&self,
user: &gpodder::User,
since: Option<DateTime<Utc>>,
podcast: Option<String>,
device: Option<String>,
aggregated: bool,
) -> Result<Vec<gpodder::EpisodeAction>, gpodder::AuthErr> {
(|| {
let since = since.map(|ts| ts.timestamp()).unwrap_or(0);
let conn = &mut self.pool.get()?;
let mut query = episode_actions::table
.left_join(devices::table)
.filter(
episode_actions::user_id
.eq(user.id)
.and(episode_actions::time_changed.ge(since)),
)
.select((devices::device_id.nullable(), EpisodeAction::as_select()))
.into_boxed();
if let Some(device_id) = device {
query = query.filter(devices::device_id.eq(device_id));
}
if let Some(podcast_url) = podcast {
query = query.filter(episode_actions::podcast_url.eq(podcast_url));
}
let db_actions: Vec<(Option<String>, EpisodeAction)> = if aggregated {
// https://stackoverflow.com/a/7745635
// For each episode URL, we want to return the row with the highest `time_changed`
// value. We achieve this be left joining with self on the URL, as well as whether the
// left row's time_changed value is less than the right one. Rows with the largest
// time_changed value for a given URL will join with a NULL value (because of the left
// join), so we filter those out to retrieve the correct rows.
let a2 = diesel::alias!(episode_actions as a2);
query
.left_join(
a2.on(episode_actions::episode_url
.eq(a2.field(episode_actions::episode_url))
.and(
episode_actions::time_changed
.lt(a2.field(episode_actions::time_changed)),
)),
)
.filter(a2.field(episode_actions::episode_url).is_null())
.get_results(conn)?
} else {
query.get_results(conn)?
};
let actions = db_actions.into_iter().map(to_gpodder_action).collect();
Ok::<_, DbError>(actions)
})()
.map_err(AuthErr::from)
}
}

View File

@ -1,33 +0,0 @@
mod auth;
mod device;
mod episode_action;
mod subscription;
use std::path::Path;
use super::DbPool;
#[derive(Clone)]
pub struct SqliteRepository {
pool: DbPool,
}
impl From<DbPool> for SqliteRepository {
fn from(value: DbPool) -> Self {
Self { pool: value }
}
}
impl SqliteRepository {
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, gpodder::AuthErr> {
let pool = super::initialize_db(path, true)?;
Ok(Self { pool })
}
pub fn in_memory() -> Result<Self, gpodder::AuthErr> {
let pool = super::initialize_db_in_memory(true)?;
Ok(Self { pool })
}
}

View File

@ -1,49 +0,0 @@
mod common;
use chrono::SubsecRound;
use gpodder::{AuthStore, Session};
use gpodder_sqlite::SqliteRepository;
#[test]
fn test_create_user() {
let store = SqliteRepository::in_memory().unwrap();
let user = store.get_user("test1");
assert!(user.is_ok());
assert_eq!(user.unwrap(), None);
let new_user = store.insert_user("test1", "dummyhash");
assert!(new_user.is_ok());
let new_user = new_user.unwrap();
assert_eq!(new_user.username, "test1");
assert_eq!(new_user.password_hash, "dummyhash");
let user = store.get_user("test1");
assert!(user.is_ok());
assert_eq!(user.unwrap(), Some(new_user));
}
#[test]
fn test_create_session() {
let (store, users) = common::setup();
let session = store.get_session(123).expect("operation shouldn't fail");
assert_eq!(session, None);
let new_session = Session {
id: 123,
//
last_seen: chrono::Utc::now().trunc_subsecs(0),
user: users[0].clone(),
};
store
.insert_session(&new_session)
.expect("insert session shouldn't fail");
let session = store.get_session(123).expect("operation shouldn't fail");
assert_eq!(session, Some(new_session));
}

View File

@ -1,16 +0,0 @@
use gpodder::{AuthStore, User};
use gpodder_sqlite::SqliteRepository;
pub fn setup() -> (SqliteRepository, Vec<User>) {
let store = SqliteRepository::in_memory().unwrap();
let mut users = Vec::new();
for i in 0..4 {
let username = format!("test{}", i + 1);
let password_hash = format!("dummyhash{}", i + 1);
users.push(store.insert_user(&username, &password_hash).unwrap());
}
(store, users)
}

View File

@ -1,5 +1,7 @@
use clap::Subcommand;
use crate::db;
#[derive(Subcommand)]
pub enum Command {
/// Add devices of a specific user to the same sync group
@ -13,10 +15,9 @@ pub enum Command {
impl Command {
pub fn run(&self, config: &crate::config::Config) -> u8 {
let store =
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
.unwrap();
let store = gpodder::GpodderRepository::new(store);
let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap();
let repo = db::SqliteRepository::from(pool);
let store = crate::gpodder::GpodderRepository::new(repo);
match self {
Self::Sync { username, devices } => {

View File

@ -1,4 +1,4 @@
// mod db;
mod db;
mod gpo;
mod serve;
@ -48,8 +48,9 @@ pub struct ClapConfig {
#[derive(Subcommand)]
pub enum Command {
Serve,
// #[command(subcommand)]
// Db(db::DbCommand),
#[command(subcommand)]
Db(db::DbCommand),
/// Perform operations on the database through the Gpodder abstraction, allowing operations
/// identical to the ones performed by the API.
#[command(subcommand)]
@ -79,7 +80,7 @@ impl Cli {
match &self.cmd {
Command::Serve => serve::serve(&config),
// Command::Db(cmd) => cmd.run(&config),
Command::Db(cmd) => cmd.run(&config),
Command::Gpo(cmd) => cmd.run(&config),
}
}

View File

@ -1,18 +1,18 @@
use std::time::Duration;
use crate::server;
use crate::{db, server};
pub fn serve(config: &crate::config::Config) -> u8 {
tracing_subscriber::fmt::init();
tracing::info!("Initializing database and running migrations");
let store =
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
.unwrap();
let store = gpodder::GpodderRepository::new(store);
let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap();
let repo = db::SqliteRepository::from(pool);
let ctx = server::Context { store };
let ctx = server::Context {
store: crate::gpodder::GpodderRepository::new(repo),
};
let app = server::app(ctx.clone());
let rt = tokio::runtime::Builder::new_multi_thread()

View File

@ -1,4 +1,4 @@
mod models;
pub mod models;
mod repository;
mod schema;
@ -6,6 +6,13 @@ use diesel::connection::InstrumentationEvent;
use diesel::r2d2::CustomizeConnection;
use diesel::Connection;
pub use models::device::{Device, DeviceType, NewDevice};
pub use models::device_subscription::{DeviceSubscription, NewDeviceSubscription};
pub use models::episode_action::{ActionType, EpisodeAction, NewEpisodeAction};
pub use models::session::Session;
pub use models::sync_group::SyncGroup;
pub use models::user::{NewUser, User};
pub use repository::SqliteRepository;
use diesel::{
@ -57,15 +64,6 @@ impl From<diesel::result::Error> for DbError {
}
}
impl From<DbError> for gpodder::AuthErr {
fn from(value: DbError) -> Self {
match value {
DbError::Pool(err) => Self::Other(Box::new(err)),
DbError::Db(err) => Self::Other(Box::new(err)),
}
}
}
#[derive(Debug)]
pub struct AddQueryDebugLogs;
@ -94,16 +92,3 @@ pub fn initialize_db(path: impl AsRef<Path>, run_migrations: bool) -> Result<DbP
Ok(pool)
}
pub fn initialize_db_in_memory(run_migrations: bool) -> Result<DbPool, DbError> {
let manager = ConnectionManager::<SqliteConnection>::new(":memory:");
let pool = Pool::builder()
.connection_customizer(Box::new(AddQueryDebugLogs))
.build(manager)?;
if run_migrations {
pool.get()?.run_pending_migrations(MIGRATIONS).unwrap();
}
Ok(pool)
}

View File

@ -8,10 +8,11 @@ use diesel::{
sql_types::Text,
sqlite::{Sqlite, SqliteValue},
};
use serde::{Deserialize, Serialize};
use crate::schema::*;
use crate::db::{schema::*, DbPool, DbResult};
#[derive(Clone, Queryable, Selectable)]
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
#[diesel(table_name = devices)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Device {
@ -23,7 +24,7 @@ pub struct Device {
pub sync_group_id: Option<i64>,
}
#[derive(Insertable)]
#[derive(Deserialize, Insertable)]
#[diesel(table_name = devices)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewDevice {
@ -33,8 +34,9 @@ pub struct NewDevice {
pub type_: DeviceType,
}
#[derive(FromSqlRow, Debug, AsExpression, Clone)]
#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)]
#[diesel(sql_type = Text)]
#[serde(rename_all = "lowercase")]
pub enum DeviceType {
Desktop,
Laptop,
@ -44,6 +46,13 @@ pub enum DeviceType {
}
impl Device {
pub fn for_user(pool: &DbPool, user_id: i64) -> DbResult<Vec<Self>> {
Ok(devices::dsl::devices
.select(Self::as_select())
.filter(devices::user_id.eq(user_id))
.get_results(&mut pool.get()?)?)
}
pub fn device_id_to_id(
conn: &mut SqliteConnection,
user_id: i64,
@ -73,6 +82,22 @@ impl Device {
)
.get_result(conn)
}
pub fn update(&self, pool: &DbPool) -> DbResult<()> {
Ok(diesel::update(
devices::table.filter(
devices::user_id
.eq(self.user_id)
.and(devices::device_id.eq(&self.device_id)),
),
)
.set((
devices::caption.eq(&self.caption),
devices::type_.eq(&self.type_),
))
.execute(&mut pool.get()?)
.map(|_| ())?)
}
}
impl NewDevice {
@ -84,6 +109,13 @@ impl NewDevice {
type_,
}
}
pub fn insert(self, pool: &DbPool) -> DbResult<Device> {
Ok(diesel::insert_into(devices::table)
.values(&self)
.returning(Device::as_returning())
.get_result(&mut pool.get()?)?)
}
}
impl fmt::Display for DeviceType {

View File

@ -1,8 +1,9 @@
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use crate::schema::*;
use crate::db::schema::*;
#[derive(Clone, Queryable, Selectable)]
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
#[diesel(table_name = device_subscriptions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct DeviceSubscription {
@ -13,7 +14,7 @@ pub struct DeviceSubscription {
pub deleted: bool,
}
#[derive(Insertable)]
#[derive(Deserialize, Insertable)]
#[diesel(table_name = device_subscriptions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewDeviceSubscription {

View File

@ -9,10 +9,11 @@ use diesel::{
sqlite::{Sqlite, SqliteValue},
Selectable,
};
use serde::{Deserialize, Serialize};
use crate::schema::*;
use crate::db::schema::*;
#[derive(Clone, Queryable, Selectable)]
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
#[diesel(table_name = episode_actions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct EpisodeAction {
@ -29,7 +30,7 @@ pub struct EpisodeAction {
pub total: Option<i32>,
}
#[derive(Insertable)]
#[derive(Deserialize, Insertable)]
#[diesel(table_name = episode_actions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewEpisodeAction {
@ -45,8 +46,9 @@ pub struct NewEpisodeAction {
pub total: Option<i32>,
}
#[derive(FromSqlRow, Debug, AsExpression, Clone)]
#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)]
#[diesel(sql_type = Text)]
#[serde(rename_all = "lowercase")]
pub enum ActionType {
New,
Download,

View File

@ -0,0 +1,62 @@
use diesel::prelude::*;
use rand::Rng;
use super::user::User;
use crate::db::{schema::*, DbPool, DbResult};
#[derive(Clone, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(super::user::User))]
#[diesel(table_name = sessions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Session {
pub id: i64,
pub user_id: i64,
pub last_seen: i64,
}
impl Session {
pub fn new_for_user(pool: &DbPool, user_id: i64, last_seen: i64) -> DbResult<Self> {
let id: i64 = rand::thread_rng().gen();
Ok(Self {
id,
user_id,
last_seen,
}
.insert_into(sessions::table)
.returning(Self::as_returning())
.get_result(&mut pool.get()?)?)
}
pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult<Option<super::user::User>> {
Ok(sessions::dsl::sessions
.inner_join(users::table)
.filter(sessions::id.eq(id))
.select(User::as_select())
.get_result(&mut pool.get()?)
.optional()?)
}
pub fn user(&self, pool: &DbPool) -> DbResult<Option<super::user::User>> {
Self::user_from_id(pool, self.id)
}
pub fn by_id(pool: &DbPool, id: i64) -> DbResult<Option<Self>> {
Ok(sessions::dsl::sessions
.find(id)
.get_result(&mut pool.get()?)
.optional()?)
}
pub fn remove(self, pool: &DbPool) -> DbResult<bool> {
Self::remove_by_id(pool, self.id)
}
pub fn remove_by_id(pool: &DbPool, id: i64) -> DbResult<bool> {
Ok(
diesel::delete(sessions::dsl::sessions.filter(sessions::id.eq(id)))
.execute(&mut pool.get()?)?
> 0,
)
}
}

View File

@ -3,7 +3,7 @@ use diesel::{
prelude::*,
};
use crate::schema::*;
use crate::db::schema::*;
#[derive(Queryable, Selectable)]
#[diesel(table_name = sync_groups)]

View File

@ -0,0 +1,67 @@
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use diesel::prelude::*;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use crate::db::{schema::*, DbPool, DbResult};
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct User {
pub id: i64,
pub username: String,
pub password_hash: String,
}
#[derive(Deserialize, Insertable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewUser {
pub username: String,
pub password_hash: String,
}
fn hash_password(password: impl AsRef<str>) -> String {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password.as_ref().as_bytes(), &salt)
.unwrap()
.to_string()
}
impl NewUser {
pub fn new(username: String, password: String) -> Self {
Self {
username,
password_hash: hash_password(&password),
}
}
pub fn insert(self, pool: &DbPool) -> DbResult<User> {
Ok(diesel::insert_into(users::table)
.values(self)
.returning(User::as_returning())
.get_result(&mut pool.get()?)?)
}
}
impl User {
pub fn by_username(pool: &DbPool, username: impl AsRef<str>) -> DbResult<Option<Self>> {
Ok(users::dsl::users
.select(User::as_select())
.filter(users::username.eq(username.as_ref()))
.first(&mut pool.get()?)
.optional()?)
}
pub fn verify_password(&self, password: impl AsRef<str>) -> bool {
let password_hash = PasswordHash::new(&self.password_hash).unwrap();
Argon2::default()
.verify_password(password.as_ref().as_bytes(), &password_hash)
.is_ok()
}
}

View File

@ -1,19 +1,26 @@
use chrono::DateTime;
use diesel::prelude::*;
use gpodder::AuthErr;
use super::SqliteRepository;
use crate::{
models::{
session::Session,
user::{NewUser, User},
},
schema::*,
DbError,
db::{self, schema::*},
gpodder::{self, AuthErr},
};
impl From<User> for gpodder::User {
fn from(value: User) -> Self {
impl From<diesel::r2d2::PoolError> for gpodder::AuthErr {
fn from(value: diesel::r2d2::PoolError) -> Self {
Self::Other(Box::new(value))
}
}
impl From<diesel::result::Error> for gpodder::AuthErr {
fn from(value: diesel::result::Error) -> Self {
Self::Other(Box::new(value))
}
}
impl From<db::User> for gpodder::User {
fn from(value: db::User) -> Self {
Self {
id: value.id,
username: value.username,
@ -25,65 +32,46 @@ impl From<User> for gpodder::User {
impl gpodder::AuthStore for SqliteRepository {
fn get_user(&self, username: &str) -> Result<Option<gpodder::models::User>, AuthErr> {
Ok(users::table
.select(User::as_select())
.select(db::User::as_select())
.filter(users::username.eq(username))
.first(&mut self.pool.get().map_err(DbError::from)?)
.optional()
.map_err(DbError::from)?
.first(&mut self.pool.get()?)
.optional()?
.map(gpodder::User::from))
}
fn insert_user(&self, username: &str, password_hash: &str) -> Result<gpodder::User, AuthErr> {
let conn = &mut self.pool.get().map_err(DbError::from)?;
Ok(diesel::insert_into(users::table)
.values(NewUser {
username,
password_hash,
})
.returning(User::as_returning())
.get_result(conn)
.map(gpodder::User::from)
.map_err(DbError::from)?)
}
fn get_session(&self, session_id: i64) -> Result<Option<gpodder::models::Session>, AuthErr> {
match sessions::table
.inner_join(users::table)
.filter(sessions::id.eq(session_id))
.select((Session::as_select(), User::as_select()))
.get_result(&mut self.pool.get().map_err(DbError::from)?)
.optional()
.select((db::Session::as_select(), db::User::as_select()))
.get_result(&mut self.pool.get()?)
{
Ok(Some((session, user))) => Ok(Some(gpodder::Session {
Ok((session, user)) => Ok(Some(gpodder::Session {
id: session.id,
last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(),
user: user.into(),
})),
Ok(None) => Ok(None),
Err(err) => Err(DbError::from(err).into()),
Err(err) => Err(AuthErr::from(err)),
}
}
fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
Ok(
diesel::delete(sessions::table.filter(sessions::id.eq(session_id)))
.execute(&mut self.pool.get().map_err(DbError::from)?)
.map(|_| ())
.map_err(DbError::from)?,
.execute(&mut self.pool.get()?)
.map(|_| ())?,
)
}
fn insert_session(&self, session: &gpodder::Session) -> Result<(), AuthErr> {
Ok(Session {
Ok(db::Session {
id: session.id,
user_id: session.user.id,
last_seen: session.last_seen.timestamp(),
}
.insert_into(sessions::table)
.execute(&mut self.pool.get().map_err(DbError::from)?)
.map(|_| ())
.map_err(DbError::from)?)
.execute(&mut self.pool.get()?)
.map(|_| ())?)
}
fn refresh_session(
@ -93,8 +81,7 @@ impl gpodder::AuthStore for SqliteRepository {
) -> Result<(), AuthErr> {
if diesel::update(sessions::table.filter(sessions::id.eq(session.id)))
.set(sessions::last_seen.eq(timestamp.timestamp()))
.execute(&mut self.pool.get().map_err(DbError::from)?)
.map_err(DbError::from)?
.execute(&mut self.pool.get()?)?
== 0
{
Err(AuthErr::UnknownSession)
@ -108,8 +95,7 @@ impl gpodder::AuthStore for SqliteRepository {
Ok(
diesel::delete(sessions::table.filter(sessions::last_seen.lt(min_last_seen)))
.execute(&mut self.pool.get().map_err(DbError::from)?)
.map_err(DbError::from)?,
.execute(&mut self.pool.get()?)?,
)
}
}

View File

@ -0,0 +1,275 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use diesel::{alias, dsl::not, prelude::*};
use super::SqliteRepository;
use crate::{
db::{self, schema::*, SyncGroup},
gpodder,
};
impl From<db::DeviceType> for gpodder::DeviceType {
fn from(value: db::DeviceType) -> Self {
match value {
db::DeviceType::Desktop => Self::Desktop,
db::DeviceType::Laptop => Self::Laptop,
db::DeviceType::Mobile => Self::Mobile,
db::DeviceType::Server => Self::Server,
db::DeviceType::Other => Self::Other,
}
}
}
impl From<gpodder::DeviceType> for db::DeviceType {
fn from(value: gpodder::DeviceType) -> Self {
match value {
gpodder::DeviceType::Desktop => Self::Desktop,
gpodder::DeviceType::Laptop => Self::Laptop,
gpodder::DeviceType::Mobile => Self::Mobile,
gpodder::DeviceType::Server => Self::Server,
gpodder::DeviceType::Other => Self::Other,
}
}
}
impl gpodder::DeviceRepository for SqliteRepository {
fn devices_for_user(
&self,
user: &gpodder::User,
) -> Result<Vec<gpodder::Device>, gpodder::AuthErr> {
Ok(devices::table
.select(db::Device::as_select())
.filter(devices::user_id.eq(user.id))
.get_results(&mut self.pool.get()?)?
.into_iter()
.map(|d| gpodder::Device {
id: d.device_id,
caption: d.caption,
r#type: d.type_.into(),
// TODO implement subscription count
subscriptions: 0,
})
.collect())
}
fn update_device_info(
&self,
user: &gpodder::User,
device_id: &str,
patch: gpodder::DevicePatch,
) -> Result<(), gpodder::AuthErr> {
if let Some(mut device) = devices::table
.select(db::Device::as_select())
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
.get_result(&mut self.pool.get()?)
.optional()?
{
if let Some(caption) = patch.caption {
device.caption = caption;
}
if let Some(type_) = patch.r#type {
device.type_ = type_.into();
}
diesel::update(devices::table.filter(devices::id.eq(device.id)))
.set((
devices::caption.eq(&device.caption),
devices::type_.eq(&device.type_),
))
.execute(&mut self.pool.get()?)?;
} else {
let device = db::NewDevice {
device_id: device_id.to_string(),
user_id: user.id,
caption: patch.caption.unwrap_or(String::new()),
type_: patch.r#type.unwrap_or(gpodder::DeviceType::Other).into(),
};
diesel::insert_into(devices::table)
.values(device)
.execute(&mut self.pool.get()?)?;
}
Ok(())
}
fn merge_sync_groups(
&self,
user: &gpodder::User,
device_ids: Vec<&str>,
) -> Result<i64, gpodder::AuthErr> {
let conn = &mut self.pool.get()?;
Ok(conn.transaction(|conn| {
let devices: Vec<(i64, Option<i64>)> = devices::table
.select((devices::id, devices::sync_group_id))
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq_any(device_ids)),
)
.get_results(conn)?;
let mut sync_group_ids: Vec<i64> = devices
.iter()
.filter_map(|(_, group_id)| *group_id)
.collect();
// Remove any duplicates, giving us each sync group ID once
sync_group_ids.sort();
sync_group_ids.dedup();
// If any of the devices are already in a sync group, we reuse the first one we find.
// Otherwise, we generate a new one.
let sync_group_id = if let Some(id) = sync_group_ids.pop() {
id
} else {
db::SyncGroup::new(conn)?.id
};
// Move all devices in the other sync groups into the new sync group
diesel::update(
devices::table.filter(devices::sync_group_id.eq_any(sync_group_ids.iter())),
)
.set(devices::sync_group_id.eq(sync_group_id))
.execute(conn)?;
// Add the non-synchronized devices into the new sync group
let unsynced_device_ids =
devices
.iter()
.filter_map(|(id, group_id)| if group_id.is_none() { Some(id) } else { None });
diesel::update(devices::table.filter(devices::id.eq_any(unsynced_device_ids)))
.set(devices::sync_group_id.eq(sync_group_id))
.execute(conn)?;
// Remove the other now unused sync groups
diesel::delete(sync_groups::table.filter(sync_groups::id.eq_any(sync_group_ids)))
.execute(conn)?;
Ok::<_, diesel::result::Error>(sync_group_id)
})?)
}
fn remove_from_sync_group(
&self,
user: &gpodder::User,
device_ids: Vec<&str>,
) -> Result<(), gpodder::AuthErr> {
let conn = &mut self.pool.get()?;
diesel::update(
devices::table.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq_any(device_ids)),
),
)
.set(devices::sync_group_id.eq(None::<i64>))
.execute(conn)?;
// This is in a different transaction on purpose, as the success of this removal shouldn't
// fail the entire query
SyncGroup::remove_unused(conn)?;
Ok(())
}
fn synchronize_sync_group(
&self,
group_id: i64,
time_changed: DateTime<Utc>,
) -> Result<(), gpodder::AuthErr> {
let time_changed = time_changed.timestamp();
let conn = &mut self.pool.get()?;
conn.transaction(|conn| {
let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id))
.select(devices::id)
.get_results(conn)?;
// For each device in the group, we get the list of subscriptions not yet in its own
// non-deleted list, and add it to the database
for device_id in device_ids.iter().copied() {
let d1 = alias!(device_subscriptions as d1);
let own_subscriptions = d1
.filter(
d1.field(device_subscriptions::device_id)
.eq(device_id)
.and(d1.field(device_subscriptions::deleted).eq(false)),
)
.select(d1.field(device_subscriptions::podcast_url));
let urls_to_add = device_subscriptions::table
.select(device_subscriptions::podcast_url)
.filter(
device_subscriptions::device_id
.eq_any(device_ids.iter())
.and(device_subscriptions::deleted.eq(false))
.and(not(
device_subscriptions::podcast_url.eq_any(own_subscriptions)
)),
)
.distinct()
.load_iter(conn)?
.collect::<Result<HashSet<String>, _>>()?;
super::subscription::insert_subscriptions_for_single_device(
conn,
device_id,
urls_to_add.iter(),
time_changed,
)?;
}
Ok::<_, diesel::result::Error>(())
})?;
Ok(())
}
fn devices_by_sync_group(
&self,
user: &gpodder::User,
) -> Result<(Vec<String>, Vec<Vec<String>>), gpodder::AuthErr> {
let mut not_synchronized = Vec::new();
let mut synchronized = Vec::new();
let conn = &mut self.pool.get()?;
let mut devices = devices::table
.select((devices::device_id, devices::sync_group_id))
.filter(devices::user_id.eq(user.id))
.order(devices::sync_group_id)
.load_iter::<(String, Option<i64>), _>(conn)?;
let mut cur_group = &mut not_synchronized;
let mut cur_group_id: Option<i64> = None;
while let Some((device_id, group_id)) = devices.next().transpose()? {
if group_id != cur_group_id {
if group_id.is_none() {
cur_group = &mut not_synchronized;
} else {
synchronized.push(Vec::new());
let index = synchronized.len() - 1;
cur_group = &mut synchronized[index];
}
cur_group_id = group_id;
}
cur_group.push(device_id);
}
Ok((not_synchronized, synchronized))
}
}

View File

@ -0,0 +1,170 @@
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use super::SqliteRepository;
use crate::{
db::{self, schema::*},
gpodder,
};
impl From<gpodder::EpisodeAction> for db::NewEpisodeAction {
fn from(value: gpodder::EpisodeAction) -> Self {
let (action, started, position, total) = match value.action {
gpodder::EpisodeActionType::New => (db::ActionType::New, None, None, None),
gpodder::EpisodeActionType::Delete => (db::ActionType::Delete, None, None, None),
gpodder::EpisodeActionType::Download => (db::ActionType::Download, None, None, None),
gpodder::EpisodeActionType::Play {
started,
position,
total,
} => (db::ActionType::Play, started, Some(position), total),
};
db::NewEpisodeAction {
user_id: 0,
device_id: None,
podcast_url: value.podcast,
episode_url: value.episode,
time_changed: 0,
timestamp: value.timestamp.map(|t| t.timestamp()),
action,
started,
position,
total,
}
}
}
impl From<(Option<String>, db::EpisodeAction)> for gpodder::EpisodeAction {
fn from((device_id, db_action): (Option<String>, db::EpisodeAction)) -> Self {
let action = match db_action.action {
db::ActionType::Play => gpodder::EpisodeActionType::Play {
started: db_action.started,
// SAFETY: the condition that this isn't null if the action type is "play" is
// explicitely enforced by the database using a CHECK constraint.
position: db_action.position.unwrap(),
total: db_action.total,
},
db::ActionType::New => gpodder::EpisodeActionType::New,
db::ActionType::Delete => gpodder::EpisodeActionType::Delete,
db::ActionType::Download => gpodder::EpisodeActionType::Download,
};
Self {
podcast: db_action.podcast_url,
episode: db_action.episode_url,
timestamp: db_action
.timestamp
// SAFETY the input to the from_timestamp function is always the result of a
// previous timestamp() function call, which is guaranteed to be each other's
// reverse
.map(|ts| DateTime::from_timestamp(ts, 0).unwrap()),
time_changed: DateTime::from_timestamp(db_action.time_changed, 0).unwrap(),
device: device_id,
action,
}
}
}
impl gpodder::EpisodeActionRepository for SqliteRepository {
fn add_episode_actions(
&self,
user: &gpodder::User,
actions: Vec<gpodder::EpisodeAction>,
time_changed: DateTime<Utc>,
) -> Result<(), gpodder::AuthErr> {
let time_changed = time_changed.timestamp();
// TODO optimize this query
// 1. The lookup for a device could be replaced with a subquery, although Diesel seems to
// have a problem using an Option<String> to match equality with a String
// 2. Ideally the for loop would be replaced with a single query inserting multiple values,
// although each value would need its own subquery
self.pool.get()?.transaction(|conn| {
for action in actions {
let device_id = if let Some(device) = &action.device {
Some(db::Device::device_id_to_id(conn, user.id, device)?)
} else {
None
};
let mut new_action: db::NewEpisodeAction = action.into();
new_action.user_id = user.id;
new_action.device_id = device_id;
new_action.time_changed = time_changed;
diesel::insert_into(episode_actions::table)
.values(&new_action)
.execute(conn)?;
}
Ok::<_, diesel::result::Error>(())
})?;
Ok(())
}
fn episode_actions_for_user(
&self,
user: &gpodder::User,
since: Option<DateTime<Utc>>,
podcast: Option<String>,
device: Option<String>,
aggregated: bool,
) -> Result<Vec<gpodder::EpisodeAction>, gpodder::AuthErr> {
let since = since.map(|ts| ts.timestamp()).unwrap_or(0);
let conn = &mut self.pool.get()?;
let mut query = episode_actions::table
.left_join(devices::table)
.filter(
episode_actions::user_id
.eq(user.id)
.and(episode_actions::time_changed.ge(since)),
)
.select((
devices::device_id.nullable(),
db::EpisodeAction::as_select(),
))
.into_boxed();
if let Some(device_id) = device {
query = query.filter(devices::device_id.eq(device_id));
}
if let Some(podcast_url) = podcast {
query = query.filter(episode_actions::podcast_url.eq(podcast_url));
}
let db_actions: Vec<(Option<String>, db::EpisodeAction)> = if aggregated {
// https://stackoverflow.com/a/7745635
// For each episode URL, we want to return the row with the highest `time_changed`
// value. We achieve this be left joining with self on the URL, as well as whether the
// left row's time_changed value is less than the right one. Rows with the largest
// time_changed value for a given URL will join with a NULL value (because of the left
// join), so we filter those out to retrieve the correct rows.
let a2 = diesel::alias!(episode_actions as a2);
query
.left_join(
a2.on(episode_actions::episode_url
.eq(a2.field(episode_actions::episode_url))
.and(
episode_actions::time_changed
.lt(a2.field(episode_actions::time_changed)),
)),
)
.filter(a2.field(episode_actions::episode_url).is_null())
.get_results(conn)?
} else {
query.get_results(conn)?
};
let actions = db_actions
.into_iter()
.map(gpodder::EpisodeAction::from)
.collect();
Ok(actions)
}
}

View File

@ -0,0 +1,17 @@
mod auth;
mod device;
mod episode_action;
mod subscription;
use super::DbPool;
#[derive(Clone)]
pub struct SqliteRepository {
pool: DbPool,
}
impl From<DbPool> for SqliteRepository {
fn from(value: DbPool) -> Self {
Self { pool: value }
}
}

View File

@ -2,15 +2,22 @@ use std::collections::HashSet;
use chrono::DateTime;
use diesel::prelude::*;
use gpodder::AuthErr;
use super::SqliteRepository;
use crate::{
models::device_subscription::{DeviceSubscription, NewDeviceSubscription},
schema::*,
DbError,
db::{self, schema::*},
gpodder,
};
impl From<(String, i64)> for gpodder::Subscription {
fn from((url, ts): (String, i64)) -> Self {
Self {
url,
time_changed: DateTime::from_timestamp(ts, 0).unwrap(),
}
}
}
fn set_subscriptions_for_single_device(
conn: &mut SqliteConnection,
device_id: i64,
@ -73,7 +80,7 @@ fn set_subscriptions_for_single_device(
.values(
urls_to_insert
.into_iter()
.map(|url| NewDeviceSubscription {
.map(|url| db::NewDeviceSubscription {
device_id,
podcast_url: url.to_string(),
deleted: false,
@ -98,7 +105,7 @@ pub fn insert_subscriptions_for_single_device<'a>(
diesel::insert_into(device_subscriptions::table)
.values(
urls.into_iter()
.map(|url| NewDeviceSubscription {
.map(|url| db::NewDeviceSubscription {
device_id,
podcast_url: url.to_string(),
deleted: false,
@ -177,26 +184,18 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
&self,
user: &gpodder::User,
) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> {
(|| {
Ok::<_, DbError>(
device_subscriptions::table
.inner_join(devices::table)
.filter(devices::user_id.eq(user.id))
.select((
device_subscriptions::podcast_url,
device_subscriptions::time_changed,
))
.distinct()
.get_results::<(String, i64)>(&mut self.pool.get()?)?
.into_iter()
.map(|(url, ts)| gpodder::Subscription {
url,
time_changed: DateTime::from_timestamp(ts, 0).unwrap(),
})
.collect(),
)
})()
.map_err(AuthErr::from)
Ok(device_subscriptions::table
.inner_join(devices::table)
.filter(devices::user_id.eq(user.id))
.select((
device_subscriptions::podcast_url,
device_subscriptions::time_changed,
))
.distinct()
.get_results::<(String, i64)>(&mut self.pool.get()?)?
.into_iter()
.map(Into::into)
.collect())
}
fn subscriptions_for_device(
@ -204,29 +203,21 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
user: &gpodder::User,
device_id: &str,
) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> {
(|| {
Ok::<_, DbError>(
device_subscriptions::table
.inner_join(devices::table)
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
.select((
device_subscriptions::podcast_url,
device_subscriptions::time_changed,
))
.get_results::<(String, i64)>(&mut self.pool.get()?)?
.into_iter()
.map(|(url, ts)| gpodder::Subscription {
url,
time_changed: DateTime::from_timestamp(ts, 0).unwrap(),
})
.collect(),
Ok(device_subscriptions::table
.inner_join(devices::table)
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
})()
.map_err(AuthErr::from)
.select((
device_subscriptions::podcast_url,
device_subscriptions::time_changed,
))
.get_results::<(String, i64)>(&mut self.pool.get()?)?
.into_iter()
.map(Into::into)
.collect())
}
fn set_subscriptions_for_device(
@ -236,39 +227,38 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
urls: Vec<String>,
time_changed: chrono::DateTime<chrono::Utc>,
) -> Result<(), gpodder::AuthErr> {
(|| {
let time_changed = time_changed.timestamp();
let urls: HashSet<String> = urls.into_iter().collect();
let time_changed = time_changed.timestamp();
let urls: HashSet<String> = urls.into_iter().collect();
self.pool.get()?.transaction(|conn| {
let (device_id, group_id) = devices::table
.select((devices::id, devices::sync_group_id))
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
.get_result::<(i64, Option<i64>)>(conn)?;
self.pool.get()?.transaction(|conn| {
let (device_id, group_id) = devices::table
.select((devices::id, devices::sync_group_id))
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
.get_result::<(i64, Option<i64>)>(conn)?;
// If the device is part of a sync group, we need to perform the update on every device
// in the group
if let Some(group_id) = group_id {
let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id))
.select(devices::id)
.get_results(conn)?;
// If the device is part of a sync group, we need to perform the update on every device
// in the group
if let Some(group_id) = group_id {
let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id))
.select(devices::id)
.get_results(conn)?;
for device_id in device_ids {
set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?;
}
} else {
for device_id in device_ids {
set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?;
}
} else {
set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?;
}
Ok::<_, DbError>(())
})
})()
.map_err(AuthErr::from)
Ok::<_, diesel::result::Error>(())
})?;
Ok(())
}
fn update_subscriptions_for_device(
@ -279,42 +269,32 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
remove: Vec<String>,
time_changed: chrono::DateTime<chrono::Utc>,
) -> Result<(), gpodder::AuthErr> {
(|| {
let time_changed = time_changed.timestamp();
let time_changed = time_changed.timestamp();
// TODO URLs that are in both the added and removed lists will currently get "re-added",
// meaning their change timestamp will be updated even though they haven't really changed.
let add: HashSet<_> = add.into_iter().collect();
let remove: HashSet<_> = remove.into_iter().collect();
// TODO URLs that are in both the added and removed lists will currently get "re-added",
// meaning their change timestamp will be updated even though they haven't really changed.
let add: HashSet<_> = add.into_iter().collect();
let remove: HashSet<_> = remove.into_iter().collect();
self.pool.get()?.transaction(|conn| {
let (device_id, group_id) = devices::table
.select((devices::id, devices::sync_group_id))
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
.get_result::<(i64, Option<i64>)>(conn)?;
self.pool.get()?.transaction(|conn| {
let (device_id, group_id) = devices::table
.select((devices::id, devices::sync_group_id))
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id)),
)
.get_result::<(i64, Option<i64>)>(conn)?;
// If the device is part of a sync group, we need to perform the update on every device
// in the group
if let Some(group_id) = group_id {
let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id))
.select(devices::id)
.get_results(conn)?;
// If the device is part of a sync group, we need to perform the update on every device
// in the group
if let Some(group_id) = group_id {
let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id))
.select(devices::id)
.get_results(conn)?;
for device_id in device_ids {
update_subscriptions_for_single_device(
conn,
device_id,
&add,
&remove,
time_changed,
)?;
}
} else {
for device_id in device_ids {
update_subscriptions_for_single_device(
conn,
device_id,
@ -323,11 +303,20 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
time_changed,
)?;
}
} else {
update_subscriptions_for_single_device(
conn,
device_id,
&add,
&remove,
time_changed,
)?;
}
Ok::<_, DbError>(())
})
})()
.map_err(AuthErr::from)
Ok::<_, diesel::result::Error>(())
})?;
Ok(())
}
fn subscription_updates_for_device(
@ -336,39 +325,36 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
device_id: &str,
since: chrono::DateTime<chrono::Utc>,
) -> Result<(Vec<gpodder::Subscription>, Vec<gpodder::Subscription>), gpodder::AuthErr> {
(|| {
let since = since.timestamp();
let since = since.timestamp();
let (mut added, mut removed) = (Vec::new(), Vec::new());
let (mut added, mut removed) = (Vec::new(), Vec::new());
let query = device_subscriptions::table
.inner_join(devices::table)
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id))
.and(device_subscriptions::time_changed.ge(since)),
)
.select(DeviceSubscription::as_select());
let query = device_subscriptions::table
.inner_join(devices::table)
.filter(
devices::user_id
.eq(user.id)
.and(devices::device_id.eq(device_id))
.and(device_subscriptions::time_changed.ge(since)),
)
.select(db::DeviceSubscription::as_select());
for sub in query.load_iter(&mut self.pool.get()?)? {
let sub = sub?;
for sub in query.load_iter(&mut self.pool.get()?)? {
let sub = sub?;
if sub.deleted {
removed.push(gpodder::Subscription {
url: sub.podcast_url,
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
});
} else {
added.push(gpodder::Subscription {
url: sub.podcast_url,
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
});
}
if sub.deleted {
removed.push(gpodder::Subscription {
url: sub.podcast_url,
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
});
} else {
added.push(gpodder::Subscription {
url: sub.podcast_url,
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
});
}
}
Ok::<_, DbError>((added, removed))
})()
.map_err(AuthErr::from)
Ok((added, removed))
}
}

View File

@ -1,7 +1,11 @@
pub mod models;
mod repository;
use std::fmt::Display;
use crate::models::*;
use chrono::{DateTime, Utc};
pub use models::*;
pub use repository::GpodderRepository;
#[derive(Debug)]
pub enum AuthErr {
@ -36,17 +40,12 @@ impl<T> Store for T where
pub trait AuthStore {
/// Retrieve the session with the given session ID
fn get_session(&self, session_id: i64) -> Result<Option<Session>, AuthErr>;
fn get_session(&self, session_id: i64) -> Result<Option<models::Session>, AuthErr>;
/// Retrieve the user with the given username
fn get_user(&self, username: &str) -> Result<Option<User>, AuthErr>;
/// Insert a new user into the data store
fn insert_user(&self, username: &str, password_hash: &str) -> Result<User, AuthErr>;
fn get_user(&self, username: &str) -> Result<Option<models::User>, AuthErr>;
/// Create a new session for a user with the given session ID
///
/// The `last_seen` timestamp's precision should be at least accurate to the second
fn insert_session(&self, session: &Session) -> Result<(), AuthErr>;
/// Remove the session with the given session ID
@ -110,10 +109,10 @@ pub trait SubscriptionRepository {
&self,
user: &User,
device_id: &str,
) -> Result<Vec<Subscription>, AuthErr>;
) -> Result<Vec<models::Subscription>, AuthErr>;
/// Return all subscriptions for a given user
fn subscriptions_for_user(&self, user: &User) -> Result<Vec<Subscription>, AuthErr>;
fn subscriptions_for_user(&self, user: &User) -> Result<Vec<models::Subscription>, AuthErr>;
/// Replace the list of subscriptions for a device and all devices in its sync group with the
/// given list

View File

@ -1,13 +1,12 @@
use chrono::{DateTime, Utc};
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone)]
pub struct User {
pub id: i64,
pub username: String,
pub password_hash: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviceType {
Desktop,
Laptop,
@ -16,7 +15,6 @@ pub enum DeviceType {
Other,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Device {
pub id: String,
pub caption: String,
@ -24,13 +22,11 @@ pub struct Device {
pub subscriptions: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DevicePatch {
pub caption: Option<String>,
pub r#type: Option<DeviceType>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EpisodeActionType {
Download,
Play {
@ -42,7 +38,6 @@ pub enum EpisodeActionType {
New,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EpisodeAction {
pub podcast: String,
pub episode: String,
@ -52,14 +47,12 @@ pub struct EpisodeAction {
pub action: EpisodeActionType,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Session {
pub id: i64,
pub last_seen: DateTime<Utc>,
pub user: User,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Subscription {
pub url: String,
pub time_changed: DateTime<Utc>,

View File

@ -1,13 +1,10 @@
use std::{collections::HashSet, sync::Arc};
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use chrono::{DateTime, TimeDelta, Utc};
use rand::{rngs::OsRng, Rng};
use rand::Rng;
use crate::{
models,
store::{AuthErr, Store},
};
use super::{models, AuthErr, Store};
const MAX_SESSION_AGE: i64 = 60 * 60 * 24 * 7;
@ -41,17 +38,6 @@ impl GpodderRepository {
self.store.get_user(username)?.ok_or(AuthErr::UnknownUser)
}
pub fn create_user(&self, username: &str, password: &str) -> Result<models::User, AuthErr> {
let salt = SaltString::generate(&mut OsRng);
let password_hash = Argon2::default()
.hash_password(password.as_bytes(), &salt)
.unwrap()
.to_string();
self.store.insert_user(username, &password_hash)
}
pub fn validate_credentials(
&self,
username: &str,

View File

@ -1,5 +1,7 @@
mod cli;
mod config;
mod db;
mod gpodder;
mod server;
use clap::Parser;

View File

@ -2,13 +2,13 @@ use std::fmt;
use axum::{http::StatusCode, response::IntoResponse};
use crate::ErrorExt;
use crate::{db, ErrorExt};
pub type AppResult<T> = Result<T, AppError>;
#[derive(Debug)]
pub enum AppError {
// Db(db::DbError),
Db(db::DbError),
IO(std::io::Error),
Other(Box<dyn std::error::Error + 'static + Send + Sync>),
BadRequest,
@ -19,7 +19,7 @@ pub enum AppError {
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Self::Db(_) => write!(f, "database error"),
Self::Db(_) => write!(f, "database error"),
Self::IO(_) => write!(f, "io error"),
Self::Other(_) => write!(f, "other error"),
Self::BadRequest => write!(f, "bad request"),
@ -32,7 +32,7 @@ impl fmt::Display for AppError {
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
// Self::Db(err) => Some(err),
Self::Db(err) => Some(err),
Self::IO(err) => Some(err),
Self::Other(err) => Some(err.as_ref()),
Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
@ -40,6 +40,12 @@ impl std::error::Error for AppError {
}
}
impl From<db::DbError> for AppError {
fn from(value: db::DbError) -> Self {
Self::Db(value)
}
}
impl From<std::io::Error> for AppError {
fn from(value: std::io::Error) -> Self {
Self::IO(value)

View File

@ -10,10 +10,13 @@ use axum_extra::{
};
use cookie::time::Duration;
use crate::server::{
error::{AppError, AppResult},
gpodder::SESSION_ID_COOKIE,
Context,
use crate::{
gpodder,
server::{
error::{AppError, AppResult},
gpodder::SESSION_ID_COOKIE,
Context,
},
};
pub fn router() -> Router<Context> {

View File

@ -5,14 +5,17 @@ use axum::{
Extension, Json, Router,
};
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models,
use crate::{
gpodder,
server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models,
},
Context,
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -7,15 +7,18 @@ use axum::{
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models,
models::UpdatedUrlsResponse,
use crate::{
gpodder,
server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models,
models::UpdatedUrlsResponse,
},
Context,
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -6,14 +6,17 @@ use axum::{
};
use serde::Deserialize;
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
use crate::{
gpodder,
server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
},
Context,
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -5,14 +5,17 @@ use axum::{
Extension, Json, Router,
};
use crate::server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models::{SyncStatus, SyncStatusDelta},
use crate::{
gpodder,
server::{
error::{AppError, AppResult},
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models::{SyncStatus, SyncStatusDelta},
},
Context,
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -17,7 +17,7 @@ use axum_extra::{
};
use tower_http::set_header::SetResponseHeaderLayer;
use crate::server::error::AppError;
use crate::{gpodder, server::error::AppError};
use super::Context;

View File

@ -1,6 +1,8 @@
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::gpodder;
#[derive(Deserialize, Debug)]
pub struct SubscriptionDelta {
pub add: Vec<String>,

View File

@ -5,10 +5,13 @@ use axum::{
Extension, Json, Router,
};
use crate::server::{
error::{AppError, AppResult},
gpodder::{auth_middleware, format::StringWithFormat},
Context,
use crate::{
gpodder,
server::{
error::{AppError, AppResult},
gpodder::{auth_middleware, format::StringWithFormat},
Context,
},
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -1,27 +1,18 @@
mod error;
mod gpodder;
use axum::{
body::Body,
extract::Request,
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
Router,
};
use http_body_util::BodyExt;
use axum::{extract::Request, middleware::Next, response::Response, Router};
use tower_http::trace::TraceLayer;
#[derive(Clone)]
pub struct Context {
pub store: ::gpodder::GpodderRepository,
pub store: crate::gpodder::GpodderRepository,
}
pub fn app(ctx: Context) -> Router {
Router::new()
.merge(gpodder::router(ctx.clone()))
.layer(axum::middleware::from_fn(header_logger))
.layer(axum::middleware::from_fn(body_logger))
.layer(TraceLayer::new_for_http())
.with_state(ctx)
}
@ -35,41 +26,3 @@ async fn header_logger(request: Request, next: Next) -> Response {
res
}
async fn body_logger(request: Request, next: Next) -> Response {
let (parts, body) = request.into_parts();
let bytes = match body
.collect()
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())
{
Ok(res) => res.to_bytes(),
Err(err) => {
return err;
}
};
tracing::debug!("request body = {:?}", String::from_utf8(bytes.to_vec()));
let res = next
.run(Request::from_parts(parts, Body::from(bytes)))
.await;
let (parts, body) = res.into_parts();
let bytes = match body
.collect()
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())
{
Ok(res) => res.to_bytes(),
Err(err) => {
return err;
}
};
tracing::debug!("response body = {:?}", String::from_utf8(bytes.to_vec()));
Response::from_parts(parts, Body::from(bytes))
}