mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Remove deprecated camera async_handle_web_rtc_offer function (#144561)
This commit is contained in:
parent
4dde314338
commit
7b23f21712
@ -55,7 +55,6 @@ from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
deprecated_function,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
@ -86,10 +85,10 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
|
||||
from .webrtc import (
|
||||
DATA_ICE_SERVERS,
|
||||
CameraWebRTCProvider,
|
||||
WebRTCAnswer,
|
||||
WebRTCAnswer, # noqa: F401
|
||||
WebRTCCandidate, # noqa: F401
|
||||
WebRTCClientConfiguration,
|
||||
WebRTCError,
|
||||
WebRTCError, # noqa: F401
|
||||
WebRTCMessage, # noqa: F401
|
||||
WebRTCSendMessage,
|
||||
async_get_supported_provider,
|
||||
@ -473,9 +472,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
self.async_update_token()
|
||||
self._create_stream_lock: asyncio.Lock | None = None
|
||||
self._webrtc_provider: CameraWebRTCProvider | None = None
|
||||
self._supports_native_sync_webrtc = (
|
||||
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
|
||||
)
|
||||
self._supports_native_async_webrtc = (
|
||||
type(self).async_handle_async_webrtc_offer
|
||||
!= Camera.async_handle_async_webrtc_offer
|
||||
@ -579,15 +575,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""
|
||||
return None
|
||||
|
||||
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
|
||||
"""Handle the WebRTC offer and return an answer.
|
||||
|
||||
This is used by cameras with CameraEntityFeature.STREAM
|
||||
and StreamType.WEB_RTC.
|
||||
|
||||
Integrations can override with a native WebRTC implementation.
|
||||
"""
|
||||
|
||||
async def async_handle_async_webrtc_offer(
|
||||
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
|
||||
) -> None:
|
||||
@ -600,42 +587,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
|
||||
Integrations can override with a native WebRTC implementation.
|
||||
"""
|
||||
if self._supports_native_sync_webrtc:
|
||||
try:
|
||||
answer = await deprecated_function(
|
||||
"async_handle_async_webrtc_offer",
|
||||
breaks_in_ha_version="2025.6",
|
||||
)(self.async_handle_web_rtc_offer)(offer_sdp)
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Error handling WebRTC offer: %s", ex)
|
||||
send_message(
|
||||
WebRTCError(
|
||||
"webrtc_offer_failed",
|
||||
str(ex),
|
||||
)
|
||||
)
|
||||
except TimeoutError:
|
||||
# This catch was already here and should stay through the deprecation
|
||||
_LOGGER.error("Timeout handling WebRTC offer")
|
||||
send_message(
|
||||
WebRTCError(
|
||||
"webrtc_offer_failed",
|
||||
"Timeout handling WebRTC offer",
|
||||
)
|
||||
)
|
||||
else:
|
||||
if answer:
|
||||
send_message(WebRTCAnswer(answer))
|
||||
else:
|
||||
_LOGGER.error("Error handling WebRTC offer: No answer")
|
||||
send_message(
|
||||
WebRTCError(
|
||||
"webrtc_offer_failed",
|
||||
"No answer on WebRTC offer",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if self._webrtc_provider:
|
||||
await self._webrtc_provider.async_handle_async_webrtc_offer(
|
||||
self, offer_sdp, session_id, send_message
|
||||
@ -764,9 +715,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
new_provider = None
|
||||
|
||||
# Skip all providers if the camera has a native WebRTC implementation
|
||||
if not (
|
||||
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
|
||||
):
|
||||
if not self._supports_native_async_webrtc:
|
||||
# Camera doesn't have a native WebRTC implementation
|
||||
new_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_provider
|
||||
@ -798,9 +747,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
|
||||
config = self._async_get_webrtc_client_configuration()
|
||||
|
||||
if not self._supports_native_sync_webrtc:
|
||||
# Until 2024.11, the frontend was not resolving any ice servers
|
||||
# The async approach was added 2024.11 and new integrations need to use it
|
||||
ice_servers = [
|
||||
server
|
||||
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
|
||||
@ -808,8 +754,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
]
|
||||
config.configuration.ice_servers.extend(ice_servers)
|
||||
|
||||
config.get_candidates_upfront = self._supports_native_sync_webrtc
|
||||
|
||||
return config
|
||||
|
||||
async def async_on_webrtc_candidate(
|
||||
@ -838,7 +782,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the camera capabilities."""
|
||||
frontend_stream_types = set()
|
||||
if CameraEntityFeature.STREAM in self.supported_features_compat:
|
||||
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
|
||||
if self._supports_native_async_webrtc:
|
||||
# The camera has a native WebRTC implementation
|
||||
frontend_stream_types.add(StreamType.WEB_RTC)
|
||||
else:
|
||||
|
@ -111,13 +111,11 @@ class WebRTCClientConfiguration:
|
||||
|
||||
configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
|
||||
data_channel: str | None = None
|
||||
get_candidates_upfront: bool = False
|
||||
|
||||
def to_frontend_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict that can be used by the frontend."""
|
||||
data: dict[str, Any] = {
|
||||
"configuration": self.configuration.to_dict(),
|
||||
"getCandidatesUpfront": self.get_candidates_upfront,
|
||||
}
|
||||
if self.data_channel is not None:
|
||||
data["dataChannel"] = self.data_channel
|
||||
|
@ -165,13 +165,15 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
|
||||
async def stream_source(self) -> str | None:
|
||||
return STREAM_SOURCE
|
||||
|
||||
class SyncCamera(BaseCamera):
|
||||
"""Mock Camera with native sync WebRTC support."""
|
||||
class AsyncNoCandidateCamera(BaseCamera):
|
||||
"""Mock Camera with native async WebRTC support but not implemented candidate support."""
|
||||
|
||||
_attr_name = "Sync"
|
||||
_attr_name = "Async No Candidate"
|
||||
|
||||
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
|
||||
return WEBRTC_ANSWER
|
||||
async def async_handle_async_webrtc_offer(
|
||||
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
|
||||
) -> None:
|
||||
send_message(WebRTCAnswer(WEBRTC_ANSWER))
|
||||
|
||||
class AsyncCamera(BaseCamera):
|
||||
"""Mock Camera with native async WebRTC support."""
|
||||
@ -221,7 +223,10 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
|
||||
),
|
||||
)
|
||||
setup_test_component_platform(
|
||||
hass, camera.DOMAIN, [SyncCamera(), AsyncCamera()], from_config_entry=True
|
||||
hass,
|
||||
camera.DOMAIN,
|
||||
[AsyncNoCandidateCamera(), AsyncCamera()],
|
||||
from_config_entry=True,
|
||||
)
|
||||
mock_platform(hass, f"{domain}.config_flow", Mock())
|
||||
|
||||
|
@ -968,24 +968,19 @@ async def test_camera_capabilities_webrtc(
|
||||
"""Test WebRTC camera capabilities."""
|
||||
|
||||
await _test_capabilities(
|
||||
hass, hass_ws_client, "camera.sync", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
|
||||
hass, hass_ws_client, "camera.async", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expect_native_async_webrtc"),
|
||||
[("camera.sync", False), ("camera.async", True)],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras", "register_test_provider")
|
||||
async def test_webrtc_provider_not_added_for_native_webrtc(
|
||||
hass: HomeAssistant, entity_id: str, expect_native_async_webrtc: bool
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that a WebRTC provider is not added to a camera when the camera has native WebRTC support."""
|
||||
camera_obj = get_camera_from_entity_id(hass, entity_id)
|
||||
camera_obj = get_camera_from_entity_id(hass, "camera.async")
|
||||
assert camera_obj
|
||||
assert camera_obj._webrtc_provider is None
|
||||
assert camera_obj._supports_native_sync_webrtc is not expect_native_async_webrtc
|
||||
assert camera_obj._supports_native_async_webrtc is expect_native_async_webrtc
|
||||
assert camera_obj._supports_native_async_webrtc is True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
|
||||
@ -1016,14 +1011,12 @@ async def test_camera_capabilities_changing_non_native_support(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
@pytest.mark.parametrize(("entity_id"), ["camera.sync", "camera.async"])
|
||||
async def test_camera_capabilities_changing_native_support(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test WebRTC camera capabilities."""
|
||||
cam = get_camera_from_entity_id(hass, entity_id)
|
||||
cam = get_camera_from_entity_id(hass, "camera.async")
|
||||
assert cam.supported_features == camera.CameraEntityFeature.STREAM
|
||||
|
||||
await _test_capabilities(
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Test camera WebRTC."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
@ -10,9 +9,7 @@ from webrtc_models import RTCIceCandidate, RTCIceCandidateInit, RTCIceServer
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
DATA_ICE_SERVERS,
|
||||
DOMAIN as CAMERA_DOMAIN,
|
||||
Camera,
|
||||
CameraEntityFeature,
|
||||
CameraWebRTCProvider,
|
||||
StreamType,
|
||||
WebRTCAnswer,
|
||||
@ -25,22 +22,12 @@ from homeassistant.components.camera import (
|
||||
get_camera_from_entity_id,
|
||||
)
|
||||
from homeassistant.components.websocket_api import TYPE_RESULT
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockModule,
|
||||
mock_config_flow,
|
||||
mock_integration,
|
||||
mock_platform,
|
||||
setup_test_component_platform,
|
||||
)
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
WEBRTC_OFFER = "v=0\r\n"
|
||||
@ -57,84 +44,6 @@ class Go2RTCProvider(SomeTestProvider):
|
||||
return "go2rtc"
|
||||
|
||||
|
||||
class MockCamera(Camera):
|
||||
"""Mock Camera Entity."""
|
||||
|
||||
_attr_name = "Test"
|
||||
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the mock entity."""
|
||||
super().__init__()
|
||||
self._sync_answer: str | None | Exception = WEBRTC_ANSWER
|
||||
|
||||
def set_sync_answer(self, value: str | None | Exception) -> None:
|
||||
"""Set sync offer answer."""
|
||||
self._sync_answer = value
|
||||
|
||||
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
|
||||
"""Handle the WebRTC offer and return the answer."""
|
||||
if isinstance(self._sync_answer, Exception):
|
||||
raise self._sync_answer
|
||||
return self._sync_answer
|
||||
|
||||
async def stream_source(self) -> str | None:
|
||||
"""Return the source of the stream.
|
||||
|
||||
This is used by cameras with CameraEntityFeature.STREAM
|
||||
and StreamType.HLS.
|
||||
"""
|
||||
return "rtsp://stream"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_test_integration(
|
||||
hass: HomeAssistant,
|
||||
) -> MockCamera:
|
||||
"""Initialize components."""
|
||||
|
||||
entry = MockConfigEntry(domain=TEST_INTEGRATION_DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
async def async_setup_entry_init(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up test config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, [CAMERA_DOMAIN]
|
||||
)
|
||||
return True
|
||||
|
||||
async def async_unload_entry_init(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Unload test config entry."""
|
||||
await hass.config_entries.async_forward_entry_unload(
|
||||
config_entry, CAMERA_DOMAIN
|
||||
)
|
||||
return True
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
TEST_INTEGRATION_DOMAIN,
|
||||
async_setup_entry=async_setup_entry_init,
|
||||
async_unload_entry=async_unload_entry_init,
|
||||
),
|
||||
)
|
||||
test_camera = MockCamera()
|
||||
setup_test_component_platform(
|
||||
hass, CAMERA_DOMAIN, [test_camera], from_config_entry=True
|
||||
)
|
||||
mock_platform(hass, f"{TEST_INTEGRATION_DOMAIN}.config_flow", Mock())
|
||||
|
||||
with mock_config_flow(TEST_INTEGRATION_DOMAIN, ConfigFlow):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return test_camera
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
|
||||
async def test_async_register_webrtc_provider(
|
||||
hass: HomeAssistant,
|
||||
@ -302,7 +211,6 @@ async def test_ws_get_client_config(
|
||||
},
|
||||
],
|
||||
},
|
||||
"getCandidatesUpfront": False,
|
||||
}
|
||||
|
||||
@callback
|
||||
@ -341,30 +249,6 @@ async def test_ws_get_client_config(
|
||||
},
|
||||
],
|
||||
},
|
||||
"getCandidatesUpfront": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_ws_get_client_config_sync_offer(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test get WebRTC client config, when camera is supporting sync offer."""
|
||||
await async_setup_component(hass, "camera", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{"type": "camera/webrtc/get_client_config", "entity_id": "camera.sync"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
# Assert WebSocket response
|
||||
assert msg["type"] == TYPE_RESULT
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
"configuration": {},
|
||||
"getCandidatesUpfront": True,
|
||||
}
|
||||
|
||||
|
||||
@ -391,7 +275,6 @@ async def test_ws_get_client_config_custom_config(
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
"configuration": {"iceServers": [{"urls": ["stun:custom_stun_server:3478"]}]},
|
||||
"getCandidatesUpfront": False,
|
||||
}
|
||||
|
||||
|
||||
@ -625,144 +508,6 @@ async def test_websocket_webrtc_offer_missing_offer(
|
||||
assert response["error"]["code"] == "invalid_format"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error", "expected_message"),
|
||||
[
|
||||
(ValueError("value error"), "value error"),
|
||||
(HomeAssistantError("offer failed"), "offer failed"),
|
||||
(TimeoutError(), "Timeout handling WebRTC offer"),
|
||||
],
|
||||
)
|
||||
async def test_websocket_webrtc_offer_failure(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
init_test_integration: MockCamera,
|
||||
error: Exception,
|
||||
expected_message: str,
|
||||
) -> None:
|
||||
"""Test WebRTC stream that fails handling the offer."""
|
||||
client = await hass_ws_client(hass)
|
||||
init_test_integration.set_sync_answer(error)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/offer",
|
||||
"entity_id": "camera.test",
|
||||
"offer": WEBRTC_OFFER,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert response["success"]
|
||||
subscription_id = response["id"]
|
||||
|
||||
# Session id
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"]["type"] == "session"
|
||||
|
||||
# Error
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"] == {
|
||||
"type": "error",
|
||||
"code": "webrtc_offer_failed",
|
||||
"message": expected_message,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_websocket_webrtc_offer_sync(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test sync WebRTC stream offer."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/offer",
|
||||
"entity_id": "camera.sync",
|
||||
"offer": WEBRTC_OFFER,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert (
|
||||
"tests.components.camera.conftest",
|
||||
logging.WARNING,
|
||||
(
|
||||
"async_handle_web_rtc_offer was called from camera, this is a deprecated "
|
||||
"function which will be removed in HA Core 2025.6. Use "
|
||||
"async_handle_async_webrtc_offer instead"
|
||||
),
|
||||
) in caplog.record_tuples
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert response["success"]
|
||||
subscription_id = response["id"]
|
||||
|
||||
# Session id
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"]["type"] == "session"
|
||||
|
||||
# Answer
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"] == {"type": "answer", "answer": WEBRTC_ANSWER}
|
||||
|
||||
|
||||
async def test_websocket_webrtc_offer_sync_no_answer(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
init_test_integration: MockCamera,
|
||||
) -> None:
|
||||
"""Test sync WebRTC stream offer with no answer."""
|
||||
client = await hass_ws_client(hass)
|
||||
init_test_integration.set_sync_answer(None)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/offer",
|
||||
"entity_id": "camera.test",
|
||||
"offer": WEBRTC_OFFER,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert response["success"]
|
||||
subscription_id = response["id"]
|
||||
|
||||
# Session id
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"]["type"] == "session"
|
||||
|
||||
# Answer
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"] == {
|
||||
"type": "error",
|
||||
"code": "webrtc_offer_failed",
|
||||
"message": "No answer on WebRTC offer",
|
||||
}
|
||||
assert (
|
||||
"homeassistant.components.camera",
|
||||
logging.ERROR,
|
||||
"Error handling WebRTC offer: No answer",
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera")
|
||||
async def test_websocket_webrtc_offer_invalid_stream_type(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
@ -901,7 +646,7 @@ async def test_ws_webrtc_candidate_not_supported(
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/candidate",
|
||||
"entity_id": "camera.sync",
|
||||
"entity_id": "camera.async_no_candidate",
|
||||
"session_id": "session_id",
|
||||
"candidate": {"candidate": "candidate"},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user