mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
refactor(desktop): feature-gate mesh-llm-sdk behind optional Cargo feature (#823)
This commit is contained in:
@@ -51,10 +51,10 @@ const overrides = new Map([
|
||||
["src/shared/api/relayClientSession.ts", 1040], // durable websocket session manager with reconnect/replay/recovery state + sendTypingIndicator + fetchChannelHistoryBefore + subscribeToChannelLive (huddle TTS) + subscribeToHuddleEvents (huddle indicator) + disconnect() for workspace switch teardown + fetchEvents/subscribeLive/publishEvent for NIP-RS read state + publishUserStatus/subscribeToUserStatusUpdates (NIP-38) + ConnectionState plumbing & stall-watchdog wiring for half-open WS detection (Warp orange-icon case) + terminal session latch (auth rejection no longer racing back to reconnecting) — emitter + watchdog + reconnect policy logic extracted to relayConnectionStateEmitter.ts / relayStallWatchdog.ts / relayReconnectPolicy.ts
|
||||
["src-tauri/src/migration.rs", 1010], // worktree shared-agent-data symlink sync (SHARED_AGENT_FILES + SHARED_AGENT_DIRS symlink-to-canonical + sibling pack migration) + mcp_command provider reconciliation + persona_pack_path reconciliation + tests
|
||||
["src-tauri/src/commands/media.rs", 730], // ffmpeg video transcode + poster frame extraction + run_ffmpeg_with_timeout (find_ffmpeg via resolve_command, is_video_file, transcode_to_mp4, extract_poster_frame, transcode_and_extract_poster) + spawn_blocking wrappers + tests
|
||||
["src-tauri/src/commands/agents.rs", 900], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field + NIP-OA auth_tag in deploy payload + relay-mesh client preflight (start_local_agent_with_preflight + StartTarget split: async ensure outside store lock, then sync spawn under lock)
|
||||
["src-tauri/src/commands/agents.rs", 910], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field + NIP-OA auth_tag in deploy payload + relay-mesh client preflight (start_local_agent_with_preflight + StartTarget split: async ensure outside store lock, then sync spawn under lock)
|
||||
["src-tauri/src/commands/messages.rs", 515], // feed multi-query + NIP-50 search + forum thread resolution + thread ref + reactions via REQ + edit_message media_tags param (Slack-style attachment-editable edits)
|
||||
["src-tauri/src/nostr_convert.rs", 1150], // 12 Nostr event→model converters (channels, profiles, members, notes, search, agents, relay members) + rank_user_search_results helper for NIP-50 user search + 33 unit tests
|
||||
["src-tauri/src/managed_agents/runtime.rs", 1385], // ... + respond-to gate env (SPROUT_ACP_RESPOND_TO[_ALLOWLIST]) + per-mode env builder + tests + persona/agent env_vars spawn merge (helper + tests now in env_vars.rs) + system-wide orphan sweep (proc_listallpids/proc on macOS, /proc on Linux) + SPROUT_MANAGED_AGENT env marker check (KERN_PROCARGS2 on macOS, /proc/environ on Linux), instance-scoped by app identifier so coexisting Sprout instances don't reap each other's agents + relay_mesh_model_id detector (mesh preset-env match) + tests
|
||||
["src-tauri/src/managed_agents/runtime.rs", 1390], // ... + respond-to gate env (SPROUT_ACP_RESPOND_TO[_ALLOWLIST]) + per-mode env builder + tests + persona/agent env_vars spawn merge (helper + tests now in env_vars.rs) + system-wide orphan sweep (proc_listallpids/proc on macOS, /proc on Linux) + SPROUT_MANAGED_AGENT env marker check (KERN_PROCARGS2 on macOS, /proc/environ on Linux), instance-scoped by app identifier so coexisting Sprout instances don't reap each other's agents + relay_mesh_model_id detector (mesh preset-env match) + tests
|
||||
["src-tauri/src/managed_agents/discovery.rs", 680], // KNOWN_ACP_PROVIDERS catalog + resolve_command cache + login_shell_path + classify_provider (four-state: Available/AdapterMissing/CliMissing/NotInstalled) + discover_acp_providers with dynamic install_hint + known_acp_provider/known_acp_provider_exact + normalize_agent_args + 15 unit tests
|
||||
["src-tauri/src/managed_agents/types.rs", 745], // ManagedAgentRecord/Summary + Create/Update request structs + AcpProviderCatalogEntry + InstallRuntimeResult + RespondTo enum + validate_respond_to_allowlist + tests + persona/agent env_vars field
|
||||
["src-tauri/src/managed_agents/backend.rs", 700], // provider IPC, validation, discovery, binary resolution + tests + redact_secrets_with for user env values + env_secrets_from_request + redact_env_values_in (shared with model discovery)
|
||||
@@ -81,7 +81,7 @@ const overrides = new Map([
|
||||
["src-tauri/src/huddle/tts.rs", 1380], // TTS pipeline + session warmup + cancel/shutdown handling + apply_fade_out (fade-out only — leading fade removed 2026-05-18 after onset-attenuation regression measured in examples/pocket_onset_probe.rs) + FIRST_APPEND_LEAD_IN_SAMPLES + build_sentence_append_plan (pure helper enforcing the lead-in fires exactly once per utterance, not per sentence — see lead_in_pad_fires_exactly_once_per_utterance regression test) + normalize_for_playback (per-sentence peak normalization to -3 dBFS ceiling with MAX_GAIN cap) + 30 unit tests (18 interrupt + 5 fade-out + 1 first-append-lead-in + 3 build-sentence-append-plan + 6 normalize)
|
||||
["src-tauri/src/relay.rs", 510], // +4 lines for NIP-OA auth tag injection in profile sync (build_profile_event) + verification test
|
||||
["src-tauri/src/commands/pairing.rs", 600], // NIP-AB pairing actor: 3 Tauri commands + background WS task + NIP-42 auth + NIP-43 probe + event parsing helpers
|
||||
["src-tauri/src/lib.rs", 780], // + mesh command registrations + PairingHandle managed state + 3 pairing command registrations + parse_message_deep_link helper extracted with 6 unit tests covering empty-param filter regression + mod migration + sync_shared_agent_data/reconcile_provider_mcp_commands/reconcile_persona_pack_paths calls on launch + SIGINT/SIGTERM/SIGHUP signal handlers for agent process cleanup
|
||||
["src-tauri/src/lib.rs", 835], // + mesh_llm_stubs module (feature-gated no-op Tauri commands) + mesh command registrations + PairingHandle managed state + 3 pairing command registrations + parse_message_deep_link helper extracted with 6 unit tests covering empty-param filter regression + mod migration + sync_shared_agent_data/reconcile_provider_mcp_commands/reconcile_persona_pack_paths calls on launch + SIGINT/SIGTERM/SIGHUP signal handlers for agent process cleanup
|
||||
["src/shared/api/tauri.ts", 1212], // pairing command wrappers + applyWorkspace + NIP-44 encrypt/decrypt wrappers + observer_url field + relay member API functions (list/get/add/remove/change-role) + prevent sleep + AcpProviderCatalogEntry raw types + fromRawAcpProviderCatalogEntry converter + installAcpRuntime
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[env]
|
||||
# Workaround for mesh-llm prepare-llama.sh `git -C` directory-escape bug.
|
||||
# The script's git commands walk upward into the enclosing Sprout repo when
|
||||
# .deps/llama.cpp/.git is missing. Disabling auto-build prevents the script
|
||||
# from running. Remove once mesh-llm-sdk is bumped past Mesh-LLM/mesh-llm#780.
|
||||
SKIPPY_LLAMA_AUTO_BUILD = "0"
|
||||
@@ -16,6 +16,10 @@ edition = "2021"
|
||||
name = "sprout_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
mesh-llm = ["dep:mesh-llm-sdk"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
@@ -57,7 +61,7 @@ url = "2"
|
||||
sprout-core = { path = "../../crates/sprout-core" }
|
||||
sprout-persona = { path = "../../crates/sprout-persona" }
|
||||
sprout-sdk = { path = "../../crates/sprout-sdk" }
|
||||
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "bd16da4955a9dceee8f1e0a02c5814944f4e12a5", package = "mesh-llm-sdk", default-features = false, features = ["client", "serve"] }
|
||||
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "bd16da4955a9dceee8f1e0a02c5814944f4e12a5", package = "mesh-llm-sdk", default-features = false, features = ["client", "serve"], optional = true }
|
||||
base64 = "0.22"
|
||||
sha2 = "0.11"
|
||||
tar = "0.4"
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
|
||||
use nostr::{Keys, ToBech32};
|
||||
use tauri::{AppHandle, Manager};
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
use crate::huddle::HuddleState;
|
||||
@@ -35,6 +36,7 @@ pub struct AppState {
|
||||
/// IOKit power assertion state — prevents idle sleep while agents run.
|
||||
pub prevent_sleep: Arc<Mutex<crate::prevent_sleep::PreventSleepState>>,
|
||||
/// In-process mesh-llm node started by Sprout Desktop.
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
pub mesh_llm_runtime: AsyncMutex<Option<crate::mesh_llm::DesktopMeshRuntime>>,
|
||||
}
|
||||
|
||||
@@ -81,6 +83,7 @@ pub fn build_app_state() -> AppState {
|
||||
prevent_sleep: Arc::new(Mutex::new(
|
||||
crate::prevent_sleep::PreventSleepState::default(),
|
||||
)),
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
mesh_llm_runtime: AsyncMutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use nostr::{Keys, ToBech32};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
use crate::managed_agents::relay_mesh_model_id;
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
managed_agents::{
|
||||
build_managed_agent_summary, discover_provider_candidates, ensure_persona_is_active,
|
||||
find_managed_agent_mut, invoke_provider, load_managed_agents, load_personas,
|
||||
managed_agent_avatar_url, managed_agent_log_path, managed_agents_base_dir,
|
||||
normalize_agent_args, provider_deploy, read_log_tail, relay_mesh_model_id,
|
||||
resolve_provider_binary, save_managed_agents, start_managed_agent_process,
|
||||
stop_managed_agent_process, sync_managed_agent_processes, try_regenerate_nest,
|
||||
validate_provider_config, BackendKind, BackendProviderInfo, CreateManagedAgentRequest,
|
||||
CreateManagedAgentResponse, ManagedAgentLogResponse, ManagedAgentRecord,
|
||||
ManagedAgentSummary, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_PARALLELISM,
|
||||
DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND,
|
||||
normalize_agent_args, provider_deploy, read_log_tail, resolve_provider_binary,
|
||||
save_managed_agents, start_managed_agent_process, stop_managed_agent_process,
|
||||
sync_managed_agent_processes, try_regenerate_nest, validate_provider_config, BackendKind,
|
||||
BackendProviderInfo, CreateManagedAgentRequest, CreateManagedAgentResponse,
|
||||
ManagedAgentLogResponse, ManagedAgentRecord, ManagedAgentSummary, DEFAULT_ACP_COMMAND,
|
||||
DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
|
||||
DEFAULT_MCP_COMMAND,
|
||||
},
|
||||
relay::{relay_ws_url_with_override, sync_managed_agent_profile},
|
||||
util::now_iso,
|
||||
@@ -27,6 +29,7 @@ fn workspace_owner_hex(state: &AppState) -> Result<String, String> {
|
||||
Ok(keys.public_key().to_hex())
|
||||
}
|
||||
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
async fn ensure_relay_mesh_for_record(
|
||||
state: &AppState,
|
||||
record: &ManagedAgentRecord,
|
||||
@@ -38,6 +41,14 @@ async fn ensure_relay_mesh_for_record(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mesh-llm"))]
|
||||
async fn ensure_relay_mesh_for_record(
|
||||
_state: &AppState,
|
||||
_record: &ManagedAgentRecord,
|
||||
) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_local_agent_with_preflight(
|
||||
app: &AppHandle,
|
||||
state: &AppState,
|
||||
|
||||
@@ -12,6 +12,7 @@ mod identity_archive;
|
||||
mod media;
|
||||
mod media_download;
|
||||
mod media_transcode;
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
mod mesh_llm;
|
||||
mod messages;
|
||||
pub mod pairing;
|
||||
@@ -36,6 +37,7 @@ pub use identity::*;
|
||||
pub use identity_archive::*;
|
||||
pub use media::*;
|
||||
pub use media_download::*;
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
pub use mesh_llm::*;
|
||||
pub use messages::*;
|
||||
pub use pairing::*;
|
||||
|
||||
@@ -4,6 +4,7 @@ mod events;
|
||||
mod huddle;
|
||||
mod managed_agents;
|
||||
mod media_proxy;
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
mod mesh_llm;
|
||||
mod migration;
|
||||
mod models;
|
||||
@@ -13,6 +14,62 @@ mod relay;
|
||||
mod templates;
|
||||
mod util;
|
||||
|
||||
#[cfg(not(feature = "mesh-llm"))]
|
||||
mod mesh_llm_stubs {
|
||||
use tauri::State;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
type CmdResult<T> = Result<T, String>;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_availability(_state: State<'_, AppState>) -> CmdResult<serde_json::Value> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_start_node(
|
||||
_app: tauri::AppHandle,
|
||||
_state: State<'_, AppState>,
|
||||
_request: serde_json::Value,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_ensure_client_node(
|
||||
_state: State<'_, AppState>,
|
||||
_request: serde_json::Value,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_stop_node(_state: State<'_, AppState>) -> CmdResult<serde_json::Value> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_node_status(_state: State<'_, AppState>) -> CmdResult<serde_json::Value> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_installed_models(
|
||||
_state: State<'_, AppState>,
|
||||
) -> CmdResult<Vec<serde_json::Value>> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn mesh_agent_preset(_request: serde_json::Value) -> CmdResult<serde_json::Value> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mesh-llm"))]
|
||||
use mesh_llm_stubs::*;
|
||||
|
||||
use app_state::{build_app_state, resolve_persisted_identity, AppState};
|
||||
use commands::*;
|
||||
use huddle::audio_output::{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
use super::relay_mesh_model_id;
|
||||
use super::{
|
||||
find_managed_agent_mut, kill_stale_tracked_processes, load_managed_agents, relay_mesh_model_id,
|
||||
save_managed_agents, spawn_agent_child, sync_managed_agent_processes, BackendKind,
|
||||
ManagedAgentProcess,
|
||||
find_managed_agent_mut, kill_stale_tracked_processes, load_managed_agents, save_managed_agents,
|
||||
spawn_agent_child, sync_managed_agent_processes, BackendKind, ManagedAgentProcess,
|
||||
};
|
||||
use crate::app_state::AppState;
|
||||
use crate::util;
|
||||
@@ -103,20 +104,25 @@ pub async fn restore_managed_agents_on_launch(
|
||||
.ok()
|
||||
.map(|k| k.public_key().to_hex());
|
||||
|
||||
let mut mesh_preflight_failures = std::collections::HashSet::new();
|
||||
for record in &agents_to_start {
|
||||
let Some(model_id) = relay_mesh_model_id(record) else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = crate::commands::ensure_client_node_for_model(&state, model_id).await {
|
||||
persist_restore_error(app, &state, &record.pubkey, error)?;
|
||||
mesh_preflight_failures.insert(record.pubkey.clone());
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
let agents_to_start = {
|
||||
let mut mesh_preflight_failures = std::collections::HashSet::new();
|
||||
for record in &agents_to_start {
|
||||
let Some(model_id) = relay_mesh_model_id(record) else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) =
|
||||
crate::commands::mesh_llm::ensure_client_node_for_model(&state, model_id).await
|
||||
{
|
||||
persist_restore_error(app, &state, &record.pubkey, error)?;
|
||||
mesh_preflight_failures.insert(record.pubkey.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let agents_to_start: Vec<_> = agents_to_start
|
||||
.into_iter()
|
||||
.filter(|record| !mesh_preflight_failures.contains(&record.pubkey))
|
||||
.collect();
|
||||
agents_to_start
|
||||
.into_iter()
|
||||
.filter(|record| !mesh_preflight_failures.contains(&record.pubkey))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
if agents_to_start.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -183,6 +189,7 @@ pub async fn restore_managed_agents_on_launch(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
fn persist_restore_error(
|
||||
app: &tauri::AppHandle,
|
||||
state: &AppState,
|
||||
|
||||
@@ -1046,6 +1046,7 @@ fn child_rust_log_filter() -> String {
|
||||
/// Returns the relay-mesh model id for agents whose provider env points at the
|
||||
/// local mesh client endpoint. The caller is responsible for ensuring that
|
||||
/// endpoint is alive before spawning the agent process.
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
pub fn relay_mesh_model_id(record: &ManagedAgentRecord) -> Option<String> {
|
||||
let base_url = record.env_vars.get("OPENAI_COMPAT_BASE_URL")?.trim();
|
||||
if base_url.trim_end_matches('/') != "http://127.0.0.1:9337/v1" {
|
||||
@@ -1169,6 +1170,7 @@ pub fn stop_managed_agent_process(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
use super::relay_mesh_model_id;
|
||||
use crate::managed_agents::known_acp_provider;
|
||||
|
||||
@@ -1261,6 +1263,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
#[test]
|
||||
fn relay_mesh_model_id_detects_mesh_preset_env() {
|
||||
let mut rec = fixture(RespondTo::OwnerOnly, vec![], Some("tag".into()));
|
||||
@@ -1276,6 +1279,7 @@ mod tests {
|
||||
assert_eq!(relay_mesh_model_id(&rec).as_deref(), Some("Qwen3"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
#[test]
|
||||
fn relay_mesh_model_id_ignores_non_mesh_openai_env() {
|
||||
let mut rec = fixture(RespondTo::OwnerOnly, vec![], Some("tag".into()));
|
||||
|
||||
@@ -181,7 +181,7 @@ desktop-release-build target="aarch64-apple-darwin":
|
||||
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/sprout-$TARGET"
|
||||
pnpm install
|
||||
cd {{desktop_dir}} && pnpm tauri build --target {{target}}
|
||||
cd {{desktop_dir}} && TAURI_CLI_EXTRA_CARGO_ARGS="--features=mesh-llm" pnpm tauri build --target {{target}}
|
||||
|
||||
# Run desktop checks suitable for CI / pre-push
|
||||
desktop-ci: desktop-check desktop-test desktop-tauri-fmt-check desktop-build desktop-tauri-check desktop-tauri-test
|
||||
@@ -263,7 +263,7 @@ dev *ARGS: _ensure-sidecar-stubs
|
||||
[[ -d node_modules ]] || pnpm install
|
||||
source ../scripts/instance-env.sh
|
||||
echo "Starting on Vite port ${SPROUT_VITE_PORT}, relay ${SPROUT_RELAY_URL}"
|
||||
pnpm exec tauri dev --config "$SPROUT_TAURI_CONFIG" {{ARGS}}
|
||||
TAURI_CLI_EXTRA_CARGO_ARGS="--features=mesh-llm" pnpm exec tauri dev --config "$SPROUT_TAURI_CONFIG" {{ARGS}}
|
||||
|
||||
# Run the desktop app against the internal staging relay (installs deps + builds agent tools automatically)
|
||||
staging *ARGS: _ensure-sidecar-stubs
|
||||
@@ -279,7 +279,7 @@ staging *ARGS: _ensure-sidecar-stubs
|
||||
source ../scripts/instance-env.sh
|
||||
export SPROUT_RELAY_URL="wss://sprout-oss.stage.blox.sqprod.co"
|
||||
echo "Starting staging on Vite port ${SPROUT_VITE_PORT}, relay ${SPROUT_RELAY_URL}"
|
||||
pnpm exec tauri dev --config "$SPROUT_TAURI_CONFIG" {{ARGS}}
|
||||
TAURI_CLI_EXTRA_CARGO_ARGS="--features=mesh-llm" pnpm exec tauri dev --config "$SPROUT_TAURI_CONFIG" {{ARGS}}
|
||||
|
||||
# Run the desktop frontend dev server (port derived from worktree)
|
||||
desktop-dev:
|
||||
|
||||
Reference in New Issue
Block a user