diff options
author | Lorenz Diener <[email protected]> | 2019-10-12 22:55:17 +0200 |
---|---|---|
committer | Lorenz Diener <[email protected]> | 2019-10-12 22:55:17 +0200 |
commit | 45908b6f4e9c66fe102b30764e6d153a8d0339a5 (patch) | |
tree | b8e1caaa65c0b43a181cbd672199c7e8b919616c | |
parent | 1f36deb11950a627b52a587fc3ec30c37f9bb456 (diff) | |
download | mastodon.py-45908b6f4e9c66fe102b30764e6d153a8d0339a5.tar.gz |
Add, test and document last-read markers. Fixes #192
-rw-r--r-- | docs/index.rst | 35 | ||||
-rw-r--r-- | mastodon/Mastodon.py | 50 | ||||
-rw-r--r-- | tests/cassettes/test_markers.yaml | 117 | ||||
-rw-r--r-- | tests/test_markers.py | 17 |
4 files changed, 216 insertions, 3 deletions
diff --git a/docs/index.rst b/docs/index.rst index ef94b2a..1e64927 100644 --- a/docs/index.rst +++ b/docs/index.rst | |||
@@ -773,6 +773,20 @@ Featured tag dicts | |||
773 | # (can be None if there are none) | 773 | # (can be None if there are none) |
774 | } | 774 | } |
775 | 775 | ||
776 | Read marker dicts | ||
777 | ~~~~~~~~~~~~~~~~~ | ||
778 | .. _read marker dict: | ||
779 | |||
780 | .. code-block:: python | ||
781 | |||
782 | mastodon.markers_get()["home"] | ||
783 | # Returns the following dictionary: | ||
784 | { | ||
785 | 'last_read_id': # ID of the last read object in the timeline | ||
786 | 'version': # A counter that is incremented whenever the marker is set to a new status | ||
787 | 'updated_at': # The time the marker was last set, as a datetime object | ||
788 | } | ||
789 | |||
776 | Admin account dicts | 790 | Admin account dicts |
777 | ~~~~~~~~~~~~~~~~~~~ | 791 | ~~~~~~~~~~~~~~~~~~~ |
778 | .. _admin account dict: | 792 | .. _admin account dict: |
@@ -990,11 +1004,20 @@ muted or blocked by the logged in user. | |||
990 | .. automethod:: Mastodon.mutes | 1004 | .. automethod:: Mastodon.mutes |
991 | .. automethod:: Mastodon.blocks | 1005 | .. automethod:: Mastodon.blocks |
992 | 1006 | ||
993 | Reading data: Reports (REMOVED IN 2.5.0) | 1007 | Reading data: Reports |
994 | ---------------------------------------- | 1008 | --------------------- |
1009 | In Mastodon versions before 2.5.0 this function allowed for the retrieval | ||
1010 | of reports filed by the logged in user. It has since been removed. | ||
995 | 1011 | ||
996 | .. automethod:: Mastodon.reports | 1012 | .. automethod:: Mastodon.reports |
997 | 1013 | ||
1014 | |||
1015 | Writing data: Last-read markers | ||
1016 | -------------------------- | ||
1017 | This function allows you to set get last read position for timelines. | ||
1018 | |||
1019 | .. automethod:: Mastodon.markers_get | ||
1020 | |||
998 | Reading data: Domain blocks | 1021 | Reading data: Domain blocks |
999 | --------------------------- | 1022 | --------------------------- |
1000 | 1023 | ||
@@ -1142,6 +1165,14 @@ Writing data: Reports | |||
1142 | 1165 | ||
1143 | .. automethod:: Mastodon.report | 1166 | .. automethod:: Mastodon.report |
1144 | 1167 | ||
1168 | Writing data: Last-read markers | ||
1169 | -------------------------- | ||
1170 | This function allows you to set the last read position for timelines to | ||
1171 | allow for persisting where the user was reading a timeline between sessions | ||
1172 | and clients / devices. | ||
1173 | |||
1174 | .. automethod:: Mastodon.markers_set | ||
1175 | |||
1145 | Writing data: Domain blocks | 1176 | Writing data: Domain blocks |
1146 | --------------------------- | 1177 | --------------------------- |
1147 | These functions allow you to block and unblock all statuses from a domain | 1178 | These functions allow you to block and unblock all statuses from a domain |
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index d07bae7..b8ad976 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -213,6 +213,7 @@ class Mastodon: | |||
213 | __DICT_VERSION_PREFERENCES = "2.8.0" | 213 | __DICT_VERSION_PREFERENCES = "2.8.0" |
214 | __DICT_VERSION_ADMIN_ACCOUNT = "2.9.1" | 214 | __DICT_VERSION_ADMIN_ACCOUNT = "2.9.1" |
215 | __DICT_VERSION_FEATURED_TAG = "3.0.0" | 215 | __DICT_VERSION_FEATURED_TAG = "3.0.0" |
216 | __DICT_VERSION_MARKER = "3.0.0" | ||
216 | 217 | ||
217 | ### | 218 | ### |
218 | # Registering apps | 219 | # Registering apps |
@@ -1590,6 +1591,25 @@ class Mastodon: | |||
1590 | """ | 1591 | """ |
1591 | return self.__api_request('GET', '/api/v1/preferences') | 1592 | return self.__api_request('GET', '/api/v1/preferences') |
1592 | 1593 | ||
1594 | ## | ||
1595 | # Reading data: Read markers | ||
1596 | ## | ||
1597 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_MARKER) | ||
1598 | def markers_get(self, timeline=["home"]): | ||
1599 | """ | ||
1600 | Get the last-read-location markers for the specified timelines. Valid timelines | ||
1601 | are the same as in `timeline()`_ | ||
1602 | |||
1603 | Note that despite the singular name, `timeline` can be a list. | ||
1604 | |||
1605 | Returns a dict of `read marker dicts`_, keyed by timeline name. | ||
1606 | """ | ||
1607 | if not isinstance(timeline, (list, tuple)): | ||
1608 | timeline = [timeline] | ||
1609 | params = self.__generate_params(locals()) | ||
1610 | |||
1611 | return self.__api_request('GET', '/api/v1/markers', params) | ||
1612 | |||
1593 | ### | 1613 | ### |
1594 | # Writing data: Statuses | 1614 | # Writing data: Statuses |
1595 | ### | 1615 | ### |
@@ -2450,6 +2470,34 @@ class Mastodon: | |||
2450 | params = self.__generate_params(locals()) | 2470 | params = self.__generate_params(locals()) |
2451 | self.__api_request('DELETE', '/api/v1/domain_blocks', params) | 2471 | self.__api_request('DELETE', '/api/v1/domain_blocks', params) |
2452 | 2472 | ||
2473 | ## | ||
2474 | # Writing data: Read markers | ||
2475 | ## | ||
2476 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_MARKER) | ||
2477 | def markers_set(self, timelines, last_read_ids): | ||
2478 | """ | ||
2479 | Set the "last read" marker(s) for the given timeline(s) to the given id(s) | ||
2480 | |||
2481 | Note that if you give an invalid timeline name, this will silently do nothing. | ||
2482 | |||
2483 | Returns a dict with the updated `read marker dicts`_, keyed by timeline name. | ||
2484 | """ | ||
2485 | if not isinstance(timelines, (list, tuple)): | ||
2486 | timelines = [timelines] | ||
2487 | |||
2488 | if not isinstance(last_read_ids, (list, tuple)): | ||
2489 | last_read_ids = [last_read_ids] | ||
2490 | |||
2491 | if len(last_read_ids) != len(timelines): | ||
2492 | raise MastodonIllegalArgumentError("Number of specified timelines and ids must be the same") | ||
2493 | |||
2494 | params = collections.OrderedDict() | ||
2495 | for timeline, last_read_id in zip(timelines, last_read_ids): | ||
2496 | params[timeline] = collections.OrderedDict() | ||
2497 | params[timeline]["last_read_id"] = self.__unpack_id(last_read_id) | ||
2498 | |||
2499 | return self.__api_request('POST', '/api/v1/markers', params, use_json=True) | ||
2500 | |||
2453 | ### | 2501 | ### |
2454 | # Writing data: Push subscriptions | 2502 | # Writing data: Push subscriptions |
2455 | ### | 2503 | ### |
@@ -3071,7 +3119,7 @@ class Mastodon: | |||
3071 | """ | 3119 | """ |
3072 | Converts json string numerals to native python bignums. | 3120 | Converts json string numerals to native python bignums. |
3073 | """ | 3121 | """ |
3074 | for key in ('id', 'week', 'in_reply_to_id', 'in_reply_to_account_id', 'logins', 'registrations', 'statuses', 'day'): | 3122 | for key in ('id', 'week', 'in_reply_to_id', 'in_reply_to_account_id', 'logins', 'registrations', 'statuses', 'day', 'last_read_id'): |
3075 | if (key in json_object and isinstance(json_object[key], six.text_type)): | 3123 | if (key in json_object and isinstance(json_object[key], six.text_type)): |
3076 | try: | 3124 | try: |
3077 | json_object[key] = int(json_object[key]) | 3125 | json_object[key] = int(json_object[key]) |
diff --git a/tests/cassettes/test_markers.yaml b/tests/cassettes/test_markers.yaml new file mode 100644 index 0000000..eceda6d --- /dev/null +++ b/tests/cassettes/test_markers.yaml | |||
@@ -0,0 +1,117 @@ | |||
1 | interactions: | ||
2 | - request: | ||
3 | body: status=Toot%21 | ||
4 | headers: | ||
5 | Accept: ['*/*'] | ||
6 | Accept-Encoding: ['gzip, deflate'] | ||
7 | Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] | ||
8 | Connection: [keep-alive] | ||
9 | Content-Length: ['14'] | ||
10 | Content-Type: [application/x-www-form-urlencoded] | ||
11 | User-Agent: [python-requests/2.18.4] | ||
12 | method: POST | ||
13 | uri: http://localhost:3000/api/v1/statuses | ||
14 | response: | ||
15 | body: {string: '{"id":"102951400589982753","created_at":"2019-10-12T20:55:05.296Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost/users/mastodonpy_test/statuses/102951400589982753","url":"http://localhost/@mastodonpy_test/102951400589982753","replies_count":0,"reblogs_count":0,"favourites_count":0,"favourited":false,"reblogged":false,"muted":false,"pinned":false,"content":"\u003cp\u003eToot!\u003c/p\u003e","reblog":null,"application":{"name":"Mastodon.py | ||
16 | test suite","website":null},"account":{"id":"1234567890123456","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":false,"bot":false,"created_at":"2019-06-22T23:11:52.441Z","note":"\u003cp\u003e\u003c/p\u003e","url":"http://localhost/@mastodonpy_test","avatar":"http://localhost/avatars/original/missing.png","avatar_static":"http://localhost/avatars/original/missing.png","header":"http://localhost/headers/original/missing.png","header_static":"http://localhost/headers/original/missing.png","followers_count":0,"following_count":1,"statuses_count":3,"last_status_at":"2019-10-12T20:55:05.311Z","emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}'} | ||
17 | headers: | ||
18 | Cache-Control: ['no-cache, no-store'] | ||
19 | Content-Type: [application/json; charset=utf-8] | ||
20 | Referrer-Policy: [strict-origin-when-cross-origin] | ||
21 | Transfer-Encoding: [chunked] | ||
22 | Vary: ['Accept-Encoding, Origin'] | ||
23 | X-Content-Type-Options: [nosniff] | ||
24 | X-Download-Options: [noopen] | ||
25 | X-Frame-Options: [SAMEORIGIN] | ||
26 | X-Permitted-Cross-Domain-Policies: [none] | ||
27 | X-Request-Id: [d6e097f3-e4e8-4d83-8c30-c4fe5ed88a87] | ||
28 | X-Runtime: ['0.151706'] | ||
29 | X-XSS-Protection: [1; mode=block] | ||
30 | content-length: ['1280'] | ||
31 | status: {code: 200, message: OK} | ||
32 | - request: | ||
33 | body: '{"home": {"last_read_id": 102951400589982753}}' | ||
34 | headers: | ||
35 | Accept: ['*/*'] | ||
36 | Accept-Encoding: ['gzip, deflate'] | ||
37 | Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] | ||
38 | Connection: [keep-alive] | ||
39 | Content-Length: ['46'] | ||
40 | Content-Type: [application/json] | ||
41 | User-Agent: [python-requests/2.18.4] | ||
42 | method: POST | ||
43 | uri: http://localhost:3000/api/v1/markers | ||
44 | response: | ||
45 | body: {string: '{"home":{"last_read_id":"102951400589982753","version":2,"updated_at":"2019-10-12T20:55:05.457Z"}}'} | ||
46 | headers: | ||
47 | Cache-Control: ['no-cache, no-store'] | ||
48 | Content-Type: [application/json; charset=utf-8] | ||
49 | Referrer-Policy: [strict-origin-when-cross-origin] | ||
50 | Transfer-Encoding: [chunked] | ||
51 | Vary: ['Accept-Encoding, Origin'] | ||
52 | X-Content-Type-Options: [nosniff] | ||
53 | X-Download-Options: [noopen] | ||
54 | X-Frame-Options: [SAMEORIGIN] | ||
55 | X-Permitted-Cross-Domain-Policies: [none] | ||
56 | X-Request-Id: [2e3c3ede-edf6-43c9-9477-fddca87ffed3] | ||
57 | X-Runtime: ['0.036295'] | ||
58 | X-XSS-Protection: [1; mode=block] | ||
59 | content-length: ['98'] | ||
60 | status: {code: 200, message: OK} | ||
61 | - request: | ||
62 | body: null | ||
63 | headers: | ||
64 | Accept: ['*/*'] | ||
65 | Accept-Encoding: ['gzip, deflate'] | ||
66 | Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] | ||
67 | Connection: [keep-alive] | ||
68 | User-Agent: [python-requests/2.18.4] | ||
69 | method: GET | ||
70 | uri: http://localhost:3000/api/v1/markers?timeline%5B%5D=home | ||
71 | response: | ||
72 | body: {string: '{"home":{"last_read_id":"102951400589982753","version":2,"updated_at":"2019-10-12T20:55:05.457Z"}}'} | ||
73 | headers: | ||
74 | Cache-Control: ['no-cache, no-store'] | ||
75 | Content-Type: [application/json; charset=utf-8] | ||
76 | Referrer-Policy: [strict-origin-when-cross-origin] | ||
77 | Transfer-Encoding: [chunked] | ||
78 | Vary: ['Accept-Encoding, Origin'] | ||
79 | X-Content-Type-Options: [nosniff] | ||
80 | X-Download-Options: [noopen] | ||
81 | X-Frame-Options: [SAMEORIGIN] | ||
82 | X-Permitted-Cross-Domain-Policies: [none] | ||
83 | X-Request-Id: [2d3ddb8a-b93c-48e2-bdda-c4e0edf29adf] | ||
84 | X-Runtime: ['0.022064'] | ||
85 | X-XSS-Protection: [1; mode=block] | ||
86 | content-length: ['98'] | ||
87 | status: {code: 200, message: OK} | ||
88 | - request: | ||
89 | body: null | ||
90 | headers: | ||
91 | Accept: ['*/*'] | ||
92 | Accept-Encoding: ['gzip, deflate'] | ||
93 | Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] | ||
94 | Connection: [keep-alive] | ||
95 | Content-Length: ['0'] | ||
96 | User-Agent: [python-requests/2.18.4] | ||
97 | method: DELETE | ||
98 | uri: http://localhost:3000/api/v1/statuses/102951400589982753 | ||
99 | response: | ||
100 | body: {string: '{"id":"102951400589982753","created_at":"2019-10-12T20:55:05.296Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost/users/mastodonpy_test/statuses/102951400589982753","url":"http://localhost/@mastodonpy_test/102951400589982753","replies_count":0,"reblogs_count":0,"favourites_count":0,"favourited":false,"reblogged":false,"muted":false,"pinned":false,"text":"Toot!","reblog":null,"application":{"name":"Mastodon.py | ||
101 | test suite","website":null},"account":{"id":"1234567890123456","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":false,"bot":false,"created_at":"2019-06-22T23:11:52.441Z","note":"\u003cp\u003e\u003c/p\u003e","url":"http://localhost/@mastodonpy_test","avatar":"http://localhost/avatars/original/missing.png","avatar_static":"http://localhost/avatars/original/missing.png","header":"http://localhost/headers/original/missing.png","header_static":"http://localhost/headers/original/missing.png","followers_count":0,"following_count":1,"statuses_count":3,"last_status_at":"2019-10-12T20:55:05.311Z","emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}'} | ||
102 | headers: | ||
103 | Cache-Control: ['no-cache, no-store'] | ||
104 | Content-Type: [application/json; charset=utf-8] | ||
105 | Referrer-Policy: [strict-origin-when-cross-origin] | ||
106 | Transfer-Encoding: [chunked] | ||
107 | Vary: ['Accept-Encoding, Origin'] | ||
108 | X-Content-Type-Options: [nosniff] | ||
109 | X-Download-Options: [noopen] | ||
110 | X-Frame-Options: [SAMEORIGIN] | ||
111 | X-Permitted-Cross-Domain-Policies: [none] | ||
112 | X-Request-Id: [526d76d7-5428-45d2-9a84-4f520b09524c] | ||
113 | X-Runtime: ['0.121781'] | ||
114 | X-XSS-Protection: [1; mode=block] | ||
115 | content-length: ['1250'] | ||
116 | status: {code: 200, message: OK} | ||
117 | version: 1 | ||
diff --git a/tests/test_markers.py b/tests/test_markers.py new file mode 100644 index 0000000..a35c56c --- /dev/null +++ b/tests/test_markers.py | |||
@@ -0,0 +1,17 @@ | |||
1 | import pytest | ||
2 | |||
3 | @pytest.mark.vcr() | ||
4 | def test_markers(api, status): | ||
5 | marker_a = api.markers_set("home", status) | ||
6 | assert marker_a | ||
7 | assert marker_a["home"] | ||
8 | |||
9 | marker_b = api.markers_get("home") | ||
10 | assert marker_b | ||
11 | assert marker_b["home"] | ||
12 | |||
13 | assert marker_a.home.version == marker_b.home.version | ||
14 | assert marker_a.home.last_read_id == status.id | ||
15 | assert marker_b.home.last_read_id == status.id | ||
16 | |||
17 | |||