Major improvement, security handling, file handling +fixes
This commit is contained in:
@@ -9,5 +9,6 @@ cgcx-config = { path = "../cgcx-config" }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled", "chrono"] }
|
||||
rusqlite_migration = "1.3"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["sync", "rt"] }
|
||||
tracing = "0.1"
|
||||
|
||||
@@ -49,6 +49,9 @@ impl Database {
|
||||
rusqlite_migration::M::up(include_str!("../../../migrations/002_indexes.sql")),
|
||||
rusqlite_migration::M::up(include_str!("../../../migrations/003_forward_system.sql")),
|
||||
rusqlite_migration::M::up(include_str!("../../../migrations/004_punishments.sql")),
|
||||
rusqlite_migration::M::up(include_str!("../../../migrations/005_show_author.sql")),
|
||||
rusqlite_migration::M::up(include_str!("../../../migrations/006_dedup.sql")),
|
||||
rusqlite_migration::M::up(include_str!("../../../migrations/007_hash_blacklist.sql")),
|
||||
]);
|
||||
migrations.to_latest(&mut *conn)
|
||||
.map_err(|e| CgcxError::Database(format!("migration failed: {}", e)))?;
|
||||
|
||||
@@ -12,16 +12,43 @@ impl UserRepo {
|
||||
Self { conn }
|
||||
}
|
||||
|
||||
pub async fn ensure_exists(&self, id: i64, username: Option<&str>, first_name: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().await;
|
||||
conn.execute(
|
||||
"INSERT INTO users (id, telegram_username, first_name) VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT(id) DO UPDATE SET telegram_username=excluded.telegram_username, first_name=excluded.first_name",
|
||||
params![id, username, first_name],
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
pub async fn ensure_exists(&self, id: i64, username: Option<&str>, first_name: &str, chat_id: i64, uname_changes_path: Option<&str>) -> Result<()> {
|
||||
let old_username = {
|
||||
let conn = self.conn.lock().await;
|
||||
let old: Option<String> = conn.query_row(
|
||||
"SELECT telegram_username FROM users WHERE id = ?1",
|
||||
params![id],
|
||||
|row| row.get(0),
|
||||
).optional().map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
conn.execute(
|
||||
"INSERT INTO users (id, telegram_username, first_name) VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT(id) DO UPDATE SET telegram_username=excluded.telegram_username, first_name=excluded.first_name",
|
||||
params![id, username, first_name],
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
old
|
||||
};
|
||||
if let (Some(path), Some(ref old)) = (uname_changes_path, old_username) {
|
||||
if old.as_str() != username.unwrap_or("") {
|
||||
Self::log_username_change(id, chat_id, Some(old.as_str()), username, path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn log_username_change(user_id: i64, chat_id: i64, old: Option<&str>, new: Option<&str>, path: &str) {
|
||||
let entry = serde_json::json!({
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
"user_id": user_id,
|
||||
"old_username": old.unwrap_or(""),
|
||||
"new_username": new.unwrap_or(""),
|
||||
"chat_id": chat_id
|
||||
});
|
||||
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
|
||||
use std::io::Write;
|
||||
let _ = writeln!(file, "{}", entry);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, id: i64) -> Result<Option<User>> {
|
||||
let conn = self.conn.lock().await;
|
||||
let row = conn.query_row(
|
||||
@@ -78,8 +105,8 @@ impl ContentRepo {
|
||||
let conn = self.conn.lock().await;
|
||||
let status = format!("{:?}", content.status).to_lowercase();
|
||||
conn.execute(
|
||||
"INSERT INTO contents (id, user_id, status, view_count, max_views, allow_download, password_hash, created_at, deleted_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
"INSERT INTO contents (id, user_id, status, view_count, max_views, allow_download, password_hash, show_author, created_at, deleted_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
|
||||
ON CONFLICT(id) DO NOTHING",
|
||||
params![
|
||||
content.id.as_str(),
|
||||
@@ -89,6 +116,7 @@ impl ContentRepo {
|
||||
content.max_views.map(|v| v as i64),
|
||||
content.allow_download as i64,
|
||||
content.password_hash.as_ref(),
|
||||
content.show_author as i64,
|
||||
content.created_at,
|
||||
content.deleted_at,
|
||||
],
|
||||
@@ -99,7 +127,7 @@ impl ContentRepo {
|
||||
pub async fn get(&self, id: &ContentId) -> Result<Option<Content>> {
|
||||
let conn = self.conn.lock().await;
|
||||
let row = conn.query_row(
|
||||
"SELECT id, user_id, status, view_count, max_views, allow_download, password_hash, created_at, deleted_at
|
||||
"SELECT id, user_id, status, view_count, max_views, allow_download, password_hash, show_author, created_at, deleted_at
|
||||
FROM contents WHERE id = ?1",
|
||||
params![id.as_str()],
|
||||
|row| {
|
||||
@@ -117,8 +145,9 @@ impl ContentRepo {
|
||||
max_views: row.get::<_, Option<i64>>(4)?.map(|v| v as u64),
|
||||
allow_download: row.get::<_, i64>(5)? != 0,
|
||||
password_hash: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
deleted_at: row.get(8)?,
|
||||
show_author: row.get::<_, i64>(7)? != 0,
|
||||
created_at: row.get(8)?,
|
||||
deleted_at: row.get(9)?,
|
||||
})
|
||||
},
|
||||
).optional().map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
@@ -128,7 +157,7 @@ impl ContentRepo {
|
||||
pub async fn list_by_user(&self, user_id: i64, limit: usize, offset: usize) -> Result<Vec<Content>> {
|
||||
let conn = self.conn.lock().await;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, user_id, status, view_count, max_views, allow_download, password_hash, created_at, deleted_at
|
||||
"SELECT id, user_id, status, view_count, max_views, allow_download, password_hash, show_author, created_at, deleted_at
|
||||
FROM contents WHERE user_id = ?1 AND status != 'deleted' ORDER BY created_at DESC LIMIT ?2 OFFSET ?3"
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let rows = stmt.query_map(params![user_id, limit as i64, offset as i64], |row| {
|
||||
@@ -146,8 +175,9 @@ impl ContentRepo {
|
||||
max_views: row.get::<_, Option<i64>>(4)?.map(|v| v as u64),
|
||||
allow_download: row.get::<_, i64>(5)? != 0,
|
||||
password_hash: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
deleted_at: row.get(8)?,
|
||||
show_author: row.get::<_, i64>(7)? != 0,
|
||||
created_at: row.get(8)?,
|
||||
deleted_at: row.get(9)?,
|
||||
})
|
||||
}).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let mut out = Vec::new();
|
||||
@@ -220,8 +250,8 @@ impl ContentFileRepo {
|
||||
pub async fn insert(&self, file: &ContentFile) -> Result<()> {
|
||||
let conn = self.conn.lock().await;
|
||||
conn.execute(
|
||||
"INSERT INTO content_files (content_id, file_index, original_name, stored_path, mime_type, size_bytes, ciphertext_size_bytes, encrypted_key_wrapped, encrypted_hash, render_flags, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
|
||||
"INSERT INTO content_files (content_id, file_index, original_name, stored_path, mime_type, size_bytes, ciphertext_size_bytes, encrypted_key_wrapped, encrypted_hash, render_flags, created_at, plaintext_hash, ref_count)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
|
||||
ON CONFLICT(content_id, file_index) DO NOTHING",
|
||||
params![
|
||||
file.content_id.as_str(),
|
||||
@@ -235,6 +265,8 @@ impl ContentFileRepo {
|
||||
&file.encrypted_hash,
|
||||
file.render_flags as i64,
|
||||
file.created_at,
|
||||
&file.plaintext_hash,
|
||||
file.ref_count as i64,
|
||||
],
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
@@ -243,7 +275,7 @@ impl ContentFileRepo {
|
||||
pub async fn list_by_content(&self, content_id: &ContentId) -> Result<Vec<ContentFile>> {
|
||||
let conn = self.conn.lock().await;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT content_id, file_index, original_name, stored_path, mime_type, size_bytes, ciphertext_size_bytes, encrypted_key_wrapped, encrypted_hash, render_flags, created_at
|
||||
"SELECT content_id, file_index, original_name, stored_path, mime_type, size_bytes, ciphertext_size_bytes, encrypted_key_wrapped, encrypted_hash, render_flags, created_at, plaintext_hash, ref_count
|
||||
FROM content_files WHERE content_id = ?1 ORDER BY file_index"
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let rows = stmt.query_map(params![content_id.as_str()], |row| {
|
||||
@@ -259,6 +291,8 @@ impl ContentFileRepo {
|
||||
encrypted_hash: row.get(8)?,
|
||||
render_flags: row.get::<_, i64>(9)? as u32,
|
||||
created_at: row.get(10)?,
|
||||
plaintext_hash: row.get(11)?,
|
||||
ref_count: row.get::<_, i64>(12)? as u64,
|
||||
})
|
||||
}).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let mut out = Vec::new();
|
||||
@@ -284,6 +318,100 @@ impl ContentFileRepo {
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub async fn find_active_by_plaintext_hash(&self, hash: &[u8]) -> Result<Option<ContentFile>> {
|
||||
let conn = self.conn.lock().await;
|
||||
let row = conn.query_row(
|
||||
"SELECT cf.content_id, cf.file_index, cf.original_name, cf.stored_path, cf.mime_type, cf.size_bytes, cf.ciphertext_size_bytes, cf.encrypted_key_wrapped, cf.encrypted_hash, cf.render_flags, cf.created_at, cf.plaintext_hash, cf.ref_count
|
||||
FROM content_files cf
|
||||
JOIN contents c ON c.id = cf.content_id
|
||||
WHERE cf.plaintext_hash = ?1 AND c.status NOT IN ('deleted', 'blacklisted')
|
||||
LIMIT 1",
|
||||
params![hash],
|
||||
|row| {
|
||||
Ok(ContentFile {
|
||||
content_id: ContentId::new_unchecked(row.get(0)?),
|
||||
file_index: row.get::<_, i64>(1)? as u32,
|
||||
original_name: row.get(2)?,
|
||||
stored_path: std::path::PathBuf::from(row.get::<_, String>(3)?),
|
||||
mime_type: row.get(4)?,
|
||||
size_bytes: row.get::<_, i64>(5)? as u64,
|
||||
ciphertext_size_bytes: row.get::<_, i64>(6)? as u64,
|
||||
encrypted_key_wrapped: row.get(7)?,
|
||||
encrypted_hash: row.get(8)?,
|
||||
render_flags: row.get::<_, i64>(9)? as u32,
|
||||
created_at: row.get(10)?,
|
||||
plaintext_hash: row.get(11)?,
|
||||
ref_count: row.get::<_, i64>(12)? as u64,
|
||||
})
|
||||
},
|
||||
).optional().map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn increment_ref_count(&self, content_id: &ContentId, file_index: u32) -> Result<()> {
|
||||
let conn = self.conn.lock().await;
|
||||
conn.execute(
|
||||
"UPDATE content_files SET ref_count = ref_count + 1 WHERE content_id = ?1 AND file_index = ?2",
|
||||
params![content_id.as_str(), file_index as i64],
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn decrement_ref_count(&self, content_id: &ContentId, file_index: u32) -> Result<u64> {
|
||||
let conn = self.conn.lock().await;
|
||||
conn.execute(
|
||||
"UPDATE content_files SET ref_count = ref_count - 1 WHERE content_id = ?1 AND file_index = ?2",
|
||||
params![content_id.as_str(), file_index as i64],
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT ref_count FROM content_files WHERE content_id = ?1 AND file_index = ?2",
|
||||
params![content_id.as_str(), file_index as i64],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(count as u64)
|
||||
}
|
||||
|
||||
pub async fn decrement_ref_count_for_path(&self, stored_path: &std::path::Path) -> Result<Option<u64>> {
|
||||
let mut conn = self.conn.lock().await;
|
||||
let tx = conn.transaction().map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let existing: Option<i64> = tx.query_row(
|
||||
"SELECT ref_count FROM content_files WHERE stored_path = ?1 AND ref_count > 0 LIMIT 1",
|
||||
params![stored_path.to_str()],
|
||||
|row| row.get(0),
|
||||
).optional().map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
if let Some(rc) = existing {
|
||||
tx.execute(
|
||||
"UPDATE content_files SET ref_count = ref_count - 1 WHERE stored_path = ?1 AND ref_count > 0",
|
||||
params![stored_path.to_str()],
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
tx.commit().map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(Some((rc - 1) as u64))
|
||||
} else {
|
||||
tx.commit().map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn count_by_path_excluding_content(&self, stored_path: &std::path::Path, content_id: &ContentId) -> Result<usize> {
|
||||
let conn = self.conn.lock().await;
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM content_files WHERE stored_path = ?1 AND content_id != ?2",
|
||||
params![stored_path.to_str(), content_id.as_str()],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
pub async fn count_by_path(&self, stored_path: &std::path::Path) -> Result<usize> {
|
||||
let conn = self.conn.lock().await;
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM content_files WHERE stored_path = ?1",
|
||||
params![stored_path.to_str()],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(count as usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReportRepo {
|
||||
@@ -607,6 +735,25 @@ impl ForwardRepo {
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_active_chat_ids(&self) -> Result<Vec<i64>> {
|
||||
let conn = self.conn.lock().await;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT DISTINCT source_chat_id FROM forward_definitions WHERE revoked_at IS NULL
|
||||
UNION
|
||||
SELECT DISTINCT destination_chat_id FROM forward_definitions WHERE revoked_at IS NULL
|
||||
UNION
|
||||
SELECT DISTINCT review_group_id FROM forward_definitions WHERE revoked_at IS NULL"
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
row.get::<_, i64>(0)
|
||||
}).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
let mut out = Vec::new();
|
||||
for r in rows {
|
||||
out.push(r.map_err(|e| CgcxError::Database(e.to_string()))?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
pub struct PunishmentRepo {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
@@ -697,3 +844,32 @@ impl PunishmentRepo {
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HashBlacklistRepo {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl HashBlacklistRepo {
|
||||
pub fn new(conn: Arc<Mutex<Connection>>) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
|
||||
pub async fn insert(&self, hash: &[u8], reason: Option<&str>) -> Result<()> {
|
||||
let conn = self.conn.lock().await;
|
||||
conn.execute(
|
||||
"INSERT INTO hash_blacklist (hash, reason) VALUES (?1, ?2) ON CONFLICT(hash) DO NOTHING",
|
||||
params![hash, reason],
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn contains(&self, hash: &[u8]) -> Result<bool> {
|
||||
let conn = self.conn.lock().await;
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM hash_blacklist WHERE hash = ?1",
|
||||
params![hash],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| CgcxError::Database(e.to_string()))?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user