very massive commit that adds type annotations everywhere and breaks all the tests lol

pull/350/head
halcy 2023-06-20 00:35:03 +03:00
rodzic 703bddfacf
commit 9b77278b81
33 zmienionych plików z 14810 dodań i 826 usunięć

Wyświetl plik

@ -16,18 +16,19 @@ import re
import copy
from .compat import IMPL_HAS_CRYPTO, IMPL_HAS_ECE, IMPL_HAS_BLURHASH
from .compat import cryptography, default_backend, ec, serialization
from .compat import http_ece
from .compat import blurhash
from .compat import urlparse
from mastodon.compat import IMPL_HAS_CRYPTO, IMPL_HAS_ECE, IMPL_HAS_BLURHASH
from mastodon.compat import cryptography, default_backend, ec, serialization
from mastodon.compat import http_ece
from mastodon.compat import blurhash
from mastodon.compat import urlparse
from .utility import parse_version_string, max_version, api_version
from .utility import AttribAccessDict, AttribAccessDict
from .utility import Mastodon as Utility
from mastodon.utility import parse_version_string, max_version, api_version
from mastodon.utility import Mastodon as MastoUtility
from .errors import *
from .versions import _DICT_VERSION_APPLICATION, _DICT_VERSION_MENTION, _DICT_VERSION_MEDIA, _DICT_VERSION_ACCOUNT, _DICT_VERSION_POLL, \
from mastodon.types import *
from mastodon.errors import *
from mastodon.versions import _DICT_VERSION_APPLICATION, _DICT_VERSION_MENTION, _DICT_VERSION_MEDIA, _DICT_VERSION_ACCOUNT, _DICT_VERSION_POLL, \
_DICT_VERSION_STATUS, _DICT_VERSION_INSTANCE, _DICT_VERSION_HASHTAG, _DICT_VERSION_EMOJI, _DICT_VERSION_RELATIONSHIP, \
_DICT_VERSION_NOTIFICATION, _DICT_VERSION_CONTEXT, _DICT_VERSION_LIST, _DICT_VERSION_CARD, _DICT_VERSION_SEARCHRESULT, \
_DICT_VERSION_ACTIVITY, _DICT_VERSION_REPORT, _DICT_VERSION_PUSH, _DICT_VERSION_PUSH_NOTIF, _DICT_VERSION_FILTER, \
@ -36,33 +37,33 @@ from .versions import _DICT_VERSION_APPLICATION, _DICT_VERSION_MENTION, _DICT_VE
_DICT_VERSION_FAMILIAR_FOLLOWERS, _DICT_VERSION_ADMIN_DOMAIN_BLOCK, _DICT_VERSION_ADMIN_MEASURE, _DICT_VERSION_ADMIN_DIMENSION, \
_DICT_VERSION_ADMIN_RETENTION
from .defaults import _DEFAULT_TIMEOUT, _DEFAULT_SCOPES, _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC
from .defaults import _SCOPE_SETS
from mastodon.defaults import _DEFAULT_TIMEOUT, _DEFAULT_SCOPES, _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC
from mastodon.defaults import _SCOPE_SETS
from .internals import Mastodon as Internals
from .authentication import Mastodon as Authentication
from .accounts import Mastodon as Accounts
from .instance import Mastodon as Instance
from .timeline import Mastodon as Timeline
from .statuses import Mastodon as Statuses
from .media import Mastodon as Media
from .polls import Mastodon as Polls
from .notifications import Mastodon as Notifications
from .conversations import Mastodon as Conversations
from .hashtags import Mastodon as Hashtags
from .filters import Mastodon as Filters
from .suggestions import Mastodon as Suggestions
from .endorsements import Mastodon as Endorsements
from .relationships import Mastodon as Relationships
from .lists import Mastodon as Lists
from .trends import Mastodon as Trends
from .search import Mastodon as Search
from .favourites import Mastodon as Favourites
from .reports import Mastodon as Reports
from .preferences import Mastodon as Preferences
from .push import Mastodon as Push
from .admin import Mastodon as Admin
from .streaming_endpoints import Mastodon as Streaming
from mastodon.internals import Mastodon as Internals
from mastodon.authentication import Mastodon as MastoAuthentication
from mastodon.accounts import Mastodon as MastoAccounts
from mastodon.instance import Mastodon as MastoInstance
from mastodon.timeline import Mastodon as MastoTimeline
from mastodon.statuses import Mastodon as MastoStatuses
from mastodon.media import Mastodon as MastoMedia
from mastodon.polls import Mastodon as MastoPolls
from mastodon.notifications import Mastodon as MastoNotifications
from mastodon.conversations import Mastodon as MastoConversations
from mastodon.hashtags import Mastodon as MastoHashtags
from mastodon.filters import Mastodon as MastoFilters
from mastodon.suggestions import Mastodon as MastoSuggestions
from mastodon.endorsements import Mastodon as MastoEndorsements
from mastodon.relationships import Mastodon as MastoRelationships
from mastodon.lists import Mastodon as MastoLists
from mastodon.trends import Mastodon as MastoTrends
from mastodon.search import Mastodon as MastoSearch
from mastodon.favourites import Mastodon as MastoFavourites
from mastodon.reports import Mastodon as MastoReports
from mastodon.preferences import Mastodon as MastoPreferences
from mastodon.push import Mastodon as MastoPush
from mastodon.admin import Mastodon as MastoAdmin
from mastodon.streaming_endpoints import Mastodon as MastoStreaming
###
@ -70,9 +71,9 @@ from .streaming_endpoints import Mastodon as Streaming
#
# Almost all code is now imported from smaller files to make editing a bit more pleasant
###
class Mastodon(Utility, Authentication, Accounts, Instance, Timeline, Statuses, Polls, Notifications, Hashtags,
Filters, Suggestions, Endorsements, Relationships, Lists, Trends, Search, Favourites, Reports,
Preferences, Push, Admin, Conversations, Media, Streaming):
class Mastodon(MastoUtility, MastoAuthentication, MastoAccounts, MastoInstance, MastoTimeline, MastoStatuses, MastoPolls, MastoNotifications, MastoHashtags,
MastoFilters, MastoSuggestions, MastoEndorsements, MastoRelationships, MastoLists, MastoTrends, MastoSearch, MastoFavourites, MastoReports,
MastoPreferences, MastoPush, MastoAdmin, MastoConversations, MastoMedia, MastoStreaming):
"""
Thorough and easy to use Mastodon
API wrapper in Python.
@ -83,7 +84,7 @@ class Mastodon(Utility, Authentication, Accounts, Instance, Timeline, Statuses,
__SUPPORTED_MASTODON_VERSION = "3.5.5"
@staticmethod
def get_supported_version():
def get_supported_version() -> str:
"""
Retrieve the maximum version of Mastodon supported by this version of Mastodon.py
"""

Wyświetl plik

@ -2,17 +2,22 @@
import collections
from .versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_STATUS, _DICT_VERSION_RELATIONSHIP, _DICT_VERSION_LIST, _DICT_VERSION_FAMILIAR_FOLLOWERS, _DICT_VERSION_HASHTAG
from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS
from .errors import MastodonIllegalArgumentError, MastodonAPIError, MastodonNotFoundError
from .utility import api_version
from mastodon.versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_STATUS, _DICT_VERSION_RELATIONSHIP, _DICT_VERSION_LIST, _DICT_VERSION_FAMILIAR_FOLLOWERS, _DICT_VERSION_HASHTAG
from mastodon.defaults import _DEFAULT_SCOPES, _SCOPE_SETS
from mastodon.errors import MastodonIllegalArgumentError, MastodonAPIError, MastodonNotFoundError
from mastodon.utility import api_version
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
from typing import Union, Optional, Tuple, List
from mastodon.types import AccountCreationError, Account, IdType, Status, PaginatableList, NonPaginatableList, UserList, Relationship, FamiliarFollowers, Tag, IdType, PathOrFile
from datetime import datetime
class Mastodon(Internals):
@api_version("2.7.0", "2.7.0", "3.4.0")
def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=_DEFAULT_SCOPES, to_file=None, return_detailed_error=False):
def create_account(self, username: str, password: str, email: str, agreement: bool = False, reason: Optional[str] = None,
locale: str = "en", scopes: List[str] = _DEFAULT_SCOPES, to_file: Optional[str] = None,
return_detailed_error: bool = False) -> Union[Optional[str], Tuple[Optional[str], AccountCreationError]]:
"""
Creates a new user account with the given username, password and email. "agreement"
must be set to true (after showing the user the instance's user agreement and having
@ -75,7 +80,7 @@ class Mastodon(Internals):
if "error" in response:
if return_detailed_error:
return None, response
raise MastodonIllegalArgumentError(f'Invalid request: {e}')
raise MastodonIllegalArgumentError(f'Invalid request: {response["error"]}')
self.access_token = response['access_token']
self.__set_refresh_token(response.get('refresh_token'))
self.__set_token_expired(int(response.get('expires_in', 0)))
@ -119,28 +124,24 @@ class Mastodon(Internals):
# Reading data: Accounts
###
@api_version("1.0.0", "1.0.0", _DICT_VERSION_ACCOUNT)
def account(self, id):
def account(self, id: Union[Account, IdType]) -> Account:
"""
Fetch account information by user `id`.
Does not require authentication for publicly visible accounts.
Returns a :ref:`account dict <account dict>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/accounts/{id}')
@api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
def account_verify_credentials(self):
def account_verify_credentials(self) -> Account:
"""
Fetch logged-in user's account information.
Returns a :ref:`account dict <account dict>` (Starting from 2.1.0, with an additional "source" field).
Fetch logged-in user's account information. Returns the version of the Account object with `source` field.
"""
return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
@api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
def me(self):
def me(self) -> Account:
"""
Get this user's account. Synonym for `account_verify_credentials()`, does exactly
the same thing, just exists because `account_verify_credentials()` has a confusing
@ -149,7 +150,10 @@ class Mastodon(Internals):
return self.account_verify_credentials()
@api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
def account_statuses(self, id, only_media=False, pinned=False, exclude_replies=False, exclude_reblogs=False, tagged=None, max_id=None, min_id=None, since_id=None, limit=None):
def account_statuses(self, id: Union[Account, IdType], only_media: bool = False, pinned: bool = False, exclude_replies: bool = False,
exclude_reblogs: bool = False, tagged: Optional[str] = None, max_id: Optional[Union[Status, IdType, datetime]] = None,
min_id: Optional[Union[Status, IdType, datetime]] = None, since_id: Optional[Union[Status, IdType, datetime]] = None,
limit: Optional[int] = None) -> PaginatableList[Status]:
"""
Fetch statuses by user `id`. Same options as :ref:`timeline() <timeline()>` are permitted.
Returned toots are from the perspective of the logged-in user, i.e.
@ -165,8 +169,6 @@ class Mastodon(Internals):
Does not require authentication for Mastodon versions after 2.7.0 (returns
publicly visible statuses in that case), for publicly visible accounts.
Returns a list of :ref:`status dicts <status dicts>`.
"""
id = self.__unpack_id(id)
if max_id is not None:
@ -191,66 +193,65 @@ class Mastodon(Internals):
return self.__api_request('GET', f'/api/v1/accounts/{id}/statuses', params)
@api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
def account_following(self, id, max_id=None, min_id=None, since_id=None, limit=None):
def account_following(self, id: Union[Account, IdType], max_id: Optional[Union[Account, IdType]] = None,
min_id: Optional[Union[Account, IdType]] = None, since_id: Optional[Union[Account, IdType]] = None,
limit: Optional[int] = None) -> PaginatableList[Account]:
"""
Fetch users the given user is following.
Returns a list of :ref:`account dicts <account dicts>`.
"""
id = self.__unpack_id(id)
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals(), ['id'])
return self.__api_request('GET', f'/api/v1/accounts/{id}/following', params)
@api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
def account_followers(self, id, max_id=None, min_id=None, since_id=None, limit=None):
def account_followers(self, id: Union[Account, IdType], max_id: Optional[Union[Account, IdType]] = None,
min_id: Optional[Union[Account, IdType]] = None, since_id: Optional[Union[Account, IdType]] = None,
limit: Optional[int] = None) -> PaginatableList[Account]:
"""
Fetch users the given user is followed by.
Returns a list of :ref:`account dicts <account dicts>`.
"""
id = self.__unpack_id(id)
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals(), ['id'])
return self.__api_request('GET', f'/api/v1/accounts/{id}/followers', params)
@api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
def account_relationships(self, id):
def account_relationships(self, id: Union[List[Union[Account, IdType]], Union[Account, IdType]]) -> NonPaginatableList[Relationship]:
"""
Fetch relationship (following, followed_by, blocking, follow requested) of
the logged in user to a given account. `id` can be a list.
Returns a list of :ref:`relationship dicts <relationship dicts>`.
"""
id = self.__unpack_id(id)
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/accounts/relationships',
params)
@api_version("1.0.0", "2.3.0", _DICT_VERSION_ACCOUNT)
def account_search(self, q, limit=None, following=False, resolve=False):
@api_version("1.0.0", "2.8.0", _DICT_VERSION_ACCOUNT)
def account_search(self, q: str, limit: Optional[int] = None, following: bool = False, resolve: bool = False, offset: Optional[int] = None) -> NonPaginatableList[Account]:
"""
Fetch matching accounts. Will lookup an account remotely if the search term is
in the username@domain format and not yet in the database. Set `following` to
True to limit the search to users the logged-in user follows.
Returns a list of :ref:`account dicts <account dicts>`.
Paginated in a weird way ("limit" / "offset"), if you want to fetch all results
here please do it yourself for now.
"""
params = self.__generate_params(locals())
@ -260,55 +261,46 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/accounts/search', params)
@api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
def account_lists(self, id):
def account_lists(self, id: Union[Account, IdType]) -> NonPaginatableList[UserList]:
"""
Get all of the logged-in user's lists which the specified user is
a member of.
Returns a list of :ref:`list dicts <list dicts>`.
"""
id = self.__unpack_id(id)
params = self.__generate_params(locals(), ['id'])
return self.__api_request('GET', f'/api/v1/accounts/{id}/lists', params)
@api_version("3.4.0", "3.4.0", _DICT_VERSION_ACCOUNT)
def account_lookup(self, acct):
def account_lookup(self, acct: str) -> Account:
"""
Look up an account from user@instance form (@instance allowed but not required for
local accounts). Will only return accounts that the instance already knows about,
and not do any webfinger requests. Use `account_search` if you need to resolve users
through webfinger from remote.
Returns an :ref:`account dict <account dict>`.
"""
return self.__api_request('GET', '/api/v1/accounts/lookup', self.__generate_params(locals()))
@api_version("3.5.0", "3.5.0", _DICT_VERSION_FAMILIAR_FOLLOWERS)
def account_familiar_followers(self, id):
def account_familiar_followers(self, id: Union[List[Union[Account, IdType]], Union[Account, IdType]]) -> NonPaginatableList[FamiliarFollowers]:
"""
Find followers for the account given by id (can be a list) that also follow the
logged in account.
Returns a list of :ref:`familiar follower dicts <familiar follower dicts>`
"""
if not isinstance(id, list):
id = [id]
for i in range(len(id)):
id[i] = self.__unpack_id(id[i])
id = self.__unpack_id(id, listify = True)
return self.__api_request('GET', '/api/v1/accounts/familiar_followers', {'id': id}, use_json=True)
###
# Writing data: Accounts
###
@api_version("1.0.0", "3.3.0", _DICT_VERSION_RELATIONSHIP)
def account_follow(self, id, reblogs=True, notify=False):
def account_follow(self, id: Union[Account, IdType], reblogs: bool =True, notify: bool = False) -> Relationship:
"""
Follow a user.
Set `reblogs` to False to hide boosts by the followed user.
Set `notify` to True to get a notification every time the followed user posts.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
params = self.__generate_params(locals(), ["id"])
@ -319,7 +311,7 @@ class Mastodon(Internals):
return self.__api_request('POST', f'/api/v1/accounts/{id}/follow', params)
@api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
def follows(self, uri):
def follows(self, uri: str) -> Relationship:
"""
Follow a remote user with username given in username@domain form.
@ -334,48 +326,48 @@ class Mastodon(Internals):
return self.account_follow(acct)
@api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
def account_unfollow(self, id):
def account_unfollow(self, id: Union[Account, IdType]) -> Relationship:
"""
Unfollow a user.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/accounts/{id}/unfollow')
@api_version("3.5.0", "3.5.0", _DICT_VERSION_RELATIONSHIP)
def account_remove_from_followers(self, id):
def account_remove_from_followers(self, id: Union[Account, IdType]) -> Relationship:
"""
Remove a user from the logged in users followers (i.e. make them unfollow the logged in
user / "softblock" them).
Returns a :ref:`relationship dict <relationship dict>` reflecting the updated following status.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/accounts/{id}/remove_from_followers')
@api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
def account_block(self, id):
def account_block(self, id: Union[Account, IdType]) -> Relationship:
"""
Block a user.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/accounts/{id}/block')
@api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
def account_unblock(self, id):
def account_unblock(self, id: Union[Account, IdType]) -> Relationship:
"""
Unblock a user.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/accounts/{id}/unblock')
@api_version("1.1.0", "2.4.3", _DICT_VERSION_RELATIONSHIP)
def account_mute(self, id, notifications=True, duration=None):
def account_mute(self, id: Union[Account, IdType], notifications: bool = True, duration: Optional[int] = None) -> Relationship:
"""
Mute a user.
@ -383,28 +375,28 @@ class Mastodon(Internals):
muted from timelines. Pass a `duration` in seconds to have Mastodon automatically
lift the mute after that many seconds.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
params = self.__generate_params(locals(), ['id'])
return self.__api_request('POST', f'/api/v1/accounts/{id}/mute', params)
@api_version("1.1.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
def account_unmute(self, id):
def account_unmute(self, id: Union[Account, IdType]) -> Relationship:
"""
Unmute a user.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/accounts/{id}/unmute')
@api_version("1.1.1", "3.1.0", _DICT_VERSION_ACCOUNT)
def account_update_credentials(self, display_name=None, note=None,
avatar=None, avatar_mime_type=None,
header=None, header_mime_type=None,
locked=None, bot=None,
discoverable=None, fields=None):
def account_update_credentials(self, display_name: Optional[str] = None, note: Optional[str] = None,
avatar: Optional[PathOrFile] = None, avatar_mime_type: Optional[str] = None,
header: Optional[PathOrFile] = None, header_mime_type: Optional[str] = None,
locked: Optional[bool] = None, bot: Optional[bool] = None,
discoverable: Optional[bool] = None, fields: Optional[List[Tuple[str, str]]] = None) -> Account:
"""
Update the profile for the currently logged-in user.
@ -422,7 +414,7 @@ class Mastodon(Internals):
`fields` can be a list of up to four name-value pairs (specified as tuples) to
appear as semi-structured information in the user's profile.
Returns the updated `account dict` of the logged-in user.
The returned object reflects the updated account.
"""
params_initial = collections.OrderedDict(locals())
@ -453,42 +445,40 @@ class Mastodon(Internals):
return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files)
@api_version("2.5.0", "2.5.0", _DICT_VERSION_RELATIONSHIP)
def account_pin(self, id):
def account_pin(self, id: Union[Account, IdType]) -> Relationship:
"""
Pin / endorse a user.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/accounts/{id}/pin')
@api_version("2.5.0", "2.5.0", _DICT_VERSION_RELATIONSHIP)
def account_unpin(self, id):
def account_unpin(self, id: Union[Account, IdType]) -> Relationship:
"""
Unpin / un-endorse a user.
Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
The returned object reflects the updated relationship with the user.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/accounts/{id}/unpin')
@api_version("3.2.0", "3.2.0", _DICT_VERSION_RELATIONSHIP)
def account_note_set(self, id, comment):
def account_note_set(self, id: Union[Account, IdType], comment: str) -> Account:
"""
Set a note (visible to the logged in user only) for the given account.
Returns a :ref:`status dict <status dict>` with the `note` updated.
The returned object contains the updated note.
"""
id = self.__unpack_id(id)
params = self.__generate_params(locals(), ["id"])
return self.__api_request('POST', f'/api/v1/accounts/{id}/note', params)
@api_version("3.3.0", "3.3.0", _DICT_VERSION_HASHTAG)
def account_featured_tags(self, id):
def account_featured_tags(self, id: Union[Account, IdType]) -> NonPaginatableList[Tag]:
"""
Get an account's featured hashtags.
Returns a list of :ref:`hashtag dicts <hashtag dicts>` (NOT `featured tag dicts`_).
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/accounts/{id}/featured_tags')

Wyświetl plik

@ -1,20 +1,25 @@
# admin.py - admin / moderation endpoints
from .versions import _DICT_VERSION_ADMIN_ACCOUNT, _DICT_VERSION_REPORT, _DICT_VERSION_HASHTAG, _DICT_VERSION_STATUS, _DICT_VERSION_CARD, \
from mastodon.versions import _DICT_VERSION_ADMIN_ACCOUNT, _DICT_VERSION_REPORT, _DICT_VERSION_HASHTAG, _DICT_VERSION_STATUS, _DICT_VERSION_CARD, \
_DICT_VERSION_ADMIN_DOMAIN_BLOCK, _DICT_VERSION_ADMIN_MEASURE, _DICT_VERSION_ADMIN_DIMENSION, _DICT_VERSION_ADMIN_RETENTION
from .errors import MastodonIllegalArgumentError
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.errors import MastodonIllegalArgumentError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from typing import Optional, List, Union
from mastodon.types import IdType, PrimitiveIdType, Account, AdminAccount, AdminReport, PaginatableList, NonPaginatableList, Status, Tag,\
PreviewCard, AdminDomainBlock, AdminMeasure, AdminDimension, AdminRetention
from datetime import datetime
class Mastodon(Internals):
###
# Moderation API
###
@api_version("2.9.1", "4.0.0", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_accounts_v2(self, origin=None, by_domain=None, status=None, username=None, display_name=None, email=None, ip=None,
permissions=None, invited_by=None, role_ids=None, max_id=None, min_id=None, since_id=None, limit=None):
def admin_accounts_v2(self, origin: Optional[str] = None, by_domain: Optional[str] = None, status: Optional[str] = None, username: Optional[str] = None,
display_name: Optional[str] = None, email: Optional[str] = None, ip: Optional[str] = None, permissions: Optional[str] = None,
invited_by: Union[Account, IdType] = None, role_ids: Optional[List[IdType]] = None, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None,
since_id: Optional[IdType] = None, limit: Optional[int] = None) -> AdminAccount:
"""
Fetches a list of accounts that match given criteria. By default, local accounts are returned.
@ -30,15 +35,17 @@ class Mastodon(Internals):
* Set `role_ids` to a list of role IDs to get only accounts with those roles.
Returns a list of :ref:`admin account dicts <admin account dicts>`.
Pagination on this is a bit weird, so I would recommend not doing that and instead manually fetching.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
if role_ids is not None:
if not isinstance(role_ids, list):
@ -64,11 +71,16 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v2/admin/accounts', params)
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_accounts(self, remote=False, by_domain=None, status='active', username=None, display_name=None, email=None, ip=None, staff_only=False, max_id=None, min_id=None, since_id=None, limit=None):
def admin_accounts(self, remote: bool = False, by_domain: Optional[str] = None, status: str = 'active', username: Optional[str] = None,
display_name: Optional[str] = None, email: Optional[str] = None, ip: Optional[str] = None, staff_only: bool = False,
max_id: Optional[IdType] = None, min_id: Optional[IdType] = None, since_id: Optional[IdType] = None,
limit: Optional[int] = None):
"""
Currently a synonym for admin_accounts_v1, now deprecated. You are strongly encouraged to use admin_accounts_v2 instead, since this one is kind of bad.
!!!!! This function may be switched to calling the v2 API in the future. This is your warning. If you want to keep using v1, use it explicitly. !!!!!
Pagination on this is a bit weird, so I would recommend not doing that and instead manually fetching.
"""
return self.admin_accounts_v1(
remote=remote,
@ -85,7 +97,10 @@ class Mastodon(Internals):
)
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_accounts_v1(self, remote=False, by_domain=None, status='active', username=None, display_name=None, email=None, ip=None, staff_only=False, max_id=None, min_id=None, since_id=None, limit=None):
def admin_accounts_v1(self, remote: bool = False, by_domain: Optional[str] = None, status: str = 'active', username: Optional[str] = None,
display_name: Optional[str] = None, email: Optional[str] = None, ip: Optional[str] = None, staff_only: bool = False,
max_id: Optional[IdType] = None, min_id: Optional[IdType] = None, since_id: Optional[IdType] = None,
limit: Optional[int] = None) -> AdminAccount:
"""
Fetches a list of accounts that match given criteria. By default, local accounts are returned.
@ -104,15 +119,17 @@ class Mastodon(Internals):
Deprecated in Mastodon version 3.5.0.
Returns a list of :ref:`admin account dicts <admin account dicts>`.
Pagination on this is a bit weird, so I would recommend not doing that and instead manually fetching.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals(), ['remote', 'status', 'staff_only'])
@ -136,87 +153,86 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/admin/accounts', params)
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account(self, id):
def admin_account(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Fetches a single :ref:`admin account dict <admin account dict>` for the user with the given id.
Returns that dict.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/admin/accounts/{id}')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account_enable(self, id):
def admin_account_enable(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Reenables login for a local account for which login has been disabled.
Returns the updated :ref:`admin account dict <admin account dict>`.
The returned object reflects the updates to the account.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/accounts/{id}/enable')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account_approve(self, id):
def admin_account_approve(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Approves a pending account.
Returns the updated :ref:`admin account dict <admin account dict>`.
The returned object reflects the updates to the account.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/accounts/{id}/approve')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account_reject(self, id):
def admin_account_reject(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Rejects and deletes a pending account.
Returns the updated :ref:`admin account dict <admin account dict>` for the account that is now gone.
The returned object is that of the now-deleted account.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/accounts/{id}/reject')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account_unsilence(self, id):
def admin_account_unsilence(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Unsilences an account.
Returns the updated :ref:`admin account dict <admin account dict>`.
The returned object reflects the updates to the account.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/accounts/{id}/unsilence')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account_unsuspend(self, id):
def admin_account_unsuspend(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Unsuspends an account.
Returns the updated :ref:`admin account dict <admin account dict>`.
The returned object reflects the updates to the account.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/accounts/{id}/unsuspend')
@api_version("3.3.0", "3.3.0", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account_delete(self, id):
def admin_account_delete(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Delete a local user account.
The deleted accounts :ref:`admin account dict <admin account dict>`.
The returned object reflects the updates to the account.
"""
id = self.__unpack_id(id)
return self.__api_request('DELETE', f'/api/v1/admin/accounts/{id}')
@api_version("3.3.0", "3.3.0", _DICT_VERSION_ADMIN_ACCOUNT)
def admin_account_unsensitive(self, id):
def admin_account_unsensitive(self, id: Union[Account, AdminAccount, IdType]) -> AdminAccount:
"""
Unmark an account as force-sensitive.
Returns the updated :ref:`admin account dict <admin account dict>`.
The returned object reflects the updates to the account.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/accounts/{id}/unsensitive')
@api_version("2.9.1", "2.9.1", "2.9.1")
def admin_account_moderate(self, id, action=None, report_id=None, warning_preset_id=None, text=None, send_email_notification=True):
def admin_account_moderate(self, id: Union[Account, AdminAccount, IdType], action: Optional[str] = None, report_id: Optional[Union[AdminReport, PrimitiveIdType]] = None,
warning_preset_id: Optional[PrimitiveIdType] = None, text: Optional[str] = None, send_email_notification: Optional[bool] = True):
"""
Perform a moderation action on an account.
@ -252,7 +268,9 @@ class Mastodon(Internals):
self.__api_request('POST', f'/api/v1/admin/accounts/{id}/action', params)
@api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
def admin_reports(self, resolved=False, account_id=None, target_account_id=None, max_id=None, min_id=None, since_id=None, limit=None):
def admin_reports(self, resolved: Optional[bool] = False, account_id = Optional[Union[Account, AdminAccount, IdType]],
target_account_id: Optional[Union[Account, AdminAccount, IdType]] = None, max_id: Optional[IdType] = None,
min_id: Optional[IdType] = None, since_id: Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[AdminReport]:
"""
Fetches the list of reports.
@ -262,13 +280,13 @@ class Mastodon(Internals):
Returns a list of :ref:`report dicts <report dicts>`.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
if account_id is not None:
account_id = self.__unpack_id(account_id)
@ -283,90 +301,86 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/admin/reports', params)
@api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
def admin_report(self, id):
def admin_report(self, id: Union[AdminReport, IdType]) -> AdminReport:
"""
Fetches the report with the given id.
Returns a :ref:`report dict <report dict>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/admin/reports/{id}')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
def admin_report_assign(self, id):
def admin_report_assign(self, id: Union[AdminReport, IdType]) -> AdminReport:
"""
Assigns the given report to the logged-in user.
Returns the updated :ref:`report dict <report dict>`.
The returned object reflects the updates to the report.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/reports/{id}/assign_to_self')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
def admin_report_unassign(self, id):
def admin_report_unassign(self, id: Union[AdminReport, IdType]) -> AdminReport:
"""
Unassigns the given report from the logged-in user.
Returns the updated :ref:`report dict <report dict>`.
The returned object reflects the updates to the report.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/reports/{id}/unassign')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
def admin_report_reopen(self, id):
def admin_report_reopen(self, id: Union[AdminReport, IdType]) -> AdminReport:
"""
Reopens a closed report.
Returns the updated :ref:`report dict <report dict>`.
The returned object reflects the updates to the report.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/reports/{id}/reopen')
@api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
def admin_report_resolve(self, id):
def admin_report_resolve(self, id: Union[AdminReport, IdType]) -> AdminReport:
"""
Marks a report as resolved (without taking any action).
Returns the updated :ref:`report dict <report dict>`.
The returned object reflects the updates to the report.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/admin/reports/{id}/resolve')
@api_version("3.5.0", "3.5.0", _DICT_VERSION_HASHTAG)
def admin_trending_tags(self, limit=None):
def admin_trending_tags(self, limit: Optional[int] = None) -> NonPaginatableList[Tag]:
"""
Admin version of :ref:`trending_tags() <trending_tags()>`. Includes unapproved tags.
Returns a list of :ref:`hashtag dicts <hashtag dicts>`, sorted by the instance's trending algorithm,
descending.
The returned list is sorted, descending, by the instance's trending algorithm.
"""
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/admin/trends/tags', params)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
def admin_trending_statuses(self):
def admin_trending_statuses(self) -> NonPaginatableList[Status]:
"""
Admin version of :ref:`trending_statuses() <trending_statuses()>`. Includes unapproved tags.
Returns a list of :ref:`status dicts <status dicts>`, sorted by the instance's trending algorithm,
descending.
The returned list is sorted, descending, by the instance's trending algorithm.
"""
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/admin/trends/statuses', params)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_CARD)
def admin_trending_links(self):
def admin_trending_links(self) -> NonPaginatableList[PreviewCard]:
"""
Admin version of :ref:`trending_links() <trending_links()>`. Includes unapproved tags.
Returns a list of :ref:`card dicts <card dicts>`, sorted by the instance's trending algorithm,
descending.
The returned list is sorted, descending, by the instance's trending algorithm.
"""
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/admin/trends/links', params)
@api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
def admin_domain_blocks(self, id=None, max_id=None, min_id=None, since_id=None, limit=None):
def admin_domain_blocks(self, id: Optional[IdType] = None, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None,
since_id: Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[AdminDomainBlock]:
"""
Fetches a list of blocked domains. Requires scope `admin:read:domain_blocks`.
@ -391,7 +405,9 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/admin/domain_blocks/', params)
@api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
def admin_create_domain_block(self, domain:str, severity:str=None, reject_media:bool=None, reject_reports:bool=None, private_comment:str=None, public_comment:str=None, obfuscate:bool=None):
def admin_create_domain_block(self, domain: str, severity: Optional[str] = None, reject_media: Optional[bool] = None,
reject_reports: Optional[bool] = None, private_comment: Optional[str] = None,
public_comment: Optional[str] = None, obfuscate: Optional[bool] = None) -> AdminDomainBlock:
"""
Perform a moderation action on a domain. Requires scope `admin:write:domain_blocks`.
@ -416,7 +432,8 @@ class Mastodon(Internals):
return self.__api_request('POST', '/api/v1/admin/domain_blocks/', params)
@api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
def admin_update_domain_block(self, id, severity:str=None, reject_media:bool=None, reject_reports:bool=None, private_comment:str=None, public_comment:str=None, obfuscate:bool=None):
def admin_update_domain_block(self, id, severity: Optional[str] = None, reject_media: Optional[bool] = None, reject_reports: Optional[bool] = None,
private_comment: Optional[str] = None, public_comment: Optional[str] = None, obfuscate: Optional[bool] = None) -> AdminDomainBlock:
"""
Modify existing moderation action on a domain. Requires scope `admin:write:domain_blocks`.
@ -442,7 +459,7 @@ class Mastodon(Internals):
return self.__api_request('PUT', f'/api/v1/admin/domain_blocks/{id}', params)
@api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
def admin_delete_domain_block(self, id=None):
def admin_delete_domain_block(self, id = Union[AdminDomainBlock, IdType]):
"""
Removes moderation action against a given domain. Requires scope `admin:write:domain_blocks`.
@ -457,9 +474,10 @@ class Mastodon(Internals):
raise AttributeError("You must provide an id of an existing domain block to remove it.")
@api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_MEASURE)
def admin_measures(self, start_at, end_at, active_users=False, new_users=False, interactions=False, opened_reports = False, resolved_reports=False,
tag_accounts=None, tag_uses=None, tag_servers=None, instance_accounts=None, instance_media_attachments=None, instance_reports=None,
instance_statuses=None, instance_follows=None, instance_followers=None):
def admin_measures(self, start_at, end_at, active_users: bool = False, new_users: bool = False, interactions: bool = False, opened_reports: bool = False, resolved_reports: bool = False,
tag_accounts: Optional[Union[Tag, IdType]] = None, tag_uses: Optional[Union[Tag, IdType]] = None, tag_servers: Optional[Union[Tag, IdType]] = None,
instance_accounts: Optional[str] = None, instance_media_attachments: Optional[str] = None, instance_reports: Optional[str] = None,
instance_statuses: Optional[str] = None, instance_follows: Optional[str] = None, instance_followers: Optional[str] = None) -> NonPaginatableList[AdminMeasure]:
"""
Retrieves numerical instance information for the time period (at day granularity) between `start_at` and `end_at`.
@ -512,8 +530,9 @@ class Mastodon(Internals):
return self.__api_request('POST', '/api/v1/admin/measures', params, use_json=True)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_DIMENSION)
def admin_dimensions(self, start_at, end_at, limit=None, languages=False, sources=False, servers=False, space_usage=False, software_versions=False,
tag_servers=None, tag_languages=None, instance_accounts=None, instance_languages=None):
def admin_dimensions(self, start_at: datetime, end_at: datetime, limit: Optional[int] = None, languages: bool = False, sources: bool = False,
servers: bool = False, space_usag: bool = False, software_versions: bool = False, tag_servers: Optional[Union[Tag, IdType]] = None,
tag_languages: Optional[Union[Tag, IdType]] = None, instance_accounts: Optional[str] = None, instance_languages: Optional[str] = None) -> NonPaginatableList[AdminDimension]:
"""
Retrieves primarily categorical instance information for the time period (at day granularity) between `start_at` and `end_at`.
@ -564,7 +583,7 @@ class Mastodon(Internals):
return self.__api_request('POST', '/api/v1/admin/dimensions', params, use_json=True)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_RETENTION)
def admin_retention(self, start_at, end_at, frequency="day"):
def admin_retention(self, start_at: datetime, end_at: datetime, frequency: str = "day") -> AdminRetention:
"""
Gets user retention statistics (at `frequency` - "day" or "month" - granularity) between `start_at` and `end_at`.

Wyświetl plik

@ -7,21 +7,25 @@ import os
import time
import collections
from .errors import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError
from .versions import _DICT_VERSION_APPLICATION
from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT, _DEFAULT_USER_AGENT
from .utility import parse_version_string, api_version
from .internals import Mastodon as Internals
from mastodon.errors import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError
from mastodon.versions import _DICT_VERSION_APPLICATION
from mastodon.defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT, _DEFAULT_USER_AGENT
from mastodon.utility import parse_version_string, api_version
from mastodon.internals import Mastodon as Internals
from mastodon.utility import Mastodon as Utility
from typing import List, Optional, Union, Tuple
from mastodon.types import Application
from mastodon.compat import PurePath
class Mastodon(Internals):
###
# Registering apps
###
@staticmethod
def create_app(client_name, scopes=_DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None,
api_base_url=None, request_timeout=_DEFAULT_TIMEOUT, session=None, user_agent=_DEFAULT_USER_AGENT):
def create_app(client_name, scopes: List[str] = _DEFAULT_SCOPES, redirect_uris: Optional[Union[str, List[str]]] = None, website: Optional[str] = None,
to_file: Optional[Union[str, PurePath]] = None, api_base_url: Optional[str] = None, request_timeout: float = _DEFAULT_TIMEOUT,
session: Optional[requests.Session] = None, user_agent: str = _DEFAULT_USER_AGENT) -> Tuple[str, str]:
"""
Create a new app with given `client_name` and `scopes` (The basic scopes are "read", "write", "follow" and "push"
- more granular scopes are available, please refer to Mastodon documentation for which) on the instance given
@ -42,7 +46,6 @@ class Mastodon(Internals):
Presently, app registration is open by default, but this is not guaranteed to be the case for all
Mastodon instances in the future.
Returns `client_id` and `client_secret`, both as strings.
"""
if api_base_url is None:
@ -87,9 +90,11 @@ class Mastodon(Internals):
###
# Authentication, including constructor
###
def __init__(self, client_id=None, client_secret=None, access_token=None, api_base_url=None, debug_requests=False,
ratelimit_method="wait", ratelimit_pacefactor=1.1, request_timeout=_DEFAULT_TIMEOUT, mastodon_version=None,
version_check_mode="created", session=None, feature_set="mainline", user_agent=_DEFAULT_USER_AGENT, lang=None):
def __init__(self, client_id: Optional[Union[str, PurePath]] = None, client_secret: Optional[str] = None,
access_token: Optional[Union[str, PurePath]] = None, api_base_url: Optional[str] = None, debug_requests: bool = False,
ratelimit_method: str = "wait", ratelimit_pacefactor: float = 1.1, request_timeout: float = _DEFAULT_TIMEOUT,
mastodon_version: Optional[str] =None, version_check_mode: str = "created", session: Optional[requests.Session] = None,
feature_set: str = "mainline", user_agent: str = _DEFAULT_USER_AGENT, lang: Optional[str] = None):
"""
Create a new API wrapper instance based on the given `client_secret` and `client_id` on the
instance given by `api_base_url`. If you give a `client_id` and it is not a file, you must
@ -247,7 +252,9 @@ class Mastodon(Internals):
if ratelimit_method not in ["throw", "wait", "pace"]:
raise MastodonIllegalArgumentError("Invalid ratelimit method.")
def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", scopes=_DEFAULT_SCOPES, force_login=False, state=None, lang=None):
def auth_request_url(self, client_id: Optional[Union[str, PurePath]] = None, redirect_uris: str = "urn:ietf:wg:oauth:2.0:oob",
scopes: List[str] =_DEFAULT_SCOPES, force_login: bool = False, state: Optional[str] = None,
lang: Optional[str] = None) -> str:
"""
Returns the URL that a client needs to request an OAuth grant from the server.
@ -256,8 +263,9 @@ class Mastodon(Internals):
`scopes` are as in :ref:`log_in() <log_in()>`, redirect_uris is where the user should be redirected to
after authentication. Note that `redirect_uris` must be one of the URLs given during
app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed,
otherwise it is added to the given URL as the "code" request parameter.
app registration, and that despite the plural-like name, you only get to use one here.
When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed, otherwise it is added
to the given URL as the "code" request parameter.
Pass force_login if you want the user to always log in even when already logged
into web Mastodon (i.e. when registering multiple different accounts in an app).
@ -269,6 +277,7 @@ class Mastodon(Internals):
Pass an ISO 639-1 (two letter) or, for languages that do not have one, 639-3 (three letter)
language code as `lang` to control the display language for the oauth form.
"""
assert self.api_base_url is not None
if client_id is None:
client_id = self.client_id
else:
@ -287,7 +296,9 @@ class Mastodon(Internals):
formatted_params = urlencode(params)
return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])
def log_in(self, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=_DEFAULT_SCOPES, to_file=None):
def log_in(self, username: Optional[str] = None, password: Optional[str] = None, code: Optional[str] = None,
redirect_uri: str = "urn:ietf:wg:oauth:2.0:oob", refresh_token: Optional[str] = None, scopes: List[str] = _DEFAULT_SCOPES,
to_file = Union[str, PurePath]) -> str:
"""
Get the access token for a user.
@ -346,12 +357,14 @@ class Mastodon(Internals):
raise MastodonAPIError('Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".')
if to_file is not None:
assert self.api_base_url is not None
assert self.client_id is not None and isinstance(self.client_id, str)
assert self.client_secret is not None
with open(to_file, 'w') as token_file:
token_file.write(response['access_token'] + "\n")
token_file.write(self.api_base_url + "\n")
token_file.write(self.client_id + "\n")
token_file.write(self.client_secret + "\n")
self.__logged_in_id = None
# Retry version check if needed (might be required in limited federation mode)
@ -383,10 +396,8 @@ class Mastodon(Internals):
# Reading data: Apps
###
@api_version("2.0.0", "2.7.2", _DICT_VERSION_APPLICATION)
def app_verify_credentials(self):
def app_verify_credentials(self) -> Application:
"""
Fetch information about the current application.
Returns an :ref:`application dict <application dict>`.
"""
return self.__api_request('GET', '/api/v1/apps/verify_credentials')

Wyświetl plik

@ -1,30 +1,30 @@
# conversations.py - conversation endpoints
from .versions import _DICT_VERSION_CONVERSATION
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_CONVERSATION
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from typing import Union, Optional
from mastodon.types import IdType, PaginatableList, Conversation
class Mastodon(Internals):
###
# Reading data: Conversations
###
@api_version("2.6.0", "2.6.0", _DICT_VERSION_CONVERSATION)
def conversations(self, max_id=None, min_id=None, since_id=None, limit=None):
def conversations(self, max_id: Optional[Union[Conversation, IdType]] = None, min_id: Optional[Union[Conversation, IdType]] = None, since_id:
Optional[Union[Conversation, IdType]] = None, limit: Optional[int] = None) -> PaginatableList[Conversation]:
"""
Fetches a user's conversations.
Returns a list of :ref:`conversation dicts <conversation dicts>`.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals())
return self.__api_request('GET', "/api/v1/conversations/", params)
@ -33,11 +33,11 @@ class Mastodon(Internals):
# Writing data: Conversations
###
@api_version("2.6.0", "2.6.0", _DICT_VERSION_CONVERSATION)
def conversations_read(self, id):
def conversations_read(self, id: Union[Conversation, IdType]):
"""
Marks a single conversation as read.
Returns the updated :ref:`conversation dict <conversation dict>`.
The returned object reflects the conversation's new read status.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/conversations/{id}/read')

Wyświetl plik

@ -1,20 +1,18 @@
# notifications.py - endorsement endpoints
from .versions import _DICT_VERSION_ACCOUNT
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_ACCOUNT
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Account, NonPaginatableList
class Mastodon(Internals):
###
# Reading data: Endorsements
###
@api_version("2.5.0", "2.5.0", _DICT_VERSION_ACCOUNT)
def endorsements(self):
def endorsements(self) -> NonPaginatableList[Account]:
"""
Fetch list of users endorsed by the logged-in user.
Returns a list of :ref:`account dicts <account dicts>`.
"""
return self.__api_request('GET', '/api/v1/endorsements')

Wyświetl plik

@ -1,35 +1,36 @@
# favourites.py - favourites and also bookmarks
from .versions import _DICT_VERSION_STATUS
from .utility import api_version
from mastodon.versions import _DICT_VERSION_STATUS
from mastodon.utility import api_version
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
from mastodon.types import Status, IdType, PaginatableList
from typing import Optional, Union
class Mastodon(Internals):
###
# Reading data: Favourites
###
@api_version("1.0.0", "2.6.0", _DICT_VERSION_STATUS)
def favourites(self, max_id=None, min_id=None, since_id=None, limit=None):
def favourites(self, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None,
since_id: Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[Status]:
"""
Fetch the logged-in user's favourited statuses.
This endpoint uses internal ids for pagination, passing status ids to
`max_id`, `min_id`, or `since_id` will not work. Pagination functions
:ref:`fetch_next() <fetch_next()>`
and :ref:`fetch_previous() <fetch_previous()>` should be used instead.
`max_id`, `min_id`, or `since_id` will not work.
Returns a list of :ref:`status dicts <status dicts>`.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/favourites', params)
@ -38,25 +39,22 @@ class Mastodon(Internals):
# Reading data: Bookmarks
###
@api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None):
def bookmarks(self, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None,
since_id: Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[Status]:
"""
Get a list of statuses bookmarked by the logged-in user.
This endpoint uses internal ids for pagination, passing status ids to
`max_id`, `min_id`, or `since_id` will not work. Pagination functions
:ref:`fetch_next() <fetch_next()>`
and :ref:`fetch_previous() <fetch_previous()>` should be used instead.
Returns a list of :ref:`status dicts <status dicts>`.
`max_id`, `min_id`, or `since_id` will not work.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/bookmarks', params)

Wyświetl plik

@ -1,31 +1,29 @@
# hashtags.py - hashtag and featured-hashtag endpoints
from .versions import _DICT_VERSION_FEATURED_TAG, _DICT_VERSION_HASHTAG
from .utility import api_version
from mastodon.versions import _DICT_VERSION_FEATURED_TAG, _DICT_VERSION_HASHTAG
from mastodon.utility import api_version
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
from mastodon.types import Tag, NonPaginatableList, FeaturedTag, IdType
from typing import Union
class Mastodon(Internals):
###
# Reading data: Featured hashtags
###
@api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
def featured_tags(self):
def featured_tags(self) -> NonPaginatableList[Tag]:
"""
Return the hashtags the logged-in user has set to be featured on
their profile as a list of :ref:`featured tag dicts <featured tag dicts>`.
Returns a list of :ref:`featured tag dicts <featured tag dicts>`.
"""
return self.__api_request('GET', '/api/v1/featured_tags')
@api_version("3.0.0", "3.0.0", _DICT_VERSION_HASHTAG)
def featured_tag_suggestions(self):
def featured_tag_suggestions(self) -> NonPaginatableList[Tag]:
"""
Returns the logged-in user's 10 most commonly-used hashtags.
Returns a list of :ref:`hashtag dicts <hashtag dicts>`.
"""
return self.__api_request('GET', '/api/v1/featured_tags/suggestions')
@ -33,17 +31,17 @@ class Mastodon(Internals):
# Writing data: Featured hashtags
###
@api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
def featured_tag_create(self, name):
def featured_tag_create(self, name: str) -> FeaturedTag:
"""
Creates a new featured hashtag displayed on the logged-in user's profile.
Returns a :ref:`featured tag dict <featured tag dict>` with the newly featured tag.
The returned object is the newly featured tag.
"""
params = self.__generate_params(locals())
return self.__api_request('POST', '/api/v1/featured_tags', params)
@api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
def featured_tag_delete(self, id):
def featured_tag_delete(self, id: Union[FeaturedTag, IdType]):
"""
Deletes one of the logged-in user's featured hashtags.
"""

Wyświetl plik

@ -1,49 +1,74 @@
# instance.py - instance-level endpoints, directory, emoji, announcements
from .versions import _DICT_VERSION_INSTANCE, _DICT_VERSION_ACTIVITY, _DICT_VERSION_ACCOUNT, _DICT_VERSION_EMOJI, _DICT_VERSION_ANNOUNCEMENT
from .errors import MastodonIllegalArgumentError, MastodonNotFoundError
from .utility import api_version
from .compat import urlparse
from mastodon.versions import _DICT_VERSION_INSTANCE, _DICT_VERSION_ACTIVITY, _DICT_VERSION_ACCOUNT, _DICT_VERSION_EMOJI, _DICT_VERSION_ANNOUNCEMENT
from mastodon.errors import MastodonIllegalArgumentError, MastodonNotFoundError
from mastodon.utility import api_version
from mastodon.compat import urlparse
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
from mastodon.types import Instance, InstanceV2, NonPaginatableList, Activity, Nodeinfo, AttribAccessDict, Rule, Announcement, CustomEmoji, Account, IdType
from typing import Union, Optional
class Mastodon(Internals):
###
# Reading data: Instances
###
@api_version("1.1.0", "2.3.0", _DICT_VERSION_INSTANCE)
def instance(self):
def instance_v1(self) -> Instance:
"""
Retrieve basic information about the instance, including the URI and administrative contact email.
Does not require authentication unless locked down by the administrator.
This is the explicit v1 version of this function. The v2 version is available through instance_v2().
It contains a bit more information than this one, but does not include whether invites are enabled.
Returns an :ref:`instance dict <instance dict>`.
"""
return self.__instance()
def __instance(self):
"""
Internal, non-version-checking helper that does the same as instance()
Internal, non-version-checking helper that does the same as instance_v1()
"""
instance = self.__api_request('GET', '/api/v1/instance/')
return instance
@api_version("4.0.0", "4.0.0", _DICT_VERSION_INSTANCE)
def instance_v2(self) -> InstanceV2:
"""
Retrieve basic information about the instance, including the URI and administrative contact email.
Does not require authentication unless locked down by the administrator. This is the explicit v2 variant.
Returns an :ref:`instance dict <instance dict>`.
"""
return self.__api_request('GET', '/api/v2/instance/')
@api_version("1.1.0", "4.0.0", _DICT_VERSION_INSTANCE)
def instance(self) -> Union[Instance, InstanceV2]:
"""
Retrieve basic information about the instance, including the URI and administrative contact email.
Does not require authentication unless locked down by the administrator.
Returns an :ref:`instance dict <instance dict>`.
"""
return self.__api_request('GET', '/api/v2/instance/')
@api_version("2.1.2", "2.1.2", _DICT_VERSION_ACTIVITY)
def instance_activity(self):
def instance_activity(self) -> NonPaginatableList[Activity]:
"""
Retrieve activity stats about the instance. May be disabled by the instance administrator - throws
a MastodonNotFoundError in that case.
Activity is returned for 12 weeks going back from the current week.
Returns a list of :ref:`activity dicts <activity dicts>`.
"""
return self.__api_request('GET', '/api/v1/instance/activity')
@api_version("2.1.2", "2.1.2", "2.1.2")
def instance_peers(self):
def instance_peers(self) -> NonPaginatableList[str]:
"""
Retrieve the instances that this instance knows about. May be disabled by the instance administrator - throws
a MastodonNotFoundError in that case.
@ -53,7 +78,7 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/instance/peers')
@api_version("3.0.0", "3.0.0", "3.0.0")
def instance_health(self):
def instance_health(self) -> bool:
"""
Basic health check. Returns True if healthy, False if not.
"""
@ -61,13 +86,15 @@ class Mastodon(Internals):
return status in ["OK", "success"]
@api_version("3.0.0", "3.0.0", "3.0.0")
def instance_nodeinfo(self, schema="http://nodeinfo.diaspora.software/ns/schema/2.0"):
def instance_nodeinfo(self, schema: str = "http://nodeinfo.diaspora.software/ns/schema/2.0") -> Union[Nodeinfo, AttribAccessDict]:
"""
Retrieves the instance's nodeinfo information.
For information on what the nodeinfo can contain, see the nodeinfo
specification: https://github.com/jhass/nodeinfo . By default,
Mastodon.py will try to retrieve the version 2.0 schema nodeinfo.
Mastodon.py will try to retrieve the version 2.0 schema nodeinfo, for which
we have a well defined return object. If you go outside of that, all bets
are off.
To override the schema, specify the desired schema with the `schema`
parameter.
@ -90,11 +117,9 @@ class Mastodon(Internals):
return self.__api_request('GET', parse.path + parse.params + parse.query + parse.fragment)
@api_version("3.4.0", "3.4.0", _DICT_VERSION_INSTANCE)
def instance_rules(self):
def instance_rules(self) -> NonPaginatableList[Rule]:
"""
Retrieve instance rules.
Returns a list of `id` + `text` dicts, same as the `rules` field in the :ref:`instance dicts <instance dicts>`.
"""
return self.__api_request('GET', '/api/v1/instance/rules')
@ -102,7 +127,8 @@ class Mastodon(Internals):
# Reading data: Directory
###
@api_version("3.0.0", "3.0.0", _DICT_VERSION_ACCOUNT)
def directory(self, offset=None, limit=None, order=None, local=None):
def directory(self, offset: Optional[int] = None, limit: Optional[int] = None,
order: Optional[str] = None, local: Optional[bool] = None) -> NonPaginatableList[Account]:
"""
Fetch the contents of the profile directory, if enabled on the server.
@ -110,13 +136,13 @@ class Mastodon(Internals):
`limit` how many accounts to load. Default 40.
`order` "active" to sort by most recently posted statuses (default) or
`order` "active" to sort by most recently posted statuses (usually the default) or
"new" to sort by most recently created profiles.
`local` True to return only local accounts.
Returns a list of :ref:`account dicts <account dicts>`.
Uses offset/limit pagination, not currently handled by the pagination utility functions,
do it manually if you have to.
"""
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/directory', params)
@ -125,13 +151,11 @@ class Mastodon(Internals):
# Reading data: Emoji
###
@api_version("2.1.0", "2.1.0", _DICT_VERSION_EMOJI)
def custom_emojis(self):
def custom_emojis(self) -> NonPaginatableList[CustomEmoji]:
"""
Fetch the list of custom emoji the instance has installed.
Does not require authentication unless locked down by the administrator.
Returns a list of :ref:`emoji dicts <emoji dicts>`.
"""
return self.__api_request('GET', '/api/v1/custom_emojis')
@ -139,7 +163,7 @@ class Mastodon(Internals):
# Reading data: Announcements
##
@api_version("3.1.0", "3.1.0", _DICT_VERSION_ANNOUNCEMENT)
def announcements(self):
def announcements(self) -> NonPaginatableList[Announcement]:
"""
Fetch currently active announcements.
@ -151,16 +175,15 @@ class Mastodon(Internals):
# Writing data: Annoucements
###
@api_version("3.1.0", "3.1.0", "3.1.0")
def announcement_dismiss(self, id):
def announcement_dismiss(self, id: Union[Announcement, IdType]):
"""
Set the given annoucement to read.
"""
id = self.__unpack_id(id)
self.__api_request('POST', f'/api/v1/announcements/{id}/dismiss')
@api_version("3.1.0", "3.1.0", "3.1.0")
def announcement_reaction_create(self, id, reaction):
def announcement_reaction_create(self, id: Union[Announcement, IdType], reaction: str):
"""
Add a reaction to an announcement. `reaction` can either be a unicode emoji
or the name of one of the instances custom emoji.
@ -170,16 +193,14 @@ class Mastodon(Internals):
reaction that a different user added is legal and increments the count).
"""
id = self.__unpack_id(id)
self.__api_request('PUT', f'/api/v1/announcements/{id}/reactions/{reaction}')
@api_version("3.1.0", "3.1.0", "3.1.0")
def announcement_reaction_delete(self, id, reaction):
def announcement_reaction_delete(self, id: Union[Announcement, IdType], reaction: str):
"""
Remove a reaction to an announcement.
Will throw an API error if the reaction does not exist.
"""
id = self.__unpack_id(id)
self.__api_request('DELETE', f'/api/v1/announcements/{id}/reactions/{reaction}')

Wyświetl plik

@ -1,6 +1,6 @@
# internals.py - many internal helpers
import datetime
from datetime import timezone, datetime
from contextlib import closing
import mimetypes
import threading
@ -14,20 +14,22 @@ import re
import collections
import base64
import os
import inspect
from .utility import AttribAccessDict, AttribAccessList, parse_version_string
from .errors import MastodonNetworkError, MastodonIllegalArgumentError, MastodonRatelimitError, MastodonNotFoundError, \
from mastodon.versions import parse_version_string
from mastodon.errors import MastodonNetworkError, MastodonIllegalArgumentError, MastodonRatelimitError, MastodonNotFoundError, \
MastodonUnauthorizedError, MastodonInternalServerError, MastodonBadGatewayError, MastodonServiceUnavailableError, \
MastodonGatewayTimeoutError, MastodonServerError, MastodonAPIError, MastodonMalformedEventError
from .compat import urlparse, magic, PurePath, Path
from .defaults import _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC
from mastodon.compat import urlparse, magic, PurePath, Path
from mastodon.defaults import _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC
from mastodon.types import AttribAccessDict, try_cast_recurse
from mastodon.types import *
###
# Internal helpers, dragons probably
###
class Mastodon():
def __datetime_to_epoch(self, date_time):
def __datetime_to_epoch(self, date_time: datetime) -> float:
"""
Converts a python datetime to unix epoch, accounting for
time zones and such.
@ -35,7 +37,7 @@ class Mastodon():
Assumes UTC if timezone is not given.
"""
if date_time.tzinfo is None:
date_time = date_time.replace(tzinfo=datetime.timezone.utc)
date_time = date_time.replace(tzinfo=timezone.utc)
return date_time.timestamp()
def __get_logged_in_id(self):
@ -47,94 +49,48 @@ class Mastodon():
return self.__logged_in_id
@staticmethod
def __json_allow_dict_attrs(json_object):
"""
Makes it possible to use attribute notation to access a dicts
elements, while still allowing the dict to act as a dict.
"""
if isinstance(json_object, dict):
return AttribAccessDict(json_object)
return json_object
@staticmethod
def __json_date_parse(json_object):
"""
Parse dates in certain known json fields, if possible.
"""
known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at",
"updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at", "date", "period"]
mark_delete = []
for k, v in json_object.items():
if k in known_date_fields:
if v is not None:
try:
if isinstance(v, int):
json_object[k] = datetime.datetime.fromtimestamp(v, datetime.timezone.utc)
else:
json_object[k] = dateutil.parser.parse(v)
except:
# When we can't parse a date, we just leave the field out
mark_delete.append(k)
# Two step process because otherwise python gets very upset
for k in mark_delete:
del json_object[k]
return json_object
@staticmethod
def __json_truefalse_parse(json_object):
"""
Parse 'True' / 'False' strings in certain known fields
"""
for key in ('follow', 'favourite', 'reblog', 'mention', 'confirmed', 'suspended', 'silenced', 'disabled', 'approved', 'all_day'):
if (key in json_object and isinstance(json_object[key], six.text_type)):
if json_object[key].lower() == 'true':
json_object[key] = True
if json_object[key].lower() == 'false':
json_object[key] = False
return json_object
@staticmethod
def __json_strnum_to_bignum(json_object):
"""
Converts json string numerals to native python bignums.
"""
for key in ('id', 'week', 'in_reply_to_id', 'in_reply_to_account_id', 'logins', 'registrations', 'statuses',
'day', 'last_read_id', 'value', 'frequency', 'rate', 'invited_by_account_id', 'count'):
if (key in json_object and isinstance(json_object[key], six.text_type)):
try:
json_object[key] = int(json_object[key])
except ValueError:
pass
return json_object
@staticmethod
def __json_hooks(json_object):
"""
All the json hooks. Used in request parsing.
"""
json_object = Mastodon.__json_strnum_to_bignum(json_object)
json_object = Mastodon.__json_date_parse(json_object)
json_object = Mastodon.__json_truefalse_parse(json_object)
json_object = Mastodon.__json_allow_dict_attrs(json_object)
return json_object
@staticmethod
def __consistent_isoformat_utc(datetime_val):
def __consistent_isoformat_utc(datetime_val: datetime) -> str:
"""
Function that does what isoformat does but it actually does the same
every time instead of randomly doing different things on some systems
and also it represents that time as the equivalent UTC time.
"""
isotime = datetime_val.astimezone(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S%z")
isotime = datetime_val.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S%z")
if isotime[-2] != ":":
isotime = isotime[:-2] + ":" + isotime[-2:]
return isotime
def __try_cast_to_type(self, value, override_type = None):
"""
Tries to cast a value to the type of the function two levels up in the call stack.
Tries to cast to AttribAccessDict if it doesn't know what to cast to.
This is used internally inside of __api_request.
"""
try:
if override_type is None:
# Find type of function two frames up
caller_frame = inspect.currentframe().f_back.f_back
caller_function = caller_frame.f_code
caller_func_name = caller_function.co_name
func_obj = getattr(self, caller_func_name)
# Very carefully try to find what we need to cast to
return_type = AttribAccessDict
if func_obj is not None:
return_type = func_obj.__annotations__.get('return', AttribAccessDict)
else:
return_type = override_type
except:
return_type = AttribAccessDict
return try_cast_recurse(return_type, value)
def __api_request(self, method, endpoint, params={}, files={}, headers={}, access_token_override=None, base_url_override=None,
do_ratelimiting=True, use_json=False, parse=True, return_response_object=False, skip_error_check=False, lang_override=None):
do_ratelimiting=True, use_json=False, parse=True, return_response_object=False, skip_error_check=False, lang_override=None, override_type=None):
"""
Internal API request helper.
Does a large amount of different things that I should document one day, but not today.
"""
response = None
remaining_wait = 0
@ -248,7 +204,7 @@ class Mastodon():
if not response_object.ok:
try:
response = response_object.json(object_hook=self.__json_hooks)
response = self.__try_cast_to_type(response_object.json(), override_type = override_type) # TODO actually cast to an error type
if isinstance(response, dict) and 'error' in response:
error_msg = response['error']
elif isinstance(response, str):
@ -301,12 +257,16 @@ class Mastodon():
if parse:
try:
response = response_object.json(object_hook=self.__json_hooks)
except:
# The new parsing is very basic, type conversion happens later,
# within the new type system. This should be overall more robust.
response = response_object.json()
except Exception as e:
raise MastodonAPIError(
f"Could not parse response as JSON, response code was {response_object.status_code}, "
f"bad json content was {response_object.content!r}."
f"bad json content was {response_object.content!r}.",
f"Exception was: {e}"
)
response = self.__try_cast_to_type(response, override_type = override_type)
else:
response = response_object.content
@ -391,10 +351,10 @@ class Mastodon():
# Will be removed in future
if isinstance(response[0], AttribAccessDict):
response[0]._pagination_prev = prev_params
return response
def __get_streaming_base(self):
def __get_streaming_base(self) -> str:
"""
Internal streaming API helper.
@ -415,6 +375,7 @@ class Mastodon():
)
else:
url = self.api_base_url
assert not url is None
return url
def __stream(self, endpoint, listener, params={}, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
@ -570,7 +531,7 @@ class Mastodon():
return params
def __unpack_id(self, id, dateconv=False):
def __unpack_id(self, id, dateconv = False, listify = False):
"""
Internal object-to-id converter
@ -579,10 +540,18 @@ class Mastodon():
the id straight.
Also unpacks datetimes to snowflake IDs if requested.
TODO: Rework this to use the new type system.
"""
if not isinstance(id, list) and listify:
id = [id]
if isinstance(id, list):
for i in range(len(id)):
id[i] = self.__unpack_id(id[i], dateconv = dateconv, listify = False)
return id
if isinstance(id, dict) and "id" in id:
id = id["id"]
if dateconv and isinstance(id, datetime.datetime):
if dateconv and isinstance(id, datetime):
id = (int(id.timestamp()) << 16) * 1000
return id
@ -623,6 +592,7 @@ class Mastodon():
return mime_type
def __load_media_file(self, media_file, mime_type=None, file_name=None):
"""Internal helper to load a media file"""
if isinstance(media_file, PurePath):
media_file = str(media_file)
if isinstance(media_file, str):

Wyświetl plik

@ -1,51 +1,49 @@
# list.py - list endpoints
from .versions import _DICT_VERSION_LIST, _DICT_VERSION_ACCOUNT
from .utility import api_version
from mastodon.versions import _DICT_VERSION_LIST, _DICT_VERSION_ACCOUNT
from mastodon.utility import api_version
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
from mastodon.types import NonPaginatableList, UserList, IdType, PaginatableList, Account
from typing import List, Union, Optional
class Mastodon(Internals):
###
# Reading data: Lists
###
@api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
def lists(self):
def lists(self) -> NonPaginatableList[UserList]:
"""
Fetch a list of all the Lists by the logged-in user.
Returns a list of :ref:`list dicts <list dicts>`.
"""
return self.__api_request('GET', '/api/v1/lists')
@api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
def list(self, id):
def list(self, id: Union[UserList, IdType]) -> UserList:
"""
Fetch info about a specific list.
Returns a :ref:`list dict <list dict>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/lists/{id}')
@api_version("2.1.0", "2.6.0", _DICT_VERSION_ACCOUNT)
def list_accounts(self, id, max_id=None, min_id=None, since_id=None, limit=None):
def list_accounts(self, id: Union[UserList, IdType], max_id: Optional[Union[UserList, IdType]] = None,
min_id: Optional[Union[UserList, IdType]] = None, since_id: Optional[Union[UserList, IdType]] = None,
limit: Optional[int] = None) -> PaginatableList[Account]:
"""
Get the accounts that are on the given list.
Returns a list of :ref:`account dicts <account dicts>`.
"""
id = self.__unpack_id(id)
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals(), ['id'])
return self.__api_request('GET', f'/api/v1/lists/{id}/accounts', params)
@ -54,28 +52,26 @@ class Mastodon(Internals):
# Writing data: Lists
###
@api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
def list_create(self, title):
def list_create(self, title: str) -> UserList:
"""
Create a new list with the given `title`.
Returns the :ref:`list dict <list dict>` of the created list.
"""
params = self.__generate_params(locals())
return self.__api_request('POST', '/api/v1/lists', params)
@api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
def list_update(self, id, title):
def list_update(self, id: Union[UserList, IdType], title: str) -> UserList:
"""
Update info about a list, where "info" is really the lists `title`.
Returns the :ref:`list dict <list dict>` of the modified list.
The returned object reflects the updated list.
"""
id = self.__unpack_id(id)
params = self.__generate_params(locals(), ['id'])
return self.__api_request('PUT', f'/api/v1/lists/{id}', params)
@api_version("2.1.0", "2.1.0", "2.1.0")
def list_delete(self, id):
def list_delete(self, id: Union[UserList, IdType]):
"""
Delete a list.
"""
@ -83,29 +79,22 @@ class Mastodon(Internals):
self.__api_request('DELETE', f'/api/v1/lists/{id}')
@api_version("2.1.0", "2.1.0", "2.1.0")
def list_accounts_add(self, id, account_ids):
def list_accounts_add(self, id: Union[UserList, IdType], account_ids: List[Union[Account, IdType]]):
"""
Add the account(s) given in `account_ids` to the list.
"""
id = self.__unpack_id(id)
if not isinstance(account_ids, list):
account_ids = [account_ids]
account_ids = [self.__unpack_id(x) for x in account_ids]
account_ids = self.__unpack_id(account_ids, listify = True)
params = self.__generate_params(locals(), ['id'])
self.__api_request('POST', f'/api/v1/lists/{id}/accounts', params)
@api_version("2.1.0", "2.1.0", "2.1.0")
def list_accounts_delete(self, id, account_ids):
def list_accounts_delete(self, id: Union[UserList, IdType], account_ids: List[Union[Account, IdType]]):
"""
Remove the account(s) given in `account_ids` from the list.
"""
id = self.__unpack_id(id)
if not isinstance(account_ids, list):
account_ids = [account_ids]
account_ids = [self.__unpack_id(x) for x in account_ids]
account_ids = self.__unpack_id(account_ids, listify = True)
params = self.__generate_params(locals(), ['id'])
self.__api_request('DELETE', f'/api/v1/lists/{id}/accounts', params)

Wyświetl plik

@ -2,19 +2,21 @@
import time
from .versions import _DICT_VERSION_MEDIA
from .errors import MastodonVersionError, MastodonAPIError
from .utility import api_version
from mastodon.versions import _DICT_VERSION_MEDIA
from mastodon.errors import MastodonVersionError, MastodonAPIError
from mastodon.utility import api_version
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
from mastodon.types import MediaAttachment, PathOrFile, IdType
from typing import Optional, Union, Tuple, List, Dict, Any
class Mastodon(Internals):
###
# Reading data: Media
###
@api_version("3.1.4", "3.1.4", _DICT_VERSION_MEDIA)
def media(self, id):
def media(self, id: Union[MediaAttachment, IdType]) -> MediaAttachment:
"""
Get the updated JSON for one non-attached / in progress media upload belonging
to the logged-in user.
@ -26,7 +28,10 @@ class Mastodon(Internals):
# Writing data: Media
###
@api_version("1.0.0", "3.2.0", _DICT_VERSION_MEDIA)
def media_post(self, media_file, mime_type=None, description=None, focus=None, file_name=None, thumbnail=None, thumbnail_mime_type=None, synchronous=False):
def media_post(self, media_file: PathOrFile, mime_type: Optional[str] = None, description: Optional[str] = None,
focus: Optional[Tuple[float, float]] = None, file_name: Optional[str] = None,
thumbnail: Optional[PathOrFile] = None, thumbnail_mime_type: Optional[str] = None,
synchronous: bool = False) -> MediaAttachment:
"""
Post an image, video or audio file. `media_file` can either be data or
a file name. If data is passed directly, the mime type has to be specified
@ -63,8 +68,7 @@ class Mastodon(Internals):
if thumbnail is not None:
if not self.verify_minimum_version("3.2.0", cached=True):
raise MastodonVersionError('Thumbnail requires version > 3.2.0')
files["thumbnail"] = self.__load_media_file(
thumbnail, thumbnail_mime_type)
files["thumbnail"] = self.__load_media_file(thumbnail, thumbnail_mime_type)
# Disambiguate URL by version
if self.verify_minimum_version("3.1.4", cached=True):
@ -90,12 +94,14 @@ class Mastodon(Internals):
return ret_dict
@api_version("2.3.0", "3.2.0", _DICT_VERSION_MEDIA)
def media_update(self, id, description=None, focus=None, thumbnail=None, thumbnail_mime_type=None):
def media_update(self, id: Union[MediaAttachment, IdType], description: Optional[str] = None,
focus: Optional[Tuple[float, float]] = None, thumbnail: Optional[PathOrFile] = None,
thumbnail_mime_type=None) -> MediaAttachment:
"""
Update the metadata of the media file with the given `id`. `description` and
`focus` and `thumbnail` are as in :ref:`media_post() <media_post()>` .
Returns the updated :ref:`media dict <media dict>`.
The returned dict reflects the updates to the media attachment.
"""
id = self.__unpack_id(id)

Wyświetl plik

@ -1,18 +1,21 @@
# notifications.py - notification endpoints
from .versions import _DICT_VERSION_NOTIFICATION
from .errors import MastodonIllegalArgumentError
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_NOTIFICATION
from mastodon.errors import MastodonIllegalArgumentError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Notification, IdType, PaginatableList, Account
from typing import Union, Optional, List
class Mastodon(Internals):
###
# Reading data: Notifications
###
@api_version("1.0.0", "3.5.0", _DICT_VERSION_NOTIFICATION)
def notifications(self, id=None, account_id=None, max_id=None, min_id=None, since_id=None, limit=None, exclude_types=None, types=None, mentions_only=None):
def notifications(self, id: Optional[Union[Notification, IdType]] = None, account_id: Optional[Union[Account, IdType]] = None, max_id: Optional[Union[Notification, IdType]] = None,
min_id: Optional[Union[Notification, IdType]] = None, since_id: Optional[Union[Notification, IdType]] = None, limit: Optional[int] = None,
exclude_types: Optional[List[str]] = None, types: Optional[List[str]] = None, mentions_only: Optional[bool] = None) -> PaginatableList[Notification]:
"""
Fetch notifications (mentions, favourites, reblogs, follows) for the logged-in
user. Pass `account_id` to get only notifications originating from the given account.
@ -49,13 +52,13 @@ class Mastodon(Internals):
del mentions_only
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
if account_id is not None:
account_id = self.__unpack_id(account_id)
@ -78,7 +81,7 @@ class Mastodon(Internals):
self.__api_request('POST', '/api/v1/notifications/clear')
@api_version("1.3.0", "2.9.2", "2.9.2")
def notifications_dismiss(self, id):
def notifications_dismiss(self, id: Union[Notification, IdType]):
"""
Deletes a single notification
"""

Wyświetl plik

@ -1,21 +1,20 @@
# polls.py - poll related endpoints and tooling
from .versions import _DICT_VERSION_POLL
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_POLL
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Poll, IdType
from typing import Union, List
class Mastodon(Internals):
###
# Reading data: Polls
###
@api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
def poll(self, id):
def poll(self, id: Union[Poll, IdType]) -> Poll:
"""
Fetch information about the poll with the given id
Returns a :ref:`poll dict <poll dict>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/polls/{id}')
@ -24,7 +23,7 @@ class Mastodon(Internals):
# Writing data: Polls
###
@api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
def poll_vote(self, id, choices):
def poll_vote(self, id: Union[Poll, IdType], choices: Union[int, List[int]]) -> Poll:
"""
Vote in the given poll.
@ -37,7 +36,7 @@ class Mastodon(Internals):
single-option polls, or only once per option in case of multi-option
polls.
Returns the updated :ref:`poll dict <poll dict>`
The returned object will reflect the updated votes.
"""
id = self.__unpack_id(id)
if not isinstance(choices, list):
@ -46,12 +45,14 @@ class Mastodon(Internals):
return self.__api_request('POST', f'/api/v1/polls/{id}/votes', params)
def make_poll(self, options, expires_in, multiple=False, hide_totals=False):
@api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
def make_poll(self, options: List[str], expires_in: int, multiple: bool = False, hide_totals: bool = False) -> Poll:
"""
Generate a poll object that can be passed as the `poll` option when posting a status.
options is an array of strings with the poll options (Maximum, by default: 4),
expires_in is the time in seconds for which the poll should be open.
`options` is an array of strings with the poll options (Maximum, by default: 4 - see
the instance configuration for the actual value on any given instance, if stated).
`expires_in` is the time in seconds for which the poll should be open.
Set multiple to True to allow people to choose more than one answer. Set
hide_totals to True to hide the results of the poll until it has expired.
"""

Wyświetl plik

@ -2,19 +2,20 @@
import collections
from .versions import _DICT_VERSION_PREFERENCES, _DICT_VERSION_MARKER
from .errors import MastodonIllegalArgumentError
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_PREFERENCES, _DICT_VERSION_MARKER
from mastodon.errors import MastodonIllegalArgumentError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Preferences, Marker, Status, IdType
from typing import Union, List, Dict
class Mastodon(Internals):
###
# Reading data: Preferences
###
@api_version("2.8.0", "2.8.0", _DICT_VERSION_PREFERENCES)
def preferences(self):
def preferences(self) -> Preferences:
"""
Fetch the user's preferences, which can be used to set some default options.
As of 2.8.0, apps can only fetch, not update preferences.
@ -27,7 +28,7 @@ class Mastodon(Internals):
# Reading data: Read markers
##
@api_version("3.0.0", "3.0.0", _DICT_VERSION_MARKER)
def markers_get(self, timeline=["home"]):
def markers_get(self, timeline: Union[str, List[str]] = ["home"]) -> List[Marker]:
"""
Get the last-read-location markers for the specified timelines. Valid timelines
are the same as in :ref:`timeline() <timeline()>`
@ -46,13 +47,13 @@ class Mastodon(Internals):
# Writing data: Read markers
##
@api_version("3.0.0", "3.0.0", _DICT_VERSION_MARKER)
def markers_set(self, timelines, last_read_ids):
def markers_set(self, timelines: Union[str, List[str]], last_read_ids: Union[Status, IdType, List[Status], List[IdType]]) -> Dict[str, Marker]:
"""
Set the "last read" marker(s) for the given timeline(s) to the given id(s)
Note that if you give an invalid timeline name, this will silently do nothing.
Returns a dict with the updated :ref:`read marker dicts <read marker dicts>`, keyed by timeline name.
Returns a dict with the updated markers, keyed by timeline name.
"""
if not isinstance(timelines, (list, tuple)):
timelines = [timelines]

Wyświetl plik

@ -4,36 +4,39 @@ import base64
import os
import json
from .versions import _DICT_VERSION_PUSH, _DICT_VERSION_PUSH_NOTIF
from .errors import MastodonIllegalArgumentError
from .utility import api_version
from .compat import IMPL_HAS_CRYPTO, ec, serialization, default_backend
from .compat import IMPL_HAS_ECE, http_ece
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_PUSH, _DICT_VERSION_PUSH_NOTIF
from mastodon.errors import MastodonIllegalArgumentError
from mastodon.utility import api_version
from mastodon.compat import IMPL_HAS_CRYPTO, ec, serialization, default_backend
from mastodon.compat import IMPL_HAS_ECE, http_ece
from mastodon.internals import Mastodon as Internals
from mastodon.types import WebpushCryptoParamsPubkey, WebpushCryptoParamsPrivkey, WebPushSubscription, PushNotification, try_cast_recurse
from typing import Optional, Tuple
class Mastodon(Internals):
###
# Reading data: Webpush subscriptions
###
@api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
def push_subscription(self):
def push_subscription(self) -> WebPushSubscription:
"""
Fetch the current push subscription the logged-in user has for this app.
Returns a :ref:`push subscription dict <push subscription dict>`.
Only one webpush subscription can be active at a time for any given app.
"""
return self.__api_request('GET', '/api/v1/push/subscription')
###
# Writing data: Push subscriptions
###
@api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
def push_subscription_set(self, endpoint, encrypt_params, follow_events=None,
favourite_events=None, reblog_events=None,
mention_events=None, poll_events=None,
follow_request_events=None, status_events=None, policy='all'):
@api_version("2.4.0", "4..0", _DICT_VERSION_PUSH)
def push_subscription_set(self, endpoint: str, encrypt_params: WebpushCryptoParamsPubkey, follow_events: Optional[bool] = None,
favourite_events: Optional[bool] = None, reblog_events: Optional[bool] = None,
mention_events: Optional[bool] = None, poll_events: Optional[bool] = None,
follow_request_events: Optional[bool] = None, status_events: Optional[bool] = None,
policy: str = 'all', update_events: Optional[bool] = None, admin_sign_up_events: Optional[bool] = None,
admin_report_events: Optional[bool] = None) -> WebPushSubscription:
"""
Sets up or modifies the push subscription the logged-in user has for this app.
@ -47,6 +50,18 @@ class Mastodon(Internals):
`all`, `none`, `follower` and `followed`.
The rest of the parameters controls what kind of events you wish to subscribe to.
Events whose names start with "admin" require admin privileges to subscribe to.
* `follow_events` controls whether you receive events when someone follows the logged in user.
* `favourite_events` controls whether you receive events when someone favourites one of the logged in users statuses.
* `reblog_events` controls whether you receive events when someone boosts one of the logged in users statuses.
* `mention_events` controls whether you receive events when someone mentions the logged in user in a status.
* `poll_events` controls whether you receive events when a poll the logged in user has voted in has ended.
* `follow_request_events` controls whether you receive events when someone requests to follow the logged in user.
* `status_events` controls whether you receive events when someone the logged in user has subscribed to notifications for posts a new status.
* `update_events` controls whether you receive events when a status that the logged in user has boosted has been edited.
* `admin_sign_up_events` controls whether you receive events when a new user signs up.
* `admin_report_events` controls whether you receive events when a new report is received.
Returns a :ref:`push subscription dict <push subscription dict>`.
"""
@ -86,22 +101,37 @@ class Mastodon(Internals):
if follow_request_events is not None:
params['data[alerts][status]'] = status_events
if update_events is not None:
params['data[alerts][update]'] = update_events
if admin_sign_up_events is not None:
params['data[alerts][admin.sign_up]'] = admin_sign_up_events
if admin_report_events is not None:
params['data[alerts][admin.report]'] = admin_report_events
# Canonicalize booleans
params = self.__generate_params(params)
return self.__api_request('POST', '/api/v1/push/subscription', params)
@api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
def push_subscription_update(self, follow_events=None,
favourite_events=None, reblog_events=None,
mention_events=None, poll_events=None,
follow_request_events=None):
def push_subscription_update(self, follow_events: Optional[bool] = None,
favourite_events: Optional[bool] = None, reblog_events: Optional[bool] = None,
mention_events: Optional[bool] = None, poll_events: Optional[bool] = None,
follow_request_events: Optional[bool] = None, status_events: Optional[bool] = None,
policy: Optional[str] = 'all', update_events: Optional[bool] = None, admin_sign_up_events: Optional[bool] = None,
admin_report_events: Optional[bool] = None) -> WebPushSubscription:
"""
Modifies what kind of events the app wishes to subscribe to.
Returns the updated :ref:`push subscription dict <push subscription dict>`.
Parameters are as in :ref:`push_subscription_create() <push_subscription_create()>`.
Returned object reflects the updated push subscription.
"""
params = {}
if policy is not None:
params['policy'] = policy
if follow_events is not None:
params['data[alerts][follow]'] = follow_events
@ -121,6 +151,18 @@ class Mastodon(Internals):
if follow_request_events is not None:
params['data[alerts][follow_request]'] = follow_request_events
if follow_request_events is not None:
params['data[alerts][status]'] = status_events
if update_events is not None:
params['data[alerts][update]'] = update_events
if admin_sign_up_events is not None:
params['data[alerts][admin.sign_up]'] = admin_sign_up_events
if admin_report_events is not None:
params['data[alerts][admin.report]'] = admin_report_events
# Canonicalize booleans
params = self.__generate_params(params)
@ -136,7 +178,7 @@ class Mastodon(Internals):
###
# Push subscription crypto utilities
###
def push_subscription_generate_keys(self):
def push_subscription_generate_keys(self) -> Tuple[WebpushCryptoParamsPubkey, WebpushCryptoParamsPrivkey]:
"""
Generates a private key, public key and shared secret for use in webpush subscriptions.
@ -144,8 +186,7 @@ class Mastodon(Internals):
public key and shared secret.
"""
if not IMPL_HAS_CRYPTO:
raise NotImplementedError(
'To use the crypto tools, please install the webpush feature dependencies.')
raise NotImplementedError('To use the crypto tools, please install the webpush feature dependencies.')
push_key_pair = ec.generate_private_key(ec.SECP256R1(), default_backend())
push_key_priv = push_key_pair.private_numbers().private_value
@ -172,17 +213,14 @@ class Mastodon(Internals):
return priv_dict, pub_dict
@api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH_NOTIF)
def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header):
def push_subscription_decrypt_push(self, data: bytes, decrypt_params: WebpushCryptoParamsPrivkey, encryption_header: str, crypto_key_header: str) -> PushNotification:
"""
Decrypts `data` received in a webpush request. Requires the private key dict
from :ref:`push_subscription_generate_keys() <push_subscription_generate_keys()>` (`decrypt_params`) as well as the
Encryption and server Crypto-Key headers from the received webpush
Returns the decoded webpush as a :ref:`push notification dict <push notification dict>`.
"""
if (not IMPL_HAS_ECE) or (not IMPL_HAS_CRYPTO):
raise NotImplementedError(
'To use the crypto tools, please install the webpush feature dependencies.')
raise NotImplementedError('To use the crypto tools, please install the webpush feature dependencies.')
salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip())
dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip())
@ -199,4 +237,4 @@ class Mastodon(Internals):
version="aesgcm"
)
return json.loads(decrypted.decode('utf-8'), object_hook=Mastodon.__json_hooks)
return try_cast_recurse(PushNotification, json.loads(decrypted.decode('utf-8')))

Wyświetl plik

@ -1,49 +1,48 @@
# relationships.py - endpoints for user and domain blocks and mutes as well as follow requests
from .versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_RELATIONSHIP
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_RELATIONSHIP
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Account, Relationship, PaginatableList, IdType
from typing import Optional, Union
class Mastodon(Internals):
###
# Reading data: Mutes and Blocks
###
@api_version("1.1.0", "2.6.0", _DICT_VERSION_ACCOUNT)
def mutes(self, max_id=None, min_id=None, since_id=None, limit=None):
def mutes(self, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None, since_id:
Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[Account]:
"""
Fetch a list of users muted by the logged-in user.
Returns a list of :ref:`account dicts <account dicts>`.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/mutes', params)
@api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
def blocks(self, max_id=None, min_id=None, since_id=None, limit=None):
def blocks(self, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None, since_id:
Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[Account]:
"""
Fetch a list of users blocked by the logged-in user.
Returns a list of :ref:`account dicts <account dicts>`.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/blocks', params)
@ -52,20 +51,19 @@ class Mastodon(Internals):
# Reading data: Follow requests
###
@api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
def follow_requests(self, max_id=None, min_id=None, since_id=None, limit=None):
def follow_requests(self, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None, since_id:
Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[Account]:
"""
Fetch the logged-in user's incoming follow requests.
Returns a list of :ref:`account dicts <account dicts>`.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/follow_requests', params)
@ -74,20 +72,21 @@ class Mastodon(Internals):
# Reading data: Domain blocks
###
@api_version("1.4.0", "2.6.0", "1.4.0")
def domain_blocks(self, max_id=None, min_id=None, since_id=None, limit=None):
def domain_blocks(self, max_id: Optional[IdType] = None, min_id: Optional[IdType] = None, since_id:
Optional[IdType] = None, limit: Optional[int] = None) -> PaginatableList[str]:
"""
Fetch the logged-in user's blocked domains.
Returns a list of blocked domain URLs (as strings, without protocol specifier).
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
max_id = self.__unpack_id(max_id)
if min_id is not None:
min_id = self.__unpack_id(min_id, dateconv=True)
min_id = self.__unpack_id(min_id)
if since_id is not None:
since_id = self.__unpack_id(since_id, dateconv=True)
since_id = self.__unpack_id(since_id)
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/domain_blocks', params)
@ -96,21 +95,17 @@ class Mastodon(Internals):
# Writing data: Follow requests
###
@api_version("1.0.0", "3.0.0", _DICT_VERSION_RELATIONSHIP)
def follow_request_authorize(self, id):
def follow_request_authorize(self, id: Union[Account, IdType]) -> Relationship:
"""
Accept an incoming follow request.
Returns the updated :ref:`relationship dict <relationship dict>` for the requesting account.
Accept an incoming follow request from the given Account and returns the updated Relationship.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/follow_requests/{id}/authorize')
@api_version("1.0.0", "3.0.0", _DICT_VERSION_RELATIONSHIP)
def follow_request_reject(self, id):
def follow_request_reject(self, id: Union[Account, IdType]) -> Relationship:
"""
Reject an incoming follow request.
Returns the updated :ref:`relationship dict <relationship dict>` for the requesting account.
Reject an incoming follow request from the given Account and returns the updated Relationship.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/follow_requests/{id}/reject')
@ -119,7 +114,7 @@ class Mastodon(Internals):
# Writing data: Domain blocks
###
@api_version("1.4.0", "1.4.0", "1.4.0")
def domain_block(self, domain=None):
def domain_block(self, domain: str):
"""
Add a block for all statuses originating from the specified domain for the logged-in user.
"""
@ -127,7 +122,7 @@ class Mastodon(Internals):
self.__api_request('POST', '/api/v1/domain_blocks', params)
@api_version("1.4.0", "1.4.0", "1.4.0")
def domain_unblock(self, domain=None):
def domain_unblock(self, domain: str):
"""
Remove a domain block for the logged-in user.
"""

Wyświetl plik

@ -1,28 +1,27 @@
# reports.py - report endpoints
from .versions import _DICT_VERSION_REPORT
from .errors import MastodonVersionError
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_REPORT
from mastodon.errors import MastodonVersionError, MastodonIllegalArgumentError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import NonPaginatableList, Report, Account, IdType, Status, Rule
from typing import Union, Optional, List
class Mastodon(Internals):
###
# Reading data: Reports
###
@api_version("1.1.0", "1.1.0", _DICT_VERSION_REPORT)
def reports(self):
def reports(self) -> NonPaginatableList[Report]:
"""
Fetch a list of reports made by the logged-in user.
Returns a list of :ref:`report dicts <report dicts>`.
Warning: This method has now finally been removed, and will not
work on Mastodon versions 2.5.0 and above.
"""
if self.verify_minimum_version("2.5.0", cached=True):
if self.verify_minimum_version("2.5.0", cached = True):
raise MastodonVersionError("API removed in Mastodon 2.5.0")
return self.__api_request('GET', '/api/v1/reports')
@ -30,7 +29,8 @@ class Mastodon(Internals):
# Writing data: Reports
###
@api_version("1.1.0", "3.5.0", _DICT_VERSION_REPORT)
def report(self, account_id, status_ids=None, comment=None, forward=False, category=None, rule_ids=None):
def report(self, account_id: Union[Account, IdType], status_ids: Optional[Union[Status, IdType]] = None, comment: Optional[str] = None,
forward: bool = False, category: Optional[str] = None, rule_ids: Optional[List[Union[Rule, IdType]]] = None) -> Report:
"""
Report statuses to the instances administrators.
@ -42,8 +42,6 @@ class Mastodon(Internals):
Set `forward` to True to forward a report of a remote user to that users
instance as well as sending it to the instance local administrators.
Returns a :ref:`report dict <report dict>`.
"""
if category is not None and not category in ["spam", "violation", "other"]:
raise MastodonIllegalArgumentError("Invalid report category (must be spam, violation or other)")

Wyświetl plik

@ -1,11 +1,12 @@
# search.py - search endpoints
from .versions import _DICT_VERSION_SEARCHRESULT
from .errors import MastodonVersionError
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_SEARCHRESULT
from mastodon.errors import MastodonVersionError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Search, SearchV2, Account, IdType
from typing import Union, Optional
class Mastodon(Internals):
###
@ -21,7 +22,10 @@ class Mastodon(Internals):
raise MastodonVersionError("Advanced search parameters require Mastodon 2.8.0+")
@api_version("1.1.0", "2.8.0", _DICT_VERSION_SEARCHRESULT)
def search(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True):
def search(self, q: str, resolve: bool = True, result_type: Optional[str] = None,
account_id: Optional[Union[Account, IdType]] = None, offset: Optional[int] = None,
min_id: Optional[IdType] = None, max_id: Optional[IdType] = None,
exclude_unreviewed: bool = True) -> Union[Search, SearchV2]:
"""
Fetch matching hashtags, accounts and statuses. Will perform webfinger
lookups if resolve is True. Full-text search is only enabled if
@ -44,17 +48,15 @@ class Mastodon(Internals):
on Mastodon 2.8.0 or above - this function will throw a MastodonVersionError
if you try to use them on versions before that. Note that the cached version
number will be used for this to avoid uneccesary requests.
Returns a :ref:`search result dict <search result dict>`, with tags as `hashtag dicts`_.
"""
if self.verify_minimum_version("2.4.1", cached=True):
return self.search_v2(q, resolve=resolve, result_type=result_type, account_id=account_id, offset=offset, min_id=min_id, max_id=max_id, exclude_unreviewed=exclude_unreviewed)
return self.search_v2(q, resolve=resolve, result_type=result_type, account_id=account_id, offset=offset, min_id=min_id, max_id=max_id, exclude_unreviewed=exclude_unreviewed, override_type=SearchV2)
else:
self.__ensure_search_params_acceptable(account_id, offset, min_id, max_id)
return self.search_v1(q, resolve=resolve)
return self.search_v1(q, resolve=resolve, override_type=Search)
@api_version("1.1.0", "2.1.0", "2.1.0")
def search_v1(self, q, resolve=False):
def search_v1(self, q: str, resolve: bool = False) -> Search:
"""
Identical to `search_v2()`, except in that it does not return
tags as :ref:`hashtag dicts <hashtag dicts>`.
@ -67,7 +69,10 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/search', params)
@api_version("2.4.1", "2.8.0", _DICT_VERSION_SEARCHRESULT)
def search_v2(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True):
def search_v2(self, q, resolve: bool = True, result_type: Optional[str] = None,
account_id: Optional[Union[Account, IdType]] = None, offset: Optional[int] = None,
min_id: Optional[IdType] = None, max_id: Optional[IdType] = None,
exclude_unreviewed: bool = True) -> SearchV2:
"""
Identical to `search_v1()`, except in that it returns tags as
:ref:`hashtag dicts <hashtag dicts>`, has more parameters, and resolves by default.

Wyświetl plik

@ -1,33 +1,35 @@
# statuses.py - status endpoints (regular and scheduled)
import collections
from datetime import datetime
from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS, \
from mastodon.versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS, \
_DICT_VERSION_STATUS_EDIT
from .errors import MastodonIllegalArgumentError
from .utility import api_version
from mastodon.errors import MastodonIllegalArgumentError
from mastodon.utility import api_version
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
from mastodon.types import Status, IdType, ScheduledStatus, PreviewCard, Context, NonPaginatableList, Account,\
MediaAttachment, Poll, StatusSource
from typing import Union, Optional, List
class Mastodon(Internals):
###
# Reading data: Statuses
###
@api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
def status(self, id):
def status(self, id: Union[Status, IdType]) -> Status:
"""
Fetch information about a single toot.
Does not require authentication for publicly visible statuses.
Returns a :ref:`status dict <status dict>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/statuses/{id}')
@api_version("1.0.0", "3.0.0", _DICT_VERSION_CARD)
def status_card(self, id):
def status_card(self, id: Union[Status, IdType]) -> PreviewCard:
"""
Fetch a card associated with a status. A card describes an object (such as an
external video or link) embedded into a status.
@ -38,8 +40,6 @@ class Mastodon(Internals):
exist anymore - you should just use the "card" field of the status dicts
instead. Mastodon.py will try to mimic the old behaviour, but this
is somewhat inefficient and not guaranteed to be the case forever.
Returns a :ref:`card dict <card dict>`.
"""
if self.verify_minimum_version("3.0.0", cached=True):
return self.status(id).card
@ -48,37 +48,31 @@ class Mastodon(Internals):
return self.__api_request('GET', f'/api/v1/statuses/{id}/card')
@api_version("1.0.0", "1.0.0", _DICT_VERSION_CONTEXT)
def status_context(self, id):
def status_context(self, id: Union[Status, IdType]) -> Context:
"""
Fetch information about ancestors and descendants of a toot.
Does not require authentication for publicly visible statuses.
Returns a :ref:`context dict <context dict>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/statuses/{id}/context')
@api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
def status_reblogged_by(self, id):
def status_reblogged_by(self, id: Union[Status, IdType]) -> NonPaginatableList[Account]:
"""
Fetch a list of users that have reblogged a status.
Does not require authentication for publicly visible statuses.
Returns a list of :ref:`account dicts <account dicts>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/statuses/{id}/reblogged_by')
@api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
def status_favourited_by(self, id):
def status_favourited_by(self, id: Union[Status, IdType]) -> NonPaginatableList[Account]:
"""
Fetch a list of users that have favourited a status.
Does not require authentication for publicly visible statuses.
Returns a list of :ref:`account dicts <account dicts>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/statuses/{id}/favourited_by')
@ -87,20 +81,16 @@ class Mastodon(Internals):
# Reading data: Scheduled statuses
###
@api_version("2.7.0", "2.7.0", _DICT_VERSION_SCHEDULED_STATUS)
def scheduled_statuses(self):
def scheduled_statuses(self) -> NonPaginatableList[ScheduledStatus]:
"""
Fetch a list of scheduled statuses
Returns a list of :ref:`scheduled status dicts <scheduled status dicts>`.
"""
return self.__api_request('GET', '/api/v1/scheduled_statuses')
@api_version("2.7.0", "2.7.0", _DICT_VERSION_SCHEDULED_STATUS)
def scheduled_status(self, id):
def scheduled_status(self, id: Union[ScheduledStatus, IdType]) -> ScheduledStatus:
"""
Fetch information about the scheduled status with the given id.
Returns a :ref:`scheduled status dict <scheduled status dict>`.
"""
id = self.__unpack_id(id)
return self.__api_request('GET', f'/api/v1/scheduled_statuses/{id}')
@ -108,10 +98,14 @@ class Mastodon(Internals):
###
# Writing data: Statuses
###
def __status_internal(self, status, in_reply_to_id=None, media_ids=None,
sensitive=False, visibility=None, spoiler_text=None,
language=None, idempotency_key=None, content_type=None,
scheduled_at=None, poll=None, quote_id=None, edit=False):
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: Optional[bool] = False) -> Union[Status, ScheduledStatus]:
"""
Internal statuses poster helper
"""
if quote_id is not None:
if self.feature_set != "fedibird":
raise MastodonIllegalArgumentError('quote_id is only available with feature set fedibird')
@ -151,7 +145,7 @@ class Mastodon(Internals):
del params_initial['language']
if params_initial['sensitive'] is False:
del [params_initial['sensitive']]
del params_initial['sensitive']
headers = {}
if idempotency_key is not None:
@ -177,18 +171,21 @@ class Mastodon(Internals):
use_json = True
params = self.__generate_params(params_initial, ['idempotency_key', 'edit'])
cast_type = Status
if scheduled_at is not None:
cast_type = ScheduledStatus
if edit is None:
# Post
return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json)
return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json, override_type=cast_type)
else:
# Edit
return self.__api_request('PUT', f'/api/v1/statuses/{self.__unpack_id(edit)}', params, headers=headers, use_json=use_json)
return self.__api_request('PUT', f'/api/v1/statuses/{self.__unpack_id(edit)}', params, headers=headers, use_json=use_json, override_type=cast_type)
@api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
def status_post(self, status, in_reply_to_id=None, media_ids=None,
sensitive=False, visibility=None, spoiler_text=None,
language=None, idempotency_key=None, content_type=None,
scheduled_at=None, poll=None, quote_id=None):
def status_post(self, status: str, in_reply_to_id: Optional[Union[Status, IdType]] = None, media_ids: Optional[List[Union[MediaAttachment, IdType]]] = None,
sensitive: 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) -> Union[Status, ScheduledStatus]:
"""
Post a status. Can optionally be in reply to another status and contain
media.
@ -262,23 +259,25 @@ class Mastodon(Internals):
)
@api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
def toot(self, status):
def toot(self, status: str) -> Status:
"""
Synonym for :ref:`status_post() <status_post()>` that only takes the status text as input.
Usage in production code is not recommended.
Returns a :ref:`status dict <status dict>` with the new status.
"""
return self.status_post(status)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
def status_update(self, id, status=None, spoiler_text=None, sensitive=None, media_ids=None, poll=None):
def status_update(self, id: Union[Status, IdType], status: Optional[str] = None, spoiler_text: Optional[str] = None,
sensitive: Optional[bool] = None, media_ids: Optional[List[Union[MediaAttachment, IdType]]] = None,
poll: Optional[Union[Poll, IdType]] = 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.
Note that editing a poll will reset the votes.
TODO: Currently doesn't support editing media descriptions, implement that.
"""
return self.__status_internal(
status=status,
@ -290,9 +289,9 @@ class Mastodon(Internals):
)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS_EDIT)
def status_history(self, id):
def status_history(self, id: Union[Status, IdType]) -> NonPaginatableList[Status]:
"""
Returns the edit history of a status as a list of :ref:`status edit dicts <status edit dicts>`, starting
Returns the edit history of a status as a list of Status objects, starting
from the original form. Note that this means that a status that has been edited
once will have *two* entries in this list, a status that has been edited twice
will have three, and so on.
@ -300,7 +299,7 @@ class Mastodon(Internals):
id = self.__unpack_id(id)
return self.__api_request('GET', f"/api/v1/statuses/{id}/history")
def status_source(self, id):
def status_source(self, id: Union[Status, IdType]) -> StatusSource:
"""
Returns the source of a status for editing.
@ -312,10 +311,10 @@ class Mastodon(Internals):
return self.__api_request('GET', f"/api/v1/statuses/{id}/source")
@api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None,
sensitive=False, visibility=None, spoiler_text=None,
language=None, idempotency_key=None, content_type=None,
scheduled_at=None, poll=None, untag=False):
def status_reply(self, to_status: Union[Status, IdType], status: str, media_ids: Optional[List[Union[MediaAttachment, IdType]]] = None,
sensitive: 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, untag: bool = False) -> Status:
"""
Helper function - acts like status_post, but prepends the name of all
the users that are being replied to the status text and retains
@ -362,7 +361,7 @@ class Mastodon(Internals):
return self.status_post(**keyword_args)
@api_version("1.0.0", "1.0.0", "1.0.0")
def status_delete(self, id):
def status_delete(self, id: Union[Status, IdType]) -> Status:
"""
Delete a status
@ -374,14 +373,14 @@ class Mastodon(Internals):
return self.__api_request('DELETE', f'/api/v1/statuses/{id}')
@api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
def status_reblog(self, id, visibility=None):
def status_reblog(self, id: Union[Status, IdType], visibility: Optional[str] = None) -> Status:
"""
Reblog / boost a status.
The visibility parameter functions the same as in :ref:`status_post() <status_post()>` and
allows you to reduce the visibility of a reblogged status.
Returns a :ref:`status dict <status dict>` with a new status that wraps around the reblogged one.
Returns a new Status that wraps around the reblogged status.
"""
params = self.__generate_params(locals(), ['id'])
valid_visibilities = ['private', 'public', 'unlisted', 'direct']
@ -394,91 +393,91 @@ class Mastodon(Internals):
return self.__api_request('POST', f'/api/v1/statuses/{id}/reblog', params)
@api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
def status_unreblog(self, id):
def status_unreblog(self, id: Union[Status, IdType]) -> Status:
"""
Un-reblog a status.
Returns a :ref:`status dict <status dict>` with the status that used to be reblogged.
Returns the status that used to be reblogged.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/unreblog')
@api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
def status_favourite(self, id):
def status_favourite(self, id: Union[Status, IdType]) -> Status:
"""
Favourite a status.
Returns a :ref:`status dict <status dict>` with the favourited status.
Returns the favourited status.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/favourite')
@api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
def status_unfavourite(self, id):
def status_unfavourite(self, id: Union[Status, IdType]) -> Status:
"""
Un-favourite a status.
Returns a :ref:`status dict <status dict>` with the un-favourited status.
Returns the un-favourited status.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/unfavourite')
@api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
def status_mute(self, id):
def status_mute(self, id: Union[Status, IdType]) -> Status:
"""
Mute notifications for a status.
Returns a :ref:`status dict <status dict>` with the now muted status
Returns the now muted status
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/mute')
@api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
def status_unmute(self, id):
def status_unmute(self, id: Union[Status, IdType]) -> Status:
"""
Unmute notifications for a status.
Returns a :ref:`status dict <status dict>` with the status that used to be muted.
Returns the status that used to be muted.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/unmute')
@api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
def status_pin(self, id):
def status_pin(self, id: Union[Status, IdType]) -> Status:
"""
Pin a status for the logged-in user.
Returns a :ref:`status dict <status dict>` with the now pinned status
Returns the now pinned status
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/pin')
@api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
def status_unpin(self, id):
def status_unpin(self, id: Union[Status, IdType]) -> Status:
"""
Unpin a pinned status for the logged-in user.
Returns a :ref:`status dict <status dict>` with the status that used to be pinned.
Returns the status that used to be pinned.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/unpin')
@api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
def status_bookmark(self, id):
def status_bookmark(self, id: Union[Status, IdType]) -> Status:
"""
Bookmark a status as the logged-in user.
Returns a :ref:`status dict <status dict>` with the now bookmarked status
Returns the now bookmarked status
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/bookmark')
@api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
def status_unbookmark(self, id):
def status_unbookmark(self, id: Union[Status, IdType]) -> Status:
"""
Unbookmark a bookmarked status for the logged-in user.
Returns a :ref:`status dict <status dict>` with the status that used to be bookmarked.
Returns the status that used to be bookmarked.
"""
id = self.__unpack_id(id)
return self.__api_request('POST', f'/api/v1/statuses/{id}/unbookmark')
@ -487,13 +486,13 @@ class Mastodon(Internals):
# Writing data: Scheduled statuses
###
@api_version("2.7.0", "2.7.0", _DICT_VERSION_SCHEDULED_STATUS)
def scheduled_status_update(self, id, scheduled_at):
def scheduled_status_update(self, id: Union[Status, IdType], scheduled_at: datetime) -> ScheduledStatus:
"""
Update the scheduled time of a scheduled status.
New time must be at least 5 minutes into the future.
Returns a :ref:`scheduled status dict <scheduled status dict>`
Returned object reflects the updates to the scheduled status.
"""
scheduled_at = self.__consistent_isoformat_utc(scheduled_at)
id = self.__unpack_id(id)
@ -501,7 +500,7 @@ class Mastodon(Internals):
return self.__api_request('PUT', f'/api/v1/scheduled_statuses/{id}', params)
@api_version("2.7.0", "2.7.0", "2.7.0")
def scheduled_status_delete(self, id):
def scheduled_status_delete(self, id: Union[Status, IdType]):
"""
Deletes a scheduled status.
"""

Wyświetl plik

@ -12,8 +12,9 @@ except:
from mastodon import Mastodon
from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout
from requests.exceptions import ChunkedEncodingError, ReadTimeout, ConnectionError
from mastodon.types import AttribAccessDict, Status, Notification, IdType, Conversation, Announcement, StreamReaction, try_cast_recurse
from requests.exceptions import ChunkedEncodingError, ReadTimeout, ConnectionError
class StreamListener(object):
"""Callbacks for the streaming API. Create a subclass, override the on_xxx
@ -21,6 +22,19 @@ class StreamListener(object):
of your subclass to Mastodon.user_stream(), Mastodon.public_stream(), or
Mastodon.hashtag_stream()."""
__EVENT_NAME_TO_TYPE = {
"update": Status,
"delete": IdType,
"notification": Notification,
"filters_changed": None,
"conversation": Conversation,
"announcement": Announcement,
"announcement_reaction": StreamReaction,
"announcement_delete": IdType,
"status_update": Status,
"encrypted_message": AttribAccessDict,
}
def on_update(self, status):
"""A new status has appeared. `status` is the parsed `status dict`
describing the status."""
@ -179,7 +193,9 @@ class StreamListener(object):
for_stream = json.loads(event['stream'])
except:
for_stream = None
payload = json.loads(data, object_hook=Mastodon._Mastodon__json_hooks)
payload = json.loads(data)
cast_type = self.__EVENT_NAME_TO_TYPE.get(name, AttribAccessDict)
payload = try_cast_recurse(cast_type, payload)
except KeyError as err:
exception = MastodonMalformedEventError(
'Missing field', err.args[0], event)

Wyświetl plik

@ -1,11 +1,11 @@
# relationships.py - endpoints for user and domain blocks and mutes as well as follow requests
from .versions import _DICT_VERSION_STATUS
from .errors import MastodonIllegalArgumentError
from .defaults import _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC
from .utility import api_version
from mastodon.versions import _DICT_VERSION_STATUS
from mastodon.errors import MastodonIllegalArgumentError
from mastodon.defaults import _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC
from mastodon.utility import api_version
from .internals import Mastodon as Internals
from mastodon.internals import Mastodon as Internals
class Mastodon(Internals):
@ -79,7 +79,7 @@ class Mastodon(Internals):
return self.__stream('/api/v1/streaming/direct', listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec)
@api_version("2.5.0", "2.5.0", "2.5.0")
def stream_healthy(self):
def stream_healthy(self) -> bool:
"""
Returns without True if streaming API is okay, False or raises an error otherwise.
"""

Wyświetl plik

@ -1,22 +1,20 @@
# suggestions.py - follow suggestion endpoints
from .versions import _DICT_VERSION_ACCOUNT
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_ACCOUNT
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import NonPaginatableList, Account, IdType
from typing import Union
class Mastodon(Internals):
###
# Reading data: Follow suggestions
###
@api_version("2.4.3", "2.4.3", _DICT_VERSION_ACCOUNT)
def suggestions(self):
def suggestions(self) -> NonPaginatableList[Account]:
"""
Fetch follow suggestions for the logged-in user.
Returns a list of :ref:`account dicts <account dicts>`.
"""
return self.__api_request('GET', '/api/v1/suggestions')
@ -24,7 +22,7 @@ class Mastodon(Internals):
# Writing data: Follow suggestions
###
@api_version("2.4.3", "2.4.3", _DICT_VERSION_ACCOUNT)
def suggestion_delete(self, account_id):
def suggestion_delete(self, account_id: Union[Account, IdType]):
"""
Remove the user with the given `account_id` from the follow suggestions.
"""

Wyświetl plik

@ -1,19 +1,23 @@
# timeline.py - endpoints for reading various different timelines
from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CONVERSATION
from .errors import MastodonIllegalArgumentError, MastodonNotFoundError
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_STATUS, _DICT_VERSION_CONVERSATION
from mastodon.errors import MastodonIllegalArgumentError, MastodonNotFoundError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Status, IdType, PaginatableList, UserList
from typing import Union, Optional
from datetime import datetime
class Mastodon(Internals):
###
# Reading data: Timelines
##
@api_version("1.0.0", "3.1.4", _DICT_VERSION_STATUS)
def timeline(self, timeline="home", max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False):
"""
def timeline(self, timeline: str = "home", max_id: Optional[Union[Status, IdType, datetime]] = None, min_id: Optional[Union[Status, IdType, datetime]] = None,
since_id: Optional[Union[Status, IdType, datetime]] = None, limit: Optional[int] = None, only_media: bool = False, local: bool = False,
remote: bool = False) -> PaginatableList[Status]:
"""
Fetch statuses, most recent ones first. `timeline` can be 'home', 'local', 'public',
'tag/hashtag' or 'list/id'. See the following functions documentation for what those do.
@ -23,8 +27,6 @@ class Mastodon(Internals):
and `remote` to only get remote statuses. Some options are mutually incompatible as dictated by logic.
May or may not require authentication depending on server settings and what is specifically requested.
Returns a list of :ref:`status dicts <status dicts>`.
"""
if max_id is not None:
max_id = self.__unpack_id(max_id, dateconv=True)
@ -54,39 +56,38 @@ class Mastodon(Internals):
return self.__api_request('GET', f'/api/v1/timelines/{timeline}', params)
@api_version("1.0.0", "3.1.4", _DICT_VERSION_STATUS)
def timeline_home(self, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False):
def timeline_home(self, max_id: Optional[Union[Status, IdType, datetime]] = None, min_id: Optional[Union[Status, IdType, datetime]] = None,
since_id: Optional[Union[Status, IdType, datetime]] = None, limit: Optional[int] = None, only_media: bool = False, local: bool = False,
remote: bool = False) -> PaginatableList[Status]:
"""
Convenience method: Fetches the logged-in user's home timeline (i.e. followed users and self). Params as in `timeline()`.
Returns a list of :ref:`status dicts <status dicts>`.
"""
return self.timeline('home', max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote)
@api_version("1.0.0", "3.1.4", _DICT_VERSION_STATUS)
def timeline_local(self, max_id=None, min_id=None, since_id=None, limit=None, only_media=False):
def timeline_local(self, max_id: Optional[Union[Status, IdType, datetime]] = None, min_id: Optional[Union[Status, IdType, datetime]] = None,
since_id: Optional[Union[Status, IdType, datetime]] = None, limit: Optional[int] = None, only_media: bool = False) -> PaginatableList[Status]:
"""
Convenience method: Fetches the local / instance-wide timeline, not including replies. Params as in `timeline()`.
Returns a list of :ref:`status dicts <status dicts>`.
"""
return self.timeline('local', max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media)
@api_version("1.0.0", "3.1.4", _DICT_VERSION_STATUS)
def timeline_public(self, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False):
def timeline_public(self, max_id: Optional[Union[Status, IdType, datetime]] = None, min_id: Optional[Union[Status, IdType, datetime]] = None,
since_id: Optional[Union[Status, IdType, datetime]] = None, limit: Optional[int] = None, only_media: bool = False, local: bool = False,
remote: bool = False) -> PaginatableList[Status]:
"""
Convenience method: Fetches the public / visible-network / federated timeline, not including replies. Params as in `timeline()`.
Returns a list of :ref:`status dicts <status dicts>`.
"""
return self.timeline('public', max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote)
@api_version("1.0.0", "3.1.4", _DICT_VERSION_STATUS)
def timeline_hashtag(self, hashtag, local=False, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, remote=False):
def timeline_hashtag(self, hashtag: str, local: bool = False, max_id: Optional[Union[Status, IdType, datetime]] = None, min_id: Optional[Union[Status, IdType, datetime]] = None,
since_id: Optional[Union[Status, IdType, datetime]] = None, limit: Optional[int] = None, only_media: bool = False,
remote: bool = False) -> PaginatableList[Status]:
"""
Convenience method: Fetch a timeline of toots with a given hashtag. The hashtag parameter
should not contain the leading #. Params as in `timeline()`.
Returns a list of :ref:`status dicts <status dicts>`.
"""
if hashtag.startswith("#"):
raise MastodonIllegalArgumentError(
@ -94,11 +95,11 @@ class Mastodon(Internals):
return self.timeline(f'tag/{hashtag}', max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote)
@api_version("2.1.0", "3.1.4", _DICT_VERSION_STATUS)
def timeline_list(self, id, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False):
def timeline_list(self, id: Union[UserList, IdType], max_id: Optional[Union[Status, IdType, datetime]] = None, min_id: Optional[Union[Status, IdType, datetime]] = None,
since_id: Optional[Union[Status, IdType, datetime]] = None, limit: Optional[int] = None, only_media: bool = False, local: bool = False,
remote: bool = False) -> PaginatableList[Status]:
"""
Convenience method: Fetches a timeline containing all the toots by users in a given list. Params as in `timeline()`.
Returns a list of :ref:`status dicts <status dicts>`.
"""
id = self.__unpack_id(id)
return self.timeline(f'list/{id}', max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote)

Wyświetl plik

@ -1,17 +1,18 @@
# trends.py - trend-related endpoints
from .versions import _DICT_VERSION_HASHTAG, _DICT_VERSION_STATUS, _DICT_VERSION_CARD
from .utility import api_version
from .internals import Mastodon as Internals
from mastodon.versions import _DICT_VERSION_HASHTAG, _DICT_VERSION_STATUS, _DICT_VERSION_CARD
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.types import Tag, Status, PreviewCard, NonPaginatableList
from typing import Optional, Union
class Mastodon(Internals):
###
# Reading data: Trends
###
@api_version("2.4.3", "3.5.0", _DICT_VERSION_HASHTAG)
def trends(self, limit=None):
def trends(self, limit: Optional[int] = None):
"""
Old alias for :ref:`trending_tags() <trending_tags()>`
@ -20,7 +21,7 @@ class Mastodon(Internals):
return self.trending_tags(limit=limit)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_HASHTAG)
def trending_tags(self, limit=None, lang=None):
def trending_tags(self, limit: Optional[int] = None, lang: Optional[str] = None) -> NonPaginatableList[Tag]:
"""
Fetch trending-hashtag information, if the instance provides such information.
@ -34,8 +35,7 @@ class Mastodon(Internals):
Pass `lang` to override the global locale parameter, which may affect trend ordering.
Returns a list of :ref:`hashtag dicts <hashtag dicts>`, sorted by the instance's trending algorithm,
descending.
The results are sorted by the instances's trending algorithm, descending.
"""
params = self.__generate_params(locals())
if "lang" in params:
@ -47,7 +47,7 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/trends', params, lang_override=lang)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
def trending_statuses(self, limit=None, lang=None):
def trending_statuses(self, limit: Optional[int] = None, lang: Optional[str] = None) -> NonPaginatableList[Status]:
"""
Fetch trending-status information, if the instance provides such information.
@ -56,8 +56,7 @@ class Mastodon(Internals):
Pass `lang` to override the global locale parameter, which may affect trend ordering.
Returns a list of :ref:`status dicts <status dicts>`, sorted by the instances's trending algorithm,
descending.
The results are sorted by the instances's trending algorithm, descending.
"""
params = self.__generate_params(locals())
if "lang" in params:
@ -65,15 +64,14 @@ class Mastodon(Internals):
return self.__api_request('GET', '/api/v1/trends/statuses', params, lang_override=lang)
@api_version("3.5.0", "3.5.0", _DICT_VERSION_CARD)
def trending_links(self, limit=None, lang=None):
def trending_links(self, limit: Optional[int] = None, lang: Optional[str] = None) -> NonPaginatableList[PreviewCard]:
"""
Fetch trending-link information, if the instance provides such information.
Specify `limit` to limit how many results are returned (the maximum number
of results is 10, the endpoint is not paginated).
Returns a list of :ref:`card dicts <card dicts>`, sorted by the instances's trending algorithm,
descending.
The results are sorted by the instances's trending algorithm, descending.
"""
params = self.__generate_params(locals())
if "lang" in params:

5021
mastodon/types.py 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -0,0 +1,347 @@
from typing import List, Union, Optional, Dict, Any, Tuple, Callable, get_type_hints, TypeVar, IO
from datetime import datetime, timezone
import dateutil
import dateutil.parser
from collections import OrderedDict
import inspect
import json
from mastodon.compat import PurePath
# A type representing a file name as a PurePath or string, or a file-like object, for convenience
PathOrFile = Union[str, PurePath, IO[bytes]]
BASE62_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
def base62_to_int(base62: str) -> int:
"""
internal helper for *oma compat: convert a base62 string to an int since
that is what that software uses as ID type.
we don't convert IDs in general, but this is needed for snowflake ID
calculations.
"""
str_len = len(base62)
val = 0
base62 = base62.lower()
for idx, char in enumerate(base62):
power = (str_len - (idx + 1))
val += BASE62_ALPHABET.index(char) * (62 ** power)
return val
def int_to_base62(val: int) -> str:
"""
Internal helper to convert an int to a base62 string.
"""
if val == 0:
return BASE62_ALPHABET[0]
base62 = []
while val:
val, digit = divmod(val, 62)
base62.append(BASE62_ALPHABET[digit])
return ''.join(reversed(base62))
"""
The base type for all non-snowflake IDs. This is a union of int and str
because while Mastodon mostly uses IDs that are ints, it doesn't guarantee
this and other implementations do not use integer IDs.
In a change from previous versions, string IDs now take precedence over ints.
This is a breaking change, and I'm sorry about it, but this will make every piece
of software using Mastodon.py more robust in the long run.
"""
PrimitiveIdType = Union[str, int]
class MaybeSnowflakeIdType(str):
"""
Represents, maybe, a snowflake ID.
Contains what a regular ID can contain (int or str) and will convert to int if
containing an int or a str that naturally converts to an int (e.g. "123").
Can *also* contain a *datetime* which gets converted to
It's also just *maybe* a snowflake ID, because some implementations may not use those.
This may seem annoyingly complex, but the goal here is:
1) If constructed with some ID, return that ID unchanged, so equality and hashing work
2) Allow casting to int and str, just like a regular ID
3) Halfway transparently convert to and from datetime with the correct format for the server we're talking to
"""
def __new__(cls, value, *args, **kwargs):
try:
return super(cls, cls).__new__(cls, value)
except:
return object.__new__(cls)
def __init__(self, val: Union[PrimitiveIdType, datetime], assume_pleroma: bool = False):
try:
super(MaybeSnowflakeIdType, self).__init__()
except:
pass
if isinstance(val, (int, str)):
self.__val = val
elif isinstance(val, datetime):
self.__val = (int(val.timestamp()) << 16) * 1000
if assume_pleroma:
self.__val = int_to_base62(self.__val)
else:
raise TypeError(f"Expected int or str, got {type(val).__name__}")
self.assume_pleroma = assume_pleroma
def to_datetime(self) -> Optional[datetime]:
"""
Convert to datetime. This *can* fail because not every implementation of
the masto API is guaranteed to actually use snowflake IDs where masto uses
snowflake IDs, so it can in fact return None.
"""
val = self.__val
try:
# Pleroma ID compat. First, try to just cast to int. If that fails *or*
# if we are told to assume Pleroma, try to convert from base62.
if isinstance(self.__val, str):
try_base62 = False
try:
val = int(self.__val)
except:
try_base62 = True
if try_base62 or self.assume_pleroma:
val = base62_to_int(self.__val)
except:
return None
# TODO: This matches the masto approach, whether this matches the
# Pleroma approach is to be verified.
timestamp_s = int(int(val) / 1000) >> 16
return datetime.fromtimestamp(timestamp_s)
def __str__(self) -> str:
"""
Return as string representation.
"""
return str(self.__val)
def __int__(self) -> int:
"""
Return as int representation.
This is not guaranteed to work, because the ID might be a string,
though on Mastodon it is generally going to be an int.
"""
if isinstance(self.__val, str):
return int(self.__val)
return self.__val
def __repr__(self) -> str:
"""
Overriden so that the integer representation doesn't take precedence
"""
return str(self.__val)
"""
IDs returned from Mastodon.py ar either primitive (int or str) or snowflake
(still int or str, but potentially convertible to datetime).
"""
IdType = Union[PrimitiveIdType, MaybeSnowflakeIdType]
T = TypeVar('T')
class PaginatableList(List[T]):
"""
This is a list with pagination information attached.
It is returned by the API when a list of items is requested, and the response contains
a Link header with pagination information.
"""
def __getattr__(self, attr):
if attr in self:
return self[attr]
else:
raise AttributeError(f"Attribute not found: {attr}")
def __setattr__(self, attr, val):
if attr in self:
raise AttributeError("Attribute-style access is read only")
super(NonPaginatableList, self).__setattr__(attr, val)
# TODO add the pagination housekeeping stuff
class NonPaginatableList(List[T]):
"""
This is just a list. I am subclassing the regular list out of pure paranoia about
potential oversights that might require me to add things to it later.
"""
pass
# Lists in Mastodon.py are either regular or paginatable
EntityList = Union[NonPaginatableList[T], PaginatableList[T]]
# Backwards compat alias
AttribAccessList = EntityList
# Helper functions for typecasting attempts
def try_cast(t, value, retry = True):
"""
Base case casting function. Handles:
* Casting to any AttribAccessDict subclass (directly, no special handling)
* Casting to MaybeSnowflakeIdType (directly, no special handling)
* Casting to bool (with possible conversion from json bool strings)
* Casting to datetime (with possible conversion from all kinds of funny date formats because unfortunately this is the world we live in)
Gives up and returns as-is if none of the above work.
"""
try:
if issubclass(t, AttribAccessDict) or t is MaybeSnowflakeIdType:
try:
value = t(**value)
except:
try:
value = AttribAccessDict(**value)
except:
pass
elif isinstance(t, bool):
if isinstance(value, str):
if value.lower() == 'true':
value = True
elif value.lower() == 'false':
value = False
value = bool(value)
elif isinstance(t, datetime):
if isinstance(value, int):
value = datetime.fromtimestamp(value, timezone.utc)
elif isinstance(value, str):
try:
value_int = int(value)
value = datetime.fromtimestamp(value_int, timezone.utc)
except:
value = dateutil.parser.parse(value)
except:
value = try_cast(AttribAccessDict, value, False)
return value
def try_cast_recurse(t, value):
"""
Non-dict compound type casting function. Handles:
* Casting to list, tuple, EntityList or (Non)PaginatableList, converting all elements to the correct type recursively
* Casting to Union, trying all types in the union until one works
Gives up and returns as-is if none of the above work.
"""
try:
orig_type = getattr(t, '__origin__')
if orig_type in (list, tuple, EntityList, NonPaginatableList, PaginatableList):
value_cast = []
type_args = t.__args__
if len(type_args) == 1:
type_args = type_args * len(value)
for element_type, v in zip(type_args, value):
value_cast.append(try_cast_recurse(element_type, v))
value = orig_type(value_cast)
elif orig_type is Union:
for t in t.__args__:
value = try_cast_recurse(t, value)
if isinstance(value, t):
break
except Exception as e:
pass
return try_cast(t, value)
class AttribAccessDict(OrderedDict[str, Any]):
"""
Base return object class for Mastodon.py.
Inherits from dict, but allows access via attributes as well as if it was a dataclass.
While the subclasses implement specific fields with proper typing, parsing and documentation,
they all inherit from this class, and parsing is extremely permissive to allow for forward and
backward compatibility as well as compatibility with other implementations of the Mastodon API.
"""
def __init__(self, **kwargs):
"""
Constructor that calls through to dict constructor and then sets attributes for all keys.
"""
super(AttribAccessDict, self).__init__()
if __annotations__ in self.__class__.__dict__:
for attr, _ in self.__class__.__annotations__.items():
attr_name = attr
if hasattr(self.__class__, "_rename_map"):
attr_name = getattr(self.__class__, "_rename_map").get(attr, attr)
if attr_name in kwargs:
self[attr] = kwargs[attr_name]
assert not attr in kwargs, f"Duplicate attribute {attr}"
elif attr in kwargs:
self[attr] = kwargs[attr]
else:
self[attr] = None
for attr in kwargs:
if not attr in self:
self[attr] = kwargs[attr]
def __getattribute__(self, attr):
"""
Override to force access of normal attributes to go through __getattr__
"""
if attr == '__class__':
return super(AttribAccessDict, self).__getattribute__(attr)
if attr in self.__class__.__annotations__:
return self.__getattr__(attr)
return super(AttribAccessDict, self).__getattribute__(attr)
def __getattr__(self, attr):
"""
Basic attribute getter that throws if attribute is not in dict and supports redirecting access.
"""
if not hasattr(self.__class__, "_access_map"):
# Base case: no redirecting
if attr in self:
return self[attr]
else:
raise AttributeError(f"Attribute not found: {attr}")
else:
if attr in self and self[attr] is not None:
return self[attr]
elif attr in getattr(self.__class__, "_access_map"):
try:
attr_path = getattr(self.__class__, "_access_map")[attr].split('.')
cur_attr = self
for attr_path_part in attr_path:
cur_attr = getattr(cur_attr, attr_path_part)
return cur_attr
except:
raise AttributeError(f"Attribute not found: {attr}")
else:
raise AttributeError(f"Attribute not found: {attr}")
def __setattr__(self, attr, val):
"""
Attribute setter that calls through to dict setter but will throw if attribute is not in dict
"""
if attr in self:
self[attr] = val
else:
raise AttributeError(f"Attribute not found: {attr}")
def __setitem__(self, key, val):
"""
Dict setter that also sets attributes and tries to typecast if we have an
AttribAccessDict or MaybeSnowflakeIdType type hint.
For Unions, it will try the types in order.
"""
# Collate type hints that we may have
type_hints = get_type_hints(self.__class__)
init_hints = get_type_hints(self.__class__.__init__)
type_hints.update(init_hints)
# Do typecasting, possibly iterating over a list or tuple
if key in type_hints:
type_hint = type_hints[key]
val = try_cast_recurse(type_hint, val)
# Finally, call out to setattr and setitem proper
super(AttribAccessDict, self).__setattr__(key, val)
super(AttribAccessDict, self).__setitem__(key, val)
"""An entity returned by the Mastodon API is either a dict or a list"""
Entity = Union[AttribAccessDict, EntityList]
"""A type containing the parameters for a encrypting webpush data. Considered opaque / implementation detail."""
WebpushCryptoParamsPubkey = Dict[str, str]
"""A type containing the parameters for a derypting webpush data. Considered opaque / implementation detail."""
WebpushCryptoParamsPrivkey = Dict[str, str]

Wyświetl plik

@ -5,89 +5,14 @@ import dateutil
import datetime
import copy
from decorator import decorate
from .errors import MastodonVersionError, MastodonAPIError
from .compat import IMPL_HAS_BLURHASH, blurhash
# Module level:
###
# Version check functions, including decorator and parser
###
def parse_version_string(version_string):
"""Parses a semver version string, stripping off "rc" stuff if present."""
string_parts = version_string.split(".")
version_parts = (
int(re.match("([0-9]*)", string_parts[0]).group(0)),
int(re.match("([0-9]*)", string_parts[1]).group(0)),
int(re.match("([0-9]*)", string_parts[2]).group(0))
)
return version_parts
def max_version(*version_strings):
"""Returns the maximum version of all provided version strings."""
return max(version_strings, key=parse_version_string)
def api_version(created_ver, last_changed_ver, return_value_ver):
"""Version check decorator. Currently only checks Bigger Than."""
def api_min_version_decorator(function):
def wrapper(function, self, *args, **kwargs):
if not self.version_check_mode == "none":
if self.version_check_mode == "created":
version = created_ver
else:
version = max_version(last_changed_ver, return_value_ver)
major, minor, patch = parse_version_string(version)
if major > self.mastodon_major:
raise MastodonVersionError(f"Version check failed (Need version {version})")
elif major == self.mastodon_major and minor > self.mastodon_minor:
raise MastodonVersionError(f"Version check failed (Need version {version})")
elif major == self.mastodon_major and minor == self.mastodon_minor and patch > self.mastodon_patch:
raise MastodonVersionError(f"Version check failed (Need version {version}, patch is {self.mastodon_patch})")
return function(self, *args, **kwargs)
function.__doc__ += f"\n\n *Added: Mastodon v{created_ver}, last changed: Mastodon v{last_changed_ver}*"
return decorate(function, wrapper)
return api_min_version_decorator
###
# Dict helper class.
# Defined at top level so it can be pickled.
###
class AttribAccessDict(dict):
def __getattr__(self, attr):
if attr in self:
return self[attr]
else:
raise AttributeError(f"Attribute not found: {attr}")
def __setattr__(self, attr, val):
if attr in self:
raise AttributeError("Attribute-style access is read only")
super(AttribAccessDict, self).__setattr__(attr, val)
###
# List helper class.
# Defined at top level so it can be pickled.
###
class AttribAccessList(list):
def __getattr__(self, attr):
if attr in self:
return self[attr]
else:
raise AttributeError(f"Attribute not found: {attr}")
def __setattr__(self, attr, val):
if attr in self:
raise AttributeError("Attribute-style access is read only")
super(AttribAccessList, self).__setattr__(attr, val)
from mastodon.errors import MastodonAPIError
from mastodon.compat import IMPL_HAS_BLURHASH, blurhash
from mastodon.internals import Mastodon as Internals
from mastodon.versions import parse_version_string, max_version, api_version
# Class level:
class Mastodon():
class Mastodon(Internals):
def set_language(self, lang):
"""
Set the locale Mastodon will use to generate responses. Valid parameters are all ISO 639-1 (two letter) or, for languages that do
@ -104,11 +29,11 @@ class Mastodon():
try:
version_str = self.__normalize_version_string(self.__instance()["version"])
self.version_check_worked = True
except:
except Exception as e:
print(e)
# instance() was added in 1.1.0, so our best guess is 1.0.0.
version_str = "1.0.0"
self.version_check_worked = False
self.mastodon_major, self.mastodon_minor, self.mastodon_patch = parse_version_string(version_str)
return version_str

Wyświetl plik

@ -1,9 +1,55 @@
# versions.py - versioning of return values
from .utility import max_version
import re
from decorator import decorate
from mastodon.errors import MastodonVersionError
###
# Version check functions, including decorator and parser
###
def parse_version_string(version_string):
"""Parses a semver version string, stripping off "rc" stuff if present."""
string_parts = version_string.split(".")
version_parts = (
int(re.match("([0-9]*)", string_parts[0]).group(0)), # type: ignore
int(re.match("([0-9]*)", string_parts[1]).group(0)), # type: ignore
int(re.match("([0-9]*)", string_parts[2]).group(0)) # type: ignore
)
return version_parts
def max_version(*version_strings):
"""Returns the maximum version of all provided version strings."""
return max(version_strings, key=parse_version_string)
def api_version(created_ver, last_changed_ver, return_value_ver):
"""Version check decorator. Currently only checks Bigger Than."""
def api_min_version_decorator(function):
def wrapper(function, self, *args, **kwargs):
if not self.version_check_mode == "none":
if self.version_check_mode == "created":
version = created_ver
else:
version = max_version(last_changed_ver, return_value_ver)
major, minor, patch = parse_version_string(version)
if major > self.mastodon_major:
raise MastodonVersionError(f"Version check failed (Need version {version})")
elif major == self.mastodon_major and minor > self.mastodon_minor:
raise MastodonVersionError(f"Version check failed (Need version {version})")
elif major == self.mastodon_major and minor == self.mastodon_minor and patch > self.mastodon_patch:
raise MastodonVersionError(f"Version check failed (Need version {version}, patch is {self.mastodon_patch})")
return function(self, *args, **kwargs)
function.__doc__ += f"\n\n *Added: Mastodon v{created_ver}, last changed: Mastodon v{last_changed_ver}*"
return decorate(function, wrapper)
return api_min_version_decorator
###
# Dict versions
# TODO: Get rid of these
###
_DICT_VERSION_APPLICATION = "2.7.2"
_DICT_VERSION_MENTION = "1.0.0"
_DICT_VERSION_MEDIA = "3.2.0"
@ -38,3 +84,4 @@ _DICT_VERSION_ADMIN_DOMAIN_BLOCK = "4.0.0"
_DICT_VERSION_ADMIN_MEASURE = "3.5.0"
_DICT_VERSION_ADMIN_DIMENSION = "3.5.0"
_DICT_VERSION_ADMIN_RETENTION = "3.5.0"

Wyświetl plik

@ -12,7 +12,7 @@
"fields": {
"id": {
"description": "The accounts id",
"field_type": "SnowflakeIdType",
"field_type": "MaybeSnowflakeIdType",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -318,7 +318,7 @@
},
"fields": {
"description": "List of up to four (by default) AccountFields",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "AccountField",
"is_optional": false,
"is_nullable": false,
@ -332,7 +332,7 @@
},
"emojis": {
"description": "List of custom emoji used in name, bio or fields",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "CustomEmoji",
"is_optional": false,
"is_nullable": false,
@ -379,7 +379,7 @@
"roles": {
"description": "Deprecated. Was a list of strings with the users roles. Now just an empty list. Mastodon.py makes no attempt to fill it, and the field may be removed if Mastodon removes it. use role field instead.",
"is_deprecated": true,
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -649,7 +649,7 @@
},
"fields": {
"description": "Metadata about the account.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "AccountField",
"is_optional": false,
"is_nullable": false,
@ -690,7 +690,7 @@
"fields": {
"id": {
"description": "Id of this status",
"field_type": "SnowflakeIdType",
"field_type": "MaybeSnowflakeIdType",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -746,7 +746,7 @@
},
"in_reply_to_id": {
"description": "Id of the status this status is in response to",
"field_type": "SnowflakeIdType",
"field_type": "MaybeSnowflakeIdType",
"field_subtype": null,
"is_optional": false,
"is_nullable": true,
@ -759,7 +759,7 @@
},
"in_reply_to_account_id": {
"description": "Id of the account this status is in response to",
"field_type": "SnowflakeIdType",
"field_type": "MaybeSnowflakeIdType",
"field_subtype": null,
"is_optional": false,
"is_nullable": true,
@ -916,7 +916,7 @@
},
"mentions": {
"description": "A list Mentions this status includes",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Account",
"is_optional": false,
"is_nullable": false,
@ -929,7 +929,7 @@
},
"media_attachments": {
"description": "List files attached to this status",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "MediaAttachment",
"is_optional": false,
"is_nullable": false,
@ -943,7 +943,7 @@
},
"emojis": {
"description": "A list of CustomEmoji used in the status",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "CustomEmoji",
"is_optional": false,
"is_nullable": false,
@ -956,7 +956,7 @@
},
"tags": {
"description": "A list of Tags used in the status",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Tag",
"is_optional": false,
"is_nullable": false,
@ -1088,7 +1088,7 @@
},
"filtered": {
"description": "If present, a list of filter application results that indicate which of the users filters matched and what actions should be taken.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "FilterResult",
"is_optional": true,
"is_nullable": false,
@ -1184,7 +1184,7 @@
},
"media_attachments": {
"description": "List of MediaAttachment objects with the attached media for this version of the status",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "MediaAttachment",
"is_optional": false,
"is_nullable": false,
@ -1197,7 +1197,7 @@
},
"emojis": {
"description": "List of custom emoji used in this version of the status.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "CustomEmoji",
"is_optional": false,
"is_nullable": false,
@ -1251,7 +1251,7 @@
},
"keyword_matches": {
"description": "The keyword within the filter that was matched.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"is_optional": false,
"is_nullable": true,
@ -1265,7 +1265,7 @@
},
"status_matches": {
"description": "The status ID within the filter that was matched.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": true,
@ -1353,7 +1353,7 @@
"name": "Scheduled status / toot",
"python_name": "ScheduledStatus",
"func_call": "mastodon.status_post(\"futureposting\", scheduled_at=the_future)",
"func_call_real": "mastodon.status_post(\"posting in the far future\", scheduled_at=datetime(9999,12,12))",
"func_call_real": "mastodon.status_post(\"posting in the far future\", scheduled_at=datetime(2100,12,12))",
"masto_doc_link": "https://docs.joinmastodon.org/entities/ScheduledStatus/",
"func_call_additional": null,
"func_alternate_acc": false,
@ -1404,7 +1404,7 @@
},
"media_attachments": {
"description": "Array of MediaAttachment objects for the attachments to the scheduled status",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -1421,7 +1421,7 @@
"name": "Scheduled status / toot params",
"python_name": "ScheduledStatusParams",
"func_call": "mastodon.status_post(\"futureposting... 2\", scheduled_at=the_future).params",
"func_call_real": "mastodon.status_post(\"posting in the far future\", scheduled_at=datetime(9999,12,12)).params",
"func_call_real": "mastodon.status_post(\"posting in the far future\", scheduled_at=datetime(2100,12,12)).params",
"masto_doc_link": "https://docs.joinmastodon.org/entities/ScheduledStatus/",
"func_call_additional": null,
"func_alternate_acc": false,
@ -1444,7 +1444,7 @@
},
"in_reply_to_id": {
"description": "ID of the status this one is a reply to",
"field_type": "SnowflakeIdType",
"field_type": "MaybeSnowflakeIdType",
"field_subtype": null,
"is_optional": false,
"is_nullable": true,
@ -1458,7 +1458,7 @@
},
"media_ids": {
"description": "IDs of media attached to this status",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"is_optional": false,
"is_nullable": true,
@ -1587,7 +1587,7 @@
"allowed_mentions": {
"description": "Undocumented. If you know what this does, please let me know.",
"help_wanted": true,
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"is_optional": false,
"is_nullable": true,
@ -1712,7 +1712,7 @@
},
"options": {
"description": "The poll options",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "PollOption",
"is_optional": false,
"is_nullable": false,
@ -1726,7 +1726,7 @@
},
"emojis": {
"description": "List of CustomEmoji used in answer strings,",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "CustomEmoji",
"is_optional": false,
"is_nullable": false,
@ -1740,7 +1740,7 @@
},
"own_votes": {
"description": "The logged-in users votes, as a list of indices to the options.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "int",
"is_optional": false,
"is_nullable": false,
@ -1850,7 +1850,7 @@
},
"accounts": {
"description": "List of accounts (other than the logged-in account) that are part of this conversation",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Account",
"is_optional": false,
"is_nullable": false,
@ -1920,7 +1920,7 @@
},
"history": {
"description": "List of TagHistory for up to 7 days. Not present in statuses.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "TagHistory",
"is_optional": true,
"is_nullable": false,
@ -2342,7 +2342,7 @@
},
"languages": {
"description": "List of languages that the logged in user is following this user for (if any)",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"field_structuretype": "TwoLetterLanguageCodeEnum",
"is_optional": false,
@ -2421,7 +2421,7 @@
},
"context": {
"description": "List of places where the filters are applied.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"is_optional": false,
"is_nullable": false,
@ -2532,7 +2532,7 @@
},
"keywords": {
"description": "A list of keywords that will trigger this filter.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "FilterKeyword",
"is_optional": false,
"is_nullable": false,
@ -2547,7 +2547,7 @@
},
"statuses": {
"description": "A list of statuses that will trigger this filter.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "FilterStatus",
"is_optional": false,
"is_nullable": false,
@ -2679,7 +2679,7 @@
"fields": {
"ancestors": {
"description": "A list of Statuses that the Status with this Context is a reply to",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Status",
"is_optional": false,
"is_nullable": false,
@ -2693,7 +2693,7 @@
},
"descendants": {
"description": "A list of Statuses that are replies to the Status with this Context",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Status",
"is_optional": false,
"is_nullable": false,
@ -2776,7 +2776,7 @@
"fields": {
"id": {
"description": "The ID of the attachment.",
"field_type": "SnowflakeIdType",
"field_type": "MaybeSnowflakeIdType",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -3529,7 +3529,7 @@
"fields": {
"accounts": {
"description": "List of Accounts resulting from the query",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Account",
"is_optional": false,
"is_nullable": false,
@ -3543,7 +3543,7 @@
},
"hashtags": {
"description": "List of Tags resulting from the query",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Union[str, Tag]",
"is_optional": false,
"is_nullable": false,
@ -3565,7 +3565,7 @@
},
"statuses": {
"description": "List of Statuses resulting from the query",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Status",
"is_optional": false,
"is_nullable": false,
@ -3583,10 +3583,10 @@
"name": "Instance",
"python_name": "Instance",
"func_call": "mastodon.instance()",
"func_call_additional": "mastodon.instance_v1()",
"masto_doc_link": "https://docs.joinmastodon.org/entities/Instance/",
"masto_doc_link_additional": "https://docs.joinmastodon.org/entities/V1_Instance/",
"func_call_real": null,
"func_call_additional": null,
"func_alternate_acc": false,
"manual_update": false,
"has_versions": [
@ -3787,7 +3787,7 @@
},
"languages": {
"description": "Array of ISO 639-1 (two-letter) language codes the instance has chosen to advertise.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"field_structuretype": "TwoLetterLanguageCodeEnum",
"is_optional": false,
@ -3837,7 +3837,7 @@
},
"rules": {
"description": "List of Rules with `id` and `text` fields, one for each server rule set by the admin",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Rule",
"is_optional": false,
"is_nullable": false,
@ -3971,16 +3971,16 @@
"name": "Instance thumbnail versions",
"python_name": "InstanceThumbnailVersions",
"func_call": "mastodon.instance().thumbnail.versions",
"masto_doc_link": "https://docs.joinmastodon.org/entities/V1_Instance/",
"masto_doc_link": "https://docs.joinmastodon.org/entities/Instance/",
"func_call_real": null,
"func_call_additional": null,
"func_alternate_acc": false,
"manual_update": false,
"description": "Different resolution versions of the image representing the instance.",
"fields": {
"1x": {
"@1x": {
"description": "The URL for an image representing the instance, for devices with 1x resolution / 96 dpi",
"real_name": "@1x",
"python_name": "at1x",
"field_type": "str",
"field_subtype": null,
"is_optional": true,
@ -3994,10 +3994,10 @@
],
"enum": null
},
"2x": {
"description": "The blurhash for the image representing the instance, for devices with 2x resolution / 192 dpi",
"@2x": {
"description": "The URL for the image representing the instance, for devices with 2x resolution / 192 dpi",
"field_type": "str",
"real_name": "@2x",
"python_name": "at2x",
"field_subtype": null,
"is_optional": true,
"is_nullable": false,
@ -4016,15 +4016,15 @@
"name": "Instance urls",
"python_name": "InstanceURLs",
"func_call": "mastodon.instance().configuration.urls",
"func_call_additional": "mastodon.instance_v1().urls",
"masto_doc_link": "https://docs.joinmastodon.org/entities/V1_Instance/",
"func_call_real": null,
"func_call_additional": null,
"func_alternate_acc": false,
"manual_update": false,
"description": "A list of URLs related to an instance.",
"fields": {
"streaming_api": {
"description": "The Websockets URL for connecting to the streaming API.",
"streaming": {
"description": "The Websockets URL for connecting to the streaming API. Was 'streaming_api' in v1",
"field_type": "str",
"field_subtype": null,
"is_optional": false,
@ -4038,6 +4038,27 @@
],
"enum": null
},
"streaming_api": {
"description": "The Websockets URL for connecting to the streaming API. Only present in v1, though Mastodon.py will mirror the appropriate value from 'streaming' for the v2 API.",
"field_type": "str",
"moved_path": "streaming",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
"is_deprecated": true,
"field_structuretype": "URL",
"version_history": [
[
"3.4.2",
"added"
],
[
"4.0.0",
"deprecated"
]
],
"enum": null
},
"status": {
"description": "If present, a URL where the status and possibly current issues with the instance can be checked.",
"field_type": "str",
@ -4478,7 +4499,7 @@
"manual_update": false,
"description": "Configuration values relating to translation.",
"fields": {
"max_characters": {
"enabled": {
"description": "Boolean indicating whether the translation API is enabled on this instance.",
"field_type": "bool",
"field_subtype": null,
@ -4506,7 +4527,7 @@
"fields": {
"supported_mime_types": {
"description": "Mime types the instance accepts for media attachment uploads.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"is_optional": false,
"is_nullable": false,
@ -4688,7 +4709,7 @@
},
"protocols": {
"description": "A list of strings specifying the federation protocols this instance supports. Typically, just \"activitypub\".",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "str",
"is_optional": false,
"is_nullable": false,
@ -4805,7 +4826,7 @@
"fields": {
"outbound": {
"description": "List of services that this instance can send messages to. On Mastodon, typically an empty list.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -4818,7 +4839,7 @@
},
"inbound": {
"description": "List of services that this instance can retrieve messages from. On Mastodon, typically an empty list.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -5135,7 +5156,7 @@
},
"statuses": {
"description": "List of Statuses attached to the report",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Status",
"is_optional": false,
"is_nullable": false,
@ -5192,7 +5213,7 @@
},
"rules": {
"description": "Rules attached to the report, for context.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Rule",
"is_optional": false,
"is_nullable": false,
@ -5598,6 +5619,7 @@
"description": "The logged in users preferences.",
"fields": {
"posting:default:visibility": {
"python_name": "posting_default_visibility",
"description": "Default visibility for new posts. Equivalent to CredentialAccount#source\\[privacy\\]().",
"field_type": "str",
"field_subtype": null,
@ -5617,6 +5639,7 @@
}
},
"posting:default:sensitive": {
"python_name": "posting_default_sensitive",
"description": "Default sensitivity flag for new posts. Equivalent to CredentialAccount#source\\[sensitive\\]().",
"field_type": "bool",
"field_subtype": null,
@ -5631,6 +5654,7 @@
"enum": null
},
"posting:default:language": {
"python_name": "posting_default_language",
"description": "Default language for new posts. Equivalent to CredentialAccount#source\\[language\\]()",
"field_type": "str",
"field_subtype": null,
@ -5646,6 +5670,7 @@
"enum": null
},
"reading:expand:media": {
"python_name": "reading_expand_media",
"description": "String indicating whether media attachments should be automatically displayed or blurred/hidden.",
"field_type": "str",
"field_subtype": null,
@ -5664,6 +5689,7 @@
}
},
"reading:expand:spoilers": {
"python_name": "reading_expand_spoilers",
"description": "Boolean indicating whether CWs should be expanded by default.",
"field_type": "bool",
"field_subtype": null,
@ -5678,6 +5704,7 @@
"enum": null
},
"reading:autoplay:gifs": {
"python_name": "reading_autoplay_gifs",
"description": "Boolean indicating whether gifs should be autoplayed (True) or not (False)",
"field_type": "bool",
"field_subtype": null,
@ -5957,7 +5984,7 @@
},
"mentions": {
"description": "Users mentioned in the annoucement",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Mention",
"is_optional": false,
"is_nullable": false,
@ -5971,7 +5998,7 @@
},
"tags": {
"description": "Hashtags mentioned in the announcement",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -5985,7 +6012,7 @@
},
"emojis": {
"description": "Custom emoji used in the annoucement",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -5999,7 +6026,7 @@
},
"reactions": {
"description": "Reactions to the annoucement",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Reaction",
"is_optional": false,
"is_nullable": false,
@ -6013,7 +6040,7 @@
},
"statuses": {
"description": "Statuses linked in the announcement text.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": null,
"is_optional": false,
"is_nullable": false,
@ -6139,7 +6166,7 @@
},
"accounts": {
"description": "List of Accounts of the familiar followers",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "Account",
"is_optional": false,
"is_nullable": false,
@ -6418,7 +6445,7 @@
},
"ips": {
"description": "All known IP addresses associated with this account.",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "AdminIp",
"is_optional": false,
"is_nullable": false,
@ -6600,7 +6627,7 @@
},
"data": {
"description": "A list of AdminMeasureData with the measure broken down by date",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "AdminMeasureData",
"is_optional": false,
"is_nullable": false,
@ -6682,7 +6709,7 @@
},
"data": {
"description": "A list of data AdminDimensionData objects",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "AdminDimensionData",
"is_optional": false,
"is_nullable": false,
@ -6790,7 +6817,7 @@
},
"data": {
"description": "List of AdminCohort objects",
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "AdminCohort",
"is_optional": false,
"is_nullable": false,
@ -6956,7 +6983,7 @@
"enum": null
},
"private_comment": {
"description": "",
"description": "A private comment (visible only to other moderators) for the domain block",
"field_type": "str",
"field_subtype": null,
"is_optional": false,
@ -6970,7 +6997,7 @@
"enum": null
},
"public_comment": {
"description": "",
"description": "A public comment (visible to either all users, or the whole world) for the domain block",
"field_type": "str",
"field_subtype": null,
"is_optional": false,
@ -7165,7 +7192,7 @@
"added"
]
],
"field_type": "list",
"field_type": "EntityList",
"field_subtype": "AdminEmailDomainBlockHistory",
"field_structuretype": null,
"is_optional": false,
@ -7550,7 +7577,7 @@
"added"
]
],
"field_type": "SnowflakeIdType",
"field_type": "MaybeSnowflakeIdType",
"field_subtype": null,
"field_structuretype": null,
"is_optional": false,

Wyświetl plik

@ -61,7 +61,6 @@ def test_account_follow_unfollow(api, api2):
relationship = api.account_follow(api2_id)
try:
assert relationship
print(relationship)
assert relationship['following']
finally:
relationship = api.account_unfollow(api2_id)
@ -125,6 +124,7 @@ def test_account_update_credentials(api):
)
assert account
assert account.id
assert account["display_name"] == 'John Lennon'
assert re.sub("<.*?>", " ", account["note"]).strip() == 'I walk funny'
assert account["fields"][0].name == "bread"
@ -222,6 +222,7 @@ def test_preferences(api):
@pytest.mark.vcr()
def test_suggested_tags(api):
status = None
try:
status = api.status_post("cool free #ringtones")
time.sleep(2)
@ -234,6 +235,8 @@ def test_suggested_tags(api):
@pytest.mark.vcr()
def test_featured_tags(api):
featured_tag = None
featured_tag_2 = None
try:
featured_tag = api.featured_tag_create("ringtones")
assert featured_tag
@ -253,7 +256,8 @@ def test_featured_tags(api):
finally:
if featured_tag is not None:
api.featured_tag_delete(featured_tag)
api.featured_tag_delete(featured_tag_2)
if featured_tag_2 is not None:
api.featured_tag_delete(featured_tag_2)
@pytest.mark.vcr()
def test_account_notes(api, api2):
@ -264,6 +268,8 @@ def test_account_notes(api, api2):
@pytest.mark.vcr()
def test_follow_with_notify_reblog(api, api2, api3):
api2_id = api2.account_verify_credentials()
status1 = None
status2 = None
try:
api.account_follow(api2_id, notify = True, reblogs = False)
status1 = api3.toot("rootin tooting and shootin")

Wyświetl plik

@ -7,14 +7,24 @@ import os
import pickle
@pytest.mark.vcr()
def test_instance(api):
instance = api.instance()
def test_instance_v1(api):
instance = api.instance_v1()
assert isinstance(instance, dict) # hehe, instance is instance
assert isinstance(instance, dict)
expected_keys = set(('description', 'email', 'title', 'uri', 'version', 'urls'))
assert set(instance.keys()) >= expected_keys
@pytest.mark.vcr()
def test_instance(api):
instance = api.instance()
assert isinstance(instance, dict)
expected_keys = set(('description')) # TODO add some more maybe
assert set(instance.keys()) >= expected_keys
@pytest.mark.vcr()
def test_instance_activity(api):
activity = api.instance_activity()