kopia lustrzana https://github.com/halcy/Mastodon.py
add media attachment editing
rodzic
ea87feade0
commit
f7baffc085
|
@ -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
|
||||
------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
@ -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'])
|
||||
|
|
Ładowanie…
Reference in New Issue