diff --git a/tests/test_integration.py b/tests/test_integration.py index 1939fee..aa7b89a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -137,6 +137,7 @@ def test_post(app, user, run): assert status["visibility"] == "public" assert status["sensitive"] is False assert status["spoiler_text"] == "" + assert status["poll"] is None # Pleroma doesn't return the application if status["application"]: @@ -196,6 +197,92 @@ def test_post_scheduled_in(app, user, run): assert delta.total_seconds() < 5 +def test_post_poll(app, user, run): + text = str(uuid.uuid4()) + + out = run( + "post", text, + "--poll-option", "foo", + "--poll-option", "bar", + "--poll-option", "baz", + "--poll-option", "qux", + ) + + status_id = _posted_status_id(out) + + status = api.fetch_status(app, user, status_id) + assert status["poll"]["expired"] is False + assert status["poll"]["multiple"] is False + assert status["poll"]["options"] == [ + {"title": "foo", "votes_count": 0}, + {"title": "bar", "votes_count": 0}, + {"title": "baz", "votes_count": 0}, + {"title": "qux", "votes_count": 0} + ] + + # Test expires_at is 24h by default + actual = datetime.strptime(status["poll"]["expires_at"], "%Y-%m-%dT%H:%M:%S.%f%z") + expected = datetime.now(timezone.utc) + timedelta(days=1) + delta = actual - expected + assert delta.total_seconds() < 5 + + +def test_post_poll_multiple(app, user, run): + text = str(uuid.uuid4()) + + out = run( + "post", text, + "--poll-option", "foo", + "--poll-option", "bar", + "--poll-multiple" + ) + + status_id = _posted_status_id(out) + + status = api.fetch_status(app, user, status_id) + assert status["poll"]["multiple"] is True + + +def test_post_poll_expires_in(app, user, run): + text = str(uuid.uuid4()) + + out = run( + "post", text, + "--poll-option", "foo", + "--poll-option", "bar", + "--poll-expires-in", "8h", + ) + + status_id = _posted_status_id(out) + + status = api.fetch_status(app, user, status_id) + actual = datetime.strptime(status["poll"]["expires_at"], "%Y-%m-%dT%H:%M:%S.%f%z") + expected = datetime.now(timezone.utc) + timedelta(hours=8) + delta = actual - expected + assert delta.total_seconds() < 5 + + +def test_post_poll_hide_totals(app, user, run): + text = str(uuid.uuid4()) + + out = run( + "post", text, + "--poll-option", "foo", + "--poll-option", "bar", + "--poll-hide-totals" + ) + + status_id = _posted_status_id(out) + + status = api.fetch_status(app, user, status_id) + + # votes_count is None when totals are hidden + assert status["poll"]["options"] == [ + {"title": "foo", "votes_count": None}, + {"title": "bar", "votes_count": None}, + ] + + def test_post_language(app, user, run): out = run("post", "test", "--language", "hr") status_id = _posted_status_id(out) @@ -232,7 +319,7 @@ def test_media_thumbnail(app, user, run): assert media["preview_url"].endswith(".png") # Video properties - assert media["meta"]["original"]["duration"] == 5.58 + assert int(media["meta"]["original"]["duration"]) == 5 assert media["meta"]["original"]["height"] == 320 assert media["meta"]["original"]["width"] == 560 diff --git a/toot/api.py b/toot/api.py index 672849b..f1beece 100644 --- a/toot/api.py +++ b/toot/api.py @@ -172,6 +172,10 @@ def post_status( language=None, scheduled_at=None, content_type=None, + poll_options=None, + poll_expires_in=None, + poll_multiple=None, + poll_hide_totals=None, ): """ Publish a new status. @@ -184,7 +188,7 @@ def post_status( # Strip keys for which value is None # Sending null values doesn't bother Mastodon, but it breaks Pleroma - json = drop_empty_values({ + data = drop_empty_values({ 'status': status, 'media_ids': media_ids, 'visibility': visibility, @@ -193,10 +197,18 @@ def post_status( 'language': language, 'scheduled_at': scheduled_at, 'content_type': content_type, - 'spoiler_text': spoiler_text + 'spoiler_text': spoiler_text, }) - return http.post(app, user, '/api/v1/statuses', json=json, headers=headers).json() + if poll_options: + data["poll"] = { + "options": poll_options, + "expires_in": poll_expires_in, + "multiple": poll_multiple, + "hide_totals": poll_hide_totals, + } + + return http.post(app, user, '/api/v1/statuses', json=data, headers=headers).json() def fetch_status(app, user, id): diff --git a/toot/commands.py b/toot/commands.py index 18e7be6..f86e2f3 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -87,7 +87,7 @@ def post(app, user, args): raise ConsoleError("Cannot attach more than 4 files.") media_ids = _upload_media(app, user, args) - status_text = _get_status_text(args.text, args.editor) + status_text = _get_status_text(args.text, args.editor, args.media) scheduled_at = _get_scheduled_at(args.scheduled_at, args.scheduled_in) if not status_text and not media_ids: @@ -102,7 +102,11 @@ def post(app, user, args): in_reply_to_id=args.reply_to, language=args.language, scheduled_at=scheduled_at, - content_type=args.content_type + content_type=args.content_type, + poll_options=args.poll_option, + poll_expires_in=args.poll_expires_in, + poll_multiple=args.poll_multiple, + poll_hide_totals=args.poll_hide_totals, ) if "scheduled_at" in response: @@ -115,7 +119,7 @@ def post(app, user, args): delete_tmp_status_file() -def _get_status_text(text, editor): +def _get_status_text(text, editor, media): isatty = sys.stdin.isatty() if not text and not isatty: @@ -124,7 +128,7 @@ def _get_status_text(text, editor): if isatty: if editor: text = editor_input(editor, text) - elif not text: + elif not text and not media: print_out("Write or paste your toot. Press {} to post it.".format(EOF_KEY)) text = multiline_input() diff --git a/toot/console.py b/toot/console.py index ae98c6c..0a3acb5 100644 --- a/toot/console.py +++ b/toot/console.py @@ -105,6 +105,10 @@ DURATION_UNITS = { } +DURATION_EXAMPLES = """e.g. "1 day", "2 hours 30 minutes", "5 minutes 30 +seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\"""" + + def duration(value: str): match = re.match(r"""^ (([0-9]+)\s*(days|day|d))?\s* @@ -520,16 +524,35 @@ POST_COMMANDS = [ }), (["--scheduled-in"], { "type": duration, - "help": """Schedule the toot to be posted after a given amount - of time. Examples: "1 day", "2 hours 30 minutes", - "5 minutes 30 seconds" or any combination of above. - Shorthand: "1d", "2h30m", "5m30s". Must be at least 5 + "help": f"""Schedule the toot to be posted after a given amount + of time, {DURATION_EXAMPLES}. Must be at least 5 minutes.""", }), (["-t", "--content-type"], { "type": str, "help": "MIME type for the status text (not supported on all instances)", }), + (["--poll-option"], { + "action": "append", + "type": str, + "help": "Possible answer to the poll" + }), + (["--poll-expires-in"], { + "type": duration, + "help": f"""Duration that the poll should be open, + {DURATION_EXAMPLES}. Defaults to 24h.""", + "default": 24 * 60 * 60, + }), + (["--poll-multiple"], { + "action": "store_true", + "default": False, + "help": "Allow multiple answers to be selected." + }), + (["--poll-hide-totals"], { + "action": "store_true", + "default": False, + "help": "Hide vote counts until the poll ends. Defaults to false." + }), ], require_auth=True, ),