add media attachment editing

pull/397/head
halcy 2025-02-15 16:26:18 +02:00
rodzic ea87feade0
commit f7baffc085
11 zmienionych plików z 36006 dodań i 35 usunięć

Wyświetl plik

@ -18,7 +18,7 @@ Reading
.. automethod:: Mastodon.status_history
.. automethod:: Mastodon.status_source
.. automethod:: Mastodon.statuses
.. automethod:: Mastodon.favourites
.. automethod:: Mastodon.bookmarks
@ -47,6 +47,7 @@ Writing
.. automethod:: Mastodon.status_delete
.. _status_update():
.. automethod:: Mastodon.status_update
.. automethod:: Mastodon.generate_media_edit_attributes
Scheduled statuses
------------------

Wyświetl plik

@ -10,7 +10,8 @@ This function allows you to get information about a user's notifications as well
Reading
~~~~~~~
.. automethod:: Mastodon.notifications
.. automethod:: Mastodon.notifications_unread_count
Writing
~~~~~~~
.. automethod:: Mastodon.notifications_clear

Wyświetl plik

@ -6,7 +6,7 @@ from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from typing import Optional, List, Union
from mastodon.return_types import IdType, PrimitiveIdType, Account, AdminAccount, AdminReport, PaginatableList, NonPaginatableList, Status, Tag,\
PreviewCard, AdminDomainBlock, AdminMeasure, AdminDimension, AdminRetention, AdminCanonicalEmailBlock
PreviewCard, AdminDomainBlock, AdminMeasure, AdminDimension, AdminRetention, AdminCanonicalEmailBlock, AdminDomainAllow
from datetime import datetime
class Mastodon(Internals):
@ -609,3 +609,52 @@ class Mastodon(Internals):
"""
id = self.__unpack_id(id)
return self.__api_request('DELETE', f'/api/v1/admin/canonical_email_blocks/{id}')
@api_version("4.0.0", "4.0.0")
def admin_domain_allows(self, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None,
since_id: Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[AdminDomainAllow]:
"""
Fetches a list of allowed domains. Requires scope `admin:read:domain_allows`.
The returned list may be paginated using max_id, min_id, and since_id.
NB: Untested, since I don't have a Mastodon instance in allowlist mode to test this with.
"""
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/admin/domain_allows', params)
@api_version("4.0.0", "4.0.0")
def admin_domain_allow(self, id: Union[AdminDomainAllow, IdType]) -> AdminDomainAllow:
"""
Fetch a single allowed domain by ID. Requires scope `admin:read:domain_allows`.
Raises `MastodonAPIError` if the domain allow does not exist.
NB: Untested, since I don't have a Mastodon instance in allowlist mode to test this with.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/admin/domain_allows/{id}')
@api_version("4.0.0", "4.0.0")
def admin_create_domain_allow(self, domain: str) -> AdminDomainAllow:
"""
Allow a domain for federation. Requires scope `admin:write:domain_allows`.
If the domain is already allowed, returns the existing record.
NB: Untested, since I don't have a Mastodon instance in allowlist mode to test this with.
"""
params = {"domain": domain}
return self.__api_request('POST', '/api/v1/admin/domain_allows', params)
@api_version("4.0.0", "4.0.0")
def admin_delete_domain_allow(self, id: Union[AdminDomainAllow, IdType]):
"""
Remove a domain from the allowlist. Requires scope `admin:write:domain_allows`.
Raises `MastodonAPIError` if the domain allow does not exist.
NB: Untested, since I don't have a Mastodon instance in allowlist mode to test this with.
"""
id = self.__unpack_id(id)
self.__api_request('DELETE', f'/api/v1/admin/domain_allows/{id}')

Wyświetl plik

@ -158,6 +158,10 @@ class Mastodon():
kwargs['data'] = params
response_object = self.session.request(method, base_url + endpoint, **kwargs)
if self.debug_requests:
print(f'Mastodon: Request URL: {response_object.request.url}')
print(f'Mastodon: Request body: {response_object.request.body}')
print(f'Mastodon: Response body: {response_object.text}')
except Exception as e:
raise MastodonNetworkError(f"Could not complete request: {e}")
@ -480,7 +484,7 @@ class Mastodon():
with closing(connection) as r:
listener.handle_stream(r)
def __generate_params(self, params, exclude=[], dateconv=False):
def __generate_params(self, params, exclude=[], dateconv=False, for_json=False):
"""
Internal named-parameters-to-dict helper.
@ -503,11 +507,12 @@ class Mastodon():
if params[key] is None or key in exclude:
del params[key]
param_keys = list(params.keys())
for key in param_keys:
if isinstance(params[key], list):
params[key + "[]"] = params[key]
del params[key]
if not for_json:
param_keys = list(params.keys())
for key in param_keys:
if isinstance(params[key], list):
params[key + "[]"] = params[key]
del params[key]
# Unpack min/max/since_id fields, since that is a very common operation
# and we basically always want it

Wyświetl plik

@ -100,6 +100,10 @@ class Mastodon(Internals):
`focus` and `thumbnail` are as in :ref:`media_post() <media_post()>` .
The returned dict reflects the updates to the media attachment.
Note: This is for updating the metadata of an *unattached* media attachment (i.e. one that has
not been used in a status yet). For editing media attachment metadata after posting, see `status_update` and
`generate_media_edit_attributes`.
"""
id = self.__unpack_id(id)

Wyświetl plik

@ -29,7 +29,8 @@ class Mastodon(Internals):
* `status` - A user that the logged in user has enabned notifications for has enabled `notify` (see :ref:`account_follow() <account_follow()>`)
* `admin.sign_up` - For accounts with appropriate permissions: A new user has signed up
* `admin.report` - For accounts with appropriate permissions: A new report has been received
* TODO: document the rest
* `severed_relationships` - Some of the logged in users relationships have been severed due to a moderation action on this server
* `moderation_warning` - The logged in user has been warned by a moderator
Parameters `exclude_types` and `types` are array of these types, specifying them will in- or exclude the
types of notifications given. It is legal to give both parameters at the same tine, the result will then
be the intersection of the results of both filters. Specifying `mentions_only` is a deprecated way to set

Wyświetl plik

@ -2,15 +2,16 @@
import collections
from datetime import datetime
import base64
from mastodon.errors import MastodonIllegalArgumentError
from mastodon.errors import MastodonIllegalArgumentError, MastodonVersionError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.return_types import Status, IdType, ScheduledStatus, PreviewCard, Context, NonPaginatableList, Account,\
MediaAttachment, Poll, StatusSource, StatusEdit, PaginatableList
MediaAttachment, Poll, StatusSource, StatusEdit, PaginatableList, PathOrFile
from typing import Union, Optional, List
from typing import Union, Optional, List, Dict, Any, Tuple
class Mastodon(Internals):
###
@ -112,12 +113,11 @@ class Mastodon(Internals):
###
# Writing data: Statuses
###
def __status_internal(self, status: Optional[str], in_reply_to_id: Optional[Union[Status, IdType]] = None, media_ids: Optional[List[Union[MediaAttachment, IdType]]] = None,
sensitive: Optional[bool] = False, visibility: Optional[str] = None, spoiler_text: Optional[str] = None, language: Optional[str] = None,
idempotency_key: Optional[str] = None, content_type: Optional[str] = None, scheduled_at: Optional[datetime] = None,
poll: Optional[Union[Poll, IdType]] = None, quote_id: Optional[Union[Status, IdType]] = None, edit: bool = False,
strict_content_type: bool = False) -> Union[Status, ScheduledStatus]:
strict_content_type: bool = False, media_attributes: Optional[List[Dict[str, Any]]] = None) -> Union[Status, ScheduledStatus]:
"""
Internal statuses poster helper
"""
@ -185,10 +185,17 @@ class Mastodon(Internals):
del params_initial['content_type']
use_json = False
if poll is not None:
if poll is not None or media_attributes is not None:
use_json = True
params = self.__generate_params(params_initial, ['idempotency_key', 'edit', 'strict_content_type'])
# If media_attributes is set, make sure that media_ids contains at least all the IDs of the media from media_attributes
if media_attributes is not None:
if "media_ids" in params_initial and params_initial["media_ids"] is not None:
params_initial["media_ids"] = list(set(params_initial["media_ids"]) + set([x["id"] for x in media_attributes]))
else:
params_initial["media_ids"] = [x["id"] for x in media_attributes]
params = self.__generate_params(params_initial, ['idempotency_key', 'edit', 'strict_content_type'], for_json = use_json)
cast_type = Status
if scheduled_at is not None:
cast_type = ScheduledStatus
@ -290,17 +297,49 @@ class Mastodon(Internals):
"""
return self.status_post(status)
@api_version("3.5.0", "3.5.0")
def status_update(self, id: Union[Status, IdType], status: Optional[str] = None, spoiler_text: Optional[str] = None,
def generate_media_edit_attributes(self, id: Union[MediaAttachment, IdType], description: Optional[str] = None,
focus: Optional[Tuple[float, float]] = None,
thumbnail: Optional[PathOrFile] = None, thumb_mimetype: Optional[str] = None) -> Dict[str, Any]:
"""
Helper function to generate a single media edit attribute dictionary.
Parameters:
- `id` (str): The ID of the media attachment (mandatory).
- `description` (Optional[str]): A new description for the media.
- `focus` (Optional[Tuple[float, float]]): The focal point of the media.
- `thumbnail` (Optional[PathOrFile]): The thumbnail to be used.
"""
media_edit = {"id": self.__unpack_id(id)}
if description is not None:
media_edit["description"] = description
if focus is not None:
if isinstance(focus, tuple) and len(focus) == 2:
media_edit["focus"] = f"{focus[0]},{focus[1]}"
else:
raise MastodonIllegalArgumentError("Focus must be a tuple of two floats between -1 and 1")
if thumbnail is not None:
if not self.verify_minimum_version("3.2.0", cached=True):
raise MastodonVersionError('Thumbnail requires version > 3.2.0')
_, thumb_file, thumb_mimetype = self.__load_media_file(thumbnail, thumb_mimetype)
media_edit["thumbnail"] = f"data:{thumb_mimetype};base64,{base64.b64encode(thumb_file.read()).decode()}"
return media_edit
@api_version("3.5.0", "4.1.0")
def status_update(self, id: Union[Status, IdType], status: str, spoiler_text: Optional[str] = None,
sensitive: Optional[bool] = None, media_ids: Optional[List[Union[MediaAttachment, IdType]]] = None,
poll: Optional[Union[Poll, IdType]] = None) -> Status:
poll: Optional[Union[Poll, IdType]] = None, media_attributes: Optional[List[Dict[str, Any]]] = None) -> Status:
"""
Edit a status. The meanings of the fields are largely the same as in :ref:`status_post() <status_post()>`,
though not every field can be edited.
though not every field can be edited. The `status` parameter is mandatory.
Note that editing a poll will reset the votes.
TODO: Currently doesn't support editing media descriptions, implement that.
To edit media metadata, generate a list of dictionaries with the following keys:
"""
return self.__status_internal(
status=status,
@ -308,7 +347,8 @@ class Mastodon(Internals):
sensitive=sensitive,
spoiler_text=spoiler_text,
poll=poll,
edit=id
edit=id,
media_attributes=media_attributes
)
@api_version("3.5.0", "3.5.0")

Wyświetl plik

@ -44,8 +44,9 @@ class StreamListener(object):
pass
def on_notification(self, notification):
"""A new notification. `notification` is the parsed `notification dict`
describing the notification."""
"""A new notification. `notification` is the object
describing the notification. For more information, see the documentation
for the notifications() method."""
pass
def on_filters_changed(self):
@ -106,7 +107,7 @@ class StreamListener(object):
def on_any_event(self, name, data):
"""A generic event handler that is called for every event received.
The name contains the event-name and data contains the content of the event.
The name contains the event name and data contains the content of the event.
Called before the more specific on_xxx handlers.
"""

Wyświetl plik

@ -8022,7 +8022,7 @@
{
"name": "Admin email block",
"python_name": "AdminCanonicalEmailBlock",
"func_call": "api2.admin_create_canonical_email_block(email=<some email>)",
"func_call": "mastodon.admin_create_canonical_email_block(email=<some email>)",
"func_call_real": null,
"func_call_additional": null,
"func_alternate_acc": null,
@ -8065,11 +8065,11 @@
{
"name": "Admin domain allow",
"python_name": "AdminDomainAllow",
"func_call": "TODO_TO_BE_IMPLEMENTED",
"func_call": "mastodon.admin_domain_allows()[0]",
"func_call_real": null,
"func_call_additional": null,
"func_alternate_acc": null,
"manual_update": false,
"manual_update": true,
"masto_doc_link": "https://docs.joinmastodon.org/entities/Admin_DomainAllow",
"description": "The opposite of a domain block, specifically allowing a domain to federate when the instance is in allowlist mode.",
"fields": {
@ -9178,11 +9178,11 @@
{
"name": "Relationship Severance Event",
"python_name": "RelationshipSeveranceEvent",
"func_call": "TODO_TO_BE_IMPLEMENTED",
"func_call": "# There isn't really a good way to get this manually - you get it if a moderation takes action.",
"func_call_real": null,
"func_call_additional": null,
"func_alternate_acc": null,
"manual_update": false,
"manual_update": true,
"masto_doc_link": "https://docs.joinmastodon.org/entities/RelationshipSeveranceEvent",
"description": "Summary of a moderation or block event that caused follow relationships to be severed.",
"fields": {
@ -9206,7 +9206,7 @@
"version_history": [["4.3.0", "added"]],
"field_type": "str",
"field_subtype": null,
"field_structuretype": null,
"field_structuretype": "RelationshipSeveranceEventType",
"is_optional": false,
"is_nullable": false
},
@ -9547,7 +9547,7 @@
{
"name": "Account Warning",
"python_name": "AccountWarning",
"func_call": "TODO_TO_BE_IMPLEMENTED",
"func_call": "# There isn't really a good way to get this manually - you get it if a moderation takes action.",
"func_call_real": null,
"func_call_additional": null,
"func_alternate_acc": null,
@ -9643,7 +9643,7 @@
"func_call_additional": null,
"func_alternate_acc": null,
"manual_update": false,
"masto_doc_link": "https://docs.joinmastodon.org/methods/notifications/#unread_count",
"masto_doc_link": "https://docs.joinmastodon.org/methods/notifications/#unread-count",
"description": "Get the (capped) number of unread notifications for the current user.",
"fields": {
"count": {

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -240,4 +240,52 @@ def test_status_edit(api3, api2):
source = api2.status_source(status)
assert source.text == "the best editor? why, of course it is the KDE Advanced Text Editor, Kate"
@pytest.mark.vcr(match_on=['path'])
def test_status_update_with_media_edit(api2):
media = api2.media_post(
'tests/video.mp4',
description="Original description",
focus=(-0.5, 0.3),
thumbnail='tests/amewatson.jpg'
)
assert media
assert media.url is None
time.sleep(5)
media2 = api2.media(media)
assert media2.id == media.id
assert media2.url is not None
status = api2.status_post(
'Initial post with media',
media_ids=media2
)
assert status
assert status['media_attachments'][0]['description'] == "Original description"
assert status['media_attachments'][0]['meta']['focus']['x'] == -0.5
assert status['media_attachments'][0]['meta']['focus']['y'] == 0.3
try:
updated_media_attributes = api2.generate_media_edit_attributes(
id=media2.id,
description="Updated description",
focus=(0.2, -0.1),
thumbnail='tests/image.jpg'
)
updated_status = api2.status_update(
status['id'],
"I have altered the media attachment. Pray I do not alter it further.",
media_attributes=[updated_media_attributes]
)
assert updated_status
assert updated_status['media_attachments'][0]['description'] == "Updated description"
assert updated_status['media_attachments'][0]['meta']['focus']['x'] == 0.2
assert updated_status['media_attachments'][0]['meta']['focus']['y'] == -0.1
assert updated_status['media_attachments'][0]['preview_url'] != status['media_attachments'][0]['preview_url']
finally:
api2.status_delete(status['id'])