Skip to content

Commit a084e85

Browse files
authored
Add mastodon.update_profile action to Mastodon integration (#167444)
1 parent 1d7eb5e commit a084e85

7 files changed

Lines changed: 479 additions & 18 deletions

File tree

homeassistant/components/mastodon/const.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,16 @@
2323
ATTR_LANGUAGE = "language"
2424
ATTR_DURATION = "duration"
2525
ATTR_HIDE_NOTIFICATIONS = "hide_notifications"
26+
27+
ATTR_DISPLAY_NAME = "display_name"
28+
ATTR_NOTE = "note"
29+
ATTR_AVATAR = "avatar"
30+
ATTR_AVATAR_MIME_TYPE = "avatar_mime_type"
31+
ATTR_HEADER = "header"
32+
ATTR_HEADER_MIME_TYPE = "header_mime_type"
33+
ATTR_LOCKED = "locked"
34+
ATTR_BOT = "bot"
35+
ATTR_DISCOVERABLE = "discoverable"
36+
ATTR_FIELDS = "fields"
37+
ATTR_ATTRIBUTION_DOMAINS = "attribution_domains"
38+
ATTR_VALUE = "value"

homeassistant/components/mastodon/icons.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
},
4444
"unmute_account": {
4545
"service": "mdi:account-voice"
46+
},
47+
"update_profile": {
48+
"service": "mdi:account-edit"
4649
}
4750
}
4851
}

homeassistant/components/mastodon/services.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
from enum import StrEnum
55
from functools import partial
66
from math import isfinite
7+
from pathlib import Path
78
from typing import Any
89

910
from mastodon import Mastodon
1011
from mastodon.Mastodon import (
1112
Account,
1213
MastodonAPIError,
1314
MastodonNotFoundError,
15+
MastodonUnauthorizedError,
1416
MediaAttachment,
1517
)
1618
import voluptuous as vol
1719

18-
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
20+
from homeassistant.components import camera, image
21+
from homeassistant.components.media_source import async_resolve_media
22+
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, ATTR_NAME
1923
from homeassistant.core import (
2024
HomeAssistant,
2125
ServiceCall,
@@ -25,20 +29,34 @@
2529
)
2630
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
2731
from homeassistant.helpers import config_validation as cv, service
32+
from homeassistant.helpers.selector import MediaSelector
2833

2934
from .const import (
3035
ATTR_ACCOUNT_NAME,
36+
ATTR_ATTRIBUTION_DOMAINS,
37+
ATTR_AVATAR,
38+
ATTR_AVATAR_MIME_TYPE,
39+
ATTR_BOT,
3140
ATTR_CONTENT_WARNING,
41+
ATTR_DISCOVERABLE,
42+
ATTR_DISPLAY_NAME,
3243
ATTR_DURATION,
44+
ATTR_FIELDS,
45+
ATTR_HEADER,
46+
ATTR_HEADER_MIME_TYPE,
3347
ATTR_HIDE_NOTIFICATIONS,
3448
ATTR_IDEMPOTENCY_KEY,
3549
ATTR_LANGUAGE,
50+
ATTR_LOCKED,
3651
ATTR_MEDIA,
3752
ATTR_MEDIA_DESCRIPTION,
3853
ATTR_MEDIA_WARNING,
54+
ATTR_NOTE,
3955
ATTR_STATUS,
56+
ATTR_VALUE,
4057
ATTR_VISIBILITY,
4158
DOMAIN,
59+
LOGGER,
4260
)
4361
from .coordinator import MastodonConfigEntry
4462
from .utils import get_media_type
@@ -98,6 +116,24 @@ class StatusVisibility(StrEnum):
98116
}
99117
)
100118

119+
SERVICE_UPDATE_PROFILE = "update_profile"
120+
SERVICE_UPDATE_PROFILE_SCHEMA = vol.Schema(
121+
{
122+
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
123+
vol.Optional(ATTR_DISPLAY_NAME): str,
124+
vol.Optional(ATTR_NOTE): str,
125+
vol.Optional(ATTR_AVATAR): MediaSelector({"accept": ["image/*"]}),
126+
vol.Optional(ATTR_HEADER): MediaSelector({"accept": ["image/*"]}),
127+
vol.Optional(ATTR_LOCKED): bool,
128+
vol.Optional(ATTR_BOT): bool,
129+
vol.Optional(ATTR_DISCOVERABLE): bool,
130+
vol.Optional(ATTR_FIELDS): vol.All(
131+
cv.ensure_list, vol.Length(max=4), [dict[str, str]]
132+
),
133+
vol.Optional(ATTR_ATTRIBUTION_DOMAINS): vol.All(cv.ensure_list, [str]),
134+
}
135+
)
136+
101137

102138
@callback
103139
def async_setup_services(hass: HomeAssistant) -> None:
@@ -124,6 +160,13 @@ def async_setup_services(hass: HomeAssistant) -> None:
124160
hass.services.async_register(
125161
DOMAIN, SERVICE_POST, _async_post, schema=SERVICE_POST_SCHEMA
126162
)
163+
hass.services.async_register(
164+
DOMAIN,
165+
SERVICE_UPDATE_PROFILE,
166+
_async_update_profile,
167+
schema=SERVICE_UPDATE_PROFILE_SCHEMA,
168+
supports_response=SupportsResponse.ONLY,
169+
)
127170

128171

129172
async def _async_account_lookup(
@@ -319,3 +362,71 @@ def _post(hass: HomeAssistant, client: Mastodon, **kwargs: Any) -> None:
319362
translation_domain=DOMAIN,
320363
translation_key="unable_to_send_message",
321364
) from err
365+
366+
367+
async def _async_update_profile(call: ServiceCall) -> ServiceResponse:
368+
"""Update profile information."""
369+
params = dict(call.data.copy())
370+
371+
entry: MastodonConfigEntry = service.async_get_config_entry(
372+
call.hass, DOMAIN, params.pop(ATTR_CONFIG_ENTRY_ID)
373+
)
374+
client = entry.runtime_data.client
375+
376+
if avatar := params.pop(ATTR_AVATAR, None):
377+
params[ATTR_AVATAR], params[ATTR_AVATAR_MIME_TYPE] = await _resolve_media(
378+
call.hass, avatar
379+
)
380+
if header := params.pop(ATTR_HEADER, None):
381+
params[ATTR_HEADER], params[ATTR_HEADER_MIME_TYPE] = await _resolve_media(
382+
call.hass, header
383+
)
384+
if fields := params.get(ATTR_FIELDS):
385+
params[ATTR_FIELDS] = [
386+
(field[ATTR_NAME].strip(), field[ATTR_VALUE].strip())
387+
for field in fields
388+
if field[ATTR_NAME].strip()
389+
]
390+
try:
391+
return await call.hass.async_add_executor_job(
392+
lambda: client.account_update_credentials(**params)
393+
)
394+
except MastodonUnauthorizedError as error:
395+
entry.async_start_reauth(call.hass)
396+
raise HomeAssistantError(
397+
translation_domain=DOMAIN,
398+
translation_key="auth_failed",
399+
) from error
400+
except MastodonAPIError as err:
401+
LOGGER.debug("Full exception:", exc_info=err)
402+
raise HomeAssistantError(
403+
translation_domain=DOMAIN,
404+
translation_key="unable_to_update_profile",
405+
) from err
406+
407+
408+
async def _resolve_media(
409+
hass: HomeAssistant, media_source: dict[str, str]
410+
) -> tuple[bytes | Path, str | None]:
411+
"""Resolve media from a media source."""
412+
media_content_id: str = media_source["media_content_id"]
413+
if media_content_id.startswith("media-source://camera/"):
414+
entity_id = media_content_id.removeprefix("media-source://camera/")
415+
snapshot = await camera.async_get_image(hass, entity_id)
416+
return snapshot.content, snapshot.content_type
417+
418+
if media_content_id.startswith("media-source://image/"):
419+
entity_id = media_content_id.removeprefix("media-source://image/")
420+
img = await image.async_get_image(hass, entity_id)
421+
return img.content, img.content_type
422+
423+
media = await async_resolve_media(hass, media_source["media_content_id"], None)
424+
425+
if media.path is None:
426+
raise ServiceValidationError(
427+
translation_domain=DOMAIN,
428+
translation_key="media_source_not_supported",
429+
translation_placeholders={"media_content_id": media_content_id},
430+
)
431+
432+
return media.path, media.mime_type

homeassistant/components/mastodon/services.yaml

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
get_account:
22
fields:
3-
config_entry_id:
3+
config_entry_id: &config_entry_id
44
required: true
55
selector:
66
config_entry:
@@ -11,11 +11,7 @@ get_account:
1111
text:
1212
mute_account:
1313
fields:
14-
config_entry_id:
15-
required: true
16-
selector:
17-
config_entry:
18-
integration: mastodon
14+
config_entry_id: *config_entry_id
1915
account_name:
2016
required: true
2117
selector:
@@ -32,22 +28,14 @@ mute_account:
3228
boolean:
3329
unmute_account:
3430
fields:
35-
config_entry_id:
36-
required: true
37-
selector:
38-
config_entry:
39-
integration: mastodon
31+
config_entry_id: *config_entry_id
4032
account_name:
4133
required: true
4234
selector:
4335
text:
4436
post:
4537
fields:
46-
config_entry_id:
47-
required: true
48-
selector:
49-
config_entry:
50-
integration: mastodon
38+
config_entry_id: *config_entry_id
5139
status:
5240
required: true
5341
selector:
@@ -282,3 +270,55 @@ post:
282270
required: true
283271
selector:
284272
boolean:
273+
update_profile:
274+
fields:
275+
config_entry_id: *config_entry_id
276+
display_name:
277+
selector:
278+
text:
279+
note:
280+
selector:
281+
text:
282+
multiline: true
283+
avatar:
284+
required: false
285+
selector:
286+
media:
287+
accept:
288+
- "image/*"
289+
header:
290+
required: false
291+
selector:
292+
media:
293+
accept:
294+
- "image/*"
295+
locked:
296+
selector:
297+
boolean:
298+
bot:
299+
selector:
300+
boolean:
301+
discoverable:
302+
selector:
303+
boolean:
304+
fields:
305+
selector:
306+
object:
307+
label_field: "value"
308+
description_field: "name"
309+
multiple: true
310+
translation_key: fields
311+
fields:
312+
name:
313+
required: true
314+
selector:
315+
text:
316+
value:
317+
required: true
318+
selector:
319+
text:
320+
attribution_domains:
321+
selector:
322+
text:
323+
multiple: true
324+
type: url

homeassistant/components/mastodon/strings.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@
104104
"idempotency_key_too_short": {
105105
"message": "Idempotency key must be at least 4 characters long."
106106
},
107+
"media_source_not_supported": {
108+
"message": "Media source {media_content_id} is not supported."
109+
},
107110
"mute_duration_too_long": {
108111
"message": "Mute duration is too long."
109112
},
@@ -122,11 +125,26 @@
122125
"unable_to_unmute_account": {
123126
"message": "Unable to unmute account \"{account_name}\""
124127
},
128+
"unable_to_update_profile": {
129+
"message": "Unable to update profile."
130+
},
125131
"unable_to_upload_image": {
126132
"message": "Unable to upload image {media_path}."
127133
}
128134
},
129135
"selector": {
136+
"fields": {
137+
"fields": {
138+
"name": {
139+
"description": "The label for this field.",
140+
"name": "Label"
141+
},
142+
"value": {
143+
"description": "The value for this field.",
144+
"name": "Value"
145+
}
146+
}
147+
},
130148
"post_visibility": {
131149
"options": {
132150
"direct": "Direct - Mentioned accounts only",
@@ -228,6 +246,52 @@
228246
}
229247
},
230248
"name": "Unmute account"
249+
},
250+
"update_profile": {
251+
"description": "Updates your Mastodon profile information and pictures.",
252+
"fields": {
253+
"attribution_domains": {
254+
"description": "Websites allowed to credit you. Protects from false attributions. Note that setting attribution domains will replace all existing attribution domains, not just the ones specified here.",
255+
"name": "Attribution domains"
256+
},
257+
"avatar": {
258+
"description": "An image to set as your profile picture. WEBP, PNG, or JPG. At most 8 MB. Will be downscaled to 400x400px.",
259+
"name": "Profile picture"
260+
},
261+
"bot": {
262+
"description": "Signal to others that the account mainly performs automated actions.",
263+
"name": "Automated account"
264+
},
265+
"config_entry_id": {
266+
"description": "Select the Mastodon account to update the profile of.",
267+
"name": "[%key:component::mastodon::services::post::fields::config_entry_id::name%]"
268+
},
269+
"discoverable": {
270+
"description": "Whether your profile should be discoverable. Public posts and the profile may be featured or recommended across Mastodon.",
271+
"name": "Discoverable"
272+
},
273+
"display_name": {
274+
"description": "The display name to set on your profile.",
275+
"name": "Display name"
276+
},
277+
"fields": {
278+
"description": "Additional profile fields as key-value pairs. Your homepage, pronouns, age, anything you want. Note that updating fields will replace all existing fields, not just the ones specified here.",
279+
"name": "Extra fields"
280+
},
281+
"header": {
282+
"description": "An image to set as your profile header. WEBP, PNG, or JPG. At most 8 MB. Will be downscaled to 1500x500px.",
283+
"name": "Header picture"
284+
},
285+
"locked": {
286+
"description": "Whether to lock your profile. A locked profile requires you to approve followers and hides your posts from non-followers.",
287+
"name": "Lock profile"
288+
},
289+
"note": {
290+
"description": "The bio to set on your profile. You can @mention other people or #hashtags.",
291+
"name": "Bio"
292+
}
293+
},
294+
"name": "Update profile"
231295
}
232296
}
233297
}

0 commit comments

Comments
 (0)