feat(commands): expose runtime web toggle
AC: - :web on/off updates tool exposure immediately and persists the toggle. - owlen providers web --enable/--disable reflects the same setting and reports current status. - Help/docs cover the new toggle paths and troubleshooting guidance. Tests: - cargo test -p owlen-cli - cargo test -p owlen-core toggling_web_search_updates_config_and_registry
This commit is contained in:
@@ -45,6 +45,7 @@ The refreshed chrome introduces a cockpit-style header with live gradient gauges
|
|||||||
- **Context + quota cockpit**: The header shows `context used / window (percentage)` and a second gauge for hourly/weekly cloud token usage. Configure soft limits via `providers.ollama_cloud.hourly_quota_tokens` and `weekly_quota_tokens`; Owlen tracks consumption locally even when the provider omits token counters.
|
- **Context + quota cockpit**: The header shows `context used / window (percentage)` and a second gauge for hourly/weekly cloud token usage. Configure soft limits via `providers.ollama_cloud.hourly_quota_tokens` and `weekly_quota_tokens`; Owlen tracks consumption locally even when the provider omits token counters.
|
||||||
- **Web search tooling**: When cloud is enabled, models can call the `web.search` tool automatically. Toggle availability at runtime with `:web on` / `:web off` if you need a local-only session.
|
- **Web search tooling**: When cloud is enabled, models can call the `web.search` tool automatically. Toggle availability at runtime with `:web on` / `:web off` if you need a local-only session.
|
||||||
- **Docs & config parity**: Ship-ready config templates now include per-provider `list_ttl_secs` and `default_context_window` values, plus explicit `OLLAMA_API_KEY` guidance. Run `owlen config doctor` after upgrading from v0.1 to normalize legacy keys and receive deprecation warnings for `OLLAMA_CLOUD_API_KEY` and `OWLEN_OLLAMA_CLOUD_API_KEY`.
|
- **Docs & config parity**: Ship-ready config templates now include per-provider `list_ttl_secs` and `default_context_window` values, plus explicit `OLLAMA_API_KEY` guidance. Run `owlen config doctor` after upgrading from v0.1 to normalize legacy keys and receive deprecation warnings for `OLLAMA_CLOUD_API_KEY` and `OWLEN_OLLAMA_CLOUD_API_KEY`.
|
||||||
|
- **Runtime toggles**: Use `:web on` / `:web off` in the TUI or `owlen providers web --enable/--disable` from the CLI to expose or hide the `web.search` tool without editing `config.toml`.
|
||||||
|
|
||||||
## Security & Privacy
|
## Security & Privacy
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ pub enum ProvidersCommand {
|
|||||||
/// Provider identifier to disable.
|
/// Provider identifier to disable.
|
||||||
provider: String,
|
provider: String,
|
||||||
},
|
},
|
||||||
|
/// Enable or disable the web.search tool exposure.
|
||||||
|
Web(WebCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arguments for the `owlen models` command.
|
/// Arguments for the `owlen models` command.
|
||||||
@@ -45,12 +47,36 @@ pub struct ModelsArgs {
|
|||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arguments for managing the web.search tool exposure.
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct WebCommand {
|
||||||
|
/// Enable the web.search tool and allow remote lookups.
|
||||||
|
#[arg(long, conflicts_with = "disable")]
|
||||||
|
enable: bool,
|
||||||
|
/// Disable the web.search tool to keep sessions local-only.
|
||||||
|
#[arg(long, conflicts_with = "enable")]
|
||||||
|
disable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebCommand {
|
||||||
|
fn desired_state(&self) -> Option<bool> {
|
||||||
|
if self.enable {
|
||||||
|
Some(true)
|
||||||
|
} else if self.disable {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_providers_command(command: ProvidersCommand) -> Result<()> {
|
pub async fn run_providers_command(command: ProvidersCommand) -> Result<()> {
|
||||||
match command {
|
match command {
|
||||||
ProvidersCommand::List => list_providers(),
|
ProvidersCommand::List => list_providers(),
|
||||||
ProvidersCommand::Status { provider } => status_providers(provider.as_deref()).await,
|
ProvidersCommand::Status { provider } => status_providers(provider.as_deref()).await,
|
||||||
ProvidersCommand::Enable { provider } => toggle_provider(&provider, true),
|
ProvidersCommand::Enable { provider } => toggle_provider(&provider, true),
|
||||||
ProvidersCommand::Disable { provider } => toggle_provider(&provider, false),
|
ProvidersCommand::Disable { provider } => toggle_provider(&provider, false),
|
||||||
|
ProvidersCommand::Web(args) => handle_web_command(args),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +232,70 @@ fn verify_provider_filter(config: &Config, filter: Option<&str>) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_web_command(args: WebCommand) -> Result<()> {
|
||||||
|
let mut config = tui_config::try_load_config().unwrap_or_default();
|
||||||
|
let initial = web_tool_enabled(&config);
|
||||||
|
|
||||||
|
if let Some(desired) = args.desired_state() {
|
||||||
|
apply_web_toggle(&mut config, desired);
|
||||||
|
tui_config::save_config(&config).map_err(|err| anyhow!(err))?;
|
||||||
|
|
||||||
|
if initial == desired {
|
||||||
|
println!(
|
||||||
|
"Web search tool already {}.",
|
||||||
|
if desired { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Web search tool {}.",
|
||||||
|
if desired { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Remote search is {}.",
|
||||||
|
if config.privacy.enable_remote_search {
|
||||||
|
"enabled"
|
||||||
|
} else {
|
||||||
|
"disabled"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Web search tool is {}.",
|
||||||
|
if initial { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Remote search is {}.",
|
||||||
|
if config.privacy.enable_remote_search {
|
||||||
|
"enabled"
|
||||||
|
} else {
|
||||||
|
"disabled"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_web_toggle(config: &mut Config, enabled: bool) {
|
||||||
|
config.tools.web_search.enabled = enabled;
|
||||||
|
config.privacy.enable_remote_search = enabled;
|
||||||
|
|
||||||
|
if enabled
|
||||||
|
&& !config
|
||||||
|
.security
|
||||||
|
.allowed_tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool.eq_ignore_ascii_case("web_search"))
|
||||||
|
{
|
||||||
|
config.security.allowed_tools.push("web_search".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn web_tool_enabled(config: &Config) -> bool {
|
||||||
|
config.tools.web_search.enabled && config.privacy.enable_remote_search
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_provider(provider: &str, enable: bool) -> Result<()> {
|
fn toggle_provider(provider: &str, enable: bool) -> Result<()> {
|
||||||
let mut config = tui_config::try_load_config().unwrap_or_default();
|
let mut config = tui_config::try_load_config().unwrap_or_default();
|
||||||
let canonical = canonical_provider_id(provider);
|
let canonical = canonical_provider_id(provider);
|
||||||
@@ -649,3 +739,56 @@ impl ProviderStatusRow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_web_toggle_updates_flags_and_allowed_tools() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.privacy.enable_remote_search = false;
|
||||||
|
config.tools.web_search.enabled = false;
|
||||||
|
config.security.allowed_tools.clear();
|
||||||
|
|
||||||
|
apply_web_toggle(&mut config, true);
|
||||||
|
assert!(config.tools.web_search.enabled);
|
||||||
|
assert!(config.privacy.enable_remote_search);
|
||||||
|
assert_eq!(
|
||||||
|
1,
|
||||||
|
config
|
||||||
|
.security
|
||||||
|
.allowed_tools
|
||||||
|
.iter()
|
||||||
|
.filter(|tool| tool.eq_ignore_ascii_case("web_search"))
|
||||||
|
.count()
|
||||||
|
);
|
||||||
|
|
||||||
|
apply_web_toggle(&mut config, false);
|
||||||
|
assert!(!config.tools.web_search.enabled);
|
||||||
|
assert!(!config.privacy.enable_remote_search);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_web_toggle_does_not_duplicate_allowed_entries() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config
|
||||||
|
.security
|
||||||
|
.allowed_tools
|
||||||
|
.retain(|tool| !tool.eq_ignore_ascii_case("web_search"));
|
||||||
|
config.security.allowed_tools.push("web_search".to_string());
|
||||||
|
|
||||||
|
apply_web_toggle(&mut config, true);
|
||||||
|
apply_web_toggle(&mut config, true);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
1,
|
||||||
|
config
|
||||||
|
.security
|
||||||
|
.allowed_tools
|
||||||
|
.iter()
|
||||||
|
.filter(|tool| tool.eq_ignore_ascii_case("web_search"))
|
||||||
|
.count()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
140
crates/owlen-core/tests/web_search_toggle.rs
Normal file
140
crates/owlen-core/tests/web_search_toggle.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
use std::{any::Any, collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use futures::stream;
|
||||||
|
use owlen_core::{
|
||||||
|
ChatStream, Provider, Result,
|
||||||
|
config::Config,
|
||||||
|
llm::ProviderConfig,
|
||||||
|
session::SessionController,
|
||||||
|
storage::StorageManager,
|
||||||
|
types::{ChatRequest, ChatResponse, Message, ModelInfo},
|
||||||
|
ui::NoOpUiController,
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
struct StubCloudProvider;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for StubCloudProvider {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ollama_cloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_models(&self) -> Result<Vec<ModelInfo>> {
|
||||||
|
Ok(vec![ModelInfo {
|
||||||
|
id: "stub-cloud-model".to_string(),
|
||||||
|
name: "Stub Cloud Model".to_string(),
|
||||||
|
description: Some("Stub model for web toggle tests".to_string()),
|
||||||
|
provider: self.name().to_string(),
|
||||||
|
context_window: Some(8192),
|
||||||
|
capabilities: vec!["chat".to_string(), "tools".to_string()],
|
||||||
|
supports_tools: true,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_prompt(&self, _request: ChatRequest) -> Result<ChatResponse> {
|
||||||
|
Ok(ChatResponse {
|
||||||
|
message: Message::assistant(String::new()),
|
||||||
|
usage: None,
|
||||||
|
is_streaming: false,
|
||||||
|
is_final: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stream_prompt(&self, _request: ChatRequest) -> Result<ChatStream> {
|
||||||
|
Ok(Box::pin(stream::empty()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &(dyn Any + Send + Sync) {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn toggling_web_search_updates_config_and_registry() {
|
||||||
|
let temp_dir = tempdir().expect("temp dir");
|
||||||
|
let storage = Arc::new(
|
||||||
|
StorageManager::with_database_path(temp_dir.path().join("owlen-tests.db"))
|
||||||
|
.await
|
||||||
|
.expect("storage"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.privacy.encrypt_local_data = false;
|
||||||
|
config.general.default_model = Some("stub-cloud-model".into());
|
||||||
|
config.general.default_provider = "ollama_cloud".into();
|
||||||
|
|
||||||
|
let mut provider_cfg = ProviderConfig {
|
||||||
|
enabled: true,
|
||||||
|
provider_type: "ollama_cloud".to_string(),
|
||||||
|
base_url: Some("https://ollama.com".to_string()),
|
||||||
|
api_key: Some("test-key".to_string()),
|
||||||
|
api_key_env: None,
|
||||||
|
extra: HashMap::new(),
|
||||||
|
};
|
||||||
|
provider_cfg.extra.insert(
|
||||||
|
"web_search_endpoint".into(),
|
||||||
|
Value::String("/api/web_search".into()),
|
||||||
|
);
|
||||||
|
config.providers.insert("ollama_cloud".into(), provider_cfg);
|
||||||
|
|
||||||
|
let provider: Arc<dyn Provider> = Arc::new(StubCloudProvider);
|
||||||
|
let ui = Arc::new(NoOpUiController);
|
||||||
|
|
||||||
|
let mut session = SessionController::new(provider, config, storage, ui, false, None)
|
||||||
|
.await
|
||||||
|
.expect("session controller");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!session
|
||||||
|
.tool_registry()
|
||||||
|
.tools()
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool == "web_search"),
|
||||||
|
"web_search should be disabled by default"
|
||||||
|
);
|
||||||
|
|
||||||
|
session
|
||||||
|
.set_tool_enabled("web_search", true)
|
||||||
|
.await
|
||||||
|
.expect("enable web_search");
|
||||||
|
|
||||||
|
{
|
||||||
|
let cfg = session.config_async().await;
|
||||||
|
assert!(cfg.tools.web_search.enabled);
|
||||||
|
assert!(cfg.privacy.enable_remote_search);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
session
|
||||||
|
.tool_registry()
|
||||||
|
.tools()
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool == "web_search"),
|
||||||
|
"web_search should be registered when enabled"
|
||||||
|
);
|
||||||
|
|
||||||
|
session
|
||||||
|
.set_tool_enabled("web_search", false)
|
||||||
|
.await
|
||||||
|
.expect("disable web_search");
|
||||||
|
|
||||||
|
{
|
||||||
|
let cfg = session.config_async().await;
|
||||||
|
assert!(!cfg.tools.web_search.enabled);
|
||||||
|
assert!(!cfg.privacy.enable_remote_search);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!session
|
||||||
|
.tool_registry()
|
||||||
|
.tools()
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool == "web_search"),
|
||||||
|
"web_search should be removed when disabled"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1661,6 +1661,16 @@ impl ChatApp {
|
|||||||
self.error = None;
|
self.error = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn set_web_tool_enabled(&mut self, enabled: bool) -> Result<()> {
|
||||||
|
self.controller
|
||||||
|
.set_tool_enabled("web_search", enabled)
|
||||||
|
.await
|
||||||
|
.map_err(|err| anyhow!(err))?;
|
||||||
|
config::save_config(&self.controller.config())?;
|
||||||
|
self.refresh_usage_summary().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Override the status line with a custom message.
|
/// Override the status line with a custom message.
|
||||||
pub fn set_status_message<S: Into<String>>(&mut self, status: S) {
|
pub fn set_status_message<S: Into<String>>(&mut self, status: S) {
|
||||||
self.status = status.into();
|
self.status = status.into();
|
||||||
@@ -7459,6 +7469,78 @@ impl ChatApp {
|
|||||||
self.set_input_mode(InputMode::Normal);
|
self.set_input_mode(InputMode::Normal);
|
||||||
return Ok(AppState::Running);
|
return Ok(AppState::Running);
|
||||||
}
|
}
|
||||||
|
"web" => {
|
||||||
|
let action =
|
||||||
|
args.get(0).map(|value| value.to_ascii_lowercase());
|
||||||
|
match action.as_deref() {
|
||||||
|
Some("on") | Some("enable") => {
|
||||||
|
match self.set_web_tool_enabled(true).await {
|
||||||
|
Ok(_) => {
|
||||||
|
self.status = "Web search enabled; remote lookups allowed."
|
||||||
|
.to_string();
|
||||||
|
self.error = None;
|
||||||
|
self.push_toast(
|
||||||
|
ToastLevel::Info,
|
||||||
|
"Web search enabled; remote lookups allowed.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.status =
|
||||||
|
"Failed to enable web search".to_string();
|
||||||
|
self.error = Some(format!(
|
||||||
|
"Failed to enable web search: {}",
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("off") | Some("disable") => {
|
||||||
|
match self.set_web_tool_enabled(false).await {
|
||||||
|
Ok(_) => {
|
||||||
|
self.status =
|
||||||
|
"Web search disabled; staying local."
|
||||||
|
.to_string();
|
||||||
|
self.error = None;
|
||||||
|
self.push_toast(
|
||||||
|
ToastLevel::Warning,
|
||||||
|
"Web search disabled; staying local.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.status =
|
||||||
|
"Failed to disable web search".to_string();
|
||||||
|
self.error = Some(format!(
|
||||||
|
"Failed to disable web search: {}",
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("status") | None => {
|
||||||
|
{
|
||||||
|
let config = self.controller.config();
|
||||||
|
let enabled = config.tools.web_search.enabled
|
||||||
|
&& config.privacy.enable_remote_search;
|
||||||
|
if enabled {
|
||||||
|
self.status =
|
||||||
|
"Web search is enabled.".to_string();
|
||||||
|
} else {
|
||||||
|
self.status =
|
||||||
|
"Web search is disabled.".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.error = None;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.status = "Usage: :web <on|off|status>".to_string();
|
||||||
|
self.error =
|
||||||
|
Some("Usage: :web <on|off|status>".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.command_palette.clear();
|
||||||
|
self.set_input_mode(InputMode::Normal);
|
||||||
|
return Ok(AppState::Running);
|
||||||
|
}
|
||||||
"privacy-enable" => {
|
"privacy-enable" => {
|
||||||
if let Some(tool) = args.first() {
|
if let Some(tool) = args.first() {
|
||||||
match self.controller.set_tool_enabled(tool, true).await {
|
match self.controller.set_tool_enabled(tool, true).await {
|
||||||
|
|||||||
@@ -4244,6 +4244,7 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
Line::from(" :provider <name> [auto|local|cloud] → switch provider or set mode"),
|
Line::from(" :provider <name> [auto|local|cloud] → switch provider or set mode"),
|
||||||
Line::from(" :models --local | --cloud → focus models by scope"),
|
Line::from(" :models --local | --cloud → focus models by scope"),
|
||||||
Line::from(" :cloud setup [--force-cloud-base-url] → configure Ollama Cloud"),
|
Line::from(" :cloud setup [--force-cloud-base-url] → configure Ollama Cloud"),
|
||||||
|
Line::from(" :web on|off → expose or hide the web.search tool"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec in the troubleshooting guide to copy keys from a working machine or register them manually.
|
> **Tip:** If the official `ollama signin` flow fails on Linux v0.12.3, follow the [Linux Ollama sign-in workaround](#linux-ollama-sign-in-workaround-v0123) in the troubleshooting guide to copy keys from a working machine or register them manually.
|
||||||
|
|
||||||
### Managing cloud credentials via CLI
|
### Managing cloud credentials via CLI
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ If the hosted API returns `HTTP 429 Too Many Requests`, Owlen keeps the provider
|
|||||||
1. Check the cockpit header or run `:limits` to see your locally tracked hourly/weekly totals. When either bar crosses 80% Owlen warns you; 95% triggers a critical toast.
|
1. Check the cockpit header or run `:limits` to see your locally tracked hourly/weekly totals. When either bar crosses 80% Owlen warns you; 95% triggers a critical toast.
|
||||||
2. Raise or remove the soft quotas (`providers.ollama_cloud.hourly_quota_tokens`, `weekly_quota_tokens`) if your vendor allotment is higher, or pause cloud usage until the next window resets.
|
2. Raise or remove the soft quotas (`providers.ollama_cloud.hourly_quota_tokens`, `weekly_quota_tokens`) if your vendor allotment is higher, or pause cloud usage until the next window resets.
|
||||||
3. If you need the cloud-only model, retry after the provider’s cooling-off period (Ollama currently resets the rate window hourly for most SKUs). Adjust `list_ttl_secs` upward if automated refreshes are consuming too many tokens.
|
3. If you need the cloud-only model, retry after the provider’s cooling-off period (Ollama currently resets the rate window hourly for most SKUs). Adjust `list_ttl_secs` upward if automated refreshes are consuming too many tokens.
|
||||||
|
4. Use `:web off` (or `owlen providers web --disable`) to keep the session local until the rate window resets; re-enable with `:web on` / `owlen providers web --enable` when you want cloud lookups again.
|
||||||
|
|
||||||
### Linux Ollama Sign-In Workaround (v0.12.3)
|
### Linux Ollama Sign-In Workaround (v0.12.3)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user