mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
Merge branch 'main' into feature/llm-atomic-agents
Conflicts resolved: - CHANGELOG.md: kept both [2.12.0] (branch) and [2.11.4] (main), newest first - pyproject.toml: kept main's tighter version pins (click>=8.4.2, requests>=2.34.2, packaging>=26.2, pytest-cov==7.1.0) and the branch's atomic-agents>=2.0.0 dep Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
# Python dependencies (PEP 621 pyproject.toml + uv.lock).
|
||||||
|
# The native "uv" ecosystem is used instead of "pip" because it reads
|
||||||
|
# pyproject.toml and uv.lock directly (GA since 2025-03; security updates
|
||||||
|
# since 2025-12).
|
||||||
|
- package-ecosystem: "uv"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
# "chore" deliberately does NOT match the feat|fix|perf patterns in
|
||||||
|
# .github/workflows/auto-tag.yml, so dependency PRs never auto-cut a tag.
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(deps)"
|
||||||
|
prefix-development: "chore(deps-dev)"
|
||||||
|
|
||||||
|
# GitHub Actions are SHA-pinned in .github/workflows; Dependabot keeps the
|
||||||
|
# pins current (and refreshes the trailing "# vN" comment).
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
# "chore(ci)" deliberately does NOT match the feat|fix|perf patterns in
|
||||||
|
# .github/workflows/auto-tag.yml, so dependency PRs never auto-cut a tag.
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(ci)"
|
||||||
@@ -1,26 +1,50 @@
|
|||||||
name: Auto Tag
|
name: Auto Tag
|
||||||
|
|
||||||
|
# Runs only after the CI workflow finishes successfully on main, so a broken
|
||||||
|
# commit never gets tagged or released.
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_run:
|
||||||
|
workflows: ["CI"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
# Two merges landing back-to-back would otherwise both compute the same new tag
|
||||||
|
# and the second push would fail. Serialize instead of cancelling so no merge
|
||||||
|
# gets skipped.
|
||||||
|
concurrency:
|
||||||
|
group: auto-tag
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag:
|
tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'push'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
|
# workflow_run defaults to the tip of the default branch, which is not
|
||||||
|
# necessarily the commit CI validated.
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git identity
|
||||||
|
run: |
|
||||||
|
git config user.name 'github-actions[bot]'
|
||||||
|
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Determine version bump
|
- name: Determine version bump
|
||||||
id: bump
|
id: bump
|
||||||
run: |
|
run: |
|
||||||
# Get the latest semver tag
|
# Get the latest semver tag. The `|| true` keeps `set -o pipefail`
|
||||||
latest_tag=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
|
# from aborting the step when there are no matching tags yet.
|
||||||
|
latest_tag=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true)
|
||||||
if [ -z "$latest_tag" ]; then
|
if [ -z "$latest_tag" ]; then
|
||||||
echo "No semver tag found, starting at v0.1.0"
|
echo "No semver tag found, starting at v0.1.0"
|
||||||
echo "new_tag=v0.1.0" >> "$GITHUB_OUTPUT"
|
echo "new_tag=v0.1.0" >> "$GITHUB_OUTPUT"
|
||||||
@@ -29,10 +53,12 @@ jobs:
|
|||||||
|
|
||||||
echo "Latest tag: $latest_tag"
|
echo "Latest tag: $latest_tag"
|
||||||
|
|
||||||
# Get commits since last tag
|
# Commit SHAs since the last tag. We iterate SHAs rather than reading
|
||||||
commits=$(git log "$latest_tag"..HEAD --pretty=format:"%s")
|
# subjects line-by-line so that commit *bodies* (and therefore
|
||||||
|
# BREAKING CHANGE footers) are visible.
|
||||||
|
shas=$(git log "$latest_tag"..HEAD --format=%H)
|
||||||
|
|
||||||
if [ -z "$commits" ]; then
|
if [ -z "$shas" ]; then
|
||||||
echo "No new commits since $latest_tag"
|
echo "No new commits since $latest_tag"
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -44,23 +70,42 @@ jobs:
|
|||||||
minor=$(echo "$version" | cut -d. -f2)
|
minor=$(echo "$version" | cut -d. -f2)
|
||||||
patch=$(echo "$version" | cut -d. -f3)
|
patch=$(echo "$version" | cut -d. -f3)
|
||||||
|
|
||||||
# Check commit prefixes for bump type
|
# Determine bump type. Precedence: major > minor > patch.
|
||||||
bump="none"
|
bump="none"
|
||||||
while IFS= read -r msg; do
|
while IFS= read -r sha; do
|
||||||
if echo "$msg" | grep -qE '^feat(\(.+\))?!?:'; then
|
[ -n "$sha" ] || continue
|
||||||
|
body=$(git log -1 --format=%B "$sha")
|
||||||
|
subject=${body%%$'\n'*}
|
||||||
|
|
||||||
|
# Note: matches are done with bash [[ =~ ]] / here-strings rather
|
||||||
|
# than pipelines, so `set -o pipefail` cannot turn an early-exiting
|
||||||
|
# `grep -q` (SIGPIPE, status 141) into a false negative.
|
||||||
|
if [[ "$subject" =~ ^[a-zA-Z]+(\(.+\))?!: ]]; then
|
||||||
|
# e.g. "feat!: ..." / "refactor(api)!: ..."
|
||||||
|
bump="major"
|
||||||
|
elif grep -qE '^(BREAKING CHANGE|BREAKING-CHANGE):' <<<"$body"; then
|
||||||
|
# Conventional Commits breaking-change footer in the body.
|
||||||
|
bump="major"
|
||||||
|
elif [[ "$subject" =~ ^feat(\(.+\))?: ]] && [ "$bump" != "major" ]; then
|
||||||
bump="minor"
|
bump="minor"
|
||||||
elif echo "$msg" | grep -qE '^(fix|perf)(\(.+\))?:' && [ "$bump" != "minor" ]; then
|
elif [[ "$subject" =~ ^(fix|perf)(\(.+\))?: ]] && [ "$bump" = "none" ]; then
|
||||||
bump="patch"
|
bump="patch"
|
||||||
fi
|
fi
|
||||||
done <<< "$commits"
|
done <<EOF
|
||||||
|
$shas
|
||||||
|
EOF
|
||||||
|
|
||||||
if [ "$bump" = "none" ]; then
|
if [ "$bump" = "none" ]; then
|
||||||
echo "No feat/fix/perf commits, skipping tag"
|
echo "No feat/fix/perf/breaking commits, skipping tag"
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$bump" = "minor" ]; then
|
if [ "$bump" = "major" ]; then
|
||||||
|
major=$((major + 1))
|
||||||
|
minor=0
|
||||||
|
patch=0
|
||||||
|
elif [ "$bump" = "minor" ]; then
|
||||||
minor=$((minor + 1))
|
minor=$((minor + 1))
|
||||||
patch=0
|
patch=0
|
||||||
else
|
else
|
||||||
@@ -68,7 +113,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
new_tag="v${major}.${minor}.${patch}"
|
new_tag="v${major}.${minor}.${patch}"
|
||||||
echo "Bump: $bump → $new_tag"
|
echo "Bump: $bump -> $new_tag"
|
||||||
echo "new_tag=$new_tag" >> "$GITHUB_OUTPUT"
|
echo "new_tag=$new_tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Create tag
|
- name: Create tag
|
||||||
@@ -76,5 +121,25 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NEW_TAG: ${{ steps.bump.outputs.new_tag }}
|
NEW_TAG: ${{ steps.bump.outputs.new_tag }}
|
||||||
run: |
|
run: |
|
||||||
git tag "$NEW_TAG"
|
if git rev-parse -q --verify "refs/tags/$NEW_TAG" >/dev/null; then
|
||||||
git push origin "$NEW_TAG"
|
echo "Tag $NEW_TAG already exists, nothing to push"
|
||||||
|
else
|
||||||
|
git tag "$NEW_TAG"
|
||||||
|
git push origin "refs/tags/$NEW_TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GitHub never dispatches workflow events for refs pushed with
|
||||||
|
# GITHUB_TOKEN, so release.yml will not fire for the tag above. Create the
|
||||||
|
# release here instead. release.yml remains the path for tags pushed
|
||||||
|
# manually by a human.
|
||||||
|
- name: Create GitHub release
|
||||||
|
if: steps.bump.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
NEW_TAG: ${{ steps.bump.outputs.new_tag }}
|
||||||
|
run: |
|
||||||
|
if gh release view "$NEW_TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Release $NEW_TAG already exists, nothing to do"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
gh release create "$NEW_TAG" --generate-notes
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks:
|
||||||
|
name: Lint, type-check, and tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
# tests/test_main_pcfg.py needs pcfg_cracker/pcfg_guesser.py on disk
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
enable-cache: true
|
||||||
|
cache-dependency-glob: "**/uv.lock"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --dev
|
||||||
|
|
||||||
|
- name: Ruff
|
||||||
|
run: uv run ruff check hate_crack
|
||||||
|
|
||||||
|
- name: Ty
|
||||||
|
run: uv run ty check hate_crack
|
||||||
|
|
||||||
|
- name: Pytest
|
||||||
|
env:
|
||||||
|
HATE_CRACK_SKIP_INIT: "1"
|
||||||
|
run: uv run pytest -q
|
||||||
@@ -12,10 +12,10 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # master
|
- uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||||
with:
|
with:
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -28,6 +28,39 @@ Dates are omitted for releases predating this file; see the git tags for exact t
|
|||||||
- **Automatic model pulling.** hate_crack no longer pulls missing Ollama models; pull them
|
- **Automatic model pulling.** hate_crack no longer pulls missing Ollama models; pull them
|
||||||
yourself with `ollama pull <model>`.
|
yourself with `ollama pull <model>`.
|
||||||
|
|
||||||
|
## [2.11.4] - 2026-07-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **CI now runs lint, type checks, and tests on every pull request and push to `main`.**
|
||||||
|
Previously these ran only in local `prek` pre-push hooks, so a commit pushed without hooks
|
||||||
|
installed (or with `--no-verify`) reached `main` unvalidated — and the auto-tag workflow
|
||||||
|
would then cut a release from it. The new `ci.yml` runs `ruff`, `ty`, and `pytest` on
|
||||||
|
Python 3.13, and tagging is gated on it passing.
|
||||||
|
- **Dependabot configuration** for Python (`uv`) and GitHub Actions dependencies, weekly.
|
||||||
|
Action pins are updated automatically instead of drifting. Bumps use `chore` commit
|
||||||
|
prefixes so they never auto-cut a release.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Auto-tagged versions now actually get a GitHub release.** The tag was pushed using the
|
||||||
|
default `GITHUB_TOKEN`, and GitHub suppresses workflow triggers for `GITHUB_TOKEN`-created
|
||||||
|
events, so the tag-triggered release workflow never fired — `v2.11.3` was tagged with no
|
||||||
|
release to show for it. The auto-tag workflow now creates the release itself, idempotently.
|
||||||
|
- **Breaking changes no longer ship as a minor bump.** The version logic matched the `!`
|
||||||
|
breaking-change marker but only ever bumped the minor version, and `BREAKING CHANGE:`
|
||||||
|
footers were invisible because only commit subjects were inspected. Both now correctly
|
||||||
|
trigger a major bump.
|
||||||
|
- **Concurrent merges to `main` no longer collide.** Two merges in quick succession both
|
||||||
|
computed the same new tag and the second push failed; tagging is now serialized. A re-run
|
||||||
|
against an already-tagged commit is also a clean no-op instead of a hard error.
|
||||||
|
- **`softprops/action-gh-release` is pinned to a real release** (`v2.6.2`) rather than an
|
||||||
|
arbitrary mid-development `master` commit, and all workflows now pin the same
|
||||||
|
`actions/checkout` version with accurate version comments.
|
||||||
|
- **`ty` type error in the crack tailer.** `_read_new_lines` read `self._file_pos`
|
||||||
|
(`int | None`) while its only None-guard lived in the caller, so the position is now
|
||||||
|
passed in explicitly as an `int`. Behavior is unchanged.
|
||||||
|
|
||||||
## [2.11.3] - 2026-07-24
|
## [2.11.3] - 2026-07-24
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -158,19 +158,21 @@ class CrackTailer(threading.Thread):
|
|||||||
except OSError:
|
except OSError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._file_pos is None:
|
pos = self._file_pos
|
||||||
|
if pos is None:
|
||||||
self._file_pos = size
|
self._file_pos = size
|
||||||
return
|
return
|
||||||
|
|
||||||
if size < self._file_pos:
|
if size < pos:
|
||||||
# File was truncated / rewritten — reset rather than read garbage.
|
# File was truncated / rewritten — reset rather than read garbage.
|
||||||
self._file_pos = 0
|
pos = 0
|
||||||
self._buffer = b""
|
self._buffer = b""
|
||||||
|
self._file_pos = pos
|
||||||
|
|
||||||
if size == self._file_pos:
|
if size == pos:
|
||||||
return
|
return
|
||||||
|
|
||||||
new_lines = self._read_new_lines(size)
|
new_lines = self._read_new_lines(pos, size)
|
||||||
if not new_lines:
|
if not new_lines:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -187,15 +189,15 @@ class CrackTailer(threading.Thread):
|
|||||||
label = self._extract_username(line) or self.attack_name
|
label = self._extract_username(line) or self.attack_name
|
||||||
self._notify_crack(label, self.attack_name)
|
self._notify_crack(label, self.attack_name)
|
||||||
|
|
||||||
def _read_new_lines(self, size: int) -> list[bytes]:
|
def _read_new_lines(self, start: int, size: int) -> list[bytes]:
|
||||||
"""Read from ``self._file_pos`` up to ``size`` and return full lines.
|
"""Read from ``start`` up to ``size`` and return full lines.
|
||||||
|
|
||||||
Incomplete trailing bytes are buffered for the next tick.
|
Incomplete trailing bytes are buffered for the next tick.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(self.out_path, "rb") as f:
|
with open(self.out_path, "rb") as f:
|
||||||
f.seek(self._file_pos)
|
f.seek(start)
|
||||||
data = f.read(size - self._file_pos)
|
data = f.read(size - start)
|
||||||
except OSError:
|
except OSError:
|
||||||
return []
|
return []
|
||||||
self._file_pos = size
|
self._file_pos = size
|
||||||
|
|||||||
+4
-4
@@ -9,12 +9,12 @@ description = "Menu driven Python wrapper for hashcat"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests>=2.31.0",
|
"requests>=2.34.2",
|
||||||
"beautifulsoup4>=4.12.0",
|
"beautifulsoup4>=4.12.0",
|
||||||
"openpyxl>=3.0.0",
|
"openpyxl>=3.0.0",
|
||||||
"packaging>=21.0",
|
"packaging>=26.2",
|
||||||
"simple-term-menu==1.6.6",
|
"simple-term-menu==1.6.6",
|
||||||
"click>=8.0.0",
|
"click>=8.4.2",
|
||||||
"atomic-agents>=2.0.0",
|
"atomic-agents>=2.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -76,6 +76,6 @@ dev = [
|
|||||||
"ty==0.0.17",
|
"ty==0.0.17",
|
||||||
"ruff==0.15.1",
|
"ruff==0.15.1",
|
||||||
"pytest==9.0.3",
|
"pytest==9.0.3",
|
||||||
"pytest-cov==7.0.0",
|
"pytest-cov==7.1.0",
|
||||||
"pytest-timeout>=2.4.0",
|
"pytest-timeout>=2.4.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -190,6 +190,49 @@ class TestCrackTailerStop:
|
|||||||
tailer.stop()
|
tailer.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrackTailerPollOnceInternals:
|
||||||
|
"""Direct unit tests for _poll_once edge cases via the internal API."""
|
||||||
|
|
||||||
|
def _make_uninitialised(self, out_path: Path):
|
||||||
|
"""Return a tailer whose _file_pos is still None (seek never called)."""
|
||||||
|
tailer, notify, aggregate = _make_tailer(out_path)
|
||||||
|
# Do NOT call start() or _seek_to_eof(); leave _file_pos = None.
|
||||||
|
return tailer, notify, aggregate
|
||||||
|
|
||||||
|
def test_first_poll_with_file_pos_none_seeds_position(
|
||||||
|
self, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""_poll_once with _file_pos=None must record size and return without notifying."""
|
||||||
|
out = tmp_path / "hashes.out"
|
||||||
|
out.write_text("alice:hash:plain\n")
|
||||||
|
tailer, notify, aggregate = self._make_uninitialised(out)
|
||||||
|
assert tailer._file_pos is None
|
||||||
|
tailer._poll_once()
|
||||||
|
# Position seeded to current size; no notification fired.
|
||||||
|
assert tailer._file_pos == out.stat().st_size
|
||||||
|
assert notify.call_count == 0
|
||||||
|
assert aggregate.call_count == 0
|
||||||
|
|
||||||
|
def test_truncation_resets_position_directly(self, tmp_path: Path) -> None:
|
||||||
|
"""_poll_once must reset position and buffer when the file shrinks."""
|
||||||
|
out = tmp_path / "hashes.out"
|
||||||
|
content = "alice:hash:plain\nbob:hash:plain\n"
|
||||||
|
out.write_text(content)
|
||||||
|
tailer, notify, aggregate = _make_tailer(out)
|
||||||
|
tailer._file_pos = len(content.encode()) # pretend we've read it all
|
||||||
|
tailer._buffer = b"leftover"
|
||||||
|
|
||||||
|
# Truncate to a smaller file.
|
||||||
|
out.write_text("c:h:p\n")
|
||||||
|
tailer._poll_once()
|
||||||
|
|
||||||
|
# Position must have been reset and buffer cleared.
|
||||||
|
assert tailer._file_pos == len(b"c:h:p\n")
|
||||||
|
assert tailer._buffer == b""
|
||||||
|
# One new line notified.
|
||||||
|
assert notify.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
class TestCrackTailerFileHandling:
|
class TestCrackTailerFileHandling:
|
||||||
def test_missing_file_then_appearing(self, tmp_path: Path) -> None:
|
def test_missing_file_then_appearing(self, tmp_path: Path) -> None:
|
||||||
out = tmp_path / "missing.out"
|
out = tmp_path / "missing.out"
|
||||||
|
|||||||
Reference in New Issue
Block a user