Merge reworked #13690: timezone-derived region and channel mirror prepends
Drop the .cn servers.sed twins, the USTC Arch fallback and the closed #14399 timezone preselection. region.sh follows the installer's marker.
This commit is contained in:
commit
4521dac023
25 files changed
+150
-206
No files matched your search
@@ -213,7 +213,23 @@ if grep -Eq $'^(link|refresh)\t' "$log_file"; then
|
||||
fail "dev refuses an empty region profile before linking or refreshing pacman" "$(cat "$log_file")"
|
||||
fi
|
||||
mkdir -p "$checkout/default/regions/cn/pacman"
|
||||
touch "$checkout/default/regions/cn/pacman/pacman.conf.append" "$checkout/default/regions/cn/pacman/mirrorlist.append"
|
||||
cn_fragments=(pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend)
|
||||
for missing in "${cn_fragments[@]}"; do
|
||||
for fragment in "${cn_fragments[@]}"; do
|
||||
if [[ $fragment == "$missing" ]]; then
|
||||
rm -f "$checkout/default/regions/cn/pacman/$fragment"
|
||||
else
|
||||
touch "$checkout/default/regions/cn/pacman/$fragment"
|
||||
fi
|
||||
done
|
||||
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
|
||||
fail "dev refuses a region profile without $missing"
|
||||
fi
|
||||
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
|
||||
fail "dev refuses a profile without $missing before linking or refreshing pacman" "$(cat "$log_file")"
|
||||
fi
|
||||
done
|
||||
touch "${cn_fragments[@]/#/$checkout/default/regions/cn/pacman/}"
|
||||
chmod -x "$checkout/bin/omarchy-apply-pacman"
|
||||
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
|
||||
fail "dev refuses a checkout whose pacman helper cannot run"
|
||||
@@ -221,7 +237,7 @@ fi
|
||||
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
|
||||
fail "dev refuses incomplete region support before linking or refreshing pacman" "$(cat "$log_file")"
|
||||
fi
|
||||
pass "dev refuses an empty region profile or a helper that cannot run"
|
||||
pass "dev refuses a region profile missing any fragment or a helper that cannot run"
|
||||
|
||||
chmod +x "$checkout/bin/omarchy-apply-pacman"
|
||||
OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev
|
||||
|
||||
@@ -40,9 +40,6 @@ PY
|
||||
copy_boundary_file bin/omarchy-security-functions
|
||||
copy_boundary_file bin/omarchy-update-pacman
|
||||
copy_boundary_file default/omarchy/sudo-no-update/sudo
|
||||
# The host's own region must not leak into the sandbox.
|
||||
printf '#!/bin/bash\necho Global\n' >"$SUDO_TEST_ROOT/bin/omarchy-region"
|
||||
chmod +x "$SUDO_TEST_ROOT/bin/omarchy-region"
|
||||
|
||||
cat >"$SUDO_TEST_ROOT/mock/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
|
||||
@@ -27,24 +27,23 @@ pass "refresh keeps existing config file modes"
|
||||
printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region"
|
||||
for channel in stable rc edge; do
|
||||
case "$channel" in
|
||||
stable) host=stable-mirror.omarchy.org ;;
|
||||
rc) host=rc-mirror.omarchy.org ;;
|
||||
edge) host=mirror.omarchy.org ;;
|
||||
stable) host=stable-mirror.omarchy ;;
|
||||
rc) host=rc-mirror.omarchy ;;
|
||||
edge) host=mirror.omarchy ;;
|
||||
esac
|
||||
apply "$channel"
|
||||
expected=$(printf 'Server = https://%s/$repo/os/$arch\nServer = https://%s/$repo/os/$arch\nServer = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch' "${host%.org}.cn" "$host")
|
||||
expected=$(printf 'Server = https://%s.cn/$repo/os/$arch\nServer = https://%s.org/$repo/os/$arch' "$host" "$host")
|
||||
[[ $(<"$TARGET_ROOT/etc/pacman.d/mirrorlist") == "$expected" ]] || fail "$channel mirror order and variables"
|
||||
{
|
||||
sed -f "$ROOT/default/regions/cn/pacman/servers.sed" "$ROOT/default/pacman/pacman-$channel.conf"
|
||||
cat "$ROOT/default/pacman/pacman-$channel.conf"
|
||||
printf '\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n'
|
||||
} > "$scratch/expected.conf"
|
||||
grep -qx "Server = https://pkgs.omarchy.cn/$channel/\$arch" "$scratch/expected.conf" || fail "$channel lists the .cn Omarchy repository"
|
||||
cmp "$scratch/expected.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
apply "$channel"
|
||||
cmp "$TARGET_ROOT/etc/pacman.conf.bak" "$TARGET_ROOT/etc/pacman.conf"
|
||||
cmp "$TARGET_ROOT/etc/pacman.d/mirrorlist.bak" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
|
||||
done
|
||||
pass "China puts a .cn twin ahead of each channel server, appends the CN defaults, and is idempotent"
|
||||
pass "China puts its channel mirror first, keeps the global one as fallback, and is idempotent"
|
||||
|
||||
cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf"
|
||||
cp "$TARGET_ROOT/etc/pacman.d/mirrorlist" "$scratch/before.mirrors"
|
||||
@@ -76,8 +75,8 @@ mkdir -p "$scratch/rendered"
|
||||
"$ROOT/bin/omarchy-apply-pacman" --render rc "$scratch/rendered" "$TARGET_ROOT"
|
||||
rg -qxF 'Server = https://pkgs.omarchy.org/rc/$arch' "$scratch/rendered/pacman.conf"
|
||||
rg -qxF '[archlinuxcn]' "$scratch/rendered/pacman.conf"
|
||||
[[ $(<"$scratch/rendered/mirrorlist") == $'Server = https://rc-mirror.omarchy.cn/$repo/os/$arch\nServer = https://rc-mirror.omarchy.org/$repo/os/$arch\nServer = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch' ]] ||
|
||||
fail "render keeps the channel mirror first"
|
||||
[[ $(<"$scratch/rendered/mirrorlist") == $'Server = https://rc-mirror.omarchy.cn/$repo/os/$arch\nServer = https://rc-mirror.omarchy.org/$repo/os/$arch' ]] ||
|
||||
fail "render puts the regional channel mirror first"
|
||||
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
pass "render builds the regional channel files without touching the target"
|
||||
|
||||
@@ -92,8 +91,8 @@ cmp "$ROOT/default/pacman/pacman-stable.conf" "$TARGET_ROOT/etc/pacman.conf"
|
||||
cmp "$ROOT/default/pacman/mirrorlist-stable" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
|
||||
pass "users can opt out of regional repository defaults"
|
||||
|
||||
# The shipped China profile is repository scaffolding only. Locale, timezone,
|
||||
# and input defaults are separate changes built on the same mechanism.
|
||||
# The shipped China profile is repository defaults only. Language, locale, and
|
||||
# input methods follow the installer's language choice, not the region.
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -104,3 +103,39 @@ assert packages == {"archlinuxcn-keyring"}, packages
|
||||
assert not (region / "skel").exists()
|
||||
PY
|
||||
pass "China profile adds only its repository keyring"
|
||||
|
||||
# The installer derives the region from the chosen timezone, so every profile
|
||||
# needs a mapping, and no timezone may select two regions.
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
zoneinfo = Path("/usr/share/zoneinfo")
|
||||
owners = {}
|
||||
for profile in sorted((Path(os.environ["ROOT"]) / "default/regions").iterdir()):
|
||||
lines = (profile / "timezones").read_text().splitlines()
|
||||
zones = [line.strip() for line in lines if line.strip() and not line.startswith("#")]
|
||||
assert zones, f"{profile.name} maps no timezones"
|
||||
for zone in zones:
|
||||
assert zone not in owners, f"{zone} maps to {owners[zone]} and {profile.name}"
|
||||
owners[zone] = profile.name
|
||||
if zoneinfo.is_dir():
|
||||
assert (zoneinfo / zone).is_file(), f"{profile.name} lists unknown timezone {zone}"
|
||||
|
||||
# The installer offers tzdata's backward-compatible names too, so China
|
||||
# covers every name that links to its two zones and nothing else.
|
||||
# Without tzdata.zi (minimal containers) only the canonical zones can be
|
||||
# checked, so completeness is enforced where the host has the link table.
|
||||
china = {zone for zone, region in owners.items() if region == "cn"}
|
||||
expected = {"Asia/Shanghai", "Asia/Urumqi"}
|
||||
tzdata = zoneinfo / "tzdata.zi"
|
||||
if tzdata.is_file():
|
||||
for line in tzdata.read_text().splitlines():
|
||||
fields = line.split()
|
||||
if len(fields) == 3 and fields[0] == "L" and fields[1] in {"Asia/Shanghai", "Asia/Urumqi"}:
|
||||
expected.add(fields[2])
|
||||
assert china == expected, (china, expected)
|
||||
else:
|
||||
assert expected <= china, (china, expected)
|
||||
PY
|
||||
pass "region profiles map each timezone to at most one region"
|
||||
@@ -7,7 +7,7 @@ source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||
copy_boundary_file bin/omarchy-refresh-pacman
|
||||
copy_boundary_file bin/omarchy-apply-pacman
|
||||
for file in default/pacman/pacman-rc.conf default/pacman/mirrorlist-rc \
|
||||
default/regions/cn/pacman/pacman.conf.append default/regions/cn/pacman/mirrorlist.append; do
|
||||
default/regions/cn/pacman/pacman.conf.append default/regions/cn/pacman/mirrorlist-rc.prepend; do
|
||||
copy_boundary_file "$file"
|
||||
done
|
||||
|
||||
@@ -63,7 +63,7 @@ PY
|
||||
printf '\n'
|
||||
cat "$ROOT/default/regions/cn/pacman/pacman.conf.append"
|
||||
} >"$boundary_tmp/expected-pacman.conf"
|
||||
cat "$ROOT/default/pacman/mirrorlist-rc" "$ROOT/default/regions/cn/pacman/mirrorlist.append" >"$boundary_tmp/expected-mirrorlist"
|
||||
cat "$ROOT/default/regions/cn/pacman/mirrorlist-rc.prepend" "$ROOT/default/pacman/mirrorlist-rc" >"$boundary_tmp/expected-mirrorlist"
|
||||
cmp -s "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" ||
|
||||
fail "root installs the rendered China pacman.conf" "$(diff "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" 2>&1)"
|
||||
cmp -s "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" ||
|
||||
|
||||
@@ -82,30 +82,3 @@ GUM_ANSWER=0 script -qec "omarchy-region global" /dev/null >/dev/null
|
||||
grep -qxF 'gum confirm --affirmative Log out now --negative Later Log out now to switch to English and the global mirrors?' "$calls" &&
|
||||
grep -qx 'omarchy-system-logout ' "$calls" || fail "switching to Global offers to log out in English and logs out on yes" "$(<"$calls")"
|
||||
pass "a switch in a terminal offers to log out in the new region's language"
|
||||
|
||||
rc_migration="$test_tmp/1788112314.sh"
|
||||
rewrite "$ROOT/migrations/1788112314.sh" "$rc_migration"
|
||||
cp "$ROOT/default/pacman/mirrorlist-rc" "$root/etc/pacman.d/mirrorlist"
|
||||
sed -e 's|pkgs.omarchy.org/rc/|pkgs.omarchy.org/edge/|' "$ROOT/default/pacman/pacman-rc.conf" |
|
||||
sed -f "$ROOT/default/regions/cn/pacman/servers.sed" >"$root/etc/pacman.conf"
|
||||
bash -e "$rc_migration" >/dev/null
|
||||
[[ $(grep '^Server' "$root/etc/pacman.conf") == $'Server = https://pkgs.omarchy.cn/rc/$arch\nServer = https://pkgs.omarchy.org/rc/$arch' ]] ||
|
||||
fail "the rc repository migration moves the .cn twin with its .org server" "$(grep '^Server' "$root/etc/pacman.conf")"
|
||||
pass "the rc repository migration moves the .cn twin with its .org server"
|
||||
|
||||
rename_migration="$test_tmp/1791387144.sh"
|
||||
rewrite "$ROOT/migrations/1791387144.sh" "$rename_migration"
|
||||
rm -f "$root/etc/omarchy/region" "$root/etc/environment.d/90-omarchy-china.conf"
|
||||
mkdir -p "$root/etc/environment.d" "$root/etc/omarchy"
|
||||
echo china >"$root/etc/omarchy/region"
|
||||
ln -sfn /usr/share/omarchy/default/region/china/environment.conf "$root/etc/environment.d/90-omarchy-china.conf"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$test_tmp/bin/ln" && chmod +x "$test_tmp/bin/ln"
|
||||
if bash -e "$rename_migration" >/dev/null 2>&1; then
|
||||
fail "a failed link repair fails the migration"
|
||||
fi
|
||||
[[ $(<"$root/etc/omarchy/region") == "china" ]] || fail "a failed link repair leaves china for the retry"
|
||||
rm "$test_tmp/bin/ln"
|
||||
bash -e "$rename_migration" >/dev/null
|
||||
[[ $(<"$root/etc/omarchy/region") == "cn" && $(readlink "$root/etc/environment.d/90-omarchy-china.conf") == "$sandbox/default/regions/cn/environment.conf" ]] ||
|
||||
fail "the rename migration records cn and repoints the links" "$(readlink "$root/etc/environment.d/90-omarchy-china.conf")"
|
||||
pass "the china rename migration repoints the links before it records cn, so a failure retries"
|
||||
+10
-33
@@ -7,35 +7,6 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
china_sed="$ROOT/default/regions/cn/pacman/servers.sed"
|
||||
|
||||
expected_stable_omarchy='Server = https://pkgs.omarchy.cn/stable/$arch
|
||||
Server = https://pkgs.omarchy.org/stable/$arch'
|
||||
actual=$(sed -f "$china_sed" "$ROOT/default/pacman/pacman-stable.conf" | sed -n '/^\[omarchy\]/,/^$/p' | grep '^Server')
|
||||
[[ $actual == "$expected_stable_omarchy" ]] || fail "stable [omarchy] lists pkgs.omarchy.cn before pkgs.omarchy.org" "$actual"
|
||||
pass "stable [omarchy] lists pkgs.omarchy.cn before pkgs.omarchy.org"
|
||||
|
||||
declare -A mirrors=([stable]=stable-mirror [rc]=rc-mirror [edge]=mirror)
|
||||
for channel in stable rc edge; do
|
||||
host=${mirrors[$channel]}
|
||||
expected="Server = https://$host.omarchy.cn/\$repo/os/\$arch
|
||||
Server = https://$host.omarchy.org/\$repo/os/\$arch"
|
||||
actual=$(sed -f "$china_sed" "$ROOT/default/pacman/mirrorlist-$channel")
|
||||
[[ $actual == "$expected" ]] || fail "$channel mirrorlist lists $host.omarchy.cn first" "$actual"
|
||||
pass "$channel mirrorlist lists $host.omarchy.cn first"
|
||||
|
||||
for file in "pacman-$channel.conf" "mirrorlist-$channel"; do
|
||||
original=$(<"$ROOT/default/pacman/$file")
|
||||
stripped=$(sed -f "$china_sed" "$ROOT/default/pacman/$file" | grep -v '^Server = https://[a-z-]*\.omarchy\.cn/')
|
||||
[[ $stripped == "$original" ]] || fail "$file only gains .cn servers" "$(diff <(echo "$original") <(echo "$stripped"))"
|
||||
|
||||
org_count=$(grep -c '^Server = https://[a-z-]*\.omarchy\.org/' "$ROOT/default/pacman/$file")
|
||||
cn_count=$(sed -f "$china_sed" "$ROOT/default/pacman/$file" | grep -c '^Server = https://[a-z-]*\.omarchy\.cn/')
|
||||
((org_count > 0 && org_count == cn_count)) || fail "$file gets one .cn twin per .org server" "org=$org_count cn=$cn_count"
|
||||
done
|
||||
pass "$channel templates only gain one .cn twin per .org server"
|
||||
done
|
||||
|
||||
region_bin="$test_tmp/omarchy-region"
|
||||
region_file="$test_tmp/region"
|
||||
sed "s|/etc/omarchy/region|$region_file|" "$ROOT/bin/omarchy-region" >"$region_bin"
|
||||
@@ -126,15 +97,21 @@ fi
|
||||
[[ ! -e $root/etc/omarchy/region && ! -e $root/etc/gemrc && $(<"$root/etc/locale.conf") == "LANG=en_US.UTF-8" ]] ||
|
||||
fail "a failed package install leaves the machine Global"
|
||||
pass "China installs its packages before it writes any region state"
|
||||
OMARCHY_REGION=cn bash -e "$region_setup"
|
||||
echo cn >"$root/etc/omarchy/region"
|
||||
: >"$calls"
|
||||
env -u OMARCHY_REGION bash -e "$region_setup"
|
||||
[[ ! -e $root/etc/omarchy/region ]] || fail "an installer that passes no region installs Global"
|
||||
pass "Global and an unset region leave no region file"
|
||||
[[ $(<"$root/etc/omarchy/region") == "cn" && $(readlink "$root/etc/gemrc") == "$ROOT/default/regions/cn/gemrc" ]] ||
|
||||
fail "install keeps the cn marker the installer wrote and links its drop-ins"
|
||||
pass "install follows the region marker the installer wrote"
|
||||
OMARCHY_REGION=global bash -e "$region_setup"
|
||||
env -u OMARCHY_REGION bash -e "$region_setup"
|
||||
[[ ! -e $root/etc/omarchy/region && ! -e $root/etc/gemrc ]] || fail "no marker installs Global"
|
||||
pass "no marker installs Global"
|
||||
|
||||
if output=$(OMARCHY_REGION=mars bash -e "$region_setup" 2>&1); then
|
||||
fail "install rejects an unknown region" "$output"
|
||||
fi
|
||||
[[ $output == "Unknown OMARCHY_REGION: mars" ]] || fail "install names the unknown region" "$output"
|
||||
[[ $output == "Unknown region: mars" ]] || fail "install names the unknown region" "$output"
|
||||
pass "install rejects an unknown region"
|
||||
|
||||
user_region="$ROOT/install/user/region.sh"
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
scratch=$(mktemp -d)
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
|
||||
zone=$(<"$ROOT/default/regions/cn/timezone")
|
||||
[[ $zone == "Asia/Shanghai" ]] || fail "China profile names Asia/Shanghai"
|
||||
[[ -f /usr/share/zoneinfo/$zone ]] || fail "China profile timezone exists in tzdata"
|
||||
pass "China profile defaults to an existing Asia/Shanghai zone"
|
||||
|
||||
# Load the real region_timezone() from the first-boot command.
|
||||
OMARCHY_PATH="$ROOT"
|
||||
REGION_MARKER="$scratch/region"
|
||||
eval "$(sed -n '/^region_timezone() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner")"
|
||||
|
||||
[[ -z $(region_timezone) ]] || fail "missing region marker offers no timezone"
|
||||
for region in global CN ../cn zz ""; do
|
||||
printf '%s\n' "$region" >"$REGION_MARKER"
|
||||
[[ -z $(region_timezone) ]] || fail "region '$region' offers no timezone"
|
||||
done
|
||||
pass "global, unknown, and malformed regions offer no timezone"
|
||||
|
||||
printf 'cn\n' >"$REGION_MARKER"
|
||||
[[ $(region_timezone) == "Asia/Shanghai" ]] || fail "cn region offers Asia/Shanghai"
|
||||
pass "cn region offers its profile timezone"
|
||||
|
||||
grep -qF 'omarchy_prompt_timezone "$(region_timezone)"' "$ROOT/bin/omarchy-provision-owner" ||
|
||||
fail "first-boot setup passes the region timezone to the prompt"
|
||||
pass "first-boot setup preselects the region timezone"
|
||||
@@ -61,7 +61,7 @@ fi
|
||||
|
||||
trap 'if [[ ${FUNCNAME[0]:-} == "$PROMPT_FN" ]]; then printf "returned\n" >>"$MARKER"; fi' RETURN
|
||||
|
||||
"$PROMPT_FN" ${PROMPT_ARG:+"$PROMPT_ARG"}
|
||||
"$PROMPT_FN"
|
||||
|
||||
printf 'region=%s\n' "${region:-}"
|
||||
printf 'keyboard=%s\n' "${keyboard:-}"
|
||||
@@ -274,19 +274,6 @@ assert_status 0 "timezone prompt survives a failed geo guess"
|
||||
[[ $(head -n 1 "$GUM_ARGS") == filter* ]] || fail "timezone prompt filters when there is no geo guess"
|
||||
pass "timezone prompt falls back to filtering when the geo guess fails"
|
||||
|
||||
# A region's default stands in for the missing guess, so an offline China
|
||||
# install opens on Asia/Shanghai instead of the top of the list
|
||||
PROMPT_ARG=Asia/Shanghai run_prompt omarchy_prompt_timezone "0:Asia/Shanghai"
|
||||
assert_status 0 "timezone prompt accepts the caller's fallback"
|
||||
[[ $(field timezone) == "Asia/Shanghai" ]] || fail "timezone prompt keeps the preselected fallback"
|
||||
grep -qF -- '--selected Asia/Shanghai' "$GUM_ARGS" || fail "timezone prompt preselects the caller's fallback"
|
||||
[[ $(head -n 1 "$GUM_ARGS") == choose* ]] || fail "timezone prompt chooses from the list when it has a fallback"
|
||||
pass "timezone prompt preselects the caller's fallback when the geo guess fails"
|
||||
|
||||
TZ_GUESS=Europe/Copenhagen PROMPT_ARG=Asia/Shanghai run_prompt omarchy_prompt_timezone "0:Europe/Copenhagen"
|
||||
grep -qF -- '--selected Europe/Copenhagen' "$GUM_ARGS" || fail "a geo guess outranks the caller's fallback"
|
||||
pass "timezone prompt prefers the geo guess over the caller's fallback"
|
||||
|
||||
run_prompt omarchy_prompt_timezone "0:"
|
||||
assert_status 0 "timezone prompt accepts an empty selection"
|
||||
[[ $(field timezone) == "UTC" ]] || fail "timezone prompt falls back to UTC"
|
||||
|
||||
Reference in new issue
Block a user