feat: add abuse prevention and moderation (Phase 5.6)

Add server-side moderation service with report submission, user
banning/unbanning, and admin listing endpoints. Add client-side
user blocking with message filtering in ConversationStore.

Server:
- ModerationService domain logic (report, ban, unban, list)
- Storage trait methods + FileBackedStore + SqlStore implementations
- SQL migration 012_moderation.sql (reports + bans tables)
- Error codes E031-E033 for moderation
- Domain types for all moderation request/response pairs
- 10 new tests (6 domain + 4 storage)

SDK:
- blocked_users table in ConversationStore
- block_user, unblock_user, is_blocked, list_blocked methods
- load_recent_messages_filtered excludes blocked senders
- QpqClient moderation convenience methods
- 4 new tests for block/unblock/filter
This commit is contained in:
2026-03-04 20:11:20 +01:00
parent a1f0dbc514
commit 5b6d8209f0
9 changed files with 1255 additions and 4 deletions

View File

@@ -174,6 +174,46 @@ impl QpqClient {
self.session_token.as_deref()
}
// ── Moderation (client-side) ────────────────────────────────────────────
/// Block a user locally. Their messages will be hidden from display.
pub fn block_user(&self, identity_key: &[u8], reason: &str) -> Result<(), SdkError> {
let store = self.conversations()?;
store
.block_user(identity_key, reason)
.map_err(|e| SdkError::Storage(e.to_string()))?;
info!(identity = %hex::encode(identity_key), "user blocked");
Ok(())
}
/// Unblock a user locally.
pub fn unblock_user(&self, identity_key: &[u8]) -> Result<bool, SdkError> {
let store = self.conversations()?;
let removed = store
.unblock_user(identity_key)
.map_err(|e| SdkError::Storage(e.to_string()))?;
if removed {
info!(identity = %hex::encode(identity_key), "user unblocked");
}
Ok(removed)
}
/// Check if a user is blocked locally.
pub fn is_blocked(&self, identity_key: &[u8]) -> Result<bool, SdkError> {
let store = self.conversations()?;
store
.is_blocked(identity_key)
.map_err(|e| SdkError::Storage(e.to_string()))
}
/// List all locally blocked users.
pub fn list_blocked(&self) -> Result<Vec<crate::conversation::BlockedUser>, SdkError> {
let store = self.conversations()?;
store
.list_blocked()
.map_err(|e| SdkError::Storage(e.to_string()))
}
/// Disconnect from the server.
pub fn disconnect(&mut self) {
if let Some(rpc) = self.rpc.take() {