diff --git a/homeassistant/components/mastodon/const.py b/homeassistant/components/mastodon/const.py index 145b4e969feb..b26aca307efa 100644 --- a/homeassistant/components/mastodon/const.py +++ b/homeassistant/components/mastodon/const.py @@ -12,6 +12,7 @@ DATA_HASS_CONFIG = "mastodon_hass_config" DEFAULT_URL: Final = "https://mastodon.social" DEFAULT_NAME: Final = "Mastodon" +ATTR_ACCOUNT_NAME = "account_name" ATTR_STATUS = "status" ATTR_VISIBILITY = "visibility" ATTR_IDEMPOTENCY_KEY = "idempotency_key" diff --git a/homeassistant/components/mastodon/icons.json b/homeassistant/components/mastodon/icons.json index ba1f748b3c1d..2883f2e857f1 100644 --- a/homeassistant/components/mastodon/icons.json +++ b/homeassistant/components/mastodon/icons.json @@ -32,6 +32,9 @@ } }, "services": { + "get_account": { + "service": "mdi:account-search" + }, "post": { "service": "mdi:message-text" } diff --git a/homeassistant/components/mastodon/services.py b/homeassistant/components/mastodon/services.py index 2cca69fdfb1e..dbb5fc2afdc9 100644 --- a/homeassistant/components/mastodon/services.py +++ b/homeassistant/components/mastodon/services.py @@ -5,15 +5,22 @@ from functools import partial from typing import Any from mastodon import Mastodon -from mastodon.Mastodon import MastodonAPIError, MediaAttachment +from mastodon.Mastodon import Account, MastodonAPIError, MediaAttachment import voluptuous as vol from homeassistant.const import ATTR_CONFIG_ENTRY_ID -from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse, callback +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import service from .const import ( + ATTR_ACCOUNT_NAME, ATTR_CONTENT_WARNING, ATTR_IDEMPOTENCY_KEY, ATTR_LANGUAGE, @@ -37,6 +44,13 @@ class StatusVisibility(StrEnum): DIRECT = "direct" +SERVICE_GET_ACCOUNT = "get_account" +SERVICE_GET_ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_ACCOUNT_NAME): str, + } +) SERVICE_POST = "post" SERVICE_POST_SCHEMA = vol.Schema( { @@ -56,95 +70,127 @@ SERVICE_POST_SCHEMA = vol.Schema( @callback def async_setup_services(hass: HomeAssistant) -> None: """Set up the services for the Mastodon integration.""" + hass.services.async_register( + DOMAIN, + SERVICE_GET_ACCOUNT, + _async_get_account, + schema=SERVICE_GET_ACCOUNT_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + hass.services.async_register( + DOMAIN, SERVICE_POST, _async_post, schema=SERVICE_POST_SCHEMA + ) - async def async_post(call: ServiceCall) -> ServiceResponse: - """Post a status.""" - entry: MastodonConfigEntry = service.async_get_config_entry( - hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID] + +async def _async_get_account(call: ServiceCall) -> ServiceResponse: + """Get account information.""" + entry: MastodonConfigEntry = service.async_get_config_entry( + call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID] + ) + client = entry.runtime_data.client + + account_name: str = call.data[ATTR_ACCOUNT_NAME] + + try: + account: Account = await call.hass.async_add_executor_job( + partial(client.account_lookup, acct=account_name) ) - client = entry.runtime_data.client + except MastodonAPIError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="unable_to_get_account", + translation_placeholders={"account_name": account_name}, + ) from err - status: str = call.data[ATTR_STATUS] + return {"account": account} - visibility: str | None = ( - StatusVisibility(call.data[ATTR_VISIBILITY]) - if ATTR_VISIBILITY in call.data - else None + +async def _async_post(call: ServiceCall) -> ServiceResponse: + """Post a status.""" + entry: MastodonConfigEntry = service.async_get_config_entry( + call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID] + ) + client = entry.runtime_data.client + + status: str = call.data[ATTR_STATUS] + + visibility: str | None = ( + StatusVisibility(call.data[ATTR_VISIBILITY]) + if ATTR_VISIBILITY in call.data + else None + ) + idempotency_key: str | None = call.data.get(ATTR_IDEMPOTENCY_KEY) + spoiler_text: str | None = call.data.get(ATTR_CONTENT_WARNING) + language: str | None = call.data.get(ATTR_LANGUAGE) + media_path: str | None = call.data.get(ATTR_MEDIA) + media_description: str | None = call.data.get(ATTR_MEDIA_DESCRIPTION) + media_warning: str | None = call.data.get(ATTR_MEDIA_WARNING) + + if idempotency_key and len(idempotency_key) < 4: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="idempotency_key_too_short", ) - idempotency_key: str | None = call.data.get(ATTR_IDEMPOTENCY_KEY) - spoiler_text: str | None = call.data.get(ATTR_CONTENT_WARNING) - language: str | None = call.data.get(ATTR_LANGUAGE) - media_path: str | None = call.data.get(ATTR_MEDIA) - media_description: str | None = call.data.get(ATTR_MEDIA_DESCRIPTION) - media_warning: str | None = call.data.get(ATTR_MEDIA_WARNING) - if idempotency_key and len(idempotency_key) < 4: - raise ServiceValidationError( + await call.hass.async_add_executor_job( + partial( + _post, + hass=call.hass, + client=client, + status=status, + visibility=visibility, + idempotency_key=idempotency_key, + spoiler_text=spoiler_text, + language=language, + media_path=media_path, + media_description=media_description, + sensitive=media_warning, + ) + ) + + return None + + +def _post(hass: HomeAssistant, client: Mastodon, **kwargs: Any) -> None: + """Post to Mastodon.""" + + media_data: MediaAttachment | None = None + + media_path = kwargs.get("media_path") + if media_path: + if not hass.config.is_allowed_path(media_path): + raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="idempotency_key_too_short", + translation_key="not_whitelisted_directory", + translation_placeholders={"media": media_path}, ) - await hass.async_add_executor_job( - partial( - _post, - client=client, - status=status, - visibility=visibility, - idempotency_key=idempotency_key, - spoiler_text=spoiler_text, - language=language, - media_path=media_path, - media_description=media_description, - sensitive=media_warning, - ) - ) - - return None - - def _post(client: Mastodon, **kwargs: Any) -> None: - """Post to Mastodon.""" - - media_data: MediaAttachment | None = None - - media_path = kwargs.get("media_path") - if media_path: - if not hass.config.is_allowed_path(media_path): - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="not_whitelisted_directory", - translation_placeholders={"media": media_path}, - ) - - media_type = get_media_type(media_path) - media_description = kwargs.get("media_description") - try: - media_data = client.media_post( - media_file=media_path, - mime_type=media_type, - description=media_description, - ) - - except MastodonAPIError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="unable_to_upload_image", - translation_placeholders={"media_path": media_path}, - ) from err - - kwargs.pop("media_path", None) - kwargs.pop("media_description", None) - + media_type = get_media_type(media_path) + media_description = kwargs.get("media_description") try: - media_ids: str | None = None - if media_data: - media_ids = media_data.id - client.status_post(media_ids=media_ids, **kwargs) + media_data = client.media_post( + media_file=media_path, + mime_type=media_type, + description=media_description, + ) + except MastodonAPIError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="unable_to_send_message", + translation_key="unable_to_upload_image", + translation_placeholders={"media_path": media_path}, ) from err - hass.services.async_register( - DOMAIN, SERVICE_POST, async_post, schema=SERVICE_POST_SCHEMA - ) + kwargs.pop("media_path", None) + kwargs.pop("media_description", None) + + media_ids: str | None = None + if media_data: + media_ids = media_data.id + try: + client.status_post(media_ids=media_ids, **kwargs) + except MastodonAPIError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="unable_to_send_message", + ) from err diff --git a/homeassistant/components/mastodon/services.yaml b/homeassistant/components/mastodon/services.yaml index 1c0dfa7e836f..9027c6f9fcc1 100644 --- a/homeassistant/components/mastodon/services.yaml +++ b/homeassistant/components/mastodon/services.yaml @@ -1,3 +1,14 @@ +get_account: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: mastodon + account_name: + required: true + selector: + text: post: fields: config_entry_id: diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json index 5c50c348b7ce..b069e09b7abd 100644 --- a/homeassistant/components/mastodon/strings.json +++ b/homeassistant/components/mastodon/strings.json @@ -75,6 +75,9 @@ "not_whitelisted_directory": { "message": "{media} is not a whitelisted directory." }, + "unable_to_get_account": { + "message": "Unable to get account \"{account_name}\"." + }, "unable_to_send_message": { "message": "Unable to send message." }, @@ -93,6 +96,20 @@ } }, "services": { + "get_account": { + "description": "Gets information about a Mastodon account.", + "fields": { + "account_name": { + "description": "The Mastodon account username (e.g. @user@instance).", + "name": "Account name" + }, + "config_entry_id": { + "description": "Select the Mastodon instance to use to search.", + "name": "Mastodon instance" + } + }, + "name": "Get account" + }, "post": { "description": "Posts a status on your Mastodon account.", "fields": { diff --git a/tests/components/mastodon/conftest.py b/tests/components/mastodon/conftest.py index 0a0e203bf281..a5e864477a67 100644 --- a/tests/components/mastodon/conftest.py +++ b/tests/components/mastodon/conftest.py @@ -39,7 +39,10 @@ def mock_mastodon_client() -> Generator[AsyncMock]: load_fixture("instance.json", DOMAIN) ) client.account_verify_credentials.return_value = Account.from_json( - load_fixture("account_verify_credentials.json", DOMAIN) + load_fixture("account.json", DOMAIN) + ) + client.account_lookup.return_value = Account.from_json( + load_fixture("account.json", DOMAIN) ) client.mastodon_api_version = 2 client.status_post.return_value = None diff --git a/tests/components/mastodon/fixtures/account_verify_credentials.json b/tests/components/mastodon/fixtures/account.json similarity index 100% rename from tests/components/mastodon/fixtures/account_verify_credentials.json rename to tests/components/mastodon/fixtures/account.json diff --git a/tests/components/mastodon/snapshots/test_services.ambr b/tests/components/mastodon/snapshots/test_services.ambr new file mode 100644 index 000000000000..21c539dd597e --- /dev/null +++ b/tests/components/mastodon/snapshots/test_services.ambr @@ -0,0 +1,64 @@ +# serializer version: 1 +# name: test_get_account_success + dict({ + 'account': Account({ + 'id': 14715, + 'username': 'trwnh', + 'acct': 'trwnh', + 'display_name': 'infinite love ⴳ', + 'discoverable': True, + 'group': False, + 'locked': False, + 'created_at': datetime.datetime(2016, 11, 24, 0, 0, tzinfo=tzlocal()), + 'following_count': 328, + 'followers_count': 3169, + 'statuses_count': 69523, + 'note': '
i have approximate knowledge of many things. perpetual student. (nb/ace/they)
xmpp/email: a@trwnh.com
https://trwnh.com
help me live:
- https://donate.stripe.com/4gwcPCaMpcQ19RC4gg
- https://liberapay.com/trwnh
notes:
- my triggers are moths and glitter
- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise
- dm me if i did something wrong, so i can improve
- purest person on fedi, do not lewd in my presence