mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 00:37:13 +00:00
Delete subscription on shutdown of SmartThings (#140135)
* Cache subscription url in SmartThings * Cache subscription url in SmartThings * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Bump pysmartthings to 2.7.1 * 2.7.2 --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
b5c7bdd98f
commit
f2f653efcf
@ -16,12 +16,18 @@ from pysmartthings import (
|
|||||||
Scene,
|
Scene,
|
||||||
SmartThings,
|
SmartThings,
|
||||||
SmartThingsAuthenticationFailedError,
|
SmartThingsAuthenticationFailedError,
|
||||||
|
SmartThingsSinkError,
|
||||||
Status,
|
Status,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
|
from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_TOKEN,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@ -33,6 +39,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
|||||||
from .const import (
|
from .const import (
|
||||||
CONF_INSTALLED_APP_ID,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
|
CONF_SUBSCRIPTION_ID,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_BUTTON,
|
EVENT_BUTTON,
|
||||||
MAIN,
|
MAIN,
|
||||||
@ -99,6 +106,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
|||||||
|
|
||||||
client.refresh_token_function = _refresh_token
|
client.refresh_token_function = _refresh_token
|
||||||
|
|
||||||
|
def _handle_max_connections() -> None:
|
||||||
|
_LOGGER.debug("We hit the limit of max connections")
|
||||||
|
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||||
|
|
||||||
|
client.max_connections_reached_callback = _handle_max_connections
|
||||||
|
|
||||||
|
def _handle_new_subscription_identifier(identifier: str | None) -> None:
|
||||||
|
"""Handle a new subscription identifier."""
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
data={
|
||||||
|
**entry.data,
|
||||||
|
CONF_SUBSCRIPTION_ID: identifier,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if identifier is not None:
|
||||||
|
_LOGGER.debug("Updating subscription ID to %s", identifier)
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Removing subscription ID")
|
||||||
|
|
||||||
|
client.new_subscription_id_callback = _handle_new_subscription_identifier
|
||||||
|
|
||||||
|
if (old_identifier := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
|
||||||
|
_LOGGER.debug("Trying to delete old subscription %s", old_identifier)
|
||||||
|
await client.delete_subscription(old_identifier)
|
||||||
|
|
||||||
|
_LOGGER.debug("Trying to create a new subscription")
|
||||||
|
try:
|
||||||
|
subscription = await client.create_subscription(
|
||||||
|
entry.data[CONF_LOCATION_ID],
|
||||||
|
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
|
||||||
|
)
|
||||||
|
except SmartThingsSinkError as err:
|
||||||
|
_LOGGER.debug("Couldn't create a new subscription: %s", err)
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
subscription_id = subscription.subscription_id
|
||||||
|
_handle_new_subscription_identifier(subscription_id)
|
||||||
|
|
||||||
|
entry.async_create_background_task(
|
||||||
|
hass,
|
||||||
|
client.subscribe(
|
||||||
|
entry.data[CONF_LOCATION_ID],
|
||||||
|
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
|
||||||
|
subscription,
|
||||||
|
),
|
||||||
|
"smartthings_socket",
|
||||||
|
)
|
||||||
|
|
||||||
device_status: dict[str, FullDevice] = {}
|
device_status: dict[str, FullDevice] = {}
|
||||||
try:
|
try:
|
||||||
devices = await client.get_devices()
|
devices = await client.get_devices()
|
||||||
@ -145,12 +200,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
|||||||
client.add_unspecified_device_event_listener(handle_button_press)
|
client.add_unspecified_device_event_listener(handle_button_press)
|
||||||
)
|
)
|
||||||
|
|
||||||
entry.async_create_background_task(
|
async def _handle_shutdown(_: Event) -> None:
|
||||||
hass,
|
"""Handle shutdown."""
|
||||||
client.subscribe(
|
await client.delete_subscription(subscription_id)
|
||||||
entry.data[CONF_LOCATION_ID], entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID]
|
|
||||||
),
|
entry.async_on_unload(
|
||||||
"smartthings_webhook",
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _handle_shutdown)
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
@ -176,6 +231,9 @@ async def async_unload_entry(
|
|||||||
hass: HomeAssistant, entry: SmartThingsConfigEntry
|
hass: HomeAssistant, entry: SmartThingsConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
|
client = entry.runtime_data.client
|
||||||
|
if (subscription_id := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
|
||||||
|
await client.delete_subscription(subscription_id)
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,4 +33,5 @@ CONF_REFRESH_TOKEN = "refresh_token"
|
|||||||
MAIN = "main"
|
MAIN = "main"
|
||||||
OLD_DATA = "old_data"
|
OLD_DATA = "old_data"
|
||||||
|
|
||||||
|
CONF_SUBSCRIPTION_ID = "subscription_id"
|
||||||
EVENT_BUTTON = "smartthings.button"
|
EVENT_BUTTON = "smartthings.button"
|
||||||
|
@ -29,5 +29,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/smartthings",
|
"documentation": "https://www.home-assistant.io/integrations/smartthings",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pysmartthings"],
|
"loggers": ["pysmartthings"],
|
||||||
"requirements": ["pysmartthings==2.7.0"]
|
"requirements": ["pysmartthings==2.7.2"]
|
||||||
}
|
}
|
||||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -2310,7 +2310,7 @@ pysma==0.7.5
|
|||||||
pysmappee==0.2.29
|
pysmappee==0.2.29
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==2.7.0
|
pysmartthings==2.7.2
|
||||||
|
|
||||||
# homeassistant.components.smarty
|
# homeassistant.components.smarty
|
||||||
pysmarty2==0.10.2
|
pysmarty2==0.10.2
|
||||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -1882,7 +1882,7 @@ pysma==0.7.5
|
|||||||
pysmappee==0.2.29
|
pysmappee==0.2.29
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==2.7.0
|
pysmartthings==2.7.2
|
||||||
|
|
||||||
# homeassistant.components.smarty
|
# homeassistant.components.smarty
|
||||||
pysmarty2==0.10.2
|
pysmarty2==0.10.2
|
||||||
|
@ -9,6 +9,7 @@ from pysmartthings.models import (
|
|||||||
DeviceStatus,
|
DeviceStatus,
|
||||||
LocationResponse,
|
LocationResponse,
|
||||||
SceneResponse,
|
SceneResponse,
|
||||||
|
Subscription,
|
||||||
)
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -78,6 +79,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
|
|||||||
client.get_locations.return_value = LocationResponse.from_json(
|
client.get_locations.return_value = LocationResponse.from_json(
|
||||||
load_fixture("locations.json", DOMAIN)
|
load_fixture("locations.json", DOMAIN)
|
||||||
).items
|
).items
|
||||||
|
client.create_subscription.return_value = Subscription.from_json(
|
||||||
|
load_fixture("subscription.json", DOMAIN)
|
||||||
|
)
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
16
tests/components/smartthings/fixtures/subscription.json
Normal file
16
tests/components/smartthings/fixtures/subscription.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"subscriptionId": "f5768ce8-c9e5-4507-9020-912c0c60e0ab",
|
||||||
|
"registrationUrl": "https://spigot-regional.api.smartthings.com/filters/f5768ce8-c9e5-4507-9020-912c0c60e0ab/activate?filterRegion=eu-west-1",
|
||||||
|
"name": "My Home Assistant sub",
|
||||||
|
"version": 20250122,
|
||||||
|
"subscriptionFilters": [
|
||||||
|
{
|
||||||
|
"type": "LOCATIONIDS",
|
||||||
|
"value": ["88a3a314-f0c8-40b4-bb44-44ba06c9c42e"],
|
||||||
|
"eventType": ["DEVICE_EVENT"],
|
||||||
|
"attribute": null,
|
||||||
|
"capability": null,
|
||||||
|
"component": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -10,6 +10,7 @@ from homeassistant.components.smartthings.const import (
|
|||||||
CONF_INSTALLED_APP_ID,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
CONF_REFRESH_TOKEN,
|
CONF_REFRESH_TOKEN,
|
||||||
|
CONF_SUBSCRIPTION_ID,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
||||||
@ -508,6 +509,7 @@ async def test_migration(
|
|||||||
"installed_app_id": "123123123-2be1-4e40-b257-e4ef59083324",
|
"installed_app_id": "123123123-2be1-4e40-b257-e4ef59083324",
|
||||||
},
|
},
|
||||||
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
|
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
|
||||||
|
CONF_SUBSCRIPTION_ID: "f5768ce8-c9e5-4507-9020-912c0c60e0ab",
|
||||||
}
|
}
|
||||||
assert mock_old_config_entry.unique_id == "397678e5-9995-4a39-9d9f-ae6ba310236c"
|
assert mock_old_config_entry.unique_id == "397678e5-9995-4a39-9d9f-ae6ba310236c"
|
||||||
assert mock_old_config_entry.version == 3
|
assert mock_old_config_entry.version == 3
|
||||||
|
@ -2,18 +2,21 @@
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from pysmartthings import Attribute, Capability
|
from pysmartthings import Attribute, Capability, SmartThingsSinkError
|
||||||
|
from pysmartthings.models import Subscription
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.smartthings import EVENT_BUTTON
|
from homeassistant.components.smartthings import EVENT_BUTTON
|
||||||
from homeassistant.components.smartthings.const import DOMAIN
|
from homeassistant.components.smartthings.const import CONF_SUBSCRIPTION_ID, DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from . import setup_integration, trigger_update
|
from . import setup_integration, trigger_update
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
|
||||||
async def test_devices(
|
async def test_devices(
|
||||||
@ -63,6 +66,178 @@ async def test_button_event(
|
|||||||
assert events[0] == snapshot
|
assert events[0] == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_create_subscription(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a subscription."""
|
||||||
|
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
devices.create_subscription.assert_called_once()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||||
|
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||||
|
)
|
||||||
|
|
||||||
|
devices.subscribe.assert_called_once_with(
|
||||||
|
"397678e5-9995-4a39-9d9f-ae6ba310236c",
|
||||||
|
"5aaaa925-2be1-4e40-b257-e4ef59083324",
|
||||||
|
Subscription.from_json(load_fixture("subscription.json", DOMAIN)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_create_subscription_sink_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test handling an error when creating a subscription."""
|
||||||
|
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
|
||||||
|
|
||||||
|
devices.create_subscription.side_effect = SmartThingsSinkError("Sink error")
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
devices.subscribe.assert_not_called()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_update_subscription_identifier(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test updating the subscription identifier."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||||
|
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||||
|
)
|
||||||
|
|
||||||
|
devices.new_subscription_id_callback("abc")
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] == "abc"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_stale_subscription_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test updating the subscription identifier."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
mock_config_entry,
|
||||||
|
data={**mock_config_entry.data, CONF_SUBSCRIPTION_ID: "test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||||
|
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||||
|
)
|
||||||
|
devices.delete_subscription.assert_called_once_with("test")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_remove_subscription_identifier(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test removing the subscription identifier."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||||
|
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||||
|
)
|
||||||
|
|
||||||
|
devices.new_subscription_id_callback(None)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_max_connections_handling(
|
||||||
|
hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test handling reaching max connections."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||||
|
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||||
|
)
|
||||||
|
|
||||||
|
devices.create_subscription.side_effect = SmartThingsSinkError("Sink error")
|
||||||
|
|
||||||
|
devices.max_connections_reached_callback()
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_unloading(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test unloading the integration."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
devices.delete_subscription.assert_called_once_with(
|
||||||
|
"f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||||
|
)
|
||||||
|
# Deleting the subscription automatically deletes the subscription ID
|
||||||
|
devices.new_subscription_id_callback(None)
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_shutdown(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test shutting down Home Assistant."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
|
devices.delete_subscription.assert_called_once_with(
|
||||||
|
"f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||||
|
)
|
||||||
|
# Deleting the subscription automatically deletes the subscription ID
|
||||||
|
devices.new_subscription_id_callback(None)
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
async def test_removing_stale_devices(
|
async def test_removing_stale_devices(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user