Release v1.0.56: Credential & Browser Profile Manager
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
This commit is contained in:
@@ -1,15 +1,41 @@
|
||||
"""CLI 入口:用法说明与 argv 分发。"""
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CLI entry point: argv dispatch for account-manager v2.
|
||||
|
||||
Subcommand tree:
|
||||
platform list/get
|
||||
account add-web|add-secret|get|list|pick-web|mark-session|mark-error|mark-used
|
||||
credential pick|resolve
|
||||
lease release|list|cleanup
|
||||
(legacy) add|list|get|list-json|pick-web|open|delete
|
||||
|
||||
Machine commands output single-line JSON where appropriate.
|
||||
"""
|
||||
import sys
|
||||
|
||||
from service.account_service import (
|
||||
cmd_add,
|
||||
cmd_account_add_secret,
|
||||
cmd_account_add_web,
|
||||
cmd_account_get,
|
||||
cmd_account_list,
|
||||
cmd_account_mark_error,
|
||||
cmd_account_mark_session,
|
||||
cmd_account_mark_used,
|
||||
cmd_account_pick_web,
|
||||
cmd_credential_pick,
|
||||
cmd_credential_resolve,
|
||||
cmd_delete_by_id,
|
||||
cmd_delete_by_platform,
|
||||
cmd_delete_by_platform_phone,
|
||||
cmd_get,
|
||||
cmd_lease_cleanup,
|
||||
cmd_lease_list,
|
||||
cmd_lease_release,
|
||||
cmd_list,
|
||||
cmd_list_json,
|
||||
cmd_pick_web,
|
||||
cmd_platform_get,
|
||||
cmd_platform_list,
|
||||
)
|
||||
from service.browser_service import cmd_open
|
||||
from util.constants import LOG_LOGGER_NAME, SKILL_SLUG
|
||||
@@ -17,97 +43,223 @@ from util.logging_config import get_skill_logger, setup_skill_logging
|
||||
from util.platforms import _platform_list_cn_for_help, resolve_platform_key
|
||||
from util.runtime_paths import _maybe_print_paths_debug_cli
|
||||
|
||||
# =========================================================================
|
||||
# Help text
|
||||
# =========================================================================
|
||||
|
||||
def _cli_print_full_usage() -> None:
|
||||
"""总览(未带子命令或需要完整说明时)。"""
|
||||
print("用法概览(将 main.py 换为你的路径):")
|
||||
print(" python main.py add <平台中文名> <手机号>")
|
||||
print(" python main.py list [平台|all|全部]")
|
||||
print(" python main.py pick-web <平台> # 跨技能用:按最近更新选一条账号,一行 JSON")
|
||||
print(" python main.py list-json <平台|all> [--limit N] # 跨技能用:一行 JSON 数组,元素同 get")
|
||||
print(" python main.py get <id>")
|
||||
print(" python main.py open <id>")
|
||||
print(" python main.py delete id <id>")
|
||||
print(" python main.py delete platform <平台>")
|
||||
print(" python main.py delete platform <平台> <手机号>")
|
||||
print(" python main.py <平台> # 等价于只列该平台")
|
||||
print(" python main.py <平台> list # 同上")
|
||||
print()
|
||||
print("支持的平台(可用下列中文称呼,亦支持英文键如 sohu):")
|
||||
print(" " + _platform_list_cn_for_help())
|
||||
_USAGE = """account-manager v2 — Credential & Browser Profile Manager
|
||||
|
||||
平台管理:
|
||||
platform list [--domain logistics|llm|content]
|
||||
platform get <platform>
|
||||
|
||||
账号管理:
|
||||
account add-web --platform <p> --login-id <id> --login-id-type <type>
|
||||
[--label <label>] [--tenant-id <t>] [--environment <env>]
|
||||
[--role <role>] [--provider-code <code>] [--url <url>]
|
||||
[--extra-json <json>] [--profile-dir <path>]
|
||||
account add-secret --platform <p> --credential-type <type>
|
||||
--secret-storage env|windows_credential|none
|
||||
[--secret-ref <env_var>] [--secret-stdin]
|
||||
[--label <label>] [--account-id <id>]
|
||||
[--environment <env>] [--role <role>]
|
||||
account get <id> [--with-credentials]
|
||||
account list [--platform <p>] [--environment <env>]
|
||||
[--tenant-id <t>] [--role <role>] [--limit N]
|
||||
account pick-web --platform <p> [--environment <env>] [--tenant-id <t>]
|
||||
[--role <role>] [--lease] [--ttl-sec 900]
|
||||
[--holder <holder>] [--purpose <purpose>]
|
||||
account mark-session <id> --session-status <status>
|
||||
account mark-error <id> --code <code> --message <msg>
|
||||
account mark-used <id>
|
||||
account open <id>
|
||||
|
||||
凭据管理:
|
||||
credential pick --platform <p> [--type <type>] [--environment <env>]
|
||||
[--role <role>] [--reveal]
|
||||
credential resolve --id <credential_id> [--reveal]
|
||||
|
||||
租约管理:
|
||||
lease release <lease_token>
|
||||
lease list [--active]
|
||||
lease cleanup
|
||||
|
||||
浏览器:
|
||||
account open <id>
|
||||
open <id> # 兼容别名
|
||||
|
||||
兼容入口(旧版):
|
||||
get <id>
|
||||
list-json <platform|all> [--limit N]
|
||||
pick-web <platform>
|
||||
list [platform|all]
|
||||
add <platform> <phone>
|
||||
delete id|platform <...>
|
||||
"""
|
||||
|
||||
|
||||
def _cli_fail_add() -> None:
|
||||
print("ERROR:CLI_ADD_MISSING_ARGS")
|
||||
print()
|
||||
print("【add 添加账号】参数不足。")
|
||||
print(" 必填:平台名称(中文即可,如 搜狐号、知乎、微信公众号)")
|
||||
print(" 必填:中国大陆 11 位手机号(1 开头,第二位 3~9),同平台下不可重复")
|
||||
print()
|
||||
print("示例:")
|
||||
print(" python main.py add 搜狐号 13800138000")
|
||||
print(" python main.py add 知乎 13900001111")
|
||||
print()
|
||||
print("支持的平台:" + _platform_list_cn_for_help())
|
||||
# =========================================================================
|
||||
# Argument parsing helpers
|
||||
# =========================================================================
|
||||
|
||||
def _get_opt(argv: list[str], flag: str, default=None):
|
||||
"""Get value of --flag from argv, returning default if not found."""
|
||||
if flag in argv:
|
||||
idx = argv.index(flag)
|
||||
if idx + 1 < len(argv):
|
||||
return argv[idx + 1]
|
||||
return default
|
||||
|
||||
|
||||
def _cli_fail_need_account_id(subcmd: str) -> None:
|
||||
print(f"ERROR:CLI_{subcmd.upper()}_MISSING_ID")
|
||||
print()
|
||||
print(f"【{subcmd}】缺少必填参数:账号 id(数据库自增整数,可先 list 查看)。")
|
||||
print("示例:")
|
||||
print(f" python main.py {subcmd} 1")
|
||||
def _has_flag(argv: list[str], flag: str) -> bool:
|
||||
return flag in argv
|
||||
|
||||
|
||||
def _cli_fail_delete() -> None:
|
||||
print("ERROR:CLI_DELETE_MISSING_ARGS")
|
||||
print()
|
||||
print("【delete 删除账号】参数不足。支持三种方式:")
|
||||
print(" 按 id:python main.py delete id <id>")
|
||||
print(" 按平台(删该平台下全部):python main.py delete platform <平台中文名或英文键>")
|
||||
print(" 按平台+手机号:python main.py delete platform <平台> <手机号>")
|
||||
print()
|
||||
print("示例:")
|
||||
print(" python main.py delete id 3")
|
||||
print(" python main.py delete platform 搜狐号")
|
||||
print(" python main.py delete platform 头条号 18925203701")
|
||||
print()
|
||||
print("说明:会同时删除数据库记录与 profile 用户数据目录(若存在)。")
|
||||
def _get_int_opt(argv: list[str], flag: str, default: int) -> int:
|
||||
v = _get_opt(argv, flag)
|
||||
if v is not None:
|
||||
try:
|
||||
return int(v)
|
||||
except ValueError:
|
||||
return default
|
||||
return default
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Main dispatch
|
||||
# =========================================================================
|
||||
|
||||
def main() -> None:
|
||||
setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME)
|
||||
log = get_skill_logger()
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
_cli_print_full_usage()
|
||||
print(_USAGE)
|
||||
sys.exit(1)
|
||||
|
||||
setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME)
|
||||
get_skill_logger().info("cli_start argv=%s", sys.argv)
|
||||
cmd = sys.argv[1]
|
||||
log.info("cli_start argv=%s", sys.argv)
|
||||
av = sys.argv
|
||||
cmd = av[1]
|
||||
_maybe_print_paths_debug_cli()
|
||||
|
||||
# Determine if first arg is a platform key (for legacy shorthand)
|
||||
plat_from_first = resolve_platform_key(cmd)
|
||||
|
||||
if len(sys.argv) >= 3 and sys.argv[2] == "list" and plat_from_first:
|
||||
cmd_list(plat_from_first)
|
||||
elif plat_from_first and len(sys.argv) == 2:
|
||||
cmd_list(plat_from_first)
|
||||
elif cmd == "add":
|
||||
if len(sys.argv) < 4:
|
||||
_cli_fail_add()
|
||||
# ---- platform subcommand ----
|
||||
if cmd == "platform":
|
||||
if len(av) < 3:
|
||||
print("ERROR:CLI_BAD_ARGS platform 需要子命令:list | get")
|
||||
sys.exit(1)
|
||||
cmd_add(sys.argv[2], sys.argv[3])
|
||||
elif cmd == "list":
|
||||
cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all")
|
||||
sub = av[2]
|
||||
if sub == "list":
|
||||
domain = _get_opt(av, "--domain")
|
||||
cmd_platform_list(domain)
|
||||
elif sub == "get":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS platform get 需要 <platform>。")
|
||||
sys.exit(1)
|
||||
cmd_platform_get(av[3])
|
||||
else:
|
||||
print("ERROR:CLI_BAD_ARGS platform 子命令必须是 list 或 get。")
|
||||
sys.exit(1)
|
||||
|
||||
# ---- account subcommand ----
|
||||
elif cmd == "account":
|
||||
if len(av) < 3:
|
||||
print("ERROR:CLI_BAD_ARGS account 需要子命令(add-web / get / …)。")
|
||||
print(_USAGE)
|
||||
sys.exit(1)
|
||||
sub = av[2]
|
||||
if sub == "add-web":
|
||||
_cmd_add_web(av)
|
||||
elif sub == "add-secret":
|
||||
_cmd_add_secret(av)
|
||||
elif sub == "get":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS account get 需要 <id>。")
|
||||
sys.exit(1)
|
||||
aid = av[3]
|
||||
with_creds = _has_flag(av, "--with-credentials")
|
||||
cmd_account_get(aid, include_credentials=with_creds)
|
||||
elif sub == "list":
|
||||
_cmd_account_list(av)
|
||||
elif sub == "pick-web":
|
||||
_cmd_pick_web_new(av)
|
||||
elif sub == "mark-session":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS account mark-session 需要 <id>。")
|
||||
sys.exit(1)
|
||||
aid = av[3]
|
||||
ss = _get_opt(av, "--session-status", "unknown")
|
||||
cmd_account_mark_session(aid, ss)
|
||||
elif sub == "mark-error":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS account mark-error 需要 <id>。")
|
||||
sys.exit(1)
|
||||
aid = av[3]
|
||||
ec = _get_opt(av, "--code", "")
|
||||
em = _get_opt(av, "--message", "")
|
||||
cmd_account_mark_error(aid, ec, em)
|
||||
elif sub == "mark-used":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS account mark-used 需要 <id>。")
|
||||
sys.exit(1)
|
||||
aid = av[3]
|
||||
cmd_account_mark_used(aid)
|
||||
elif sub == "open":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS account open 需要 <id>。")
|
||||
sys.exit(1)
|
||||
cmd_open(av[3])
|
||||
else:
|
||||
print("ERROR:CLI_BAD_ARGS 未知的 account 子命令。")
|
||||
print(_USAGE)
|
||||
sys.exit(1)
|
||||
|
||||
# ---- credential subcommand ----
|
||||
elif cmd == "credential":
|
||||
if len(av) < 3:
|
||||
print("ERROR:CLI_BAD_ARGS credential 需要子命令:pick | resolve")
|
||||
sys.exit(1)
|
||||
sub = av[2]
|
||||
if sub == "pick":
|
||||
_cmd_cred_pick(av)
|
||||
elif sub == "resolve":
|
||||
_cmd_cred_resolve(av)
|
||||
else:
|
||||
print("ERROR:CLI_BAD_ARGS credential 子命令必须是 pick 或 resolve。")
|
||||
sys.exit(1)
|
||||
|
||||
# ---- lease subcommand ----
|
||||
elif cmd == "lease":
|
||||
if len(av) < 3:
|
||||
print("ERROR:CLI_BAD_ARGS lease 需要子命令:release | list | cleanup")
|
||||
sys.exit(1)
|
||||
sub = av[2]
|
||||
if sub == "release":
|
||||
if len(av) < 4:
|
||||
print("ERROR:CLI_BAD_ARGS lease release 需要 <lease_token>。")
|
||||
sys.exit(1)
|
||||
cmd_lease_release(av[3])
|
||||
elif sub == "list":
|
||||
active = _has_flag(av, "--active")
|
||||
cmd_lease_list(active)
|
||||
elif sub == "cleanup":
|
||||
cmd_lease_cleanup()
|
||||
else:
|
||||
print("ERROR:CLI_BAD_ARGS lease 子命令必须是 release、list 或 cleanup。")
|
||||
sys.exit(1)
|
||||
|
||||
# ---- Legacy aliases ----
|
||||
elif cmd == "get":
|
||||
if len(sys.argv) < 3:
|
||||
_cli_fail_need_account_id("get")
|
||||
print("ERROR:CLI_GET_MISSING_ID")
|
||||
sys.exit(1)
|
||||
cmd_get(sys.argv[2])
|
||||
|
||||
elif cmd == "list-json":
|
||||
if len(sys.argv) < 3:
|
||||
print("ERROR:CLI_LIST_JSON_MISSING_PLATFORM")
|
||||
print("用法:python main.py list-json <平台|all|全部> [--limit N]")
|
||||
print("说明:跨技能编排;成功时 stdout 仅一行 JSON 数组(元素与 get 一致)。")
|
||||
sys.exit(1)
|
||||
av = sys.argv[2:]
|
||||
platform_arg = av[0]
|
||||
@@ -120,21 +272,45 @@ def main() -> None:
|
||||
except ValueError:
|
||||
pass
|
||||
cmd_list_json(platform_arg, limit=lim)
|
||||
|
||||
elif cmd == "pick-web":
|
||||
if len(sys.argv) < 3:
|
||||
print("ERROR:CLI_PICK_WEB_MISSING_ARGS")
|
||||
print("用法:python main.py pick-web <平台中文名或英文键>")
|
||||
print("说明:按 updated_at 选一条账号;是否已登录由调用方网页会话自行处理。")
|
||||
sys.exit(1)
|
||||
cmd_pick_web(sys.argv[2])
|
||||
|
||||
elif cmd == "open":
|
||||
if len(sys.argv) < 3:
|
||||
_cli_fail_need_account_id("open")
|
||||
print("ERROR:CLI_OPEN_MISSING_ID")
|
||||
sys.exit(1)
|
||||
cmd_open(sys.argv[2])
|
||||
|
||||
elif cmd == "list":
|
||||
platform_arg = sys.argv[2] if len(sys.argv) >= 3 else "all"
|
||||
cmd_list(platform_arg)
|
||||
|
||||
elif cmd == "add":
|
||||
if len(sys.argv) < 4:
|
||||
print("ERROR:CLI_ADD_MISSING_ARGS")
|
||||
sys.exit(1)
|
||||
# Legacy add maps to add-web for backward compat
|
||||
cmd_account_add_web(
|
||||
platform_input=sys.argv[2],
|
||||
login_id=sys.argv[3],
|
||||
login_id_type="phone",
|
||||
label=None,
|
||||
tenant_id=None,
|
||||
environment="production",
|
||||
role="default",
|
||||
provider_code=None,
|
||||
url=None,
|
||||
extra_json_str=None,
|
||||
profile_dir_override=None,
|
||||
)
|
||||
|
||||
elif cmd == "delete":
|
||||
if len(sys.argv) < 4:
|
||||
_cli_fail_delete()
|
||||
print("ERROR:CLI_DELETE_MISSING_ARGS")
|
||||
sys.exit(1)
|
||||
mode = (sys.argv[2] or "").strip().lower()
|
||||
if mode == "id":
|
||||
@@ -145,17 +321,151 @@ def main() -> None:
|
||||
else:
|
||||
cmd_delete_by_platform(sys.argv[3])
|
||||
else:
|
||||
print(f"ERROR:CLI_DELETE_BAD_MODE 不支持的删除方式「{sys.argv[2]}」,请使用 id 或 platform。")
|
||||
_cli_fail_delete()
|
||||
print(f"ERROR:CLI_DELETE_BAD_MODE 不支持的删除方式「{sys.argv[2]}」。")
|
||||
sys.exit(1)
|
||||
|
||||
# ---- Platform shorthand: "python main.py <platform>" ----
|
||||
elif plat_from_first:
|
||||
cmd_list(sys.argv[1])
|
||||
|
||||
# ---- Unknown ----
|
||||
else:
|
||||
print(f"ERROR:CLI_UNKNOWN_OR_BAD_ARGS 子命令「{cmd}」无法识别,或参数组合不合法。")
|
||||
print(f"ERROR:CLI_UNKNOWN_OR_BAD_ARGS 子命令「{cmd}」无法识别。")
|
||||
print()
|
||||
_cli_print_full_usage()
|
||||
print()
|
||||
print("说明:list 的筛选可为 all/全部,或上面列出的任一平台中文名。")
|
||||
print(_USAGE)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Sub-parsers for complex commands
|
||||
# =========================================================================
|
||||
|
||||
def _cmd_add_web(argv: list[str]) -> None:
|
||||
plat = _get_opt(argv, "--platform", "")
|
||||
login_id = _get_opt(argv, "--login-id")
|
||||
login_id_type = _get_opt(argv, "--login-id-type", "unknown")
|
||||
label = _get_opt(argv, "--label")
|
||||
tenant_id = _get_opt(argv, "--tenant-id")
|
||||
environment = _get_opt(argv, "--environment", "production")
|
||||
role = _get_opt(argv, "--role", "default")
|
||||
provider_code = _get_opt(argv, "--provider-code")
|
||||
url = _get_opt(argv, "--url")
|
||||
extra_json_str = _get_opt(argv, "--extra-json")
|
||||
profile_dir_override = _get_opt(argv, "--profile-dir")
|
||||
|
||||
if not plat:
|
||||
print("ERROR:CLI_BAD_ARGS account add-web 需要 --platform <platform>。")
|
||||
return
|
||||
cmd_account_add_web(
|
||||
platform_input=plat,
|
||||
login_id=login_id,
|
||||
login_id_type=login_id_type,
|
||||
label=label,
|
||||
tenant_id=tenant_id,
|
||||
environment=environment,
|
||||
role=role,
|
||||
provider_code=provider_code,
|
||||
url=url,
|
||||
extra_json_str=extra_json_str,
|
||||
profile_dir_override=profile_dir_override,
|
||||
)
|
||||
|
||||
|
||||
def _cmd_add_secret(argv: list[str]) -> None:
|
||||
plat = _get_opt(argv, "--platform", "")
|
||||
ctype = _get_opt(argv, "--credential-type", "api_key")
|
||||
label = _get_opt(argv, "--label")
|
||||
storage = _get_opt(argv, "--secret-storage", "env")
|
||||
stdin_mode = _has_flag(argv, "--secret-stdin")
|
||||
sref = _get_opt(argv, "--secret-ref")
|
||||
aid = _get_int_opt(argv, "--account-id", 0) or None
|
||||
env = _get_opt(argv, "--environment", "production")
|
||||
role = _get_opt(argv, "--role", "api")
|
||||
extra_json_str = _get_opt(argv, "--extra-json")
|
||||
|
||||
if not plat:
|
||||
print("ERROR:CLI_BAD_ARGS account add-secret 需要 --platform <platform>。")
|
||||
return
|
||||
cmd_account_add_secret(
|
||||
platform_input=plat,
|
||||
credential_type=ctype,
|
||||
label=label,
|
||||
secret_storage=storage,
|
||||
secret_stdin=stdin_mode,
|
||||
secret_ref=sref,
|
||||
account_id=aid,
|
||||
environment=env,
|
||||
role=role,
|
||||
extra_json_str=extra_json_str,
|
||||
)
|
||||
|
||||
|
||||
def _cmd_account_list(argv: list[str]) -> None:
|
||||
plat = _get_opt(argv, "--platform")
|
||||
env = _get_opt(argv, "--environment")
|
||||
tenant = _get_opt(argv, "--tenant-id")
|
||||
role = _get_opt(argv, "--role")
|
||||
limit = _get_int_opt(argv, "--limit", 200)
|
||||
cmd_account_list(
|
||||
platform_input=plat,
|
||||
environment=env,
|
||||
tenant_id=tenant,
|
||||
role=role,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
def _cmd_pick_web_new(argv: list[str]) -> None:
|
||||
plat = _get_opt(argv, "--platform", "")
|
||||
env = _get_opt(argv, "--environment")
|
||||
tenant = _get_opt(argv, "--tenant-id")
|
||||
role = _get_opt(argv, "--role")
|
||||
do_lease = _has_flag(argv, "--lease")
|
||||
ttl = _get_int_opt(argv, "--ttl-sec", 900)
|
||||
holder = _get_opt(argv, "--holder")
|
||||
purpose = _get_opt(argv, "--purpose")
|
||||
|
||||
if not plat:
|
||||
print("ERROR:CLI_BAD_ARGS account pick-web 需要 --platform <platform>。")
|
||||
return
|
||||
cmd_account_pick_web(
|
||||
platform_input=plat,
|
||||
environment=env,
|
||||
tenant_id=tenant,
|
||||
role=role,
|
||||
do_lease=do_lease,
|
||||
ttl_sec=ttl,
|
||||
holder=holder,
|
||||
purpose=purpose,
|
||||
)
|
||||
|
||||
|
||||
def _cmd_cred_pick(argv: list[str]) -> None:
|
||||
plat = _get_opt(argv, "--platform", "")
|
||||
ctype = _get_opt(argv, "--type")
|
||||
env = _get_opt(argv, "--environment")
|
||||
role = _get_opt(argv, "--role")
|
||||
reveal = _has_flag(argv, "--reveal")
|
||||
if not plat:
|
||||
print("ERROR:CLI_BAD_ARGS credential pick 需要 --platform <platform>。")
|
||||
return
|
||||
cmd_credential_pick(
|
||||
platform_input=plat,
|
||||
credential_type=ctype,
|
||||
environment=env,
|
||||
role=role,
|
||||
reveal=reveal,
|
||||
)
|
||||
|
||||
|
||||
def _cmd_cred_resolve(argv: list[str]) -> None:
|
||||
cid = _get_int_opt(argv, "--id", 0)
|
||||
reveal = _has_flag(argv, "--reveal")
|
||||
if cid <= 0:
|
||||
print("ERROR:CLI_BAD_ARGS credential resolve 需要 --id <credential_id>。")
|
||||
return
|
||||
cmd_credential_resolve(cid, reveal=reveal)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user