diff --git a/TODO.md b/TODO.md index ce01ec1..7f27e51 100644 --- a/TODO.md +++ b/TODO.md @@ -65,3 +65,4 @@ General improvements that would be good to do before doing another release: * [ ] Split mastodon.py into parts in some way that makes sense, it's getting very unwieldy * [x] Fix the CI * [ ] Get test coverage like, real high +* [ ] Add all those streaming events?? diff --git a/docs/conf.py b/docs/conf.py index 0ab6777..c6fc0fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,7 +68,7 @@ author = u'Lorenz Diener' # The short X.Y version. version = u'1.6' # The full version, including alpha/beta/rc tags. -release = u'1.6.1' +release = u'1.6.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 7fc59d2..7c4f200 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -135,7 +135,7 @@ since the IDs used are Snowflake IDs and dates can be approximately converted to This is guaranteed to work on mainline Mastodon servers and very likely to work on all forks, but will **not** work on other servers implementing the API, like Pleroma, Misskey or Gotosocial. You should not use this if you want your application to be universally -compatible. +compatible. It's also relatively coarse-grained. `limit` allows you to specify how many results you would like returned. Note that an instance may choose to return less results than you requested - by default, Mastodon diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 493d485..f49df83 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py @@ -3934,6 +3934,8 @@ class Mastodon: Checks if id is a dict that contains id and returns the id inside, otherwise just returns the id straight. + + Also unpacks datetimes to snowflake IDs if requested. """ if isinstance(id, dict) and "id" in id: id = id["id"] diff --git a/setup.py b/setup.py index ff5fab7..98ed7de 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ extras = { } setup(name='Mastodon.py', - version='1.6.1', + version='1.6.2', description='Python wrapper for the Mastodon API', packages=['mastodon'], install_requires=[ diff --git a/tests/cassettes_special/test_scheduled_status_long_datetimeobjects.pkl b/tests/cassettes_special/test_scheduled_status_long_datetimeobjects.pkl new file mode 100644 index 0000000..0d2f0c1 Binary files /dev/null and b/tests/cassettes_special/test_scheduled_status_long_datetimeobjects.pkl differ diff --git a/tests/cassettes_special/test_scheduled_status_long_part1.yaml b/tests/cassettes_special/test_scheduled_status_long_part1.yaml new file mode 100644 index 0000000..0d83523 --- /dev/null +++ b/tests/cassettes_special/test_scheduled_status_long_part1.yaml @@ -0,0 +1,133 @@ +interactions: +- request: + body: status=please+ensure+maximum+headroom+at+2022-11-19+00%3A13%3A28.641336&scheduled_at=2022-11-18T22%3A13%3A28%2B00%3A00 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN + Connection: + - keep-alive + Content-Length: + - '118' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/statuses + response: + body: + string: '{"id":"5","scheduled_at":"2022-11-18T22:13:28.000Z","params":{"text":"please + ensure maximum headroom at 2022-11-19 00:13:28.641336","media_ids":null,"sensitive":null,"spoiler_text":null,"visibility":null,"language":null,"scheduled_at":null,"poll":null,"idempotency":null,"with_rate_limit":false,"in_reply_to_id":null,"application_id":1234567890123456},"media_attachments":[]}' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-oDT7L0vwLx8ThpkVmR2WkQ==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"961ec598899e68bf0f28ef2d0339120d" + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-RateLimit-Limit: + - '300' + X-RateLimit-Remaining: + - '300' + X-RateLimit-Reset: + - '2022-11-19T00:00:00.671086Z' + X-Request-Id: + - 3ccd3249-4aa7-4366-8722-caaa9c427e6f + X-Runtime: + - '0.025260' + X-XSS-Protection: + - 1; mode=block + 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/v1/scheduled_statuses + response: + body: + string: '[{"id":"5","scheduled_at":"2022-11-18T22:13:28.000Z","params":{"poll":null,"text":"please + ensure maximum headroom at 2022-11-19 00:13:28.641336","language":null,"media_ids":null,"sensitive":null,"visibility":null,"idempotency":null,"scheduled_at":null,"spoiler_text":null,"application_id":1234567890123456,"in_reply_to_id":null,"with_rate_limit":false},"media_attachments":[]},{"id":"4","scheduled_at":"2022-11-18T22:12:21.000Z","params":{"poll":null,"text":"please + ensure maximum headroom at 2022-11-19 00:12:21.805020","language":null,"media_ids":null,"sensitive":null,"visibility":null,"idempotency":null,"scheduled_at":null,"spoiler_text":null,"application_id":1234567890123456,"in_reply_to_id":null,"with_rate_limit":false},"media_attachments":[]}]' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-vlVvEXWNgaJdWW23jKfZqA==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"41f6a2d732beea8c5ef0286133506b91" + Link: + - ; rel="prev" + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 8194fc5d-2433-4359-8d25-3a0446703c61 + X-Runtime: + - '0.011068' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes_special/test_scheduled_status_long_part2.yaml b/tests/cassettes_special/test_scheduled_status_long_part2.yaml new file mode 100644 index 0000000..e14812d --- /dev/null +++ b/tests/cassettes_special/test_scheduled_status_long_part2.yaml @@ -0,0 +1,73 @@ +interactions: +- 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/v1/timelines/home + response: + body: + string: '[{"id":"109367106569496287","created_at":"2022-11-18T22:13:29.521Z","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/109367106569496287","url":"http://localhost:3000/@mastodonpy_test/109367106569496287","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eplease + ensure maximum headroom at 2022-11-19 00:13:28.641336\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"109366898092282937","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-18T00:00:00.000Z","note":"","url":"http://localhost:3000/@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":5,"last_status_at":"2022-11-18","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null},{"id":"109367102188706324","created_at":"2022-11-18T22:12:22.677Z","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/109367102188706324","url":"http://localhost:3000/@mastodonpy_test/109367102188706324","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eplease + ensure maximum headroom at 2022-11-19 00:12:21.805020\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"109366898092282937","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-18T00:00:00.000Z","note":"","url":"http://localhost:3000/@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":5,"last_status_at":"2022-11-18","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null},{"id":"109367045979515008","created_at":"2022-11-18T21:58:04.992Z","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/109367045979515008","url":"http://localhost:3000/@mastodonpy_test/109367045979515008","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eplease + ensure maximum headroom\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"109366898092282937","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-18T00:00:00.000Z","note":"","url":"http://localhost:3000/@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":5,"last_status_at":"2022-11-18","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null},{"id":"109367033669585768","created_at":"2022-11-18T21:54:57.158Z","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/109367033669585768","url":"http://localhost:3000/@mastodonpy_test/109367033669585768","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eplease + ensure maximum headroom\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"109366898092282937","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-18T00:00:00.000Z","note":"","url":"http://localhost:3000/@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":5,"last_status_at":"2022-11-18","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null},{"id":"109366923399768814","created_at":"2022-11-18T21:26:54.583Z","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/109366923399768814","url":"http://localhost:3000/@mastodonpy_test/109366923399768814","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eplease + ensure maximum headroom\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"109366898092282937","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-18T00:00:00.000Z","note":"","url":"http://localhost:3000/@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":5,"last_status_at":"2022-11-18","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}]' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-wSHlCFIT+OKGZueB4x1jVg==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"ea8a50776524b19f4a57fc911b5836e5" + Link: + - ; rel="next", + ; rel="prev" + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - a394cd1c-0cdc-47ea-9eb9-9a76402d8e2b + X-Runtime: + - '0.038512' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes_special/test_scheduled_status_long_text.pkl b/tests/cassettes_special/test_scheduled_status_long_text.pkl new file mode 100644 index 0000000..e455ae5 Binary files /dev/null and b/tests/cassettes_special/test_scheduled_status_long_text.pkl differ diff --git a/tests/test_status.py b/tests/test_status.py index 5c139e6..20e32f2 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -186,4 +186,30 @@ def test_scheduled_status(api): scheduled_toot_list_3 = api.scheduled_statuses() assert scheduled_toot_4.id in map(lambda x: x.id, statuses) assert not scheduled_toot_4.id in map(lambda x: x.id, scheduled_toot_list_3) - \ No newline at end of file + +# The following two tests need to be manually (!) ran 10 minutes apart when recording. +# Sorry, I can't think of a better way to test scheduled statuses actually work as intended. +@pytest.mark.vcr(match_on=['path']) +def test_scheduled_status_long_part1(api): + with vcr.use_cassette('test_scheduled_status_long_part1.yaml', cassette_library_dir='tests/cassettes_special', record_mode='once'): + if os.path.exists("tests/cassettes_special/test_scheduled_status_long_datetimeobjects.pkl"): + the_medium_term_future = datetime.datetime.fromtimestamp(pickle.load(open("tests/cassettes_special/test_scheduled_status_long_datetimeobjects.pkl", 'rb'))) + else: + the_medium_term_future = datetime.datetime.now() + datetime.timedelta(minutes=6) + pickle.dump(the_medium_term_future.timestamp(), open("tests/cassettes_special/test_scheduled_status_long_datetimeobjects.pkl", 'wb')) + scheduled_toot = api.status_post("please ensure maximum headroom at " + str(the_medium_term_future), scheduled_at=the_medium_term_future) + scheduled_toot_list = api.scheduled_statuses() + assert scheduled_toot.id in map(lambda x: x.id, scheduled_toot_list) + pickle.dump(scheduled_toot.params.text, open("tests/cassettes_special/test_scheduled_status_long_text.pkl", 'wb')) + +@pytest.mark.vcr(match_on=['path']) +def test_scheduled_status_long_part2(api): + with vcr.use_cassette('test_scheduled_status_long_part2.yaml', cassette_library_dir='tests/cassettes_special', record_mode='once'): + text = pickle.load(open("tests/cassettes_special/test_scheduled_status_long_text.pkl", 'rb')) + statuses = api.timeline_home() + print(text) + found_status = False + for status in statuses: + if text in status.content: + found_status = True + assert found_status