From 4f0c36799570e37756885f90f9f3576fe281cb39 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Sat, 3 Dec 2022 15:37:31 +0100 Subject: [PATCH] Add post --scheduled-in option for easier scheduling --- changelog.yaml | 5 +++++ tests/test_integration.py | 42 +++++++++++++++++++++++++++++++++++---- toot/commands.py | 22 +++++++++++++++++--- toot/console.py | 8 ++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/changelog.yaml b/changelog.yaml index dccea45..fcdf67a 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -1,3 +1,8 @@ +0.31.0: + date: "TBA" + changes: + - "Add `post --scheduled-in` option for easier scheduling" + 0.30.1: date: 2022-11-30 changes: diff --git a/tests/test_integration.py b/tests/test_integration.py index 895ba6f..e558ba8 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -25,6 +25,7 @@ from os import path from toot import CLIENT_NAME, CLIENT_WEBSITE, api, App, User from toot.console import run_command from toot.exceptions import ConsoleError, NotFoundError +from toot.tui.utils import parse_datetime from toot.utils import get_text from unittest import mock @@ -146,17 +147,50 @@ def test_post_visibility(app, user, run): assert status["visibility"] == visibility -def test_post_scheduled(app, user, run): +def test_post_scheduled_at(app, user, run): + text = str(uuid.uuid4()) scheduled_at = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(minutes=10) - out = run("post", "foo", "--scheduled-at", scheduled_at.isoformat()) + out = run("post", text, "--scheduled-at", scheduled_at.isoformat()) assert "Toot scheduled for" in out - [status] = api.scheduled_statuses(app, user) - assert status["params"]["text"] == "foo" + statuses = api.scheduled_statuses(app, user) + [status] = [s for s in statuses if s["params"]["text"] == text] assert datetime.strptime(status["scheduled_at"], "%Y-%m-%dT%H:%M:%S.%f%z") == scheduled_at +def test_post_scheduled_in(app, user, run): + text = str(uuid.uuid4()) + + variants = [ + ("1 day", timedelta(days=1)), + ("1 day 6 hours", timedelta(days=1, hours=6)), + ("1 day 6 hours 13 minutes", timedelta(days=1, hours=6, minutes=13)), + ("1 day 6 hours 13 minutes 51 second", timedelta(days=1, hours=6, minutes=13, seconds=51)), + ("2d", timedelta(days=2)), + ("2d6h", timedelta(days=2, hours=6)), + ("2d6h13m", timedelta(days=2, hours=6, minutes=13)), + ("2d6h13m51s", timedelta(days=2, hours=6, minutes=13, seconds=51)), + ] + + datetimes = [] + for scheduled_in, delta in variants: + out = run("post", text, "--scheduled-in", scheduled_in) + dttm = datetime.utcnow() + delta + assert out.startswith(f"Toot scheduled for: {str(dttm)[:16]}") + datetimes.append(dttm) + + scheduled = api.scheduled_statuses(app, user) + scheduled = [s for s in scheduled if s["params"]["text"] == text] + scheduled = sorted(scheduled, key=lambda s: s["scheduled_at"]) + assert len(scheduled) == 8 + + for expected, status in zip(datetimes, scheduled): + actual = datetime.strptime(status["scheduled_at"], "%Y-%m-%dT%H:%M:%S.%fZ") + delta = expected - actual + assert delta.total_seconds() < 5 + + def test_media_attachments(app, user, run): assets_dir = path.realpath(path.join(path.dirname(__file__), "assets")) diff --git a/toot/commands.py b/toot/commands.py index a486c39..d67f63a 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -2,11 +2,13 @@ import sys +from datetime import datetime, timedelta from toot import api, config from toot.auth import login_interactive, login_browser_interactive, create_app_interactive from toot.exceptions import ApiError, ConsoleError from toot.output import (print_out, print_instance, print_account, print_acct_list, print_search_results, print_timeline, print_notifications) +from toot.tui.utils import parse_datetime from toot.utils import editor_input, multiline_input, EOF_KEY @@ -84,6 +86,7 @@ def post(app, user, args): media_ids = _upload_media(app, user, args) status_text = _get_status_text(args.text, args.editor) + scheduled_at = _get_scheduled_at(args.scheduled_at, args.scheduled_in) if not status_text and not media_ids: raise ConsoleError("You must specify either text or media to post.") @@ -96,14 +99,16 @@ def post(app, user, args): spoiler_text=args.spoiler_text, in_reply_to_id=args.reply_to, language=args.language, - scheduled_at=args.scheduled_at, + scheduled_at=scheduled_at, content_type=args.content_type ) if "scheduled_at" in response: - print_out("Toot scheduled for: {}".format(response["scheduled_at"])) + scheduled_at = parse_datetime(response["scheduled_at"]) + scheduled_at = datetime.strftime(scheduled_at, "%Y-%m-%d %H:%M:%S%z") + print_out(f"Toot scheduled for: {scheduled_at}") else: - print_out("Toot posted: {}".format(response.get('url'))) + print_out(f"Toot posted: {response['url']}") def _get_status_text(text, editor): @@ -122,6 +127,17 @@ def _get_status_text(text, editor): return text +def _get_scheduled_at(scheduled_at, scheduled_in): + if scheduled_at: + return scheduled_at + + if scheduled_in: + scheduled_at = datetime.utcnow() + timedelta(seconds=scheduled_in) + return scheduled_at.isoformat() + + return None + + def _upload_media(app, user, args): # Match media to corresponding description and upload media = args.media or [] diff --git a/toot/console.py b/toot/console.py index 8e6a332..b9ff8c6 100644 --- a/toot/console.py +++ b/toot/console.py @@ -374,6 +374,14 @@ POST_COMMANDS = [ "help": "ISO 8601 Datetime at which to schedule a status. Must " "be at least 5 minutes in the future.", }), + (["--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 + minutes.""", + }), (["-t", "--content-type"], { "type": str, "help": "MIME type for the status text (not supported on all instances)",