add grouped notifications

pull/397/head
halcy 2025-02-15 21:06:27 +02:00
rodzic 861d1c68d5
commit 2fde93fd3c
11 zmienionych plików z 2377 dodań i 14 usunięć

Wyświetl plik

@ -43,6 +43,8 @@ v2.0.0 (IN PROGRESS)
* Add `instance_domain_blocks`
* Add notification policy and requests (`notifications_policy`, `update_notifications_policy`, `notifications_requests`, `notification_request`, `accept_notification_request`, `reject_notification_request`, `notifications_merged`, `accept_multiple_notification_requests`, `dismiss_multiple_notification_requests`)
* Add `instance_languages`
* Add notification grouping (`grouped_notifications`, `grouped_notification`, `dismiss_grouped_notification`, `grouped_notification_accounts`, `unread_grouped_notifications_count`)
v1.8.1
------

Wyświetl plik

@ -5,7 +5,7 @@ Notifications and filtering
Notifications
-------------
This function allows you to get information about a user's notifications as well as to clear all or some notifications and to mark conversations as read.
These functions allow you to get information about a user's notifications as well as to clear all or some notifications and to mark conversations as read.
Reading
~~~~~~~
@ -19,6 +19,16 @@ Writing
.. automethod:: Mastodon.conversations_read
Grouped notifications
---------------------
This is the more modern notification API, which delivers notifications grouped.
.. automethod:: Mastodon.grouped_notifications
.. automethod:: Mastodon.grouped_notification
.. automethod:: Mastodon.dismiss_grouped_notification
.. automethod:: Mastodon.grouped_notification_accounts
.. automethod:: Mastodon.unread_grouped_notifications_count
Source filtering for notifications
----------------------------------
These functions allow you to get information about source filters as well as to create and update filters, and

Wyświetl plik

@ -85,7 +85,8 @@ class Mastodon():
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, override_type=None):
do_ratelimiting=True, use_json=False, parse=True, return_response_object=False, skip_error_check=False, lang_override=None, override_type=None,
force_pagination=False):
"""
Internal API request helper.
@ -274,8 +275,8 @@ class Mastodon():
response = response_object.content
# Parse link headers
if isinstance(response, list) and 'Link' in response_object.headers and response_object.headers['Link'] != "":
if not isinstance(response, PaginatableList):
if isinstance(response, list) or force_pagination and 'Link' in response_object.headers and response_object.headers['Link'] != "":
if not isinstance(response, PaginatableList) and not force_pagination:
response = PaginatableList(response)
tmp_urls = requests.utils.parse_header_links(response_object.headers['Link'].rstrip('>').replace('>,<', ',<'))
for url in tmp_urls:

Wyświetl plik

@ -3,7 +3,7 @@ from mastodon.errors import MastodonIllegalArgumentError
from mastodon.utility import api_version
from mastodon.internals import Mastodon as Internals
from mastodon.return_types import Notification, IdType, PaginatableList, Account, UnreadNotificationsCount, NotificationPolicy, NotificationRequest
from mastodon.return_types import Notification, IdType, PaginatableList, Account, UnreadNotificationsCount, NotificationPolicy, NotificationRequest, GroupedNotificationsResults, NonPaginatableList
from typing import Union, Optional, List
class Mastodon(Internals):
@ -183,3 +183,66 @@ class Mastodon(Internals):
"""
result = self.__api_request('GET', '/api/v1/notifications/requests/merged', override_type = dict)
return result["merged"]
##
# Grouped notifications
##
@api_version("4.3.0", "4.3.0")
def grouped_notifications(self, max_id: Optional[IdType] = None, since_id: Optional[IdType] = None,
min_id: Optional[IdType] = None, limit: Optional[int] = None,
types: Optional[List[str]] = None, exclude_types: Optional[List[str]] = None,
account_id: Optional[Union[Account, IdType]] = None,
expand_accounts: Optional[str] = "partial_avatars", grouped_types: Optional[List[str]] = None,
include_filtered: Optional[bool] = None) -> GroupedNotificationsResults:
"""
Fetch grouped notifications for the user. Requires scope `read:notifications`.
For base parameters, see `notifications()`.
`grouped_types` controls which notication types can be grouped together - all, if not specified.
NB: "all" here means favourite, follow and reblog - other types are not groupable and are returned
individually (with a unique group key) always.
Pass `include_filtered=True` to include filtered notifications in the response.
Pass `expand_accounts="full"` to include full account details in the response, or "partial_avatars" to
include a smaller set of account details (in the `partial_accounts` field) for some (but not all - the most
recent account triggering a notification is always returned in full) of the included accounts.
The default is partial_avatars.
"""
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v2/notifications', params, force_pagination=True)
@api_version("4.3.0", "4.3.0")
def grouped_notification(self, group_key: str) -> GroupedNotificationsResults:
"""
Fetch details of a single grouped notification by its group key. Requires scope `read:notifications`.
"""
return self.__api_request('GET', f'/api/v2/notifications/{group_key}')
@api_version("4.3.0", "4.3.0")
def dismiss_grouped_notification(self, group_key: str) -> None:
"""
Dismiss a single grouped notification. Requires scope `write:notifications`.
"""
self.__api_request('POST', f'/api/v2/notifications/{group_key}/dismiss')
@api_version("4.3.0", "4.3.0")
def grouped_notification_accounts(self, group_key: str) -> NonPaginatableList[Account]:
"""
Fetch accounts associated with a grouped notification. Requires scope `write:notifications`.
"""
return self.__api_request('GET', f'/api/v2/notifications/{group_key}/accounts')
@api_version("4.3.0", "4.3.0")
def unread_grouped_notifications_count(self, limit: Optional[int] = None,
types: Optional[List[str]] = None, exclude_types: Optional[List[str]] = None,
account_id: Optional[Union[Account, IdType]] = None,
grouped_types: Optional[List[str]] = None) -> int:
"""
Fetch the count of unread grouped notifications. Requires scope `read:notifications`.
For parameters, see `notifications()` and `grouped_notifications()`.
"""
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v2/notifications/unread_count', params, override_type=dict)["count"]

Wyświetl plik

@ -29,7 +29,8 @@ class Mastodon(Internals):
def markers_get(self, timeline: Union[str, List[str]] = ["home"]) -> Dict[str, Marker]:
"""
Get the last-read-location markers for the specified timelines. Valid timelines
are the same as in :ref:`timeline() <timeline()>`
are `home` (the home timeline) and `notifications` (the notifications timeline,
affects which notifications are considered read).
Note that despite the singular name, `timeline` can be a list.
@ -52,6 +53,9 @@ class Mastodon(Internals):
"""
Set the "last read" marker(s) for the given timeline(s) to the given id(s)
Valid timelines are `home` (the home timeline) and `notifications` (the notifications timeline,
affects which notifications are considered read).
Note that if you give an invalid timeline name, this will silently do nothing.
Returns a dict with the updated markers, keyed by timeline name.

Wyświetl plik

@ -6330,6 +6330,22 @@ class GroupedNotificationsResults(AttribAccessDict):
* 4.3.0: added
"""
_pagination_next: "Optional[PaginationInfo]"
"""
Information about the next page of results. Added here as a special case to allow for pagination of the lists inside of this object. (optional)
Version history:
* 4.3.0: added
"""
_pagination_prev: "Optional[PaginationInfo]"
"""
Information about the previous page of results. Added here as a special case to allow for pagination of the lists inside of this object. (optional)
Version history:
* 4.3.0: added
"""
_version = "4.3.0"
class PartialAccountWithAvatar(AttribAccessDict):

Wyświetl plik

@ -11,8 +11,9 @@ from mastodon.internals import Mastodon as Internals
from mastodon.versions import parse_version_string, max_version, api_version
from typing import Optional
from mastodon.return_types import PaginatableList, PaginationInfo
from typing import Optional, Union, Dict
from mastodon.return_types import PaginatableList, PaginationInfo, PaginatableList
from mastodon.types_base import Entity
# Class level:
class Mastodon(Internals):
@ -118,7 +119,7 @@ class Mastodon(Internals):
###
# Pagination
###
def fetch_next(self, previous_page: PaginatableList) -> Optional[PaginatableList]:
def fetch_next(self, previous_page: Union[PaginatableList[Entity], Entity, Dict]) -> Optional[Union[PaginatableList[Entity], Entity]]:
"""
Fetches the next page of results of a paginated request. Pass in the
previous page in its entirety, or the pagination information dict
@ -143,9 +144,12 @@ class Mastodon(Internals):
endpoint = params['_pagination_endpoint']
del params['_pagination_endpoint']
return self.__api_request(method, endpoint, params)
force_pagination = False
if not isinstance(previous_page, list):
force_pagination = True
return self.__api_request(method, endpoint, params, force_pagination=force_pagination, override_type=type(previous_page))
def fetch_previous(self, next_page: PaginatableList) -> Optional[PaginatableList]:
def fetch_previous(self, next_page: Union[PaginatableList[Entity], Entity, Dict]) -> Optional[Union[PaginatableList[Entity], Entity]]:
"""
Fetches the previous page of results of a paginated request. Pass in the
previous page in its entirety, or the pagination information dict
@ -170,9 +174,12 @@ class Mastodon(Internals):
endpoint = params['_pagination_endpoint']
del params['_pagination_endpoint']
return self.__api_request(method, endpoint, params)
force_pagination = False
if not isinstance(next_page, list):
force_pagination = True
return self.__api_request(method, endpoint, params, force_pagination=force_pagination, override_type=type(next_page))
def fetch_remaining(self, first_page):
def fetch_remaining(self, first_page: PaginatableList[Entity]) -> PaginatableList[Entity]:
"""
Fetches all the remaining pages of a paginated request starting from a
first page and returns the entire set of results (including the first page
@ -180,6 +187,9 @@ class Mastodon(Internals):
Be careful, as this might generate a lot of requests, depending on what you are
fetching, and might cause you to run into rate limits very quickly.
Does not currently work with grouped notifications, please deal with those
yourself, for now.
"""
first_page = copy.deepcopy(first_page)

Wyświetl plik

@ -9462,7 +9462,7 @@
"is_nullable": false
},
"notification_groups": {
"description": "The grouped notifications themselves.",
"description": "The grouped notifications themselves. Is actually in fact paginatable, but via the parent object.",
"enum": null,
"version_history": [["4.3.0", "added"]],
"field_type": "NonPaginatableList",
@ -9470,6 +9470,26 @@
"field_structuretype": null,
"is_optional": false,
"is_nullable": false
},
"_pagination_next": {
"description": "Information about the next page of results. Added here as a special case to allow for pagination of the lists inside of this object.",
"enum": null,
"version_history": [["4.3.0", "added"]],
"field_type": "PaginationInfo",
"field_subtype": null,
"field_structuretype": null,
"is_optional": true,
"is_nullable": false
},
"_pagination_prev": {
"description": "Information about the previous page of results. Added here as a special case to allow for pagination of the lists inside of this object.",
"enum": null,
"version_history": [["4.3.0", "added"]],
"field_type": "PaginationInfo",
"field_subtype": null,
"field_structuretype": null,
"is_optional": true,
"is_nullable": false
}
}
},

Wyświetl plik

@ -0,0 +1,563 @@
interactions:
- request:
body: status=Testing+grouped+notifications%21&visibility=public
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN
Connection:
- keep-alive
Content-Length:
- '57'
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- tests/v311
method: POST
uri: http://localhost:3000/api/v1/statuses
response:
body:
string: '{"id":"114009357138722264","created_at":"2025-02-15T18:38:51.731Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114009357138722264","url":"http://localhost:3000/@mastodonpy_test/114009357138722264","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eTesting
grouped notifications!\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py
test suite","website":null},"account":{"id":"114009019326978043","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '1505'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"06fb25c4bf01795b2054bd152aeb8ef4"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.05, sql.active_record;dur=11.42, cache_generate.active_support;dur=3.55,
cache_write.active_support;dur=0.15, instantiation.active_record;dur=0.54,
start_processing.action_controller;dur=0.00, transaction.active_record;dur=4.50,
render.active_model_serializers;dur=15.64, process_action.action_controller;dur=63.46
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '298'
X-RateLimit-Reset:
- '2025-02-15T21:00:00.763790Z'
X-Request-Id:
- d4d9d0c4-0e3e-44ad-a7f8-c2b1dfa7efa3
X-Runtime:
- '0.093185'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN_2
Connection:
- keep-alive
Content-Length:
- '0'
User-Agent:
- tests/v311
method: POST
uri: http://localhost:3000/api/v1/statuses/114009357138722264/favourite
response:
body:
string: '{"id":"114009357138722264","created_at":"2025-02-15T18:38:51.731Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114009357138722264","url":"http://localhost:3000/@mastodonpy_test/114009357138722264","replies_count":0,"reblogs_count":0,"favourites_count":1,"edited_at":null,"favourited":true,"reblogged":false,"muted":false,"bookmarked":false,"content":"\u003cp\u003eTesting
grouped notifications!\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py
test suite","website":null},"account":{"id":"114009019326978043","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '1489'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"3ac631610fb82fcfb82750e1f7adccf9"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.06, sql.active_record;dur=7.67, cache_generate.active_support;dur=3.17,
cache_write.active_support;dur=0.13, instantiation.active_record;dur=0.67,
start_processing.action_controller;dur=0.00, transaction.active_record;dur=6.21,
render.active_model_serializers;dur=16.68, process_action.action_controller;dur=47.95
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '299'
X-RateLimit-Reset:
- '2025-02-15T18:40:00.828950Z'
X-Request-Id:
- 6ec29c54-8c91-4b9f-9c88-275a236b5775
X-Runtime:
- '0.063835'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN_3
Connection:
- keep-alive
Content-Length:
- '0'
User-Agent:
- tests/v311
method: POST
uri: http://localhost:3000/api/v1/statuses/114009357138722264/favourite
response:
body:
string: '{"id":"114009357138722264","created_at":"2025-02-15T18:38:51.731Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114009357138722264","url":"http://localhost:3000/@mastodonpy_test/114009357138722264","replies_count":0,"reblogs_count":0,"favourites_count":2,"edited_at":null,"favourited":true,"reblogged":false,"muted":false,"bookmarked":false,"content":"\u003cp\u003eTesting
grouped notifications!\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py
test suite","website":null},"account":{"id":"114009019326978043","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '1489'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"bfcc74cd78fb7eb5b1351bb0f8e1a5fb"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.05, sql.active_record;dur=7.32, cache_generate.active_support;dur=3.52,
cache_write.active_support;dur=0.14, instantiation.active_record;dur=0.68,
start_processing.action_controller;dur=0.00, transaction.active_record;dur=5.76,
render.active_model_serializers;dur=14.78, process_action.action_controller;dur=48.71
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '299'
X-RateLimit-Reset:
- '2025-02-15T18:40:00.926662Z'
X-Request-Id:
- f0f16848-82d0-457c-876c-9aa3e56deed7
X-Runtime:
- '0.063387'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN
Connection:
- keep-alive
User-Agent:
- tests/v311
method: GET
uri: http://localhost:3000/api/v2/notifications?limit=10&expand_accounts=partial_avatars
response:
body:
string: '{"accounts":[{"id":"114009019470060047","username":"mastodonpy_test_2","acct":"mastodonpy_test_2","display_name":"","locked":false,"bot":false,"discoverable":true,"indexable":true,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test_2","uri":"http://localhost:3000/users/mastodonpy_test_2","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]}],"partial_accounts":[{"id":"114009019031846737","acct":"admin","locked":false,"bot":false,"url":"http://localhost:3000/@admin","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png"}],"statuses":[{"id":"114009357138722264","created_at":"2025-02-15T18:38:51.731Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114009357138722264","url":"http://localhost:3000/@mastodonpy_test/114009357138722264","replies_count":0,"reblogs_count":0,"favourites_count":2,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eTesting
grouped notifications!\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py
test suite","website":null},"account":{"id":"114009019326978043","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}],"notification_groups":[{"group_key":"favourite-114009357138722264-483234","notifications_count":2,"type":"favourite","most_recent_notification_id":190,"page_min_id":"190","page_max_id":"190","latest_page_notification_at":"2025-02-15T18:38:52.146Z","sample_account_ids":["114009019470060047","114009019031846737"],"status_id":"114009357138722264"}]}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '2901'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"e5e803ebda94a4f2c5aeb551c6ecd9ea"
Link:
- <http://localhost:3000/api/v2/notifications?limit=10&max_id=190>; rel="next",
<http://localhost:3000/api/v2/notifications?limit=10&min_id=190>; rel="prev"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.10, sql.active_record;dur=9.10, cache_generate.active_support;dur=2.97,
cache_write.active_support;dur=0.15, instantiation.active_record;dur=1.04,
start_processing.action_controller;dur=0.00, cache_fetch_hit.active_support;dur=0.00,
render.active_model_serializers;dur=5.52, process_action.action_controller;dur=58.12
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '299'
X-RateLimit-Reset:
- '2025-02-15T18:40:00.028981Z'
X-Request-Id:
- fae9562c-f5be-4da1-af05-181bb7dd30f3
X-Runtime:
- '0.073893'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN
Connection:
- keep-alive
User-Agent:
- tests/v311
method: GET
uri: http://localhost:3000/api/v2/notifications/favourite-114009357138722264-483234
response:
body:
string: '{"accounts":[{"id":"114009019470060047","username":"mastodonpy_test_2","acct":"mastodonpy_test_2","display_name":"","locked":false,"bot":false,"discoverable":true,"indexable":true,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test_2","uri":"http://localhost:3000/users/mastodonpy_test_2","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},{"id":"114009019031846737","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@admin","uri":"http://localhost:3000/users/admin","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":5,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[{"id":"3","name":"Owner","color":""}],"fields":[]}],"statuses":[{"id":"114009357138722264","created_at":"2025-02-15T18:38:51.731Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114009357138722264","url":"http://localhost:3000/@mastodonpy_test/114009357138722264","replies_count":0,"reblogs_count":0,"favourites_count":2,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eTesting
grouped notifications!\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py
test suite","website":null},"account":{"id":"114009019326978043","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}],"notification_groups":[{"group_key":"favourite-114009357138722264-483234","notifications_count":2,"type":"favourite","most_recent_notification_id":190,"sample_account_ids":["114009019470060047","114009019031846737"],"status_id":"114009357138722264"}]}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '3295'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"b4015be5d4c161ff654954b9a12091f7"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.15, sql.active_record;dur=4.10, cache_generate.active_support;dur=2.80,
cache_write.active_support;dur=0.13, instantiation.active_record;dur=1.19,
start_processing.action_controller;dur=0.00, cache_fetch_hit.active_support;dur=0.01,
render.active_model_serializers;dur=27.82, process_action.action_controller;dur=49.35
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '299'
X-RateLimit-Reset:
- '2025-02-15T18:40:00.158661Z'
X-Request-Id:
- a0c00eab-c12d-4e1e-a03e-0c10eeab8de2
X-Runtime:
- '0.063807'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN
Connection:
- keep-alive
User-Agent:
- tests/v311
method: GET
uri: http://localhost:3000/api/v2/notifications/favourite-114009357138722264-483234/accounts
response:
body:
string: '[{"id":"114009019470060047","username":"mastodonpy_test_2","acct":"mastodonpy_test_2","display_name":"","locked":false,"bot":false,"discoverable":true,"indexable":true,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test_2","uri":"http://localhost:3000/users/mastodonpy_test_2","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},{"id":"114009019031846737","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@admin","uri":"http://localhost:3000/users/admin","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":5,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[{"id":"3","name":"Owner","color":""}],"fields":[]}]'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '1512'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"fb8e14dd97709a52f5a45c9d4e447a24"
Link:
- <http://localhost:3000/api/v2/notifications/favourite-114009357138722264-483234/accounts?min_id=190>;
rel="prev"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.08, sql.active_record;dur=1.64, cache_generate.active_support;dur=1.47,
cache_write.active_support;dur=0.11, instantiation.active_record;dur=0.60,
start_processing.action_controller;dur=0.01, cache_fetch_hit.active_support;dur=0.00,
render.active_model_serializers;dur=3.89, process_action.action_controller;dur=27.45
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '299'
X-RateLimit-Reset:
- '2025-02-15T18:40:00.295492Z'
X-Request-Id:
- 0e34fa26-6d71-4c7d-aeec-8eee17c493fb
X-Runtime:
- '0.056571'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN
Connection:
- keep-alive
Content-Length:
- '0'
User-Agent:
- tests/v311
method: POST
uri: http://localhost:3000/api/v2/notifications/favourite-114009357138722264-483234/dismiss
response:
body:
string: '{}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '2'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"44136fa355b3678a1146ad16f7e8649e"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.02, sql.active_record;dur=7.02, cache_generate.active_support;dur=0.95,
cache_write.active_support;dur=0.08, instantiation.active_record;dur=0.45,
start_processing.action_controller;dur=0.00, transaction.active_record;dur=7.46,
render.active_model_serializers;dur=0.03, process_action.action_controller;dur=30.07
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '299'
X-RateLimit-Reset:
- '2025-02-15T18:40:00.366862Z'
X-Request-Id:
- 512683b1-cf01-497d-8e16-e853aae9d9c2
X-Runtime:
- '0.044774'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN
Connection:
- keep-alive
User-Agent:
- tests/v311
method: GET
uri: http://localhost:3000/api/v2/notifications?limit=10&expand_accounts=partial_avatars
response:
body:
string: '{"accounts":[],"partial_accounts":[],"statuses":[],"notification_groups":[]}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '76'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"eda10f9499b0acd6b521397b2c54e608"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.04, sql.active_record;dur=1.65, cache_generate.active_support;dur=2.39,
cache_write.active_support;dur=0.11, instantiation.active_record;dur=0.25,
start_processing.action_controller;dur=0.00, render.active_model_serializers;dur=0.26,
process_action.action_controller;dur=22.95
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '300'
X-RateLimit-Remaining:
- '299'
X-RateLimit-Reset:
- '2025-02-15T18:40:00.414885Z'
X-Request-Id:
- a16d6183-ac0e-4bb2-bb6d-4415200bb284
X-Runtime:
- '0.037428'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- Bearer __MASTODON_PY_TEST_ACCESS_TOKEN
Connection:
- keep-alive
Content-Length:
- '0'
User-Agent:
- tests/v311
method: DELETE
uri: http://localhost:3000/api/v1/statuses/114009357138722264
response:
body:
string: '{"id":"114009357138722264","created_at":"2025-02-15T18:38:51.731Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114009357138722264","url":"http://localhost:3000/@mastodonpy_test/114009357138722264","replies_count":0,"reblogs_count":0,"favourites_count":2,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"text":"Testing
grouped notifications!","filtered":[],"reblog":null,"application":{"name":"Mastodon.py
test suite","website":null},"account":{"id":"114009019326978043","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-15T00:00:00.000Z","note":"","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}'
headers:
Cache-Control:
- private, no-store
Content-Length:
- '1475'
Content-Security-Policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
Content-Type:
- application/json; charset=utf-8
ETag:
- W/"61fa0386cc5fd7fcd2b5cbd617ebfb55"
Referrer-Policy:
- strict-origin-when-cross-origin
Server-Timing:
- cache_read.active_support;dur=0.05, sql.active_record;dur=6.22, cache_generate.active_support;dur=2.83,
cache_write.active_support;dur=0.17, instantiation.active_record;dur=0.75,
start_processing.action_controller;dur=0.00, transaction.active_record;dur=3.13,
render.active_model_serializers;dur=13.62, process_action.action_controller;dur=41.39
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-RateLimit-Limit:
- '30'
X-RateLimit-Remaining:
- '29'
X-RateLimit-Reset:
- '2025-02-15T19:00:00.460335Z'
X-Request-Id:
- d68ff7be-46c4-4ed4-8f9b-14792c89aa98
X-Runtime:
- '0.059427'
X-XSS-Protection:
- '0'
vary:
- Authorization, Origin
status:
code: 200
message: OK
version: 1

Wyświetl plik

@ -143,3 +143,70 @@ def test_notification_requests_accept(api, api2):
for status in posted:
api.status_delete(status)
api2.update_notifications_policy(for_not_following="accept", for_not_followers="accept", for_new_accounts="accept", for_limited_accounts="accept", for_private_mentions="accept")
@pytest.mark.vcr()
def test_grouped_notifications(api, api2, api3):
try:
status = api.status_post("Testing grouped notifications!", visibility="public")
api2.status_favourite(status["id"])
api3.status_favourite(status["id"])
time.sleep(2)
grouped_notifs = api.grouped_notifications(limit=10, expand_accounts="partial_avatars")
assert grouped_notifs
assert hasattr(grouped_notifs, "_pagination_next")
assert hasattr(grouped_notifs, "_pagination_prev")
group_keys = [group.group_key for group in grouped_notifs.notification_groups]
assert any("favourite" in key or "reblog" in key for key in group_keys), "Expected a grouped notification"
group_key = group_keys[0] # Take first group
single_grouped_notif = api.grouped_notification(group_key)
assert single_grouped_notif
assert single_grouped_notif.notification_groups[0].group_key == group_key
accounts = api.grouped_notification_accounts(group_key)
assert isinstance(accounts, list)
assert len(accounts) > 0
partial_accounts = [acc for acc in accounts if hasattr(acc, 'avatar_static')]
assert len(partial_accounts) > 0, "Expected at least one partial account"
api.dismiss_grouped_notification(group_key)
updated_grouped_notifs = api.grouped_notifications(limit=10)
updated_group_keys = [group.group_key for group in updated_grouped_notifs.notification_groups]
assert group_key not in updated_group_keys, "Dismissed notification still appears"
finally:
api.status_delete(status["id"])
@pytest.mark.vcr()
def test_grouped_notification_pagination(api, api2):
try:
# Post 10 statuses that mention api
posted = []
api_name = api.account_verify_credentials().username
for i in range(10):
posted.append(api2.status_post(f"@{api_name} hey how you doing - {i}!", visibility="public"))
time.sleep(5)
grouped_notifs = api.grouped_notifications(limit=5, expand_accounts="full")
assert len(grouped_notifs.notification_groups) == 5
assert grouped_notifs._pagination_next
assert grouped_notifs._pagination_prev
# Fetch next page
next_notifs = api.fetch_next(grouped_notifs)
assert len(next_notifs.notification_groups) == 5
assert next_notifs._pagination_next
assert next_notifs._pagination_prev
# Fetch previous page
prev_notifs = api.fetch_previous(next_notifs)
assert len(prev_notifs.notification_groups) == 5
assert prev_notifs._pagination_next
assert prev_notifs._pagination_prev
finally:
for status in posted:
api2.status_delete(status["id"])