chore: track Linux deploy scripts with forced git pull
Ship start/restart/pull-and-restart and deploy lib so production can one-line update; git_pull_hard uses checkout -f, restart --pull aligns then restarts.
This commit is contained in:
454
scripts/lib-aijz-deploy.sh
Normal file
454
scripts/lib-aijz-deploy.sh
Normal file
@@ -0,0 +1,454 @@
|
||||
# shellcheck shell=bash
|
||||
# AI 建站 Linux 部署公共函数。由 start/restart/stop/pull-and-restart 在 ROOT 下 source。
|
||||
|
||||
run_sudo() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_runtime_dirs() {
|
||||
mkdir -p "$ROOT/.runtime/pgdata" "$ROOT/.runtime/uploads" "$ROOT/.runtime/logs" \
|
||||
"$ROOT/.runtime/logs/generate" "$ROOT/web/dist"
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [ ! -f "$ROOT/.env" ]; then
|
||||
if [ -f "$ROOT/.env.example" ]; then
|
||||
cp "$ROOT/.env.example" "$ROOT/.env"
|
||||
echo "已从 .env.example 创建 .env,请按需填写 API Key。"
|
||||
else
|
||||
cat >"$ROOT/.env" <<'EOF'
|
||||
LLM_PROVIDER=deepseek
|
||||
DEEPSEEK_API_KEY=
|
||||
MINIMAX_API_KEY=
|
||||
EOF
|
||||
echo "已创建默认 .env"
|
||||
fi
|
||||
fi
|
||||
sed -i 's/\r$//' "$ROOT/.env" 2>/dev/null || true
|
||||
# 供 compose 变量替换与本脚本健康检查使用
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
[ -f "$ROOT/.env" ] && . "$ROOT/.env"
|
||||
set +a
|
||||
export AIJZ_WEB_PUBLISH="${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}"
|
||||
export AIJZ_GATEWAY_PUBLISH="${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}"
|
||||
export AIJZ_PLATFORM_PUBLISH="${AIJZ_PLATFORM_PUBLISH:-127.0.0.1:8888}"
|
||||
export AIJZ_AI_PUBLISH="${AIJZ_AI_PUBLISH:-127.0.0.1:8001}"
|
||||
export AIJZ_PG_PUBLISH="${AIJZ_PG_PUBLISH:-127.0.0.1:5432}"
|
||||
export AIJZ_ENABLE_HOST_NGINX="${AIJZ_ENABLE_HOST_NGINX:-0}"
|
||||
export AIJZ_DOMAIN="${AIJZ_DOMAIN:-}"
|
||||
export AIJZ_PUBLIC_BASE_URL="${AIJZ_PUBLIC_BASE_URL:-}"
|
||||
}
|
||||
|
||||
# 若 .env 设了 AIJZ_PUBLIC_BASE_URL,同步到 platform.docker.yaml(挂载文件,restart platform 即生效)
|
||||
apply_public_base_url() {
|
||||
local base="${AIJZ_PUBLIC_BASE_URL:-}"
|
||||
local pf="$ROOT/platform/etc/platform.docker.yaml"
|
||||
[ -n "$base" ] || return 0
|
||||
[ -f "$pf" ] || return 0
|
||||
base="${base%/}"
|
||||
local storage_base="${base}/api/v1/storage"
|
||||
if grep -q '^PublicBaseURL:' "$pf"; then
|
||||
sed -i "s|^PublicBaseURL:.*|PublicBaseURL: \"${base}\"|" "$pf"
|
||||
fi
|
||||
if grep -q '^ PublicBase:' "$pf"; then
|
||||
sed -i "s|^ PublicBase:.*| PublicBase: \"${storage_base}\"|" "$pf"
|
||||
fi
|
||||
echo "已同步 PublicBaseURL -> ${base}(platform/etc/platform.docker.yaml)"
|
||||
}
|
||||
|
||||
sync_ssl_certs() {
|
||||
local domain="${AIJZ_DOMAIN:-}"
|
||||
[ -n "$domain" ] || return 0
|
||||
local ssl_dir="/etc/ssl/aijianzhan/$domain"
|
||||
run_sudo mkdir -p "$ssl_dir"
|
||||
if [ -f "$ROOT/nginx/$domain.pem" ] && [ -f "$ROOT/nginx/$domain.key" ]; then
|
||||
run_sudo cp -f "$ROOT/nginx/$domain.pem" "$ssl_dir/fullchain.pem"
|
||||
run_sudo cp -f "$ROOT/nginx/$domain.key" "$ssl_dir/privkey.pem"
|
||||
elif [ -f "$ROOT/nginx/fullchain.pem" ] && [ -f "$ROOT/nginx/privkey.pem" ]; then
|
||||
run_sudo cp -f "$ROOT/nginx/fullchain.pem" "$ROOT/nginx/privkey.pem" "$ssl_dir/"
|
||||
elif [ -f "$ROOT/nginx/$domain/fullchain.pem" ] && [ -f "$ROOT/nginx/$domain/privkey.pem" ]; then
|
||||
run_sudo cp -f "$ROOT/nginx/$domain/fullchain.pem" "$ROOT/nginx/$domain/privkey.pem" "$ssl_dir/"
|
||||
else
|
||||
echo "提示: 未找到 nginx/ 下证书,宿主机 HTTPS 需手动放入 $ssl_dir" >&2
|
||||
return 0
|
||||
fi
|
||||
run_sudo chmod 644 "$ssl_dir/fullchain.pem" 2>/dev/null || true
|
||||
run_sudo chmod 600 "$ssl_dir/privkey.pem" 2>/dev/null || true
|
||||
echo "证书已同步 -> $ssl_dir"
|
||||
}
|
||||
|
||||
host_nginx_online() {
|
||||
command -v systemctl >/dev/null 2>&1 || return 1
|
||||
systemctl is-active nginx >/dev/null 2>&1 && return 0
|
||||
systemctl is-active nginx.service >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_host_nginx_started() {
|
||||
command -v nginx >/dev/null 2>&1 || {
|
||||
echo "错误: 未安装 nginx,无法启用 AIJZ_ENABLE_HOST_NGINX" >&2
|
||||
exit 1
|
||||
}
|
||||
if host_nginx_online; then
|
||||
return 0
|
||||
fi
|
||||
echo "宿主机 Nginx 未在线,尝试启动..."
|
||||
run_sudo systemctl start nginx 2>/dev/null || run_sudo systemctl start nginx.service
|
||||
run_sudo systemctl enable nginx 2>/dev/null || true
|
||||
host_nginx_online || {
|
||||
echo "错误: 无法启动宿主机 nginx" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
install_host_nginx_site_conf() {
|
||||
local domain="${AIJZ_DOMAIN:-}"
|
||||
[ -n "$domain" ] || return 0
|
||||
[ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ] || return 0
|
||||
local tpl="$ROOT/nginx/aijz.host.conf"
|
||||
local out="/etc/nginx/conf.d/aijz_${domain}.conf"
|
||||
local web_port
|
||||
web_port="$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")"
|
||||
[ -f "$tpl" ] || {
|
||||
echo "未找到 $tpl,跳过宿主机 Nginx 配置" >&2
|
||||
return 0
|
||||
}
|
||||
mkdir -p "$ROOT/verify-root"
|
||||
sync_ssl_certs
|
||||
sed -e "s|__DOMAIN__|${domain}|g" \
|
||||
-e "s|__WEB_PORT__|${web_port}|g" \
|
||||
-e "s|__VERIFY_ROOT__|${ROOT}/verify-root|g" \
|
||||
"$tpl" | run_sudo tee "$out" >/dev/null
|
||||
if ! run_sudo nginx -t 2>/dev/null; then
|
||||
echo "错误: nginx -t 失败,请检查 $out" >&2
|
||||
exit 1
|
||||
fi
|
||||
if host_nginx_online; then
|
||||
run_sudo systemctl reload nginx 2>/dev/null && echo "宿主机 Nginx 已重载($out)"
|
||||
else
|
||||
ensure_host_nginx_started
|
||||
fi
|
||||
}
|
||||
|
||||
reload_web_nginx() {
|
||||
local cid
|
||||
cid="$(compose_cmd ps -q web 2>/dev/null | head -1)"
|
||||
if [ -z "$cid" ]; then
|
||||
echo "web 容器未运行,跳过 reload" >&2
|
||||
return 1
|
||||
fi
|
||||
run_sudo docker exec "$cid" nginx -t >/dev/null 2>&1 || {
|
||||
echo "错误: 容器内 nginx -t 失败" >&2
|
||||
return 1
|
||||
}
|
||||
run_sudo docker exec "$cid" nginx -s reload
|
||||
echo "web 容器 nginx 已 reload(web/nginx.conf)"
|
||||
}
|
||||
|
||||
restart_service() {
|
||||
local svc="$1"
|
||||
compose_cmd restart "$svc"
|
||||
echo "已 restart $svc"
|
||||
}
|
||||
|
||||
reload_host_nginx() {
|
||||
[ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ] || {
|
||||
echo "AIJZ_ENABLE_HOST_NGINX 未启用,跳过宿主机 Nginx" >&2
|
||||
return 0
|
||||
}
|
||||
install_host_nginx_site_conf
|
||||
}
|
||||
|
||||
reload_all_config() {
|
||||
ensure_env_file
|
||||
apply_public_base_url
|
||||
reload_host_nginx || true
|
||||
reload_web_nginx || true
|
||||
restart_service platform
|
||||
restart_service gateway
|
||||
compose_cmd up -d --force-recreate ai >/dev/null 2>&1 || compose_cmd restart ai
|
||||
echo "配置重载完成(未 rebuild 镜像、未重建 web/dist)"
|
||||
}
|
||||
|
||||
ensure_docker() {
|
||||
if command -v docker >/dev/null 2>&1 && run_sudo docker info >/dev/null 2>&1; then
|
||||
echo "Docker 已就绪."
|
||||
return 0
|
||||
fi
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker 守护进程未连接,尝试启动..."
|
||||
run_sudo systemctl start docker 2>/dev/null || run_sudo systemctl start podman 2>/dev/null || true
|
||||
if run_sudo docker info >/dev/null 2>&1; then
|
||||
echo "Docker 已就绪."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo "错误: Docker 不可用。请先安装并启动 docker 后再执行。" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
resolve_compose_cmd() {
|
||||
if run_sudo docker compose version >/dev/null 2>&1; then
|
||||
echo "docker compose"
|
||||
return
|
||||
fi
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
echo "docker-compose"
|
||||
return
|
||||
fi
|
||||
if [ -x /usr/local/bin/docker-compose ]; then
|
||||
echo "/usr/local/bin/docker-compose"
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
COMPOSE_CMD=""
|
||||
compose_cmd() {
|
||||
# stack_down 可能早于 stack_up,先补默认值,避免 set -u 静默退出
|
||||
export_defaults
|
||||
: "${AIJZ_WEB_PUBLISH:=127.0.0.1:5173}"
|
||||
: "${AIJZ_GATEWAY_PUBLISH:=127.0.0.1:8180}"
|
||||
: "${AIJZ_PLATFORM_PUBLISH:=127.0.0.1:8888}"
|
||||
: "${AIJZ_AI_PUBLISH:=127.0.0.1:8001}"
|
||||
: "${AIJZ_PG_PUBLISH:=127.0.0.1:5432}"
|
||||
if [ -z "$COMPOSE_CMD" ]; then
|
||||
COMPOSE_CMD="$(resolve_compose_cmd)"
|
||||
fi
|
||||
if [ -z "$COMPOSE_CMD" ]; then
|
||||
echo "错误: 找不到 docker compose / docker-compose" >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
run_sudo env \
|
||||
DOCKER_BASE_REGISTRY="${DOCKER_BASE_REGISTRY}" \
|
||||
GOPROXY="${GOPROXY}" \
|
||||
AIJZ_WEB_PUBLISH="${AIJZ_WEB_PUBLISH}" \
|
||||
AIJZ_GATEWAY_PUBLISH="${AIJZ_GATEWAY_PUBLISH}" \
|
||||
AIJZ_PLATFORM_PUBLISH="${AIJZ_PLATFORM_PUBLISH}" \
|
||||
AIJZ_AI_PUBLISH="${AIJZ_AI_PUBLISH}" \
|
||||
AIJZ_PG_PUBLISH="${AIJZ_PG_PUBLISH}" \
|
||||
$COMPOSE_CMD "$@"
|
||||
}
|
||||
|
||||
export_defaults() {
|
||||
export DOCKER_BASE_REGISTRY="${DOCKER_BASE_REGISTRY:-docker.m.daocloud.io/library}"
|
||||
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
|
||||
export REGISTRY_MIRROR="${REGISTRY_MIRROR:-docker.m.daocloud.io/library/}"
|
||||
}
|
||||
|
||||
# 从 "127.0.0.1:5173" 或 "5173" 取出宿主机端口号
|
||||
publish_host_port() {
|
||||
local pub="$1"
|
||||
echo "$pub" | awk -F: '{print $NF}'
|
||||
}
|
||||
|
||||
port_in_use() {
|
||||
local port="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
run_sudo ss -tlnH "sport = :$port" 2>/dev/null | grep -q . && return 0
|
||||
ss -tlnH "sport = :$port" 2>/dev/null | grep -q . && return 0
|
||||
return 1
|
||||
fi
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# 检查 AI 建站将占用的宿主机端口;若已被本项目容器占用则放行
|
||||
check_aijz_ports() {
|
||||
local pairs=(
|
||||
"web:$AIJZ_WEB_PUBLISH"
|
||||
"gateway:$AIJZ_GATEWAY_PUBLISH"
|
||||
"platform:$AIJZ_PLATFORM_PUBLISH"
|
||||
"ai:$AIJZ_AI_PUBLISH"
|
||||
"postgres:$AIJZ_PG_PUBLISH"
|
||||
)
|
||||
echo "端口规划核对(相对 yh_web):AI建站 5173/8180/8888/8001/5432;宇恒 8088/9080/9081 + 宿主机 80/443。"
|
||||
local name pub port conflict=0
|
||||
local self_running=0
|
||||
if compose_cmd ps -q 2>/dev/null | grep -q .; then
|
||||
self_running=1
|
||||
fi
|
||||
for item in "${pairs[@]}"; do
|
||||
name="${item%%:*}"
|
||||
pub="${item#*:}"
|
||||
port="$(publish_host_port "$pub")"
|
||||
[ -z "$port" ] && continue
|
||||
if port_in_use "$port"; then
|
||||
if [ "$self_running" -eq 1 ]; then
|
||||
echo " 提示: 端口 $port($name)已占用,疑为本栈,继续。"
|
||||
else
|
||||
echo " 错误: 宿主机端口 $port 已被占用(服务 $name ← $pub)。" >&2
|
||||
echo " 请改 .env 中对应 AIJZ_*_PUBLISH,或停掉占用进程。" >&2
|
||||
conflict=1
|
||||
fi
|
||||
else
|
||||
echo " OK $name → $pub"
|
||||
fi
|
||||
done
|
||||
[ "$conflict" -eq 0 ] || exit 1
|
||||
}
|
||||
|
||||
# 构建 web/dist:优先本机 npm;否则用 Docker node 镜像
|
||||
build_web_dist() {
|
||||
echo "构建 web dist ..."
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
(
|
||||
cd "$ROOT/web"
|
||||
if [ -f package-lock.json ]; then
|
||||
npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps
|
||||
else
|
||||
npm install --legacy-peer-deps
|
||||
fi
|
||||
npm run build
|
||||
)
|
||||
else
|
||||
echo "本机无 npm,使用 Docker node 构建..."
|
||||
run_sudo docker run --rm \
|
||||
-v "$ROOT/web:/app" \
|
||||
-v "$ROOT/web/dist:/app/dist" \
|
||||
-w /app \
|
||||
"${REGISTRY_MIRROR}node:20-alpine" \
|
||||
sh -c "(npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps) && npm run build"
|
||||
fi
|
||||
if [ ! -f "$ROOT/web/dist/index.html" ]; then
|
||||
echo "错误: web/dist/index.html 不存在,构建失败。" >&2
|
||||
exit 1
|
||||
fi
|
||||
# nginx 容器用户需可读;避免 Podman 下 Permission denied → 403
|
||||
chmod -R a+rX "$ROOT/web/dist" 2>/dev/null || true
|
||||
echo "web/dist 就绪."
|
||||
}
|
||||
|
||||
stack_up() {
|
||||
local rebuild="${1:-0}"
|
||||
export_defaults
|
||||
ensure_runtime_dirs
|
||||
ensure_env_file
|
||||
apply_public_base_url
|
||||
check_aijz_ports
|
||||
build_web_dist
|
||||
if [ "$rebuild" = "1" ]; then
|
||||
compose_cmd up -d --build --force-recreate
|
||||
else
|
||||
if ! compose_cmd up -d; then
|
||||
echo "compose up 失败,尝试 --build ..."
|
||||
compose_cmd up -d --build
|
||||
fi
|
||||
fi
|
||||
install_host_nginx_site_conf || true
|
||||
}
|
||||
|
||||
stack_down() {
|
||||
# 不吞掉 unbound 等错误;compose down 失败仍继续
|
||||
if ! compose_cmd down --remove-orphans; then
|
||||
echo "提示: compose down 未完全成功,继续启动..." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
healthcheck() {
|
||||
local ok=0 i
|
||||
local gw_port web_port
|
||||
gw_port="$(publish_host_port "${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}")"
|
||||
web_port="$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")"
|
||||
echo "等待 gateway(:$gw_port) / web(:$web_port) ..."
|
||||
for i in $(seq 1 60); do
|
||||
if curl -fsS --max-time 2 "http://127.0.0.1:${gw_port}/gateway/health" >/dev/null 2>&1 \
|
||||
&& curl -fsS --max-time 2 "http://127.0.0.1:${web_port}/" >/dev/null 2>&1; then
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$ok" -eq 1 ]; then
|
||||
echo "健康检查通过."
|
||||
else
|
||||
echo "警告: 健康检查超时,请查看: docker compose -f $ROOT/docker-compose.yml logs" >&2
|
||||
fi
|
||||
compose_cmd exec -T postgres psql -U platform -d platform -c 'ALTER USER platform CREATEDB;' >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
print_endpoints() {
|
||||
local pub_line=""
|
||||
if [ -n "${AIJZ_DOMAIN:-}" ] && [ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ]; then
|
||||
pub_line=" Public https://${AIJZ_DOMAIN}"
|
||||
fi
|
||||
cat <<EOF
|
||||
|
||||
========================================
|
||||
Web http://127.0.0.1:$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")
|
||||
Gateway http://127.0.0.1:$(publish_host_port "${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}")
|
||||
${pub_line}
|
||||
Account demo / demo123
|
||||
Data $ROOT/.runtime
|
||||
Stop ./stop.sh
|
||||
重载配置 ./reload-config.sh(不整栈 rebuild)
|
||||
绑定 默认 127.0.0.1(与 yh_web 同机不抢 80/443/8088)
|
||||
========================================
|
||||
EOF
|
||||
}
|
||||
|
||||
# 默认代码源(可用 AIJZ_GIT_URL / GIT_BRANCH 覆盖)
|
||||
AIJZ_GIT_URL_DEFAULT="https://gitea.yuxindazhineng.com/whm/ai_site.git"
|
||||
|
||||
ensure_git_origin() {
|
||||
local url="${1:-${AIJZ_GIT_URL:-$AIJZ_GIT_URL_DEFAULT}}"
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "错误: 未安装 git,无法拉取代码。请先安装 git。" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "当前目录不是 git 仓库,初始化并关联 origin -> $url"
|
||||
git init
|
||||
git remote add origin "$url"
|
||||
return 0
|
||||
fi
|
||||
if git remote get-url origin >/dev/null 2>&1; then
|
||||
local cur
|
||||
cur="$(git remote get-url origin)"
|
||||
if [ "$cur" != "$url" ]; then
|
||||
echo "更新 origin: $cur -> $url"
|
||||
git remote set-url origin "$url"
|
||||
fi
|
||||
else
|
||||
echo "添加 origin -> $url"
|
||||
git remote add origin "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
# 从 Gitea ai_site 拉取并与远程对齐(默认硬重置,保留未跟踪文件如 .env)
|
||||
# 环境变量:
|
||||
# AIJZ_GIT_URL 仓库地址(默认 https://gitea.yuxindazhineng.com/whm/ai_site.git)
|
||||
# GIT_BRANCH 分支(默认 main)
|
||||
# FORCE_GIT_RESET 1=reset --hard 对齐远程(默认 1);0=仅快进合并
|
||||
git_pull_hard() {
|
||||
local url="${AIJZ_GIT_URL:-$AIJZ_GIT_URL_DEFAULT}"
|
||||
local branch="${GIT_BRANCH:-main}"
|
||||
echo "拉取代码($url branch=$branch)..."
|
||||
ensure_git_origin "$url" || return 1
|
||||
if ! git fetch origin --progress; then
|
||||
echo "错误: git fetch 失败。私有仓库请配置凭据,或设置 AIJZ_GIT_URL 为带 token 的地址。" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! git rev-parse --verify "origin/$branch" >/dev/null 2>&1; then
|
||||
echo "错误: 远程不存在分支 origin/$branch" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ "${FORCE_GIT_RESET:-1}" = "1" ]; then
|
||||
# -f:丢弃已跟踪文件的本地改动,避免 checkout 被挡住(.env 等未跟踪文件保留)
|
||||
git checkout -f -B "$branch" "origin/$branch"
|
||||
git reset --hard "origin/$branch"
|
||||
else
|
||||
git checkout "$branch" 2>/dev/null || git checkout -b "$branch" "origin/$branch"
|
||||
git merge --ff-only "origin/$branch"
|
||||
fi
|
||||
echo "当前: $(git rev-parse --short HEAD) $(git log -1 --pretty=%s)"
|
||||
}
|
||||
Reference in New Issue
Block a user