feat(db): enable SQLite community persistence

Signed-off-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
This commit is contained in:
Brother Darryl
2026-08-10 15:33:36 -04:00
parent a9898cd973
commit 4ae8bf88f2
3 changed files with 81 additions and 1 deletions

1
Cargo.lock generated
View File

@@ -4549,6 +4549,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]

View File

@@ -53,7 +53,7 @@ tower-http = { version = "0.6", features = ["trace", "cors", "compression-gzip"
# Database
sqlx = { version = "0.9", features = [
"runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json"
"runtime-tokio", "tls-rustls", "postgres", "sqlite", "uuid", "chrono", "json"
] }
# Redis

View File

@@ -265,6 +265,85 @@ pub(crate) async fn is_community_active(
Ok(count != 0)
}
pub(crate) async fn rebind_single_node_community_host(
pool: &SqlitePool,
normalized_host: &str,
owner_pubkey: &str,
) -> Result<Option<CommunityRecord>> {
let mut tx = pool.begin().await?;
// Defect-era databases may contain several loopback communities, one per
// ephemeral port. Prefer a community owned by this desktop identity, then
// the one carrying the most events. This preserves the most useful UUID and
// its channels/messages while making the current Host header resolvable.
let row = sqlx::query(
r#"SELECT c.id, c.host
FROM communities c
LEFT JOIN relay_members rm
ON rm.community_id = c.id
AND lower(rm.pubkey) = lower(?1)
AND rm.role = 'owner'
LEFT JOIN events e ON e.community_id = c.id
WHERE c.archived_at IS NULL
AND c.host LIKE '127.0.0.1:%'
GROUP BY c.id, c.host, c.created_at
ORDER BY (rm.pubkey IS NOT NULL) DESC, count(e.id) DESC, c.created_at ASC, c.id ASC
LIMIT 1"#,
)
.bind(owner_pubkey)
.fetch_optional(&mut *tx)
.await?;
let Some(row) = row else {
tx.commit().await?;
return Ok(None);
};
let record = community_record(row)?;
if record.host.eq_ignore_ascii_case(normalized_host) {
tx.commit().await?;
return Ok(Some(record));
}
// The OS may reuse a port that belongs to another stale defect-era row.
// Swap that collision to the selected row's old authority transactionally,
// using a temporary unique host to satisfy the case-insensitive constraint.
if let Some(collision_id) = sqlx::query_scalar::<_, String>(
"SELECT id FROM communities WHERE host = ?1 COLLATE NOCASE AND id <> ?2",
)
.bind(normalized_host)
.bind(record.id.as_uuid().to_string())
.fetch_optional(&mut *tx)
.await?
{
let temporary_host = format!("rebind-{}.invalid", Uuid::new_v4());
sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1")
.bind(&collision_id)
.bind(&temporary_host)
.execute(&mut *tx)
.await?;
sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1")
.bind(record.id.as_uuid().to_string())
.bind(normalized_host)
.execute(&mut *tx)
.await?;
sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1")
.bind(collision_id)
.bind(&record.host)
.execute(&mut *tx)
.await?;
} else {
sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1")
.bind(record.id.as_uuid().to_string())
.bind(normalized_host)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(Some(CommunityRecord {
id: record.id,
host: normalized_host.to_string(),
}))
}
pub(crate) async fn ensure_configured_community(
pool: &SqlitePool,
normalized_host: &str,