chore: rename quicproquo → quicprochat in Rust workspace
Rename all crate directories, package names, binary names, proto package/module paths, ALPN strings, env var prefixes, config filenames, mDNS service names, and plugin ABI symbols from quicproquo/qpq to quicprochat/qpc.
This commit is contained in:
43
proto/qpc/v1/auth.proto
Normal file
43
proto/qpc/v1/auth.proto
Normal file
@@ -0,0 +1,43 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// OPAQUE registration + login (4 methods).
|
||||
// Method IDs: 100-103.
|
||||
|
||||
message OpaqueRegisterStartRequest {
|
||||
string username = 1;
|
||||
bytes request = 2;
|
||||
}
|
||||
|
||||
message OpaqueRegisterStartResponse {
|
||||
bytes response = 1;
|
||||
}
|
||||
|
||||
message OpaqueRegisterFinishRequest {
|
||||
string username = 1;
|
||||
bytes upload = 2;
|
||||
bytes identity_key = 3;
|
||||
}
|
||||
|
||||
message OpaqueRegisterFinishResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message OpaqueLoginStartRequest {
|
||||
string username = 1;
|
||||
bytes request = 2;
|
||||
}
|
||||
|
||||
message OpaqueLoginStartResponse {
|
||||
bytes response = 1;
|
||||
}
|
||||
|
||||
message OpaqueLoginFinishRequest {
|
||||
string username = 1;
|
||||
bytes finalization = 2;
|
||||
bytes identity_key = 3;
|
||||
}
|
||||
|
||||
message OpaqueLoginFinishResponse {
|
||||
bytes session_token = 1;
|
||||
}
|
||||
29
proto/qpc/v1/blob.proto
Normal file
29
proto/qpc/v1/blob.proto
Normal file
@@ -0,0 +1,29 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Blob upload/download (2 methods).
|
||||
// Method IDs: 600-601.
|
||||
|
||||
message UploadBlobRequest {
|
||||
bytes blob_hash = 1;
|
||||
bytes chunk = 2;
|
||||
uint64 offset = 3;
|
||||
uint64 total_size = 4;
|
||||
string mime_type = 5;
|
||||
}
|
||||
|
||||
message UploadBlobResponse {
|
||||
bytes blob_id = 1;
|
||||
}
|
||||
|
||||
message DownloadBlobRequest {
|
||||
bytes blob_id = 1;
|
||||
uint64 offset = 2;
|
||||
uint32 length = 3;
|
||||
}
|
||||
|
||||
message DownloadBlobResponse {
|
||||
bytes chunk = 1;
|
||||
uint64 total_size = 2;
|
||||
string mime_type = 3;
|
||||
}
|
||||
14
proto/qpc/v1/channel.proto
Normal file
14
proto/qpc/v1/channel.proto
Normal file
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Channel create (1 method).
|
||||
// Method ID: 400.
|
||||
|
||||
message CreateChannelRequest {
|
||||
bytes peer_key = 1;
|
||||
}
|
||||
|
||||
message CreateChannelResponse {
|
||||
bytes channel_id = 1;
|
||||
bool was_new = 2;
|
||||
}
|
||||
19
proto/qpc/v1/common.proto
Normal file
19
proto/qpc/v1/common.proto
Normal file
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Common types shared across services.
|
||||
|
||||
// Auth context included in authenticated RPC requests.
|
||||
// In v2, this is carried as QUIC connection-level state (session token),
|
||||
// not per-message. Included here for federation and internal use.
|
||||
message Auth {
|
||||
bytes access_token = 1;
|
||||
bytes device_id = 2;
|
||||
}
|
||||
|
||||
// Account deletion.
|
||||
message DeleteAccountRequest {}
|
||||
|
||||
message DeleteAccountResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
85
proto/qpc/v1/delivery.proto
Normal file
85
proto/qpc/v1/delivery.proto
Normal file
@@ -0,0 +1,85 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Delivery service: enqueue, fetch, peek, ack, batch (6 methods).
|
||||
// Method IDs: 200-205.
|
||||
|
||||
message Envelope {
|
||||
uint64 seq = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
message EnqueueRequest {
|
||||
bytes recipient_key = 1;
|
||||
bytes payload = 2;
|
||||
bytes channel_id = 3;
|
||||
uint32 ttl_secs = 4;
|
||||
// Client-generated idempotency key (16 bytes, UUID v7).
|
||||
// Server deduplicates enqueue requests with the same message_id within a TTL window.
|
||||
bytes message_id = 5;
|
||||
}
|
||||
|
||||
message EnqueueResponse {
|
||||
uint64 seq = 1;
|
||||
bytes delivery_proof = 2;
|
||||
// True if this was a duplicate enqueue (message_id already seen).
|
||||
bool duplicate = 3;
|
||||
}
|
||||
|
||||
message FetchRequest {
|
||||
bytes recipient_key = 1;
|
||||
bytes channel_id = 2;
|
||||
uint32 limit = 3;
|
||||
// Device ID for multi-device scoping. When set, the server builds
|
||||
// a composite queue key (identity_key + device_id). Empty = bare key.
|
||||
bytes device_id = 4;
|
||||
}
|
||||
|
||||
message FetchResponse {
|
||||
repeated Envelope payloads = 1;
|
||||
}
|
||||
|
||||
message FetchWaitRequest {
|
||||
bytes recipient_key = 1;
|
||||
bytes channel_id = 2;
|
||||
uint64 timeout_ms = 3;
|
||||
uint32 limit = 4;
|
||||
bytes device_id = 5;
|
||||
}
|
||||
|
||||
message FetchWaitResponse {
|
||||
repeated Envelope payloads = 1;
|
||||
}
|
||||
|
||||
message PeekRequest {
|
||||
bytes recipient_key = 1;
|
||||
bytes channel_id = 2;
|
||||
uint32 limit = 3;
|
||||
bytes device_id = 4;
|
||||
}
|
||||
|
||||
message PeekResponse {
|
||||
repeated Envelope payloads = 1;
|
||||
}
|
||||
|
||||
message AckRequest {
|
||||
bytes recipient_key = 1;
|
||||
bytes channel_id = 2;
|
||||
uint64 seq_up_to = 3;
|
||||
bytes device_id = 4;
|
||||
}
|
||||
|
||||
message AckResponse {}
|
||||
|
||||
message BatchEnqueueRequest {
|
||||
repeated bytes recipient_keys = 1;
|
||||
bytes payload = 2;
|
||||
bytes channel_id = 3;
|
||||
uint32 ttl_secs = 4;
|
||||
// Client-generated idempotency key (16 bytes, UUID v7).
|
||||
bytes message_id = 5;
|
||||
}
|
||||
|
||||
message BatchEnqueueResponse {
|
||||
repeated uint64 seqs = 1;
|
||||
}
|
||||
55
proto/qpc/v1/device.proto
Normal file
55
proto/qpc/v1/device.proto
Normal file
@@ -0,0 +1,55 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Device register/list/revoke (3 methods).
|
||||
// Method IDs: 700-702.
|
||||
|
||||
message RegisterDeviceRequest {
|
||||
bytes device_id = 1;
|
||||
string device_name = 2;
|
||||
}
|
||||
|
||||
message RegisterDeviceResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message ListDevicesRequest {}
|
||||
|
||||
message ListDevicesResponse {
|
||||
repeated Device devices = 1;
|
||||
}
|
||||
|
||||
message Device {
|
||||
bytes device_id = 1;
|
||||
string device_name = 2;
|
||||
uint64 registered_at = 3;
|
||||
}
|
||||
|
||||
message RevokeDeviceRequest {
|
||||
bytes device_id = 1;
|
||||
}
|
||||
|
||||
message RevokeDeviceResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
// Push notification token registration.
|
||||
// Method ID: 710.
|
||||
// Clients call this after login to register their device for push notifications.
|
||||
|
||||
enum PushPlatform {
|
||||
PUSH_PLATFORM_UNSPECIFIED = 0;
|
||||
PUSH_PLATFORM_APNS = 1; // Apple Push Notification Service (iOS)
|
||||
PUSH_PLATFORM_FCM = 2; // Firebase Cloud Messaging (Android)
|
||||
PUSH_PLATFORM_WEB_PUSH = 3; // Web Push (browsers)
|
||||
}
|
||||
|
||||
message RegisterPushTokenRequest {
|
||||
bytes device_id = 1;
|
||||
PushPlatform platform = 2;
|
||||
string token = 3; // Platform-specific push token
|
||||
}
|
||||
|
||||
message RegisterPushTokenResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
65
proto/qpc/v1/federation.proto
Normal file
65
proto/qpc/v1/federation.proto
Normal file
@@ -0,0 +1,65 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Federation relay + proxy (6 methods).
|
||||
// Method IDs: 900-905.
|
||||
|
||||
message FederationAuth {
|
||||
string origin = 1;
|
||||
}
|
||||
|
||||
message RelayEnqueueRequest {
|
||||
bytes recipient_key = 1;
|
||||
bytes payload = 2;
|
||||
bytes channel_id = 3;
|
||||
FederationAuth auth = 4;
|
||||
}
|
||||
|
||||
message RelayEnqueueResponse {
|
||||
uint64 seq = 1;
|
||||
}
|
||||
|
||||
message RelayBatchEnqueueRequest {
|
||||
repeated bytes recipient_keys = 1;
|
||||
bytes payload = 2;
|
||||
bytes channel_id = 3;
|
||||
FederationAuth auth = 4;
|
||||
}
|
||||
|
||||
message RelayBatchEnqueueResponse {
|
||||
repeated uint64 seqs = 1;
|
||||
}
|
||||
|
||||
message ProxyFetchKeyPackageRequest {
|
||||
bytes identity_key = 1;
|
||||
FederationAuth auth = 2;
|
||||
}
|
||||
|
||||
message ProxyFetchKeyPackageResponse {
|
||||
bytes package = 1;
|
||||
}
|
||||
|
||||
message ProxyFetchHybridKeyRequest {
|
||||
bytes identity_key = 1;
|
||||
FederationAuth auth = 2;
|
||||
}
|
||||
|
||||
message ProxyFetchHybridKeyResponse {
|
||||
bytes hybrid_public_key = 1;
|
||||
}
|
||||
|
||||
message ProxyResolveUserRequest {
|
||||
string username = 1;
|
||||
FederationAuth auth = 2;
|
||||
}
|
||||
|
||||
message ProxyResolveUserResponse {
|
||||
bytes identity_key = 1;
|
||||
}
|
||||
|
||||
message FederationHealthRequest {}
|
||||
|
||||
message FederationHealthResponse {
|
||||
string status = 1;
|
||||
string server_domain = 2;
|
||||
}
|
||||
61
proto/qpc/v1/group.proto
Normal file
61
proto/qpc/v1/group.proto
Normal file
@@ -0,0 +1,61 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Group management (4 methods).
|
||||
// Method IDs: 410-413.
|
||||
|
||||
// RemoveMember (410): Remove a member from a group.
|
||||
message RemoveMemberRequest {
|
||||
bytes group_id = 1;
|
||||
bytes member_identity_key = 2;
|
||||
}
|
||||
|
||||
message RemoveMemberResponse {
|
||||
bytes commit = 1;
|
||||
}
|
||||
|
||||
// UpdateGroupMetadata (411): Set group name, description, avatar.
|
||||
message UpdateGroupMetadataRequest {
|
||||
bytes group_id = 1;
|
||||
string name = 2;
|
||||
string description = 3;
|
||||
bytes avatar_hash = 4;
|
||||
}
|
||||
|
||||
message UpdateGroupMetadataResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
// ListGroupMembers (412): List members of a group.
|
||||
message ListGroupMembersRequest {
|
||||
bytes group_id = 1;
|
||||
}
|
||||
|
||||
message ListGroupMembersResponse {
|
||||
repeated GroupMemberInfo members = 1;
|
||||
}
|
||||
|
||||
message GroupMemberInfo {
|
||||
bytes identity_key = 1;
|
||||
string username = 2;
|
||||
uint64 joined_at = 3;
|
||||
}
|
||||
|
||||
// RotateKeys (413): Trigger key rotation (Update proposal + Commit).
|
||||
message RotateKeysRequest {
|
||||
bytes group_id = 1;
|
||||
}
|
||||
|
||||
message RotateKeysResponse {
|
||||
bytes commit = 1;
|
||||
}
|
||||
|
||||
// GroupMetadata stored server-side.
|
||||
message GroupMetadata {
|
||||
bytes group_id = 1;
|
||||
string name = 2;
|
||||
string description = 3;
|
||||
bytes avatar_hash = 4;
|
||||
bytes creator_key = 5;
|
||||
uint64 created_at = 6;
|
||||
}
|
||||
84
proto/qpc/v1/keys.proto
Normal file
84
proto/qpc/v1/keys.proto
Normal file
@@ -0,0 +1,84 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Key package + hybrid key CRUD (5 methods).
|
||||
// Method IDs: 300-304.
|
||||
|
||||
message UploadKeyPackageRequest {
|
||||
bytes identity_key = 1;
|
||||
bytes package = 2;
|
||||
}
|
||||
|
||||
message UploadKeyPackageResponse {
|
||||
bytes fingerprint = 1;
|
||||
}
|
||||
|
||||
message FetchKeyPackageRequest {
|
||||
bytes identity_key = 1;
|
||||
}
|
||||
|
||||
message FetchKeyPackageResponse {
|
||||
bytes package = 1;
|
||||
}
|
||||
|
||||
message UploadHybridKeyRequest {
|
||||
bytes identity_key = 1;
|
||||
bytes hybrid_public_key = 2;
|
||||
}
|
||||
|
||||
message UploadHybridKeyResponse {}
|
||||
|
||||
message FetchHybridKeyRequest {
|
||||
bytes identity_key = 1;
|
||||
}
|
||||
|
||||
message FetchHybridKeyResponse {
|
||||
bytes hybrid_public_key = 1;
|
||||
}
|
||||
|
||||
message FetchHybridKeysRequest {
|
||||
repeated bytes identity_keys = 1;
|
||||
}
|
||||
|
||||
message FetchHybridKeysResponse {
|
||||
repeated bytes keys = 1;
|
||||
}
|
||||
|
||||
// Key revocation (method ID 510).
|
||||
message RevokeKeyRequest {
|
||||
bytes identity_key = 1;
|
||||
string reason = 2; // "compromised", "superseded", "user_revoked"
|
||||
}
|
||||
|
||||
message RevokeKeyResponse {
|
||||
bool success = 1;
|
||||
uint64 leaf_index = 2; // Index of revocation entry in the KT Merkle log
|
||||
}
|
||||
|
||||
// Check revocation status (method ID 511).
|
||||
message CheckRevocationRequest {
|
||||
bytes identity_key = 1;
|
||||
}
|
||||
|
||||
message CheckRevocationResponse {
|
||||
bool revoked = 1;
|
||||
string reason = 2;
|
||||
uint64 timestamp_ms = 3;
|
||||
}
|
||||
|
||||
// KT audit log retrieval (method ID 520).
|
||||
message AuditKeyTransparencyRequest {
|
||||
uint64 start = 1;
|
||||
uint64 end = 2; // 0 = up to current size
|
||||
}
|
||||
|
||||
message AuditKeyTransparencyResponse {
|
||||
repeated LogEntry entries = 1;
|
||||
uint64 tree_size = 2;
|
||||
bytes root = 3;
|
||||
}
|
||||
|
||||
message LogEntry {
|
||||
uint64 index = 1;
|
||||
bytes leaf_hash = 2;
|
||||
}
|
||||
68
proto/qpc/v1/moderation.proto
Normal file
68
proto/qpc/v1/moderation.proto
Normal file
@@ -0,0 +1,68 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Moderation service: report, ban, unban, list reports, list banned.
|
||||
// Method IDs: 420-424.
|
||||
|
||||
message ReportMessageRequest {
|
||||
// Encrypted report payload (asymmetric, admin-key only).
|
||||
bytes encrypted_report = 1;
|
||||
// Conversation ID where the reported message lives.
|
||||
bytes conversation_id = 2;
|
||||
}
|
||||
|
||||
message ReportMessageResponse {
|
||||
bool accepted = 1;
|
||||
}
|
||||
|
||||
message BanUserRequest {
|
||||
// Identity key of the user to ban (32 bytes).
|
||||
bytes identity_key = 1;
|
||||
// Human-readable reason for the ban.
|
||||
string reason = 2;
|
||||
// Ban duration in seconds (0 = permanent).
|
||||
uint64 duration_secs = 3;
|
||||
}
|
||||
|
||||
message BanUserResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UnbanUserRequest {
|
||||
bytes identity_key = 1;
|
||||
}
|
||||
|
||||
message UnbanUserResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message ListReportsRequest {
|
||||
uint32 limit = 1;
|
||||
uint32 offset = 2;
|
||||
}
|
||||
|
||||
message ReportEntry {
|
||||
uint64 id = 1;
|
||||
bytes encrypted_report = 2;
|
||||
bytes conversation_id = 3;
|
||||
bytes reporter_identity = 4;
|
||||
uint64 timestamp = 5;
|
||||
}
|
||||
|
||||
message ListReportsResponse {
|
||||
repeated ReportEntry reports = 1;
|
||||
}
|
||||
|
||||
message ListBannedRequest {}
|
||||
|
||||
message BannedUserEntry {
|
||||
bytes identity_key = 1;
|
||||
string reason = 2;
|
||||
uint64 banned_at = 3;
|
||||
// 0 = permanent ban.
|
||||
uint64 expires_at = 4;
|
||||
}
|
||||
|
||||
message ListBannedResponse {
|
||||
repeated BannedUserEntry users = 1;
|
||||
}
|
||||
34
proto/qpc/v1/p2p.proto
Normal file
34
proto/qpc/v1/p2p.proto
Normal file
@@ -0,0 +1,34 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// P2P endpoint publish/resolve + health (3 methods).
|
||||
// Method IDs: 800-802.
|
||||
|
||||
message PublishEndpointRequest {
|
||||
bytes identity_key = 1;
|
||||
bytes node_addr = 2;
|
||||
}
|
||||
|
||||
message PublishEndpointResponse {}
|
||||
|
||||
message ResolveEndpointRequest {
|
||||
bytes identity_key = 1;
|
||||
}
|
||||
|
||||
message ResolveEndpointResponse {
|
||||
bytes node_addr = 1;
|
||||
}
|
||||
|
||||
message HealthRequest {}
|
||||
|
||||
message HealthResponse {
|
||||
string status = 1;
|
||||
// Unique node identifier for multi-node deployments.
|
||||
string node_id = 2;
|
||||
// Server version string.
|
||||
string version = 3;
|
||||
// Uptime in seconds since process start.
|
||||
uint64 uptime_secs = 4;
|
||||
// Storage backend type (e.g. "sql", "file", "postgres").
|
||||
string storage_backend = 5;
|
||||
}
|
||||
49
proto/qpc/v1/push.proto
Normal file
49
proto/qpc/v1/push.proto
Normal file
@@ -0,0 +1,49 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Server-push event types (sent on QUIC uni-streams).
|
||||
// Event type IDs: 1000+.
|
||||
|
||||
// Wrapper for a push event.
|
||||
message PushEvent {
|
||||
oneof event {
|
||||
NewMessage new_message = 1;
|
||||
TypingIndicator typing = 2;
|
||||
PresenceUpdate presence = 3;
|
||||
GroupMembershipChange membership = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message NewMessage {
|
||||
bytes channel_id = 1;
|
||||
bytes sender_key = 2;
|
||||
uint64 seq = 3;
|
||||
bytes payload = 4;
|
||||
uint64 timestamp_ms = 5;
|
||||
}
|
||||
|
||||
message TypingIndicator {
|
||||
bytes channel_id = 1;
|
||||
bytes sender_key = 2;
|
||||
bool is_typing = 3;
|
||||
}
|
||||
|
||||
message PresenceUpdate {
|
||||
bytes identity_key = 1;
|
||||
bool online = 2;
|
||||
uint64 last_seen_ms = 3;
|
||||
}
|
||||
|
||||
message GroupMembershipChange {
|
||||
bytes channel_id = 1;
|
||||
bytes actor_key = 2;
|
||||
bytes target_key = 3;
|
||||
MembershipAction action = 4;
|
||||
}
|
||||
|
||||
enum MembershipAction {
|
||||
MEMBERSHIP_ACTION_UNSPECIFIED = 0;
|
||||
MEMBERSHIP_ACTION_ADDED = 1;
|
||||
MEMBERSHIP_ACTION_REMOVED = 2;
|
||||
MEMBERSHIP_ACTION_LEFT = 3;
|
||||
}
|
||||
37
proto/qpc/v1/recovery.proto
Normal file
37
proto/qpc/v1/recovery.proto
Normal file
@@ -0,0 +1,37 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// Recovery service — encrypted recovery bundle storage.
|
||||
// Method IDs: 750-752.
|
||||
|
||||
message StoreRecoveryBundleRequest {
|
||||
// SHA-256(recovery_token) — server-side lookup key.
|
||||
bytes token_hash = 1;
|
||||
// Encrypted recovery bundle (opaque to server).
|
||||
bytes bundle = 2;
|
||||
// TTL in seconds (default 90 days = 7776000).
|
||||
uint64 ttl_secs = 3;
|
||||
}
|
||||
|
||||
message StoreRecoveryBundleResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message FetchRecoveryBundleRequest {
|
||||
// SHA-256(recovery_token) — lookup key.
|
||||
bytes token_hash = 1;
|
||||
}
|
||||
|
||||
message FetchRecoveryBundleResponse {
|
||||
// Empty if no bundle found.
|
||||
bytes bundle = 1;
|
||||
}
|
||||
|
||||
message DeleteRecoveryBundleRequest {
|
||||
// SHA-256(recovery_token) — lookup key.
|
||||
bytes token_hash = 1;
|
||||
}
|
||||
|
||||
message DeleteRecoveryBundleResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
22
proto/qpc/v1/user.proto
Normal file
22
proto/qpc/v1/user.proto
Normal file
@@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
package qpc.v1;
|
||||
|
||||
// User resolve + identity (2 methods).
|
||||
// Method IDs: 500-501.
|
||||
|
||||
message ResolveUserRequest {
|
||||
string username = 1;
|
||||
}
|
||||
|
||||
message ResolveUserResponse {
|
||||
bytes identity_key = 1;
|
||||
bytes inclusion_proof = 2;
|
||||
}
|
||||
|
||||
message ResolveIdentityRequest {
|
||||
bytes identity_key = 1;
|
||||
}
|
||||
|
||||
message ResolveIdentityResponse {
|
||||
string username = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user