CLI update: subcommand must be specified
This commit is contained in:
+40
-73
@@ -5,40 +5,8 @@ use clap::{Args, Parser, Subcommand};
|
||||
#[command(name = "aha")]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub(crate) struct Cli {
|
||||
/// Service listen address
|
||||
#[arg(short, long, default_value = "127.0.0.1")]
|
||||
pub address: Option<String>,
|
||||
|
||||
/// Service listen port
|
||||
#[arg(short, long)]
|
||||
pub port: Option<u16>,
|
||||
|
||||
/// Model type (required for backward compatibility)
|
||||
#[arg(short, long)]
|
||||
pub model: Option<WhichModel>,
|
||||
|
||||
/// Local model weight path
|
||||
#[arg(long)]
|
||||
pub weight_path: Option<String>,
|
||||
|
||||
/// Model download save directory
|
||||
#[arg(long)]
|
||||
pub save_dir: Option<String>,
|
||||
|
||||
/// Download retry count
|
||||
#[arg(long)]
|
||||
pub download_retries: Option<u32>,
|
||||
|
||||
/// Local GGUF model weight path (required for loading models with GGUF).
|
||||
#[arg(long)]
|
||||
pub gguf_path: Option<String>,
|
||||
|
||||
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
|
||||
#[arg(long)]
|
||||
pub mmproj_path: Option<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Commands>,
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
@@ -61,7 +29,7 @@ pub(crate) enum Commands {
|
||||
|
||||
/// Common/shared arguments for server operations
|
||||
#[derive(Args, Debug)]
|
||||
pub(crate) struct CommonArgs {
|
||||
pub(crate) struct ServerCommonArgs {
|
||||
/// Service listen address
|
||||
#[arg(short, long, default_value = "127.0.0.1")]
|
||||
pub address: String,
|
||||
@@ -70,24 +38,42 @@ pub(crate) struct CommonArgs {
|
||||
#[arg(short, long, default_value_t = 10100)]
|
||||
pub port: u16,
|
||||
|
||||
/// Model type (required)
|
||||
#[arg(short, long)]
|
||||
pub model: WhichModel,
|
||||
|
||||
/// Allow remote shutdown requests (default: local only, use with caution)
|
||||
#[arg(long)]
|
||||
pub allow_remote_shutdown: bool,
|
||||
}
|
||||
|
||||
/// Arguments for the 'cli' subcommand (download + serve)
|
||||
#[derive(Args, Debug)]
|
||||
pub(crate) struct CliArgs {
|
||||
#[command(flatten)]
|
||||
pub common: CommonArgs,
|
||||
|
||||
pub(crate) struct PathCommonArgs {
|
||||
/// Local model weight path (skip download if provided)
|
||||
#[arg(long)]
|
||||
pub weight_path: Option<String>,
|
||||
/// Local GGUF model weight path (required for loading models with GGUF).
|
||||
#[arg(long)]
|
||||
pub gguf_path: Option<String>,
|
||||
|
||||
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
|
||||
#[arg(long)]
|
||||
pub mmproj_path: Option<String>,
|
||||
|
||||
/// Local path for onnx model weights (required for loading with onnx)
|
||||
#[arg(long)]
|
||||
pub onnx_path: Option<String>,
|
||||
|
||||
/// config path for onnx/gguf model need extra config file
|
||||
#[arg(long)]
|
||||
pub config_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Arguments for the 'cli' subcommand (download + serve)
|
||||
#[derive(Args, Debug)]
|
||||
pub(crate) struct CliArgs {
|
||||
/// Model type (required)
|
||||
#[arg(short, long)]
|
||||
pub model: WhichModel,
|
||||
|
||||
#[command(flatten)]
|
||||
pub server_common: ServerCommonArgs,
|
||||
|
||||
/// Model download save directory
|
||||
#[arg(long)]
|
||||
@@ -97,32 +83,22 @@ pub(crate) struct CliArgs {
|
||||
#[arg(long)]
|
||||
pub download_retries: Option<u32>,
|
||||
|
||||
/// Local GGUF model weight path (required for loading models with GGUF).
|
||||
#[arg(long)]
|
||||
pub gguf_path: Option<String>,
|
||||
|
||||
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
|
||||
#[arg(long)]
|
||||
pub mmproj_path: Option<String>,
|
||||
#[command(flatten)]
|
||||
pub path_common: PathCommonArgs,
|
||||
}
|
||||
|
||||
/// Arguments for the 'serv start' subcommand
|
||||
#[derive(Args, Debug)]
|
||||
pub(crate) struct ServArgs {
|
||||
/// Model type (required)
|
||||
#[arg(short, long)]
|
||||
pub model: WhichModel,
|
||||
|
||||
#[command(flatten)]
|
||||
pub common: CommonArgs,
|
||||
pub server_common: ServerCommonArgs,
|
||||
|
||||
/// Local model weight path (defaults to ~/.aha/{model_id} if not specified)
|
||||
#[arg(long)]
|
||||
pub weight_path: Option<String>,
|
||||
|
||||
/// Local GGUF model weight path (required for loading models with GGUF).
|
||||
#[arg(long)]
|
||||
pub gguf_path: Option<String>,
|
||||
|
||||
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
|
||||
#[arg(long)]
|
||||
pub mmproj_path: Option<String>,
|
||||
#[command(flatten)]
|
||||
pub path_common: PathCommonArgs,
|
||||
}
|
||||
|
||||
/// Arguments for the 'serv list' subcommand
|
||||
@@ -164,17 +140,8 @@ pub(crate) struct RunArgs {
|
||||
#[arg(short, long)]
|
||||
pub output: Option<String>,
|
||||
|
||||
/// Local model weight path (defaults to ~/.aha/{model_id} if not specified)
|
||||
#[arg(long)]
|
||||
pub weight_path: Option<String>,
|
||||
|
||||
/// Local GGUF model weight path (required for loading models with GGUF).
|
||||
#[arg(long)]
|
||||
pub gguf_path: Option<String>,
|
||||
|
||||
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
|
||||
#[arg(long)]
|
||||
pub mmproj_path: Option<String>,
|
||||
#[command(flatten)]
|
||||
pub path_common: PathCommonArgs,
|
||||
}
|
||||
|
||||
/// Arguments for the 'delete' subcommand (delete model from default location)
|
||||
|
||||
+58
-28
@@ -76,22 +76,27 @@ pub(crate) fn run_list(args: ListArgs) -> anyhow::Result<()> {
|
||||
/// Run the 'cli' subcommand: download model (if needed) and start service
|
||||
pub(crate) async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
|
||||
let CliArgs {
|
||||
common,
|
||||
weight_path,
|
||||
model,
|
||||
server_common,
|
||||
save_dir,
|
||||
download_retries,
|
||||
gguf_path,
|
||||
mmproj_path,
|
||||
path_common,
|
||||
} = args;
|
||||
let model_id = common.model.as_string();
|
||||
let model_id = model.as_string();
|
||||
|
||||
let (model_path, gguf, mmproj) = if model_id.contains("gguf") {
|
||||
if gguf_path.is_none() {
|
||||
let (model_path, gguf, mmproj) = if model.is_gguf() {
|
||||
if path_common.gguf_path.is_none() {
|
||||
return Err(anyhow!("gguf model path is required"));
|
||||
}
|
||||
("GGUF".to_string(), gguf_path, mmproj_path)
|
||||
(
|
||||
"GGUF".to_string(),
|
||||
path_common.gguf_path,
|
||||
path_common.mmproj_path,
|
||||
)
|
||||
} else if model.is_onnx() {
|
||||
return Err(anyhow!("onnx model not support now"));
|
||||
} else {
|
||||
let model_path = match weight_path {
|
||||
let model_path = match path_common.weight_path {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
let save_dir = match save_dir {
|
||||
@@ -106,8 +111,13 @@ pub(crate) async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
|
||||
(model_path, None, None)
|
||||
};
|
||||
|
||||
init(common.model, model_path, gguf, mmproj)?;
|
||||
start_http_server(common.address, common.port, common.allow_remote_shutdown).await?;
|
||||
init(model, model_path, gguf, mmproj)?;
|
||||
start_http_server(
|
||||
server_common.address,
|
||||
server_common.port,
|
||||
server_common.allow_remote_shutdown,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -115,27 +125,41 @@ pub(crate) async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
|
||||
/// Run the 'serv' subcommand: start service only (no download)
|
||||
pub(crate) async fn run_serv(args: ServArgs) -> anyhow::Result<()> {
|
||||
let ServArgs {
|
||||
common,
|
||||
weight_path,
|
||||
gguf_path,
|
||||
mmproj_path,
|
||||
model,
|
||||
server_common,
|
||||
path_common,
|
||||
} = args;
|
||||
let model_id = common.model.as_string();
|
||||
let (model_path, gguf, mmproj) = if model_id.contains("gguf") {
|
||||
if gguf_path.is_none() {
|
||||
let (model_path, gguf, mmproj) = if model.is_gguf() {
|
||||
if path_common.gguf_path.is_none() {
|
||||
return Err(anyhow!("gguf model path is required"));
|
||||
}
|
||||
("GGUF".to_string(), gguf_path, mmproj_path)
|
||||
(
|
||||
"GGUF".to_string(),
|
||||
path_common.gguf_path,
|
||||
path_common.mmproj_path,
|
||||
)
|
||||
} else if model.is_onnx() {
|
||||
return Err(anyhow!("onnx model not support now"));
|
||||
} else {
|
||||
let model_path = match weight_path {
|
||||
let model_path = match path_common.weight_path {
|
||||
Some(path) => path,
|
||||
None => get_default_weight_path(common.model),
|
||||
None => get_default_weight_path(model),
|
||||
};
|
||||
if !std::path::Path::new(&model_path).exists() {
|
||||
return Err(anyhow!(
|
||||
"serv subcommand will not download model, use `weight-path` to pass the model path"
|
||||
));
|
||||
}
|
||||
(model_path, None, None)
|
||||
};
|
||||
|
||||
init(common.model, model_path, gguf, mmproj)?;
|
||||
start_http_server(common.address, common.port, common.allow_remote_shutdown).await?;
|
||||
init(model, model_path, gguf, mmproj)?;
|
||||
start_http_server(
|
||||
server_common.address,
|
||||
server_common.port,
|
||||
server_common.allow_remote_shutdown,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -205,13 +229,11 @@ pub(crate) fn run_run(args: RunArgs) -> anyhow::Result<()> {
|
||||
model,
|
||||
input,
|
||||
output,
|
||||
weight_path,
|
||||
gguf_path,
|
||||
mmproj_path,
|
||||
path_common,
|
||||
} = args;
|
||||
|
||||
// Use default weight path if not specified
|
||||
let weight_path = match weight_path {
|
||||
let weight_path = match path_common.weight_path {
|
||||
Some(path) => path,
|
||||
None => get_default_weight_path(model),
|
||||
};
|
||||
@@ -243,6 +265,9 @@ pub(crate) fn run_run(args: RunArgs) -> anyhow::Result<()> {
|
||||
WhichModel::Qwen3_1_7B => {
|
||||
qwen3::Qwen3Exec::run(&input, output.as_deref(), &weight_path)?;
|
||||
}
|
||||
WhichModel::Qwen3_4B => {
|
||||
qwen3::Qwen3Exec::run(&input, output.as_deref(), &weight_path)?;
|
||||
}
|
||||
WhichModel::Qwen3_5_0_8B => {
|
||||
qwen3_5::Qwen3_5Exec::run(&input, output.as_deref(), &weight_path)?;
|
||||
}
|
||||
@@ -256,7 +281,12 @@ pub(crate) fn run_run(args: RunArgs) -> anyhow::Result<()> {
|
||||
qwen3_5::Qwen3_5Exec::run(&input, output.as_deref(), &weight_path)?;
|
||||
}
|
||||
WhichModel::Qwen3_5Gguf => {
|
||||
qwen3_5::Qwen3_5Exec::run_gguf(&input, output.as_deref(), gguf_path, mmproj_path)?;
|
||||
qwen3_5::Qwen3_5Exec::run_gguf(
|
||||
&input,
|
||||
output.as_deref(),
|
||||
path_common.gguf_path,
|
||||
path_common.mmproj_path,
|
||||
)?;
|
||||
}
|
||||
WhichModel::Qwen3ASR0_6B => {
|
||||
qwen3_asr::Qwen3ASRExec::run(&input, output.as_deref(), &weight_path)?;
|
||||
|
||||
+8
-27
@@ -1,7 +1,7 @@
|
||||
use clap::Parser;
|
||||
|
||||
use crate::cli::{
|
||||
args::{Cli, CliArgs, Commands, CommonArgs},
|
||||
args::{Cli, Commands},
|
||||
run_cli, run_delete, run_download, run_list, run_ps, run_run, run_serv,
|
||||
};
|
||||
|
||||
@@ -11,32 +11,13 @@ mod server;
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Cli(args)) => run_cli(args).await,
|
||||
Some(Commands::Serv(args)) => run_serv(args).await,
|
||||
Some(Commands::Ps(args)) => run_ps(args),
|
||||
Some(Commands::Delete(args)) => run_delete(args),
|
||||
Some(Commands::Download(args)) => run_download(args).await,
|
||||
Some(Commands::Run(args)) => run_run(args),
|
||||
Some(Commands::List(args)) => run_list(args),
|
||||
None => {
|
||||
// Backward compatibility: when no subcommand is provided, use 'cli' behavior
|
||||
let model = cli.model.expect("Model is required (use -m or --model)");
|
||||
let args = CliArgs {
|
||||
common: CommonArgs {
|
||||
address: cli.address.unwrap_or_else(|| "127.0.0.1".to_string()),
|
||||
port: cli.port.unwrap_or(10100),
|
||||
model,
|
||||
allow_remote_shutdown: false,
|
||||
},
|
||||
weight_path: cli.weight_path,
|
||||
save_dir: cli.save_dir,
|
||||
download_retries: cli.download_retries,
|
||||
gguf_path: cli.gguf_path,
|
||||
mmproj_path: cli.mmproj_path,
|
||||
};
|
||||
run_cli(args).await
|
||||
}
|
||||
Commands::Cli(args) => run_cli(args).await,
|
||||
Commands::Serv(args) => run_serv(args).await,
|
||||
Commands::Ps(args) => run_ps(args),
|
||||
Commands::Delete(args) => run_delete(args),
|
||||
Commands::Download(args) => run_download(args).await,
|
||||
Commands::Run(args) => run_run(args),
|
||||
Commands::List(args) => run_list(args),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ pub enum WhichModel {
|
||||
Qwen3_0_6B,
|
||||
#[value(name = "Qwen/Qwen3-1.7B")]
|
||||
Qwen3_1_7B,
|
||||
#[value(name = "Qwen/Qwen3-4B")]
|
||||
Qwen3_4B,
|
||||
#[value(name = "Qwen/Qwen3.5-0.8B")]
|
||||
Qwen3_5_0_8B,
|
||||
#[value(name = "Qwen/Qwen3.5-2B")]
|
||||
@@ -67,18 +69,40 @@ pub enum WhichModel {
|
||||
}
|
||||
|
||||
impl WhichModel {
|
||||
/// Get the ModelScope model ID for this model variant
|
||||
/// Get the model ID for this model variant
|
||||
pub fn as_string(&self) -> String {
|
||||
self.to_possible_value()
|
||||
.expect("not exists")
|
||||
.get_name()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Checks if the model is in GGUF format
|
||||
///
|
||||
/// Returns true if the model ID contains "gguf", false otherwise
|
||||
pub fn is_gguf(&self) -> bool {
|
||||
let model_id = self.as_string();
|
||||
model_id.to_lowercase().contains("gguf")
|
||||
}
|
||||
|
||||
/// Checks if the model is in ONNX format
|
||||
///
|
||||
/// Returns true if the model ID contains "onnx", false otherwise
|
||||
pub fn is_onnx(&self) -> bool {
|
||||
let model_id = self.as_string();
|
||||
model_id.to_lowercase().contains("onnx")
|
||||
}
|
||||
|
||||
/// Get the WhichModel enum list
|
||||
pub fn model_list() -> Vec<Self> {
|
||||
WhichModel::value_variants().to_vec()
|
||||
}
|
||||
|
||||
/// Extracts the model owner/organization from the model ID
|
||||
///
|
||||
/// Splits the model ID string on '/' and returns the first part which typically represents
|
||||
/// the organization or user who owns the model in Hugging Face format (e.g., "Qwen" from "Qwen/Qwen3-0.6B")
|
||||
/// Returns "none" if the model ID doesn't contain a '/' separator
|
||||
pub fn model_owner(&self) -> String {
|
||||
let name = self.as_string();
|
||||
let names: Vec<&str> = name.split("/").collect();
|
||||
@@ -96,6 +120,7 @@ impl WhichModel {
|
||||
WhichModel::MiniCPM4_0_5B
|
||||
| WhichModel::Qwen3_0_6B
|
||||
| WhichModel::Qwen3_1_7B
|
||||
| WhichModel::Qwen3_4B
|
||||
| WhichModel::LFM2_1_2B
|
||||
| WhichModel::LFM2_5_1_2BInstruct => "llm",
|
||||
// VLM models
|
||||
|
||||
+27
-12
@@ -129,12 +129,27 @@ impl<'a> GenerateModel for ModelInstance<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_model<'a>(
|
||||
#[allow(unused)]
|
||||
pub fn load_gguf_model<'a>(
|
||||
model_type: WhichModel,
|
||||
path: &str,
|
||||
gguf: Option<&str>,
|
||||
mmproj: Option<&str>,
|
||||
config_path: Option<&str>, // 有些gguf未包含模型其他配置,需额外指定
|
||||
gguf_path: &str,
|
||||
mmproj_path: Option<&str>,
|
||||
) -> Result<ModelInstance<'a>> {
|
||||
let model = match model_type {
|
||||
WhichModel::Qwen3_5Gguf => {
|
||||
let model = Qwen3_5GenerateModel::init_from_gguf(gguf_path, mmproj_path, None)?;
|
||||
ModelInstance::Qwen3_5(model)
|
||||
}
|
||||
_ => {
|
||||
let model_id = model_type.as_string();
|
||||
return Err(anyhow!("model id {model_id} is not gguf model"));
|
||||
}
|
||||
};
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub fn load_model<'a>(model_type: WhichModel, path: &str) -> Result<ModelInstance<'a>> {
|
||||
let model = match model_type {
|
||||
WhichModel::MiniCPM4_0_5B => {
|
||||
let model = MiniCPMGenerateModel::init(path, None, None)?;
|
||||
@@ -172,6 +187,10 @@ pub fn load_model<'a>(
|
||||
let model = Qwen3GenerateModel::init(path, None, None)?;
|
||||
ModelInstance::Qwen3(model)
|
||||
}
|
||||
WhichModel::Qwen3_4B => {
|
||||
let model = Qwen3GenerateModel::init(path, None, None)?;
|
||||
ModelInstance::Qwen3(model)
|
||||
}
|
||||
WhichModel::Qwen3_5_0_8B => {
|
||||
let model = Qwen3_5GenerateModel::init(path, None, None)?;
|
||||
ModelInstance::Qwen3_5(model)
|
||||
@@ -188,14 +207,6 @@ pub fn load_model<'a>(
|
||||
let model = Qwen3_5GenerateModel::init(path, None, None)?;
|
||||
ModelInstance::Qwen3_5(model)
|
||||
}
|
||||
WhichModel::Qwen3_5Gguf => {
|
||||
if gguf.is_none() {
|
||||
return Err(anyhow!("Qwen3_5Gguf gguf model path is required"));
|
||||
}
|
||||
let gguf = gguf.unwrap();
|
||||
let model = Qwen3_5GenerateModel::init_from_gguf(gguf, mmproj, None)?;
|
||||
ModelInstance::Qwen3_5(model)
|
||||
}
|
||||
WhichModel::Qwen3ASR0_6B => {
|
||||
let model = Qwen3AsrGenerateModel::init(path, None, None)?;
|
||||
ModelInstance::Qwen3ASR(model)
|
||||
@@ -264,6 +275,10 @@ pub fn load_model<'a>(
|
||||
let model = GlmOcrGenerateModel::init(path, None, None)?;
|
||||
ModelInstance::GlmOCR(model)
|
||||
}
|
||||
_ => {
|
||||
let model_id = model_type.as_string();
|
||||
return Err(anyhow!("model id {model_id} is not safetensor model"));
|
||||
}
|
||||
};
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
|
||||
qwen3_5,
|
||||
device,
|
||||
model_name: model_name.to_string(),
|
||||
repeat_penalty: 1.01,
|
||||
repeat_penalty: 1.0,
|
||||
repeat_last_n: 64,
|
||||
})
|
||||
}
|
||||
@@ -116,7 +116,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
|
||||
qwen3_5,
|
||||
device,
|
||||
model_name: stem.to_string(),
|
||||
repeat_penalty: 1.1,
|
||||
repeat_penalty: 1.2,
|
||||
repeat_last_n: 64,
|
||||
})
|
||||
}
|
||||
|
||||
+17
-4
@@ -2,9 +2,11 @@ use std::pin::pin;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use aha::models::load_gguf_model;
|
||||
use aha::models::{GenerateModel, ModelInstance, common::model_mapping::WhichModel, load_model};
|
||||
use aha::params::chat::ChatCompletionParameters;
|
||||
use aha::utils::string_to_static_str;
|
||||
use anyhow::anyhow;
|
||||
use rocket::futures::StreamExt;
|
||||
use rocket::serde::{Serialize, json::Json};
|
||||
use rocket::{
|
||||
@@ -37,10 +39,21 @@ pub fn init(
|
||||
gguf: Option<String>,
|
||||
mmproj: Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let model_path = string_to_static_str(path);
|
||||
let gguf = gguf.map(string_to_static_str);
|
||||
let mmproj = mmproj.map(string_to_static_str);
|
||||
let model = load_model(model_type, model_path, gguf, mmproj)?;
|
||||
let model = if model_type.is_gguf() {
|
||||
if let Some(gguf_path) = gguf {
|
||||
let gguf_path = string_to_static_str(gguf_path);
|
||||
let mmproj_path = mmproj.map(string_to_static_str);
|
||||
load_gguf_model(model_type, None, gguf_path, mmproj_path)?
|
||||
} else {
|
||||
return Err(anyhow!("gguf model need gguf model path"));
|
||||
}
|
||||
} else if model_type.is_onnx() {
|
||||
return Err(anyhow!("onnx comming soon but now not support"));
|
||||
} else {
|
||||
let model_path = string_to_static_str(path);
|
||||
load_model(model_type, model_path)?
|
||||
};
|
||||
|
||||
MODEL.get_or_init(|| {
|
||||
Arc::new(RwLock::new(StoredModel {
|
||||
which_model: model_type,
|
||||
|
||||
Reference in New Issue
Block a user