diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 70c251a..f330e7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ v2.1.0 (IN PROGRESS) * Fix `notifications` returning nonsense when passing a single `id` (Thanks @chinchalinchin for the report) * Fix `moved` accidentally being named `moved_to_account` (Thanks @unusualevent for the report) * Added a warning for deprecated endpoints if the "deprecation" header is present +* Added `oauth_userinfo` endpoint. v2.0.1 ------ diff --git a/TODO.md b/TODO.md index 496c9d6..193910e 100644 --- a/TODO.md +++ b/TODO.md @@ -8,7 +8,7 @@ Refer to mastodon changelog and API docs for details when implementing, add or m * [ ] New endpoints for endorsements, replacing "pin" api, which is now deprecated: accounts_endorsements(id), account_endorse(id), account_unendorse(id) * [ ] New endpoints for featured tags: tag_feature(name), tag_unfeature(name) * [ ] New endpoint: instance_terms, with or without date (format?) -* [ ] Some oauth stuff (userinfo? capability discovery? see issue for that) +* [x] Some oauth stuff (userinfo? capability discovery? see issue for that) * [ ] status_delete now has a media delete param * [ ] push_subscribe now has a "standard" parameter to switch between two versions. may also need to update crypto impls? * [ ] account_register now has a date of birth param (as above: format?) diff --git a/docs/02_return_values.rst b/docs/02_return_values.rst index deda648..d4d2bb4 100644 --- a/docs/02_return_values.rst +++ b/docs/02_return_values.rst @@ -395,6 +395,9 @@ Return types .. autoclass:: mastodon.return_types.OAuthServerInfo :members: +.. autoclass:: mastodon.return_types.OAuthUserInfo + :members: + Deprecated types ================ .. autoclass:: mastodon.return_types.Filter diff --git a/docs/04_auth.rst b/docs/04_auth.rst index c761104..c7d9803 100644 --- a/docs/04_auth.rst +++ b/docs/04_auth.rst @@ -39,6 +39,11 @@ Authentication .. automethod:: Mastodon.create_account .. automethod:: Mastodon.email_resend_confirmation +OAuth information +----------------- +.. automethod:: Mastodon.oauth_authorization_server_info +.. automethod:: Mastodon.oauth_userinfo + User preferences ---------------- .. automethod:: Mastodon.preferences diff --git a/mastodon/authentication.py b/mastodon/authentication.py index 09f0f67..372f312 100644 --- a/mastodon/authentication.py +++ b/mastodon/authentication.py @@ -14,7 +14,7 @@ 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.return_types import Application, AttribAccessDict, OAuthServerInfo +from mastodon.return_types import Application, AttribAccessDict, OAuthServerInfo, OAuthUserInfo from mastodon.compat import PurePath class Mastodon(Internals): @@ -358,6 +358,21 @@ class Mastodon(Internals): response = AttribAccessDict() return response + @api_version("4.3.0", "4.3.0") + def oauth_userinfo(self) -> OAuthUserInfo: + """ + Returns information about the authenticated user. + + Intended for something called "OpenID Connect", which you can find information about here: + https://openid.net/developers/how-connect-works/ + """ + oauth_url = "".join([self.api_base_url, "/oauth/userinfo"]) + oauth_info = self.oauth_authorization_server_info() + if "userinfo_endpoint" in oauth_info: + oauth_url = Mastodon.__protocolize(oauth_info["userinfo_endpoint"]) + Mastodon.__oauth_url_check(oauth_url) + return self.__api_request('GET', oauth_url, do_ratelimiting=False, base_url_override="") + 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: Optional[Union[str, PurePath]] = None, allow_http: bool = False) -> str: diff --git a/mastodon/defaults.py b/mastodon/defaults.py index b665830..afd6c38 100644 --- a/mastodon/defaults.py +++ b/mastodon/defaults.py @@ -59,7 +59,8 @@ _SCOPE_SETS = { 'admin:write:email_domain_blocks', 'admin:write:canonical_email_blocks', ], + 'profile': [] } -_VALID_SCOPES = ['read', 'write', 'follow', 'push', 'admin:read', 'admin:write'] + \ +_VALID_SCOPES = ['read', 'write', 'follow', 'push', 'admin:read', 'admin:write', 'profile'] + \ _SCOPE_SETS['read'] + _SCOPE_SETS['write'] + \ _SCOPE_SETS['admin:read'] + _SCOPE_SETS['admin:write'] diff --git a/mastodon/return_types.py b/mastodon/return_types.py index a888856..419f0c7 100644 --- a/mastodon/return_types.py +++ b/mastodon/return_types.py @@ -7110,6 +7110,74 @@ class OAuthServerInfo(AttribAccessDict): _version = "4.4.0" +class OAuthUserInfo(AttribAccessDict): + """ + Information about the currently logged in user, returned by the OAuth userinfo endpoint. + + Example: + + .. code-block:: python + + # Returns a OAuthUserInfo object + mastodon.oauth_userinfo() + + See also (Mastodon API documentation): https://docs.joinmastodon.org/methods/oauth/#userinfo + """ + + iss: "str" + """ + The issuer of the OAuth server. Can be used to avoid accidentally getting replies from a wrong server by comparing it against the `issuer` field in OAuthServerInfo. + Should contain (as text): URL + + Version history: + * 4.4.0: added + """ + + sub: "str" + """ + The subject identifier of the user. For Mastodon, the URI of the ActivityPub Actor document. + Should contain (as text): URL + + Version history: + * 4.4.0: added + """ + + name: "str" + """ + The display name of the user. + + Version history: + * 4.4.0: added + """ + + preferred_username: "str" + """ + The preferred username of the user, i.e. the part after the first and before the second @ in their account name. + + Version history: + * 4.4.0: added + """ + + profile : "str" + """ + The URL of the user’s profile page. + Should contain (as text): URL + + Version history: + * 4.4.0: added + """ + + picture: "str" + """ + The URL of the user’s profile picture. + Should contain (as text): URL + + Version history: + * 4.4.0: added + """ + + _version = "4.4.0" + ENTITY_NAME_MAP = { "Account": Account, "AccountField": AccountField, @@ -7226,6 +7294,7 @@ ENTITY_NAME_MAP = { "NotificationRequest": NotificationRequest, "SupportedLocale": SupportedLocale, "OAuthServerInfo": OAuthServerInfo, + "OAuthUserInfo": OAuthUserInfo, } __all__ = [ "Account", @@ -7343,5 +7412,6 @@ __all__ = [ "NotificationRequest", "SupportedLocale", "OAuthServerInfo", + "OAuthUserInfo", ] diff --git a/mastodon/types_base.py b/mastodon/types_base.py index 2329667..4b42bd7 100644 --- a/mastodon/types_base.py +++ b/mastodon/types_base.py @@ -209,7 +209,7 @@ if sys.version_info < (3, 9): FilterKeyword, FilterStatus, IdentityProof, StatusSource, Suggestion, Translation, \ AccountCreationError, AccountCreationErrorDetails, AccountCreationErrorDetailsField, NotificationPolicy, NotificationPolicySummary, RelationshipSeveranceEvent, \ GroupedNotificationsResults, PartialAccountWithAvatar, NotificationGroup, AccountWarning, UnreadNotificationsCount, Appeal, \ - NotificationRequest, SupportedLocale, OAuthServerInfo + NotificationRequest, SupportedLocale, OAuthServerInfo, OAuthUserInfo if isinstance(t, ForwardRef): try: t = t._evaluate(globals(), locals(), frozenset()) diff --git a/srcgen/return_types.json b/srcgen/return_types.json index a05e838..f0a0245 100644 --- a/srcgen/return_types.json +++ b/srcgen/return_types.json @@ -10479,5 +10479,78 @@ "is_nullable": false } } + }, + { + "name": "OAuth User Info", + "python_name": "OAuthUserInfo", + "func_call": "mastodon.oauth_userinfo()", + "func_call_real": null, + "func_call_additional": null, + "func_alternate_acc": null, + "manual_update": false, + "masto_doc_link": "https://docs.joinmastodon.org/methods/oauth/#userinfo", + "description": "Information about the currently logged in user, returned by the OAuth userinfo endpoint.", + "fields": { + "iss": { + "description": "The issuer of the OAuth server. Can be used to avoid accidentally getting replies from a wrong server by comparing it against the `issuer` field in OAuthServerInfo.", + "enum": null, + "version_history": [["4.4.0", "added"]], + "field_type": "str", + "field_subtype": null, + "field_structuretype": "URL", + "is_optional": false, + "is_nullable": false + }, + "sub": { + "description": "The subject identifier of the user. For Mastodon, the URI of the ActivityPub Actor document", + "enum": null, + "version_history": [["4.4.0", "added"]], + "field_type": "str", + "field_subtype": null, + "field_structuretype": "URL", + "is_optional": false, + "is_nullable": false + }, + "name": { + "description": "The display name of the user.", + "enum": null, + "version_history": [["4.4.0", "added"]], + "field_type": "str", + "field_subtype": null, + "field_structuretype": null, + "is_optional": false, + "is_nullable": false + }, + "preferred_username": { + "description": "The preferred username of the user, i.e. the part after the first and before the second @ in their account name.", + "enum": null, + "version_history": [["4.4.0", "added"]], + "field_type": "str", + "field_subtype": null, + "field_structuretype": null, + "is_optional": false, + "is_nullable": false + }, + "profile ": { + "description": "The URL of the user’s profile page.", + "enum": null, + "version_history": [["4.4.0", "added"]], + "field_type": "str", + "field_subtype": null, + "field_structuretype": "URL", + "is_optional": false, + "is_nullable": false + }, + "picture": { + "description": "The URL of the user’s profile picture.", + "enum": null, + "version_history": [["4.4.0", "added"]], + "field_type": "str", + "field_subtype": null, + "field_structuretype": "URL", + "is_optional": false, + "is_nullable": false + } + } } ] diff --git a/tests/cassettes_entity_tests/test_entity_oauthuserinfo.yaml b/tests/cassettes_entity_tests/test_entity_oauthuserinfo.yaml new file mode 100644 index 0000000..44cdfa6 --- /dev/null +++ b/tests/cassettes_entity_tests/test_entity_oauthuserinfo.yaml @@ -0,0 +1,166 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - DUMMY + Connection: + - keep-alive + User-Agent: + - mastodonpy + method: GET + uri: https://mastodon.social/.well-known/oauth-authorization-server + response: + body: + string: '{"issuer": "https://mastodon.social/", "authorization_endpoint": "https://mastodon.social/oauth/authorize", + "token_endpoint": "https://mastodon.social/oauth/token", "revocation_endpoint": + "https://mastodon.social/oauth/revoke", "userinfo_endpoint": "https://mastodon.social/oauth/userinfo", + "scopes_supported": ["read", "profile", "write", "write:accounts", "write:blocks", + "write:bookmarks", "write:conversations", "write:favourites", "write:filters", + "write:follows", "write:lists", "write:media", "write:mutes", "write:notifications", + "write:reports", "write:statuses", "read:accounts", "read:blocks", "read:bookmarks", + "read:favourites", "read:filters", "read:follows", "read:lists", "read:mutes", + "read:notifications", "read:search", "read:statuses", "follow", "push", "admin:read", + "admin:read:accounts", "admin:read:reports", "admin:read:domain_allows", "admin:read:domain_blocks", + "admin:read:ip_blocks", "admin:read:email_domain_blocks", "admin:read:canonical_email_blocks", + "admin:write", "admin:write:accounts", "admin:write:reports", "admin:write:domain_allows", + "admin:write:domain_blocks", "admin:write:ip_blocks", "admin:write:email_domain_blocks", + "admin:write:canonical_email_blocks"], "response_types_supported": ["code"], + "response_modes_supported": ["query", "fragment", "form_post"], "grant_types_supported": + ["authorization_code", "client_credentials"], "token_endpoint_auth_methods_supported": + ["client_secret_basic", "client_secret_post"], "code_challenge_methods_supported": + ["S256"], "service_documentation": "https://docs.joinmastodon.org/", "app_registration_endpoint": + "https://mastodon.social/api/v1/apps"}' + headers: + Connection: + - keep-alive + Date: + - Sun, 17 Aug 2025 16:11:31 GMT + Strict-Transport-Security: + - max-age=31557600 + Vary: + - Origin, Accept-Encoding + X-Cache: + - MISS, MISS, MISS + X-Cache-Hits: + - 0, 0, 0 + X-Served-By: + - cache-fra-eddf8230103-FRA, cache-fra-eddf8230103-FRA, cache-hel1410031-HEL + X-Timer: + - S1755447092.718686,VS0,VE76 + accept-ranges: + - none + alt-svc: + - h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 + cache-control: + - max-age=0, private, must-revalidate + content-length: + - '1563' + content-security-policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' https://mastodon.social; img-src ''self'' data: blob: https://mastodon.social + https://files.mastodon.social; media-src ''self'' data: https://mastodon.social + https://files.mastodon.social; manifest-src ''self'' https://mastodon.social; + form-action ''self''; child-src ''self'' blob: https://mastodon.social; worker-src + ''self'' blob: https://mastodon.social; connect-src ''self'' data: blob: https://mastodon.social + https://files.mastodon.social wss://streaming.mastodon.social; script-src + ''self'' https://mastodon.social ''wasm-unsafe-eval''; frame-src ''self'' + https:; style-src ''self'' https://mastodon.social ''nonce-xuGiALvgOfrc1xtVTIoYFw==''' + content-type: + - application/json; charset=utf-8 + etag: + - W/"6039ea331b50e2b000b155f90aaead96" + referrer-policy: + - same-origin + transfer-encoding: + - chunked + via: + - 1.1 varnish, 1.1 varnish, 1.1 varnish + x-content-type-options: + - nosniff + x-frame-options: + - DENY + x-request-id: + - 046bcf0334eb061f18d41d273d11bdc7 + x-runtime: + - '0.009508' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - DUMMY + Connection: + - keep-alive + User-Agent: + - mastodonpy + method: GET + uri: https://mastodon.social/oauth/userinfo + response: + body: + string: '{"iss": "https://mastodon.social/", "sub": "https://mastodon.social/users/halcy", + "name": "autumnal halcy", "preferred_username": "halcy", "profile": "https://mastodon.social/@halcy", + "picture": "https://mastodon.social/avatars/original/missing.png"}' + headers: + Connection: + - keep-alive + Date: + - Sun, 17 Aug 2025 16:11:31 GMT + Strict-Transport-Security: + - max-age=31557600 + Vary: + - Authorization, Origin, Accept-Encoding + X-Cache: + - MISS, MISS, MISS + X-Cache-Hits: + - 0, 0, 0 + X-Served-By: + - cache-fra-eddf8230036-FRA, cache-fra-eddf8230036-FRA, cache-hel1410030-HEL + X-Timer: + - S1755447092.866749,VS0,VE110 + accept-ranges: + - none + alt-svc: + - h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 + cache-control: + - private, no-store + content-length: + - '239' + content-security-policy: + - default-src 'none'; frame-ancestors 'none'; form-action 'none' + content-type: + - application/json; charset=utf-8 + etag: + - W/"2d63fbd9fa70b7e1d88f1de0be7f462c" + referrer-policy: + - same-origin + transfer-encoding: + - chunked + via: + - 1.1 varnish, 1.1 varnish, 1.1 varnish + x-content-type-options: + - nosniff + x-frame-options: + - DENY + x-request-id: + - 3d94a304cd78c66ccf962b7282cf9f24 + x-runtime: + - '0.016761' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_entities.py b/tests/test_entities.py index 25ee5b0..1dfc2d8 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -1801,3 +1801,20 @@ def test_entity_oauthserverinfo(mastodon_base, mastodon_admin): if sys.version_info >= (3, 9): assert real_issubclass(type(result), OAuthServerInfo), str(type(result)) + ' is not a subclass of OAuthServerInfo after to_json/from_json' +@pytest.mark.vcr( + filter_query_parameters=[('access_token', 'DUMMY'), ('client_id', 'DUMMY'), ('client_secret', 'DUMMY')], + filter_post_data_parameters=[('access_token', 'DUMMY'), ('client_id', 'DUMMY'), ('client_secret', 'DUMMY')], + filter_headers=[('Authorization', 'DUMMY')], + before_record_request=vcr_filter, + before_record_response=token_scrubber, + match_on=['method', 'uri'], + cassette_library_dir='tests/cassettes_entity_tests' +) +def test_entity_oauthuserinfo(mastodon_base, mastodon_admin): + mastodon = mastodon_base + result = mastodon.oauth_userinfo() + assert real_issubclass(type(result), OAuthUserInfo), str(type(result)) + ' is not a subclass of OAuthUserInfo' + result = Entity.from_json(result.to_json()) + if sys.version_info >= (3, 9): + assert real_issubclass(type(result), OAuthUserInfo), str(type(result)) + ' is not a subclass of OAuthUserInfo after to_json/from_json' +