add directory service for peer profile management with SQLite integration
This commit is contained in:
parent
5c8f57e07c
commit
b29039557a
|
|
@ -37,6 +37,18 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
@ -614,6 +626,7 @@ dependencies = [
|
||||||
"libp2p",
|
"libp2p",
|
||||||
"log",
|
"log",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -701,6 +714,18 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fiat-crypto"
|
name = "fiat-crypto"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
|
@ -938,6 +963,15 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
|
|
@ -955,6 +989,15 @@ version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -1925,6 +1968,17 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
|
|
@ -2332,6 +2386,12 @@ dependencies = [
|
||||||
"spki",
|
"spki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.11.0"
|
version = "3.11.0"
|
||||||
|
|
@ -2744,6 +2804,20 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
@ -3392,6 +3466,12 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,5 @@ reqwest = { version = "0.12", default-features = false, features = [
|
||||||
"json",
|
"json",
|
||||||
] }
|
] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
|
|
|
||||||
174
src/main.rs
174
src/main.rs
|
|
@ -29,6 +29,8 @@ use std::collections::{HashMap, VecDeque};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
connection_limits, gossipsub, identify, noise, ping, relay, rendezvous,
|
connection_limits, gossipsub, identify, noise, ping, relay, rendezvous,
|
||||||
|
|
@ -59,6 +61,8 @@ struct RelayBehaviour {
|
||||||
limits: connection_limits::Behaviour,
|
limits: connection_limits::Behaviour,
|
||||||
// gif search service - clients send GifRequest, relay responds with GifResponse
|
// gif search service - clients send GifRequest, relay responds with GifResponse
|
||||||
gif_service: cbor::Behaviour<GifRequest, GifResponse>,
|
gif_service: cbor::Behaviour<GifRequest, GifResponse>,
|
||||||
|
// persistent directory service - clients register/search peer profiles
|
||||||
|
directory_service: cbor::Behaviour<DirectoryRequest, DirectoryResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- gif protocol ----
|
// ---- gif protocol ----
|
||||||
|
|
@ -67,6 +71,8 @@ struct RelayBehaviour {
|
||||||
// relay so clients never need credentials.
|
// relay so clients never need credentials.
|
||||||
|
|
||||||
const GIF_PROTOCOL: StreamProtocol = StreamProtocol::new("/dusk/gif/1.0.0");
|
const GIF_PROTOCOL: StreamProtocol = StreamProtocol::new("/dusk/gif/1.0.0");
|
||||||
|
const DIRECTORY_PROTOCOL: libp2p::StreamProtocol =
|
||||||
|
libp2p::StreamProtocol::new("/dusk/directory/1.0.0");
|
||||||
const KLIPY_API_BASE: &str = "https://api.klipy.com/v2";
|
const KLIPY_API_BASE: &str = "https://api.klipy.com/v2";
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
@ -202,6 +208,35 @@ impl GifRateLimiter {
|
||||||
}
|
}
|
||||||
// ---- end gif rate limiter ----
|
// ---- end gif rate limiter ----
|
||||||
|
|
||||||
|
// ---- directory protocol ----
|
||||||
|
// clients register their display_name here so others can search
|
||||||
|
// even when that peer is offline. the relay only stores peer_id + display_name.
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum DirectoryRequest {
|
||||||
|
// register/update this peer's display_name in the index
|
||||||
|
// peer_id is taken from the libp2p connection, not trusted from the request
|
||||||
|
Register { display_name: String },
|
||||||
|
// search the index by display_name (LIKE %query%) or exact peer_id
|
||||||
|
Search { query: String },
|
||||||
|
// remove this peer's profile from the index
|
||||||
|
Remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum DirectoryResponse {
|
||||||
|
Ok,
|
||||||
|
Results(Vec<DirectoryProfileEntry>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct DirectoryProfileEntry {
|
||||||
|
pub peer_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub last_seen: u64,
|
||||||
|
}
|
||||||
|
// ---- end directory protocol ----
|
||||||
|
|
||||||
// fetch from klipy and normalize into our GifResult format
|
// fetch from klipy and normalize into our GifResult format
|
||||||
async fn fetch_klipy(
|
async fn fetch_klipy(
|
||||||
http: &reqwest::Client,
|
http: &reqwest::Client,
|
||||||
|
|
@ -313,6 +348,34 @@ fn load_or_generate_keypair() -> libp2p::identity::Keypair {
|
||||||
kp
|
kp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolve path for the relay's persistent directory database
|
||||||
|
fn directory_db_path() -> std::path::PathBuf {
|
||||||
|
if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "dusk-relay") {
|
||||||
|
let dir = proj_dirs.data_dir().to_path_buf();
|
||||||
|
std::fs::create_dir_all(&dir).ok();
|
||||||
|
dir.join("directory.sqlite3")
|
||||||
|
} else {
|
||||||
|
std::path::PathBuf::from("./relay_directory.sqlite3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_directory_db() -> Result<Connection, rusqlite::Error> {
|
||||||
|
let conn = Connection::open(directory_db_path())?;
|
||||||
|
conn.execute_batch(
|
||||||
|
"PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA synchronous = NORMAL;
|
||||||
|
CREATE TABLE IF NOT EXISTS peer_profiles (
|
||||||
|
peer_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
last_seen INTEGER NOT NULL,
|
||||||
|
registered_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_display_name
|
||||||
|
ON peer_profiles(display_name COLLATE NOCASE);",
|
||||||
|
)?;
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
@ -428,6 +491,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
request_response::Config::default()
|
request_response::Config::default()
|
||||||
.with_request_timeout(Duration::from_secs(15)),
|
.with_request_timeout(Duration::from_secs(15)),
|
||||||
),
|
),
|
||||||
|
// persistent directory service - clients register/search/remove profiles
|
||||||
|
directory_service: cbor::Behaviour::new(
|
||||||
|
[(DIRECTORY_PROTOCOL, ProtocolSupport::Inbound)],
|
||||||
|
request_response::Config::default()
|
||||||
|
.with_request_timeout(Duration::from_secs(15)),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
})?
|
})?
|
||||||
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(300)))
|
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(300)))
|
||||||
|
|
@ -502,6 +571,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// track peer relay connections for federation metrics
|
// track peer relay connections for federation metrics
|
||||||
let mut connected_peer_relays: Vec<PeerId> = Vec::new();
|
let mut connected_peer_relays: Vec<PeerId> = Vec::new();
|
||||||
|
|
||||||
|
let dir_db = match open_directory_db() {
|
||||||
|
Ok(db) => {
|
||||||
|
log::info!("directory db opened at {}", directory_db_path().display());
|
||||||
|
db
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("failed to open directory db: {}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let now_secs = || {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs()
|
||||||
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let event = tokio::select! {
|
let event = tokio::select! {
|
||||||
// periodic cache eviction
|
// periodic cache eviction
|
||||||
|
|
@ -800,6 +886,94 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// directory service - clients register/search/remove their profiles
|
||||||
|
SwarmEvent::Behaviour(RelayBehaviourEvent::DirectoryService(
|
||||||
|
request_response::Event::Message {
|
||||||
|
peer,
|
||||||
|
message: request_response::Message::Request { request, channel, .. },
|
||||||
|
..
|
||||||
|
},
|
||||||
|
)) => {
|
||||||
|
let response = match request {
|
||||||
|
DirectoryRequest::Register { display_name } => {
|
||||||
|
let ts = now_secs();
|
||||||
|
let result = dir_db.execute(
|
||||||
|
"INSERT INTO peer_profiles (peer_id, display_name, last_seen, registered_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?3)
|
||||||
|
ON CONFLICT(peer_id) DO UPDATE SET
|
||||||
|
display_name = excluded.display_name,
|
||||||
|
last_seen = excluded.last_seen",
|
||||||
|
params![peer.to_string(), display_name, ts as i64],
|
||||||
|
);
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("directory: registered peer {} as '{}'", peer, display_name);
|
||||||
|
DirectoryResponse::Ok
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("directory: failed to register {}: {}", peer, e);
|
||||||
|
DirectoryResponse::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DirectoryRequest::Search { query } => {
|
||||||
|
let trimmed = query.trim().to_string();
|
||||||
|
let like_pattern = format!("%{}%", trimmed);
|
||||||
|
|
||||||
|
let mut stmt = dir_db.prepare(
|
||||||
|
"SELECT peer_id, display_name, last_seen FROM peer_profiles
|
||||||
|
WHERE lower(display_name) LIKE lower(?1)
|
||||||
|
OR peer_id = ?2
|
||||||
|
ORDER BY last_seen DESC
|
||||||
|
LIMIT 20"
|
||||||
|
);
|
||||||
|
let entries = match stmt {
|
||||||
|
Ok(ref mut s) => s.query_map(
|
||||||
|
params![like_pattern, trimmed],
|
||||||
|
|row| {
|
||||||
|
let last_seen: i64 = row.get(2)?;
|
||||||
|
Ok(DirectoryProfileEntry {
|
||||||
|
peer_id: row.get(0)?,
|
||||||
|
display_name: row.get(1)?,
|
||||||
|
last_seen: last_seen.max(0) as u64,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
).map(|rows| rows.filter_map(|r| r.ok()).collect::<Vec<_>>())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("directory: search query failed: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::info!(
|
||||||
|
"directory: search '{}' -> {} results",
|
||||||
|
trimmed,
|
||||||
|
entries.len()
|
||||||
|
);
|
||||||
|
DirectoryResponse::Results(entries)
|
||||||
|
}
|
||||||
|
DirectoryRequest::Remove => {
|
||||||
|
let _ = dir_db.execute(
|
||||||
|
"DELETE FROM peer_profiles WHERE peer_id = ?1",
|
||||||
|
params![peer.to_string()],
|
||||||
|
);
|
||||||
|
log::info!("directory: removed profile for {}", peer);
|
||||||
|
DirectoryResponse::Ok
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if swarm
|
||||||
|
.behaviour_mut()
|
||||||
|
.directory_service
|
||||||
|
.send_response(channel, response)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
log::warn!("directory: failed to send response to {}", peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ignore outbound and other directory service events
|
||||||
|
SwarmEvent::Behaviour(RelayBehaviourEvent::DirectoryService(_)) => {}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue