Compare commits
6 Commits
2311cb7f8b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| aa64fe6e10 | |||
| 096c72f0d2 | |||
| 518014149e | |||
| e70360a8d6 | |||
| 375f7795fd | |||
| 0e8f31eb74 |
@@ -16,14 +16,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Run Hadolint
|
|
||||||
uses: hadolint/hadolint-action@v3.3.0
|
|
||||||
with:
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
output-file: hadolint.out
|
|
||||||
format: sonarqube
|
|
||||||
no-fail: true
|
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
@@ -53,43 +45,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
uv run coverage run -m pytest -v --junitxml=testresults.xml
|
uv run coverage run -m pytest -v --junitxml=testresults.xml
|
||||||
uv run coverage report
|
uv run coverage report
|
||||||
# sed -i 's@${{ gitea.workspace }}@/github/workspace@g' coverage.xml
|
|
||||||
|
|
||||||
- name: Minimize uv cache
|
- name: Minimize uv cache
|
||||||
run: uv cache prune --ci
|
run: uv cache prune --ci
|
||||||
|
|
||||||
# - name: SonarQube Scan
|
|
||||||
# uses: SonarSource/sonarqube-scan-action@v5.2.0
|
|
||||||
# env:
|
|
||||||
# SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }}
|
|
||||||
# SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
|
|
||||||
|
|
||||||
# - name: Set up environment for Snyk
|
|
||||||
# run: |
|
|
||||||
# uv pip freeze > requirements.txt
|
|
||||||
# mv pyproject.toml pyproject.toml.bak
|
|
||||||
# mv uv.lock uv.lock.bak
|
|
||||||
|
|
||||||
# - name: Snyk SAST Scan
|
|
||||||
# uses: snyk/actions/python@master
|
|
||||||
# env:
|
|
||||||
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# # command: snyk
|
|
||||||
# args: snyk code test #--all-projects --exclude=.archive
|
|
||||||
|
|
||||||
# - name: Snyk Vulnerability Scan
|
|
||||||
# uses: snyk/actions/python@master
|
|
||||||
# continue-on-error: true # Sometimes vulns aren't immediately fixable
|
|
||||||
# env:
|
|
||||||
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
|
||||||
# DEBUG: "*snyk*"
|
|
||||||
# with:
|
|
||||||
# # command: snyk
|
|
||||||
# args: snyk test #--all-projects --exclude=.archive
|
|
||||||
|
|
||||||
# - name: Reverse set up environment for Snyk
|
|
||||||
# run: |
|
|
||||||
# rm -f requirements.txt
|
|
||||||
# mv pyproject.toml.bak pyproject.toml
|
|
||||||
# mv uv.lock.bak uv.lock
|
|
||||||
|
|||||||
61
.gitea/workflows/sonar.yml
Normal file
61
.gitea/workflows/sonar.yml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: Sonar
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sonar:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Run Hadolint
|
||||||
|
uses: hadolint/hadolint-action@v3.3.0
|
||||||
|
with:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
output-file: hadolint.out
|
||||||
|
format: sonarqube
|
||||||
|
no-fail: true
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.14"
|
||||||
|
|
||||||
|
- name: uv cache
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: /tmp/.uv-cache
|
||||||
|
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||||
|
uv-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync
|
||||||
|
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: |
|
||||||
|
uv run pylint --exit-zero --recursive=yes --output-format=parseable --output=lintreport.txt app/ tests/
|
||||||
|
cat lintreport.txt
|
||||||
|
|
||||||
|
- name: Unit Test
|
||||||
|
run: |
|
||||||
|
uv run coverage run -m pytest -v --junitxml=testresults.xml
|
||||||
|
uv run coverage report
|
||||||
|
uv run coverage xml -q -o coverage.xml
|
||||||
|
sed -i 's@${{ gitea.workspace }}@/github/workspace@g' coverage.xml
|
||||||
|
|
||||||
|
- name: Minimize uv cache
|
||||||
|
run: uv cache prune --ci
|
||||||
|
|
||||||
|
- name: SonarQube Scan
|
||||||
|
uses: SonarSource/sonarqube-scan-action@v7.1.0
|
||||||
|
env:
|
||||||
|
SONAR_HOST_URL: ${{ vars.SONAR_URL }}
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
@@ -2,6 +2,8 @@ FROM python:3.13.12-slim
|
|||||||
LABEL maintainer="Luke Tainton <luke@tainton.uk>"
|
LABEL maintainer="Luke Tainton <luke@tainton.uk>"
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
RUN useradd -r -s /sbin/nologin -M user
|
||||||
|
|
||||||
ENV PYTHONPATH="/run:/usr/local/lib/python3.13/lib-dynload:/usr/local/lib/python3.13/site-packages:/usr/local/lib/python3.13"
|
ENV PYTHONPATH="/run:/usr/local/lib/python3.13/lib-dynload:/usr/local/lib/python3.13/site-packages:/usr/local/lib/python3.13"
|
||||||
ENV UV_PROJECT_ENVIRONMENT="/usr/local/"
|
ENV UV_PROJECT_ENVIRONMENT="/usr/local/"
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@ WORKDIR /run
|
|||||||
|
|
||||||
RUN mkdir -p /.local && \
|
RUN mkdir -p /.local && \
|
||||||
chmod -R 777 /.local && \
|
chmod -R 777 /.local && \
|
||||||
pip install -U pip uv==0.9.21
|
pip install --no-cache-dir -U pip uv==0.9.21
|
||||||
|
|
||||||
COPY pyproject.toml /run/pyproject.toml
|
COPY pyproject.toml /run/pyproject.toml
|
||||||
COPY uv.lock /run/uv.lock
|
COPY uv.lock /run/uv.lock
|
||||||
@@ -24,3 +26,6 @@ ARG version="dev"
|
|||||||
ENV APP_VERSION=$version
|
ENV APP_VERSION=$version
|
||||||
|
|
||||||
COPY app /run/app
|
COPY app /run/app
|
||||||
|
|
||||||
|
RUN chown -R user:user /run
|
||||||
|
USER user
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
sonar.projectKey=roboluke
|
sonar.projectKey=roboluke
|
||||||
sonar.projectName=roboluke
|
sonar.projectName=roboluke
|
||||||
sonar.python.version=3.13
|
sonar.python.version=3.14
|
||||||
sonar.python.coverage.reportPaths=coverage.xml
|
sonar.python.coverage.reportPaths=coverage.xml
|
||||||
sonar.python.pylint.reportPaths=lintreport.txt
|
sonar.python.pylint.reportPaths=lintreport.txt
|
||||||
sonar.python.xunit.reportPath=testresults.xml
|
sonar.python.xunit.reportPath=testresults.xml
|
||||||
sonar.docker.hadolint.reportPaths=hadolint.out
|
sonar.docker.hadolint.reportPaths=hadolint.out
|
||||||
sonar.sources=Dockerfile,app
|
sonar.sources=Dockerfile,app
|
||||||
sonar.tests=tests
|
sonar.tests=tests
|
||||||
sonar.exclusions=,.github/**,.gitignore,CODEOWNERS,CHANGELOG.md,LICENSE.md,README.md,renovate.json,requirements-dev.txt,requirements.txt
|
sonar.exclusions=.archive/**,.github/**,.gitea/**,.gitignore,CODEOWNERS,CHANGELOG.md,LICENSE.md,README.md,renovate.json,requirements-dev.txt,requirements.txt
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# ruff: noqa: E402 pylint: disable=wrong-import-position
|
# ruff: noqa: E402 pylint: disable=wrong-import-position
|
||||||
|
|
||||||
"""Provides test cases for app/utils/config.py."""
|
"""Provides test cases for app/utils/config.py."""
|
||||||
@@ -27,7 +25,7 @@ def test_config() -> None:
|
|||||||
os.environ[config_var] = value
|
os.environ[config_var] = value
|
||||||
|
|
||||||
# needs to be imported AFTER environment variables are set
|
# needs to be imported AFTER environment variables are set
|
||||||
from app.utils.config import config # pragma: no cover
|
from app.utils.config import config # pragma: no cover # NOSONAR (C0415)
|
||||||
|
|
||||||
assert config.admin_emails == config_vars["ADMIN_EMAIL"].split(",")
|
assert config.admin_emails == config_vars["ADMIN_EMAIL"].split(",")
|
||||||
assert config.admin_first_name == config_vars["ADMIN_FIRST_NAME"]
|
assert config.admin_first_name == config_vars["ADMIN_FIRST_NAME"]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# ruff: noqa: E402 pylint: disable=wrong-import-position
|
# ruff: noqa: E402 pylint: disable=wrong-import-position
|
||||||
|
|
||||||
"""Provides test cases for app/utils/config.py."""
|
"""Provides test cases for app/utils/config.py."""
|
||||||
@@ -24,7 +22,7 @@ def test_config_no_admin_vars() -> None:
|
|||||||
os.environ[config_var] = value
|
os.environ[config_var] = value
|
||||||
|
|
||||||
# needs to be imported AFTER environment variables are set
|
# needs to be imported AFTER environment variables are set
|
||||||
from app.utils.config import config # pragma: no cover
|
from app.utils.config import config # pragma: no cover # NOSONAR (C0415)
|
||||||
|
|
||||||
assert config.approved_domains == []
|
assert config.approved_domains == []
|
||||||
assert config.approved_rooms == []
|
assert config.approved_rooms == []
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""Provides test cases for app/utils/datetime.py."""
|
"""Provides test cases for app/utils/datetime.py."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""Provides test cases for app/utils/helpers.py."""
|
"""Provides test cases for app/utils/helpers.py."""
|
||||||
|
|
||||||
from app.utils.helpers import validate_email_syntax # pragma: no cover
|
from app.utils.helpers import validate_email_syntax # pragma: no cover
|
||||||
|
|||||||
28
uv.lock
generated
28
uv.lock
generated
@@ -247,11 +247,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.9.4"
|
version = "4.9.6"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -319,7 +319,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.2"
|
version = "9.0.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
@@ -328,9 +328,9 @@ dependencies = [
|
|||||||
{ name = "pluggy" },
|
{ name = "pluggy" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -498,14 +498,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zope-interface"
|
name = "zope-interface"
|
||||||
version = "8.2"
|
version = "8.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/04/0b1d92e7d31507c5fbe203d9cc1ae80fb0645688c7af751ea0ec18c2223e/zope_interface-8.3.tar.gz", hash = "sha256:e1a9de7d0b5b5c249a73b91aebf4598ce05e334303af6aa94865893283e9ff10", size = 256822, upload-time = "2026-04-10T06:12:35.036Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/47/45188fb101fa060b20e6090e500682398ab415e516a0c228fbb22bc7def2/zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec", size = 209170, upload-time = "2026-01-09T08:05:26.616Z" },
|
{ url = "https://files.pythonhosted.org/packages/27/da/ff205c5463e52ad64cc40be667fdff2b01b9754a385c6b95bac01645fa4f/zope_interface-8.3-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:1aa0e1d72212cedc38b2156bbca08cf24625c057135a7947ef6b19bc732b2772", size = 211889, upload-time = "2026-04-10T06:22:27.612Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/03/f6b9336c03c2b48403c4eb73a1ec961d94dc2fb5354c583dfb5fa05fd41f/zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c", size = 209229, upload-time = "2026-01-09T08:05:28.521Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/21/0cc848e22769b1cf4c0cd636ec2e60ea05cfb958423435ea526d5a291fe8/zope_interface-8.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54ab83218a8f6947ba4b6cb1a121f1e1abe2e418b838ccdac71639d0f97e734e", size = 211961, upload-time = "2026-04-10T06:22:29.575Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/b1/65fe1dca708569f302ade02e6cdca309eab6752bc9f80105514f5b708651/zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664", size = 259393, upload-time = "2026-01-09T08:05:29.897Z" },
|
{ url = "https://files.pythonhosted.org/packages/e3/54/815c9dbb90336c50694b4c7ef7ced06bc389e5597200c77457b557a0221c/zope_interface-8.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:34d6c10fa790005487c471e0e4ab537b0fa9a70e55a96994e51ffeef92205fa4", size = 264409, upload-time = "2026-04-10T06:22:31.426Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/a5/97b49cfceb6ed53d3dcfb3f3ebf24d83b5553194f0337fbbb3a9fec6cf78/zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0", size = 264863, upload-time = "2026-01-09T08:05:31.501Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/69/2e5c30adde0e94552d934971fa6eba107449d3d11fa086cfcfeb8ea6354d/zope_interface-8.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93108d5f8dee20177a637438bf4df4c6faf8a317c9d4a8b1d5e78123854e3317", size = 269592, upload-time = "2026-04-10T06:22:33.393Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/02/0b7a77292810efe3a0586a505b077ebafd5114e10c6e6e659f0c8e387e1f/zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb", size = 264369, upload-time = "2026-01-09T08:05:32.941Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/8a/fbb1dceb5c5400b2b27934aa102d29fe4cb06732122e7f409efebeb6e097/zope_interface-8.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f81d90f80b9fbf36602549e2f187861c9d7139837f8c9dd685ce3b933c6360f", size = 269548, upload-time = "2026-04-10T06:22:35.339Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/1d/0d1ff3846302ed1b5bbf659316d8084b30106770a5f346b7ff4e9f540f80/zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028", size = 212447, upload-time = "2026-01-09T08:05:35.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/70/abd0bb9cc9b1a9a718f30c81f46a184a2e751dd80cf57db142ffa42730da/zope_interface-8.3-cp313-cp313-win_amd64.whl", hash = "sha256:96106a5f609bb355e1aec6ab0361213c8af0843ca1e1ba9c42eacfbd0910914e", size = 214391, upload-time = "2026-04-10T06:22:36.969Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user