Горячая папка: запись прямо в Input, как в старом АРМ; таймаут 20 мин (#26) #27
12
CLAUDE.md
12
CLAUDE.md
@ -4,12 +4,12 @@
|
||||
|
||||
## Что это
|
||||
Адаптер внешней ИС Сканирование (Content AI) для PDF/PNG/JPG: положить файл во «Входящие» как `<job_id>.<ext>`
|
||||
(расширение `original_name` в нижнем регистре; `.part` пишется в `SCAN_TMP_DIR`, по умолчанию `Tmp`, на той же ФС/шаре и
|
||||
публикуется **без замены**), забрать **все** `Исходящие/<job_id>/*.json` (ИС кладёт по одному json на каждый
|
||||
(расширение `original_name` в нижнем регистре; пишется **сразу** открытием `wb`, существующий файл перезаписывается — как в
|
||||
старом АРМ), забрать **все** `Исходящие/<job_id>/*.json` (ИС кладёт по одному json на каждый
|
||||
распознанный документ), склеить и сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить
|
||||
горячую папку (только при `SCAN_CLEANUP=true`), нормализовать в `../docs/contracts/recognition-result.schema.json`
|
||||
(`statementDocument` v1.2: по одному документу на таблицу `table_structure[i]`), опубликовать `files.tabular_parsed`
|
||||
(`done` / `needs_check` / `error`). Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=300)`,
|
||||
(`done` / `needs_check` / `error`). Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=1200)`,
|
||||
старт — запись файла во «Входящие». **Нет доступа к БД.**
|
||||
|
||||
## Правила
|
||||
@ -28,9 +28,9 @@
|
||||
и golden.
|
||||
- Эталоны нормализатора лежат в **fixtures** (`../fixtures/ocr/expected_normalize/`): `make golden-update`, diff смотреть
|
||||
в fixtures и объяснять; эталоны меняются отдельным PR fixtures.
|
||||
- Горячие папки (раскладка FileWorker): имя `<job_id>.<ext>`, `.part` только в `Tmp`, запись без замены (`os.link` /
|
||||
`smbclient.rename`; существующее назначение = «доставлено»). `.part`, ставший целью публикации, снимается самой записью
|
||||
независимо от флага (№26); любое другое удаление (ответ, входящий файл, `.done`, осиротевшие `.part`) — только при
|
||||
- Горячие папки (раскладка FileWorker): имя `<job_id>.<ext>`, запись сразу в `Input` открытием `wb` с перезаписью, без
|
||||
временного файла и `Tmp` — как в старом АРМ (change `hotfolder-direct-write`, ASSUMPTION(OQ-79)); `Tmp` — только у
|
||||
эмулятора. Любое удаление (ответ, входящий файл, `.done`, недописанный при ошибке записи входящий файл) — только при
|
||||
`SCAN_CLEANUP=true` и только после сохранения `raw/` (не при `TIMEOUT`, `FILE_READ_ERROR`, ошибке записи во входящий каталог или `raw/`). Ошибка
|
||||
удаления — одна строка лога, конверт не меняется. Нет расширения у `original_name` → `RECOGNITION_FAILED`.
|
||||
- Набор ответа готов по settle: есть `*.json`, а имена и размеры не менялись `SCAN_SETTLE_SECONDS` (по умолчанию 2;
|
||||
|
||||
56
README.md
56
README.md
@ -28,7 +28,7 @@ files.img_uploaded (NATS, queue group scan-gateway)
|
||||
2. ack: files.tabular_parsed, status=processing (до чтения S3 и до ожидания слота)
|
||||
3. слот (SCAN_MAX_INFLIGHT одновременных заданий)
|
||||
4. файл из S3 (s3.bucket/s3.key)
|
||||
5. горячая папка: <Input>/<job_id>.<ext> (через <Tmp>/.<job_id>.<ext>.part, без замены)
|
||||
5. горячая папка: <Input>/<job_id>.<ext> (сразу, wb, существующий перезаписывается)
|
||||
6. опрос <Output>/<job_id>/ каждые SCAN_POLL_SECONDS до дедлайна; набор *.json готов, когда
|
||||
не менялся SCAN_SETTLE_SECONDS
|
||||
7. склейка всех *.json каталога ──► S3 raw/{job_id}.json (до нормализации)
|
||||
@ -60,19 +60,15 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
||||
`original_name` запроса (разделители пути `/` и `\` учитываются), в нижнем регистре, 1–16 букв и цифр ASCII
|
||||
(`aep_5.PDF` → `<job_id>.pdf`): ИС различает форматы по расширению, а имя файла от пользователя в горячую папку не
|
||||
попадает. Нет допустимого расширения — `RECOGNITION_FAILED` без записи в горячую папку.
|
||||
- Запись атомарна и идёт **без замены**: сначала временный файл `.<job_id>.<ext>.part` в служебном каталоге
|
||||
`<SCAN_TMP_DIR>` (по умолчанию `Tmp`), затем он публикуется в `Input` под настоящим именем. `Tmp` лежит рядом с `Input`
|
||||
и `Output`, на той же ФС/шаре: публикация атомарна, а ИС не может забрать недописанный файл (временных файлов в
|
||||
`Input` нет — решение владельца 06.10.2026, у FileWorker `.tmp` лежит рядом с назначением). В режиме `local` публикация —
|
||||
жёсткая ссылка `.part` → `Input/<имя>` и удаление `.part`; на файловой системе без жёстких ссылок
|
||||
(`EPERM`/`EXDEV`/`ENOTSUP`/`EOPNOTSUPP` от самой ссылки) — `os.replace`, если назначения ещё нет (решение №17).
|
||||
`.part` создаётся заново: остаток прежней записи удаляется (ошибка удаления подавляется), файл открывается с `O_EXCL`;
|
||||
поверх оставшегося `.part` пишем только если это не жёсткая ссылка (`st_nlink == 1`) — иначе исключение (решение №32),
|
||||
чтобы не испортить уже опубликованный файл. В режиме `smb` — `smbclient.rename` (поднимает `FileExistsError`, если
|
||||
назначение есть). Если `Input/<job_id>.<ext>` уже существует (повторная публикация задания после потери ack в другом
|
||||
процессе), файл считается доставленным: ничего не пишется, назначение не перезаписывается, `.part` не трогается (при
|
||||
`SCAN_CLEANUP=true` существующий `.part` снимается), задание продолжается опросом `Output/<job_id>/` (решение №35). Каталоги `Input` и `Tmp`
|
||||
создаются при необходимости (без родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`.
|
||||
- Запись — **сразу в `Input`**, как в старом АРМ (решение владельца 09.10.2026, change `hotfolder-direct-write`):
|
||||
файл открывается на запись с усечением (`wb`) и пишется целиком; временного файла, служебного каталога и
|
||||
переименования нет. В режиме `smb` — `smbclient.open_file(<Input>\<имя>, "wb")`: файл открыт монопольно, пока идёт
|
||||
запись, и ИС его не прочитает (ASSUMPTION(OQ-79) — так работал старый АРМ). Если `Input/<job_id>.<ext>` уже существует
|
||||
(повтор задания ядром после потери ack в другом процессе), он **перезаписывается** содержимым объекта S3, задание
|
||||
продолжается опросом `Output/<job_id>/`. Ошибка посреди записи → `RECOGNITION_FAILED`; недописанный файл снимается
|
||||
только при `SCAN_CLEANUP=true` (иначе остаётся в `Input`). Каталог `Input` создаётся при необходимости (без
|
||||
родителей; отказ в правах при существующем каталоге — не ошибка), корень горячей папки сам не создаётся: нет корня —
|
||||
`RECOGNITION_FAILED`.
|
||||
- Ответ ИС — каталог `<SCAN_OUTPUT_DIR>/<job_id>/`: ИС кладёт туда **все** распознанные документы задания одновременно,
|
||||
по одному `*.json` на файл (`Output/<job_id>/*.json`). Шлюз читает **все** `*.json` каталога и склеивает их в один
|
||||
ответ; прочие файлы каталога игнорируются. `job_id` проверяется на безопасность пути. В одном json может быть
|
||||
@ -96,24 +92,19 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
||||
- **Когда.** Воркер вызывает `HotFolder.cleanup` сразу после того, как набор ответа прочитан, склеен и сохранён в
|
||||
`raw/{job_id}.json` — при любом исходе разбора и нормализации (в том числе когда все файлы набора — ошибки ИС).
|
||||
- **Что удаляется** (всё, что есть): **всё содержимое** каталога `Output/<job_id>/` (не только `*.json`) и сам каталог,
|
||||
`Input/<job_id>.<ext>`, `Input/<job_id>.<ext>.done` (след эмулятора), `Tmp/.<job_id>.<ext>.part`. Отсутствующий файл
|
||||
`Input/<job_id>.<ext>`, `Input/<job_id>.<ext>.done` (след эмулятора). `Tmp` шлюз не трогает. Отсутствующий файл
|
||||
или каталог не ошибка. В режиме `local` каталог ответа удаляется рекурсивно; в режиме `smb` удаляются файлы каталога, а
|
||||
подкаталог внутри `Output/<job_id>/` не даст снять каталог (`rmdir` → `STATUS_DIRECTORY_NOT_EMPTY`): ошибка очистки
|
||||
поднимается и логируется, остальные пункты (входящий файл, `.done`, `.part`) в этом задании не удаляются.
|
||||
поднимается и логируется, остальные пункты (входящий файл, `.done`) в этом задании не удаляются.
|
||||
- **Когда не вызывается:** `TIMEOUT` (ИС может ещё писать ответ), `FILE_READ_ERROR`, `RECOGNITION_FAILED` записи во
|
||||
входящий каталог, ошибка записи `raw/` в S3. Входящий файл и каталог ответа тогда остаются.
|
||||
- **Ошибки удаления** (например, `PermissionError` на шаре): одна строка в логе (`warning`) на задание; конверт,
|
||||
`metrics.warnings` и `/healthcheck` не меняются — исход задания определён разбором ответа.
|
||||
- **`.part` и флаг (решение №26).** `.part`, ставший целью успешной публикации, снимается **самой записью**, независимо
|
||||
от `SCAN_CLEANUP` (в `local` — удаление после жёсткой ссылки, ошибка удаления подавляется; в `smb` `.part`
|
||||
переименовывается во входящий и отдельного удаления не требует): иначе при выключенной очистке на каждое задание
|
||||
оставалась бы лишняя ссылка, а ошибка удаления на шаре с неподтверждёнными правами превращала бы доставленный файл в
|
||||
`RECOGNITION_FAILED`. **Флаг управляет остатками:** осиротевшие `.part` (оборванная запись, `.part` при уже
|
||||
существующем назначении), каталог ответа, входящий файл и `.done`. При `SCAN_CLEANUP=false` сервис не удаляет ничего из
|
||||
этого и не пишет в лог об осиротевших `.part` (решение №27): они накапливаются в `Tmp` и убираются вручную или
|
||||
включением флага.
|
||||
- Права на SMB: для очистки учётной записи шары нужны права на удаление в `Input`, `Output` (включая каталог
|
||||
`<job_id>`) и `Tmp`; для записи — создание файлов в `Tmp` и `Input` и переименование. Пока права на удаление не
|
||||
- **Флаг управляет всем удалением:** при `SCAN_CLEANUP=false` сервис не удаляет ничего — ни каталог ответа, ни входящий
|
||||
файл и `.done`, ни недописанный при ошибке записи входящий файл — и не пишет об этом в лог (решение №27). Остатки
|
||||
`Tmp/.*.part` от версий шлюза до change `hotfolder-direct-write` сервис не трогает — убрать вручную.
|
||||
- Права на SMB: для записи учётной записи шары нужно создание и перезапись файлов в `Input`, для чтения ответа —
|
||||
листинг и чтение `Output`; для очистки — удаление в `Input` и `Output` (включая каталог `<job_id>`). Пока права на удаление не
|
||||
подтверждены, оставляйте `SCAN_CLEANUP=false`.
|
||||
|
||||
### Эмулятор ИС в режиме `local`
|
||||
@ -315,12 +306,11 @@ pydantic-валидацией; битый файл (код не из 4–5 ци
|
||||
| `SCAN_HOTFOLDER_PATH` | пусто | корень горячей папки в режиме `local` (обязателен, каталог должен существовать) |
|
||||
| `SCAN_INPUT_DIR` | `Input` | «Входящие» относительно корня |
|
||||
| `SCAN_OUTPUT_DIR` | `Output` | «Исходящие» относительно корня |
|
||||
| `SCAN_TMP_DIR` | `Tmp` | служебный каталог относительно корня (на той же ФС/шаре, что `Input`/`Output`): временный `.<job_id>.<ext>.part` записи входящего файла |
|
||||
| `SCAN_POLL_SECONDS` | `5` | период опроса «Исходящих», секунды |
|
||||
| `SCAN_SETTLE_SECONDS` | `2` | набор `*.json` в `Output/<job_id>/` готов, когда имена и размеры не менялись столько секунд, считая от первого опроса, увидевшего окончательный набор (>= 0; `0` — сразу) |
|
||||
| `SCAN_TIMEOUT_SECONDS` | `300` | потолок ожидания ответа ИС, секунды (дополнительно к `reply_deadline`) |
|
||||
| `SCAN_TIMEOUT_SECONDS` | `1200` | потолок ожидания ответа ИС, секунды (дополнительно к `reply_deadline`; 20 мин — в паре со сроком ответа OCR ядра) |
|
||||
| `SCAN_MAX_INFLIGHT` | `4` | одновременно обрабатываемых заданий |
|
||||
| `SCAN_CLEANUP` | `false` | очистка горячей папки после чтения набора ответа: удаляет всё содержимое `Output/<job_id>/` и каталог, `Input/<job_id>.<ext>` (и `.done`), `Tmp/.<job_id>.<ext>.part`; `false` — ничего не удаляется (кроме `.part`, снятого самой записью); см. «Очистка горячей папки» |
|
||||
| `SCAN_CLEANUP` | `false` | очистка горячей папки после чтения набора ответа: удаляет всё содержимое `Output/<job_id>/` и каталог, `Input/<job_id>.<ext>` (и `.done`); `false` — ничего не удаляется; см. «Очистка горячей папки» |
|
||||
| `SCAN_RULES_DIR` | `rules` | каталог справочников нормализатора (`statement_lines.yaml`, `ocr_lookalikes.yaml`, `statement_labels.yaml`), путь от текущего каталога (`api/`); битый справочник — сервис не стартует |
|
||||
| `SERVICE_NAME` | `scan-gateway` | `service.name` в конверте |
|
||||
| `VERSION` | `1.0.0` | версия сервиса (`/healthcheck`, `service.version`) |
|
||||
@ -335,7 +325,8 @@ MinIO и NATS); те же значения подхватываются из `EN
|
||||
| Переменная | По умолчанию | Назначение |
|
||||
|---|---|---|
|
||||
| `SCAN_HOTFOLDER_PATH` | — (обязательна) | корень горячей папки; должен существовать, иначе код выхода 2 |
|
||||
| `SCAN_INPUT_DIR`, `SCAN_OUTPUT_DIR`, `SCAN_TMP_DIR` | `Input`, `Output`, `Tmp` | подкаталоги корня (как у шлюза); в `Tmp` эмулятор собирает набор ответа перед rename в `Output/<uuid>/` |
|
||||
| `SCAN_INPUT_DIR`, `SCAN_OUTPUT_DIR` | `Input`, `Output` | подкаталоги корня (как у шлюза) |
|
||||
| `SCAN_TMP_DIR` | `Tmp` | служебный каталог эмулятора: в нём собирается набор ответа перед rename в `Output/<uuid>/` (шлюз эту переменную не читает) |
|
||||
| `SCAN_FIXTURES_DIR` | `../../fixtures/ocr` | каталог эталонных пар: исходник (PDF и др.) и ответы ИС (`aep_N.json`, `aep_N_<суффикс>.json`); пара ищется по sha256 содержимого входящего файла; путь от `api/` |
|
||||
| `SCAN_EMULATOR_DEFAULT` | пусто | основа имени набора (например, `aep_6`), который отдаётся при промахе по хешу; пусто или набора нет — ответ-ошибка `INVALID_DOCUMENT` |
|
||||
| `SCAN_EMULATOR_DELAY` | `2` | задержка перед выдачей ответа, секунды (>= 0) |
|
||||
@ -508,8 +499,9 @@ make golden-update # пересобрать эталоны нормализ
|
||||
- Недописанный набор ответа ИС: защита — settle (`SCAN_SETTLE_SECONDS`), а не разбор содержимого. Если ИС дописывает
|
||||
файл дольше settle, будет принят неполный набор; файл, не разобравшийся как JSON, после склейки не перечитывается:
|
||||
`response = null`, `parse_error` в `raw/`, ошибка этого файла (`INVALID_FORMAT`) — в `metrics.warnings`.
|
||||
- Запись без замены на файловой системе без жёстких ссылок (`EPERM`/`EXDEV`/`ENOTSUP`): между проверкой назначения и
|
||||
`os.replace` есть крошечное окно гонки (повтор задания с тем же `job_id` редок, решение №17).
|
||||
- Запись сразу в `Input` (change `hotfolder-direct-write`): защита от чтения недописанного файла — только монопольное
|
||||
открытие `smbclient` на время записи (ASSUMPTION(OQ-79), как в старом АРМ). Если ИС всё же возьмёт файл во время
|
||||
записи, распознавание даст ошибку; откат — вернуть запись через временный файл.
|
||||
- Эмулятор: повреждённая фикстура (не JSON, не UTF-8 или корень не объект) не останавливает его — на её месте в наборе
|
||||
он кладёт ответ `INVALID_DOCUMENT` с сообщением о повреждённой фикстуре и пишет ошибку в лог. Индекс хешей
|
||||
пересчитывается при изменении списка файлов каталога фикстур: изменённый, но не переименованный PDF не
|
||||
|
||||
@ -43,19 +43,17 @@ SCAN_HOTFOLDER_PATH=./.hotfolder
|
||||
# Подкаталоги корня: «Входящие» (сюда кладётся UUID_имя) и «Исходящие» (оттуда забираются все *.json из <UUID>/)
|
||||
SCAN_INPUT_DIR=Input
|
||||
SCAN_OUTPUT_DIR=Output
|
||||
# Служебный каталог корня: временный .part пишется сюда и переименовывается во «Входящие» (та же ФС/шара — rename
|
||||
# атомарен, во «Входящих» не бывает недописанных файлов); создаётся при первой записи
|
||||
SCAN_TMP_DIR=Tmp
|
||||
# Период опроса «Исходящих», секунды
|
||||
SCAN_POLL_SECONDS=5
|
||||
# Набор *.json в <UUID>/ считается готовым, когда имена и размеры файлов не менялись столько секунд (>= 0)
|
||||
SCAN_SETTLE_SECONDS=2
|
||||
# Потолок ожидания ответа ИС, секунды (дополнительно ограничен reply_deadline запроса); повторной отправки нет
|
||||
SCAN_TIMEOUT_SECONDS=300
|
||||
# Потолок ожидания ответа ИС, секунды (дополнительно ограничен reply_deadline запроса; 20 мин — в паре со сроком
|
||||
# ответа OCR ядра); повторной отправки нет
|
||||
SCAN_TIMEOUT_SECONDS=1200
|
||||
# Сколько заданий обрабатывается одновременно
|
||||
SCAN_MAX_INFLIGHT=4
|
||||
# Очистка горячей папки после прочтения ответа (D-2): true — удалить всё содержимое Output/<job_id>/, входящий файл
|
||||
# (и его .done), осиротевший .part в Tmp; false (по умолчанию) — ничего не удалять
|
||||
# (и его .done); false (по умолчанию) — ничего не удалять
|
||||
SCAN_CLEANUP=false
|
||||
# Каталог справочников нормализатора OCR (statement_lines.yaml, ocr_lookalikes.yaml, statement_labels.yaml);
|
||||
# относительный путь — от каталога запуска (api/); битый справочник — сервис не стартует
|
||||
@ -70,7 +68,9 @@ SCAN_RULES_DIR=rules
|
||||
# --- Эмулятор ИС Сканирование (python -m src.scan_emulator, make emulator; только dev) ---
|
||||
# Эти параметры читает сам эмулятор, сервис шлюза их не использует. Корень горячей папки и подкаталоги
|
||||
# эмулятор берёт из SCAN_HOTFOLDER_PATH / SCAN_INPUT_DIR / SCAN_OUTPUT_DIR выше; набор ответа на задание он собирает
|
||||
# во временном каталоге SCAN_TMP_DIR (выше) и переименовывает в <Output>/<uuid>/ целиком.
|
||||
# во временном каталоге SCAN_TMP_DIR и переименовывает в <Output>/<uuid>/ целиком (шлюз SCAN_TMP_DIR не читает: файл
|
||||
# во «Входящие» он пишет сразу, как в старом АРМ).
|
||||
SCAN_TMP_DIR=Tmp
|
||||
# Каталог с фикстурами ответов ИС (aep_N.json); путь от api/ (репозиторий fixtures лежит рядом с workspace)
|
||||
SCAN_FIXTURES_DIR=../../fixtures/ocr
|
||||
# Задержка перед выдачей ответа, секунды (имитация работы ИС)
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
## 1. Тесты (scan-gateway, `api/tests/`)
|
||||
|
||||
- [ ] 1.1 `unit/test_smb_hotfolder.py`: `put` пишет `open_file(<Input>\<имя>, "wb")` без `Tmp`/`rename`; существующий
|
||||
- [x] 1.1 `unit/test_smb_hotfolder.py`: `put` пишет `open_file(<Input>\<имя>, "wb")` без `Tmp`/`rename`; существующий
|
||||
файл перезаписывается; ошибка записи — снятие файла только при `cleanup=True`; `cleanup` не трогает `.part`
|
||||
- [ ] 1.2 `unit/test_local_hotfolder.py`: то же для `LocalHotFolder`; `Tmp` не создаётся `put`
|
||||
- [ ] 1.3 `unit/test_config.py`, `unit/test_hotfolder_factory.py`: нет `SCAN_TMP_DIR` у шлюза, `SCAN_TIMEOUT_SECONDS`
|
||||
- [x] 1.2 `unit/test_local_hotfolder.py`: то же для `LocalHotFolder`; `Tmp` не создаётся `put`
|
||||
- [x] 1.3 `unit/test_config.py`, `unit/test_hotfolder_factory.py`: нет `SCAN_TMP_DIR` у шлюза, `SCAN_TIMEOUT_SECONDS`
|
||||
по умолчанию 1200; интеграционные `test_roundtrip.py` — без ожиданий про `Tmp/.part`
|
||||
|
||||
## 2. Реализация (scan-gateway, `api/src/`)
|
||||
|
||||
- [ ] 2.1 `gateway/smb.py`: `put` — прямая запись (D-1, D-2, D-5), убрать `tmp_dir`, `.part` из `cleanup`
|
||||
- [ ] 2.2 `gateway/hotfolder.py`: `LocalHotFolder.put` — прямая запись; `tmp_dir` только для эмулятора (D-3)
|
||||
- [ ] 2.3 `core/config.py`, `gateway/__init__.py`: убрать `SCAN_TMP_DIR` у шлюза; `SCAN_TIMEOUT_SECONDS = 1200`
|
||||
- [x] 2.1 `gateway/smb.py`: `put` — прямая запись (D-1, D-2, D-5), убрать `tmp_dir`, `.part` из `cleanup`
|
||||
- [x] 2.2 `gateway/hotfolder.py`: `LocalHotFolder.put` — прямая запись; `tmp_dir` только для эмулятора (D-3)
|
||||
- [x] 2.3 `core/config.py`, `gateway/__init__.py`: убрать `SCAN_TMP_DIR` у шлюза; `SCAN_TIMEOUT_SECONDS = 1200`
|
||||
|
||||
## 3. Документация
|
||||
|
||||
- [ ] 3.1 `README.md`, `CLAUDE.md`, `api/.env.example` — прямая запись, `SCAN_TMP_DIR` только у эмулятора, таймаут 1200
|
||||
- [ ] 3.2 `make check-full`
|
||||
- [x] 3.1 `README.md`, `CLAUDE.md`, `api/.env.example` — прямая запись, `SCAN_TMP_DIR` только у эмулятора, таймаут 1200
|
||||
- [x] 3.2 `make check-full`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user