cleanup and rename provider to plugin
This commit is contained in:
+412
-16
@@ -12,6 +12,13 @@ from SYS.config import (
|
||||
)
|
||||
from SYS.database import LOG_DB_PATH, db
|
||||
from SYS.logger import log
|
||||
from SYS.plugin_config import (
|
||||
build_default_plugin_config,
|
||||
build_default_tool_config,
|
||||
get_configurable_plugin_types,
|
||||
get_configurable_store_types,
|
||||
get_configurable_tool_types,
|
||||
)
|
||||
from SYS import pipeline as ctx
|
||||
from SYS.result_table import Table
|
||||
from cmdnat._parsing import (
|
||||
@@ -26,6 +33,7 @@ from cmdnat._parsing import (
|
||||
_PREFERENCES_BROWSE_PATH = "__preferences__"
|
||||
_PLUGINS_BROWSE_PATH = "__plugins__"
|
||||
_PLUGIN_CATEGORY_KEYS = ("plugin", "provider", "tool")
|
||||
_CREATE_INSTANCE_FLAG = "-create-instance"
|
||||
_KNOWN_SECTION_LABELS = {
|
||||
"plugin": "Plugins",
|
||||
"provider": "Plugins",
|
||||
@@ -52,6 +60,18 @@ _SENSITIVE_CONFIG_KEYS = {
|
||||
"secret",
|
||||
"token",
|
||||
}
|
||||
_CONFIG_ITEM_FIELDS = (
|
||||
"kind",
|
||||
"key",
|
||||
"title",
|
||||
"browse_path",
|
||||
"name",
|
||||
"value",
|
||||
"value_display",
|
||||
"type",
|
||||
"display_path",
|
||||
"instance_target",
|
||||
)
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".config",
|
||||
@@ -271,6 +291,168 @@ def _format_config_entry_count(value: Any) -> str:
|
||||
return f"{count} entries"
|
||||
|
||||
|
||||
def _get_configurable_plugin_names() -> List[str]:
|
||||
try:
|
||||
return [
|
||||
str(name).strip().lower()
|
||||
for name in (get_configurable_plugin_types() or [])
|
||||
if str(name).strip()
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _get_configurable_tool_names() -> List[str]:
|
||||
try:
|
||||
return [
|
||||
str(name).strip().lower()
|
||||
for name in (get_configurable_tool_types() or [])
|
||||
if str(name).strip()
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _get_multi_instance_plugin_names() -> set[str]:
|
||||
try:
|
||||
return {
|
||||
str(name).strip().lower()
|
||||
for name in (get_configurable_store_types() or [])
|
||||
if str(name).strip()
|
||||
}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def _split_config_path(value: Optional[str]) -> List[str]:
|
||||
return [part for part in str(value or "").split(".") if part]
|
||||
|
||||
|
||||
def _is_multi_instance_plugin_name(name: str) -> bool:
|
||||
return str(name or "").strip().lower() in _get_multi_instance_plugin_names()
|
||||
|
||||
|
||||
def _is_multi_instance_plugin_root_path(browse_path: Optional[str]) -> bool:
|
||||
parts = _split_config_path(browse_path)
|
||||
return (
|
||||
len(parts) == 2
|
||||
and parts[0] in {"plugin", "provider"}
|
||||
and _is_multi_instance_plugin_name(parts[1])
|
||||
)
|
||||
|
||||
|
||||
def _plugin_schema_field_keys(plugin_name: str) -> set[str]:
|
||||
defaults = build_default_plugin_config(plugin_name)
|
||||
if not isinstance(defaults, dict):
|
||||
return set()
|
||||
return {
|
||||
str(key or "").strip().lower()
|
||||
for key in defaults.keys()
|
||||
if str(key or "").strip()
|
||||
}
|
||||
|
||||
|
||||
def _looks_like_single_instance_branch(plugin_name: str, branch: Any) -> bool:
|
||||
if not isinstance(branch, dict) or not branch:
|
||||
return False
|
||||
|
||||
schema_keys = _plugin_schema_field_keys(plugin_name)
|
||||
entry_keys = {str(key or "").strip().lower() for key in branch.keys()}
|
||||
looks_like_single = bool(schema_keys and entry_keys.intersection(schema_keys))
|
||||
if not looks_like_single:
|
||||
looks_like_single = not all(isinstance(value, dict) for value in branch.values())
|
||||
return looks_like_single
|
||||
|
||||
|
||||
def _normalize_multi_instance_branch(plugin_name: str, branch: Any) -> Dict[str, Any]:
|
||||
if not isinstance(branch, dict):
|
||||
return {}
|
||||
if _looks_like_single_instance_branch(plugin_name, branch):
|
||||
return {"default": dict(branch)}
|
||||
return {
|
||||
str(key): value
|
||||
for key, value in _visible_config_entries(branch)
|
||||
if isinstance(value, dict)
|
||||
}
|
||||
|
||||
|
||||
def _build_create_instance_item(category: str, plugin_name: str) -> Dict[str, Any]:
|
||||
target = f"{category}.{plugin_name}"
|
||||
return {
|
||||
"kind": "create_instance",
|
||||
"key": f"{target}.__new_instance__",
|
||||
"title": "Add Instance",
|
||||
"name": "add_instance",
|
||||
"value": None,
|
||||
"value_display": "Create with @N | .config <name>",
|
||||
"display_path": f"{_format_config_path_label(target)} / Add Instance",
|
||||
"type": "action",
|
||||
"instance_target": target,
|
||||
}
|
||||
|
||||
|
||||
def _build_synthetic_plugin_branch(category: str, name: str) -> Optional[Dict[str, Any]]:
|
||||
normalized_category = str(category or "").strip().lower()
|
||||
normalized_name = str(name or "").strip().lower()
|
||||
if not normalized_name:
|
||||
return None
|
||||
|
||||
if normalized_category == "tool":
|
||||
branch = build_default_tool_config(normalized_name)
|
||||
return dict(branch) if isinstance(branch, dict) else None
|
||||
|
||||
branch = build_default_plugin_config(normalized_name)
|
||||
if not isinstance(branch, dict):
|
||||
return None
|
||||
if normalized_name in _get_multi_instance_plugin_names():
|
||||
return {"default": dict(branch)}
|
||||
return dict(branch)
|
||||
|
||||
|
||||
def _find_configured_plugin_branch(
|
||||
config_data: Dict[str, Any],
|
||||
category: str,
|
||||
name: str,
|
||||
) -> Optional[tuple[str, Dict[str, Any]]]:
|
||||
category_block = config_data.get(category)
|
||||
if not isinstance(category_block, dict):
|
||||
return None
|
||||
|
||||
target = str(name or "").strip().lower()
|
||||
for raw_name, raw_value in _visible_config_entries(category_block):
|
||||
if str(raw_name or "").strip().lower() != target or not isinstance(raw_value, dict):
|
||||
continue
|
||||
return raw_name, raw_value
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_plugin_branch(
|
||||
config_data: Dict[str, Any],
|
||||
category: str,
|
||||
name: str,
|
||||
) -> Optional[tuple[str, Dict[str, Any], bool]]:
|
||||
found = _find_configured_plugin_branch(config_data, category, name)
|
||||
if found is not None:
|
||||
resolved_name, resolved_value = found
|
||||
return resolved_name, resolved_value, True
|
||||
|
||||
normalized_category = str(category or "").strip().lower()
|
||||
normalized_name = str(name or "").strip().lower()
|
||||
if not normalized_name:
|
||||
return None
|
||||
|
||||
if normalized_category == "tool":
|
||||
if normalized_name not in _get_configurable_tool_names():
|
||||
return None
|
||||
elif normalized_name not in _get_configurable_plugin_names():
|
||||
return None
|
||||
|
||||
synthetic = _build_synthetic_plugin_branch(normalized_category, normalized_name)
|
||||
if synthetic is None:
|
||||
return None
|
||||
return normalized_name, synthetic, False
|
||||
|
||||
|
||||
def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any]]:
|
||||
branches: List[tuple[str, str, Any]] = []
|
||||
if not isinstance(config_data, dict):
|
||||
@@ -285,9 +467,41 @@ def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, A
|
||||
return branches
|
||||
|
||||
|
||||
def _iter_available_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any, bool]]:
|
||||
branches: List[tuple[str, str, Any, bool]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for category, name, value in _iter_plugin_branches(config_data):
|
||||
normalized_name = str(name or "").strip().lower()
|
||||
if not normalized_name:
|
||||
continue
|
||||
branches.append((category, name, value, True))
|
||||
seen.add(normalized_name)
|
||||
|
||||
for name in _get_configurable_plugin_names():
|
||||
if name in seen:
|
||||
continue
|
||||
synthetic = _build_synthetic_plugin_branch("plugin", name)
|
||||
if synthetic is None:
|
||||
continue
|
||||
branches.append(("plugin", name, synthetic, False))
|
||||
seen.add(name)
|
||||
|
||||
for name in _get_configurable_tool_names():
|
||||
if name in seen:
|
||||
continue
|
||||
synthetic = _build_synthetic_plugin_branch("tool", name)
|
||||
if synthetic is None:
|
||||
continue
|
||||
branches.append(("tool", name, synthetic, False))
|
||||
seen.add(name)
|
||||
|
||||
return branches
|
||||
|
||||
|
||||
def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
plugin_items: Dict[str, Dict[str, Any]] = {}
|
||||
for category, name, value in _iter_plugin_branches(config_data):
|
||||
for category, name, value, is_configured in _iter_available_plugin_branches(config_data):
|
||||
key = str(name or "").strip().lower()
|
||||
if not key:
|
||||
continue
|
||||
@@ -299,7 +513,7 @@ def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, An
|
||||
"browse_path": f"{category}.{name}",
|
||||
"summary": _format_config_entry_count(value),
|
||||
"type": "section",
|
||||
"description": "Plugin configuration",
|
||||
"description": "Plugin configuration" if is_configured else "Plugin configuration (available to configure)",
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -337,8 +551,22 @@ def _resolve_config_branch(
|
||||
for item in _collect_plugin_root_items(config_data)
|
||||
}
|
||||
|
||||
parts = [part for part in text.split(".") if part]
|
||||
if len(parts) >= 2 and parts[0] in _PLUGIN_CATEGORY_KEYS:
|
||||
resolved = _resolve_plugin_branch(config_data, parts[0], parts[1])
|
||||
if resolved is None:
|
||||
return None
|
||||
_, current, _ = resolved
|
||||
if parts[0] in {"plugin", "provider"} and _is_multi_instance_plugin_name(parts[1]):
|
||||
current = _normalize_multi_instance_branch(parts[1], current)
|
||||
for part in parts[2:]:
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(part)
|
||||
return current if isinstance(current, dict) else None
|
||||
|
||||
current: Any = config_data
|
||||
for part in text.split("."):
|
||||
for part in parts:
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(part)
|
||||
@@ -386,6 +614,66 @@ def _build_value_item(
|
||||
}
|
||||
|
||||
|
||||
def _create_or_get_plugin_instance(
|
||||
config_data: Dict[str, Any],
|
||||
instance_target: str,
|
||||
instance_name: str,
|
||||
) -> tuple[str, bool]:
|
||||
parts = _split_config_path(instance_target)
|
||||
if len(parts) != 2 or parts[0] not in {"plugin", "provider"}:
|
||||
raise ValueError(f"Unsupported instance target '{instance_target}'")
|
||||
|
||||
category, plugin_name = parts
|
||||
raw_instance_name = str(instance_name or "").strip()
|
||||
if not raw_instance_name:
|
||||
raise ValueError("Instance name is required")
|
||||
if raw_instance_name.startswith("_"):
|
||||
raise ValueError("Instance names cannot start with '_' characters")
|
||||
|
||||
category_block = config_data.get(category)
|
||||
if not isinstance(category_block, dict):
|
||||
category_block = {}
|
||||
config_data[category] = category_block
|
||||
|
||||
plugin_block = category_block.get(plugin_name)
|
||||
if not isinstance(plugin_block, dict):
|
||||
plugin_block = {}
|
||||
category_block[plugin_name] = plugin_block
|
||||
|
||||
if _looks_like_single_instance_branch(plugin_name, plugin_block):
|
||||
existing_default = dict(plugin_block)
|
||||
plugin_block.clear()
|
||||
plugin_block["default"] = existing_default
|
||||
|
||||
target_key = None
|
||||
lowered_target = raw_instance_name.lower()
|
||||
for existing_key in plugin_block.keys():
|
||||
if str(existing_key or "").strip().lower() == lowered_target:
|
||||
target_key = str(existing_key)
|
||||
break
|
||||
|
||||
if target_key is not None and isinstance(plugin_block.get(target_key), dict):
|
||||
return f"{category}.{plugin_name}.{target_key}", False
|
||||
|
||||
plugin_block[raw_instance_name] = dict(build_default_plugin_config(plugin_name))
|
||||
return f"{category}.{plugin_name}.{raw_instance_name}", True
|
||||
|
||||
|
||||
def _resolve_update_key(config_data: Dict[str, Any], selection_key: str) -> str:
|
||||
parts = _split_config_path(selection_key)
|
||||
if (
|
||||
len(parts) >= 4
|
||||
and parts[0] in {"plugin", "provider"}
|
||||
and parts[2].lower() == "default"
|
||||
and _is_multi_instance_plugin_name(parts[1])
|
||||
):
|
||||
category_block = config_data.get(parts[0])
|
||||
plugin_block = category_block.get(parts[1]) if isinstance(category_block, dict) else None
|
||||
if _looks_like_single_instance_branch(parts[1], plugin_block):
|
||||
return ".".join([parts[0], parts[1], *parts[3:]])
|
||||
return selection_key
|
||||
|
||||
|
||||
def _build_root_config_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
items: List[Dict[str, Any]] = []
|
||||
visible_entries = _visible_config_entries(config_data)
|
||||
@@ -444,7 +732,13 @@ def _build_nested_config_items(
|
||||
|
||||
section_items: List[Dict[str, Any]] = []
|
||||
value_items: List[Dict[str, Any]] = []
|
||||
action_items: List[Dict[str, Any]] = []
|
||||
is_preferences_view = browse_path == _PREFERENCES_BROWSE_PATH
|
||||
parts = _split_config_path(browse_path)
|
||||
is_multi_instance_root = _is_multi_instance_plugin_root_path(browse_path)
|
||||
|
||||
if is_multi_instance_root:
|
||||
branch = _normalize_multi_instance_branch(parts[1], branch)
|
||||
|
||||
for key, value in _visible_config_entries(branch):
|
||||
full_key = key if is_preferences_view else f"{browse_path}.{key}"
|
||||
@@ -467,7 +761,9 @@ def _build_nested_config_items(
|
||||
|
||||
section_items.sort(key=lambda item: str(item.get("title") or "").lower())
|
||||
value_items.sort(key=lambda item: str(item.get("name") or "").lower())
|
||||
return section_items + value_items
|
||||
if is_multi_instance_root:
|
||||
action_items.append(_build_create_instance_item(parts[0], parts[1]))
|
||||
return section_items + value_items + action_items
|
||||
|
||||
|
||||
def _build_config_items(
|
||||
@@ -493,12 +789,31 @@ def _build_config_header_lines(browse_path: Optional[str]) -> List[str]:
|
||||
return [
|
||||
"Use @N on a section to drill in. Use @.. to go back.",
|
||||
]
|
||||
if _is_multi_instance_plugin_root_path(text):
|
||||
return [
|
||||
f"Path: {_format_config_path_label(text)}",
|
||||
"Use @N on an instance to drill in. Use @N | .config <name> on Add Instance to create a new instance, then update its fields in the table that opens. Use @.. to go back.",
|
||||
]
|
||||
parts = _split_config_path(text)
|
||||
if (
|
||||
len(parts) == 3
|
||||
and parts[0] in {"plugin", "provider"}
|
||||
and _is_multi_instance_plugin_name(parts[1])
|
||||
):
|
||||
return [
|
||||
f"Path: {_format_config_path_label(text)}",
|
||||
"Use @N | .config <value> to update a setting. After creating an instance, set its path, credentials, or other fields here. Use @.. to go back.",
|
||||
]
|
||||
return [
|
||||
f"Path: {_format_config_path_label(text)}",
|
||||
"Use @N on a section to drill in. Use @N | .config <value> to update a setting. Use @.. to go back.",
|
||||
]
|
||||
|
||||
|
||||
def _extract_create_instance_target(args: Sequence[str]) -> Optional[str]:
|
||||
return _extract_arg_value(args, flags={_CREATE_INSTANCE_FLAG, "--create-instance"}, allow_positional=False)
|
||||
|
||||
|
||||
def _extract_browse_arg(args: Sequence[str]) -> Optional[str]:
|
||||
return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False)
|
||||
|
||||
@@ -529,18 +844,49 @@ def _get_selected_config_item() -> Optional[Dict[str, Any]]:
|
||||
idx = indices[0]
|
||||
if idx < 0 or idx >= len(items):
|
||||
return None
|
||||
item = items[idx]
|
||||
if isinstance(item, dict):
|
||||
return item
|
||||
return _normalize_config_item(items[idx])
|
||||
|
||||
|
||||
def _normalize_config_item(candidate: Any) -> Optional[Dict[str, Any]]:
|
||||
if candidate is None:
|
||||
return None
|
||||
|
||||
normalized: Dict[str, Any] = {}
|
||||
for key in ("kind", "key", "title", "browse_path", "name", "value", "value_display", "type"):
|
||||
sources: List[Any] = [candidate]
|
||||
|
||||
if isinstance(candidate, dict):
|
||||
extra = candidate.get("extra")
|
||||
if isinstance(extra, dict):
|
||||
sources.append(extra)
|
||||
else:
|
||||
try:
|
||||
value = getattr(item, key, None)
|
||||
extra = getattr(candidate, "extra", None)
|
||||
except Exception:
|
||||
value = None
|
||||
if value is not None:
|
||||
normalized[key] = value
|
||||
extra = None
|
||||
if isinstance(extra, dict):
|
||||
sources.append(extra)
|
||||
|
||||
for source in sources:
|
||||
if isinstance(source, dict):
|
||||
getter = source.get
|
||||
for key in _CONFIG_ITEM_FIELDS:
|
||||
if key in normalized:
|
||||
continue
|
||||
value = getter(key)
|
||||
if value is not None:
|
||||
normalized[key] = value
|
||||
continue
|
||||
|
||||
for key in _CONFIG_ITEM_FIELDS:
|
||||
if key in normalized:
|
||||
continue
|
||||
try:
|
||||
value = getattr(source, key, None)
|
||||
except Exception:
|
||||
value = None
|
||||
if value is not None:
|
||||
normalized[key] = value
|
||||
|
||||
return normalized or None
|
||||
|
||||
|
||||
@@ -573,6 +919,11 @@ def _show_config_table(
|
||||
idx,
|
||||
[".config", "-browse", str(item.get("browse_path"))],
|
||||
)
|
||||
elif item.get("kind") == "create_instance" and item.get("instance_target"):
|
||||
table.set_row_selection_action(
|
||||
idx,
|
||||
[".config", _CREATE_INSTANCE_FLAG, str(item.get("instance_target"))],
|
||||
)
|
||||
|
||||
ctx.set_last_result_table(table, items)
|
||||
ctx.set_current_stage_table(table)
|
||||
@@ -602,6 +953,15 @@ def _resolve_direct_browse_path(
|
||||
return _PREFERENCES_BROWSE_PATH
|
||||
if lowered in {"plugins", "plugin", "providers", "provider", "tools", "tool"}:
|
||||
return _PLUGINS_BROWSE_PATH
|
||||
|
||||
plugin_branch = _resolve_plugin_branch(config_data, "plugin", lowered)
|
||||
if plugin_branch is not None:
|
||||
return f"plugin.{plugin_branch[0]}"
|
||||
|
||||
tool_branch = _resolve_plugin_branch(config_data, "tool", lowered)
|
||||
if tool_branch is not None:
|
||||
return f"tool.{tool_branch[0]}"
|
||||
|
||||
branch = _resolve_config_branch(config_data, text)
|
||||
if isinstance(branch, dict):
|
||||
return text
|
||||
@@ -629,27 +989,63 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
|
||||
if browse_path:
|
||||
return _show_config_table(current_config, browse_path=browse_path)
|
||||
|
||||
selection_item = _get_selected_config_item()
|
||||
selection_item = _get_selected_config_item() or _normalize_config_item(piped_result)
|
||||
|
||||
create_instance_target = _extract_create_instance_target(args)
|
||||
if create_instance_target:
|
||||
print(
|
||||
f"Use @N | .config <instance_name> to create a new instance under '{_format_config_path_label(create_instance_target)}', then set its fields in the table that opens."
|
||||
)
|
||||
return 0
|
||||
|
||||
value_from_pipe = _extract_piped_value(piped_result)
|
||||
selection_kind = str((selection_item or {}).get("kind") or "").strip().lower()
|
||||
selection_key = str((selection_item or {}).get("key") or "").strip() or None
|
||||
selection_browse_path = str((selection_item or {}).get("browse_path") or "").strip() or None
|
||||
selection_display_path = str((selection_item or {}).get("display_path") or selection_key or "").strip() or selection_key
|
||||
selection_instance_target = str((selection_item or {}).get("instance_target") or "").strip() or None
|
||||
|
||||
if selection_kind == "section" and selection_browse_path and not args and value_from_pipe is None:
|
||||
return _show_config_table(current_config, browse_path=selection_browse_path)
|
||||
|
||||
if selection_kind == "create_instance" and selection_instance_target:
|
||||
new_instance_name = value_from_pipe or _extract_selected_update_value(args)
|
||||
if new_instance_name is None:
|
||||
print(
|
||||
f"Use @N | .config <instance_name> to create a new instance under '{_format_config_path_label(selection_instance_target)}', then set its fields in the table that opens."
|
||||
)
|
||||
return 0
|
||||
new_instance_name = _strip_value_quotes(new_instance_name)
|
||||
try:
|
||||
new_browse_path, created = _create_or_get_plugin_instance(
|
||||
current_config,
|
||||
selection_instance_target,
|
||||
new_instance_name,
|
||||
)
|
||||
_save_updated_config(current_config, new_browse_path)
|
||||
status_text = "Created" if created else "Using existing"
|
||||
print(
|
||||
f"{status_text} instance '{new_instance_name}' at '{_format_config_path_label(new_browse_path)}'. "
|
||||
"Configure its fields in the table below."
|
||||
)
|
||||
return _show_config_table(current_config, browse_path=new_browse_path)
|
||||
except Exception as exc:
|
||||
log(f"Error creating config instance '{selection_instance_target}': {exc}")
|
||||
print(f"Error creating config instance: {exc}")
|
||||
return 1
|
||||
|
||||
if selection_kind == "value" and selection_key:
|
||||
new_value = value_from_pipe or _extract_selected_update_value(args)
|
||||
if new_value is not None:
|
||||
new_value = _strip_value_quotes(new_value)
|
||||
target_key = _resolve_update_key(current_config, selection_key)
|
||||
try:
|
||||
set_nested_config(current_config, selection_key, new_value)
|
||||
_save_updated_config(current_config, selection_key)
|
||||
set_nested_config(current_config, target_key, new_value)
|
||||
_save_updated_config(current_config, target_key)
|
||||
print(f"Updated '{selection_display_path}' to '{new_value}'")
|
||||
return 0
|
||||
except Exception as exc:
|
||||
log(f"Error updating config '{selection_key}': {exc}")
|
||||
log(f"Error updating config '{target_key}': {exc}")
|
||||
print(f"Error updating config: {exc}")
|
||||
return 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user