1
0
mirror of https://github.com/ihabunek/toot synced 2024-12-22 07:01:46 +01:00

Add post --reply-last option

fixes #464
This commit is contained in:
Ivan Habunek 2024-09-22 22:13:57 +02:00
parent 3ff9bc7942
commit 76bb1b5484
No known key found for this signature in database
GPG Key ID: 01DB3DD0D824504C
3 changed files with 124 additions and 2 deletions

View File

@ -7,6 +7,7 @@ from os import path
from tests.integration.conftest import ASSETS_DIR, Run, assert_ok, posted_status_id
from toot import CLIENT_NAME, CLIENT_WEBSITE, api, cli
from toot.cache import clear_last_post_id, get_last_post_id
from toot.utils import get_text
from unittest import mock
@ -340,7 +341,7 @@ def test_media_attachment_without_text(mock_read, mock_ml, app, user, run):
assert attachment["meta"]["original"]["size"] == "50x50"
def test_reply_thread(app, user, friend, run):
def test_reply_to(app, user, friend, run):
status = api.post_status(app, friend, "This is the status").json()
result = run(cli.post.post, "--reply-to", status["id"], "This is the reply")
@ -362,3 +363,38 @@ def test_reply_thread(app, user, friend, run):
assert user.username in s2
assert status["id"] in s1
assert reply["id"] in s2
def test_reply_last(app, user, run):
result_1 = run(cli.post.post, "one")
status_id_1 = posted_status_id(result_1.stdout)
assert get_last_post_id(app, user) == status_id_1
result_2 = run(cli.post.post, "two", "--reply-last")
status_id_2 = posted_status_id(result_2.stdout)
assert get_last_post_id(app, user) == status_id_2
result_3 = run(cli.post.post, "two", "--reply-last")
status_id_3 = posted_status_id(result_3.stdout)
assert get_last_post_id(app, user) == status_id_3
status_1 = api.fetch_status(app, user, status_id_1).json()
status_2 = api.fetch_status(app, user, status_id_2).json()
status_3 = api.fetch_status(app, user, status_id_3).json()
assert status_1["in_reply_to_id"] is None
assert status_2["in_reply_to_id"] == status_id_1
assert status_3["in_reply_to_id"] == status_id_2
def test_reply_last_fails_if_no_last_id(app, user, run: Run):
clear_last_post_id(app, user)
result = run(cli.post.post, "one", "--reply-last")
assert result.exit_code == 1
assert result.stderr.strip() == f"Error: Cannot reply-last, no previous post ID found for {user.username}@{app.instance}"
def test_reply_last_and_reply_to_are_exclusive(app, user, run: Run):
result = run(cli.post.post, "one", "--reply-last", "--reply-to", "123")
assert result.exit_code == 1
assert result.stderr.strip() == f"Error: --reply-last and --reply-to are mutually exclusive"

61
toot/cache.py Normal file
View File

@ -0,0 +1,61 @@
import os
import sys
from pathlib import Path
from typing import Optional
from toot import App, User
CACHE_SUBFOLDER = "toot"
def save_last_post_id(app: App, user: User, id: str) -> None:
"""Save ID of the last post posted to this instance"""
path = _last_post_id_path(app, user)
with open(path, "w") as f:
f.write(id)
def get_last_post_id(app: App, user: User) -> Optional[str]:
"""Retrieve ID of the last post posted to this instance"""
path = _last_post_id_path(app, user)
if path.exists():
with open(path, "r") as f:
return f.read()
def clear_last_post_id(app: App, user: User) -> None:
"""Delete the cached last post ID for this instance"""
path = _last_post_id_path(app, user)
path.unlink(missing_ok=True)
def _last_post_id_path(app: App, user: User):
return get_cache_dir("last_post_ids") / f"{user.username}_{app.instance}"
def get_cache_dir(subdir: Optional[str] = None) -> Path:
path = _cache_dir_path()
if subdir:
path = path / subdir
path.mkdir(parents=True, exist_ok=True)
return path
def _cache_dir_path() -> Path:
"""Returns the path to the cache directory"""
# Windows
if sys.platform == "win32" and "LOCALAPPDATA" in os.environ:
return Path(os.environ["LOCALAPPDATA"], CACHE_SUBFOLDER)
# Mac OS
if sys.platform == "darwin":
return Path.home() / "Library" / "Caches" / CACHE_SUBFOLDER
# Respect XDG_CONFIG_HOME env variable if set
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if "XDG_CACHE_HOME" in os.environ:
return Path(os.environ["XDG_CACHE_HOME"], CACHE_SUBFOLDER)
return Path.home() / ".cache" / CACHE_SUBFOLDER

View File

@ -7,6 +7,7 @@ from time import sleep, time
from typing import BinaryIO, Optional, Tuple
from toot import api, config
from toot.cache import get_last_post_id, save_last_post_id
from toot.cli import AccountParamType, cli, json_option, pass_context, Context
from toot.cli import DURATION_EXAMPLES, VISIBILITY_CHOICES
from toot.tui.constants import VISIBILITY_OPTIONS # move to top-level ?
@ -60,6 +61,12 @@ from toot.utils.datetime import parse_datetime
"--reply-to", "-r",
help="ID of the status being replied to, if status is a reply.",
)
@click.option(
"--reply-last", "-R",
help="Reply to the last posted status to continue the thread.",
is_flag=True,
default=False,
)
@click.option(
"--language", "-l",
help="ISO 639-1 language code of the toot, to skip automatic detection.",
@ -137,7 +144,8 @@ def post(
poll_multiple: bool,
poll_hide_totals: bool,
json: bool,
using: str
using: str,
reply_last: bool,
):
"""Post a new status"""
if len(media) > 4:
@ -153,6 +161,7 @@ def post(
media_ids = _upload_media(app, user, media, descriptions, thumbnails)
status_text = _get_status_text(text, editor, media)
scheduled_at = _get_scheduled_at(scheduled_at, scheduled_in)
reply_to = _get_reply_to(app, user, reply_to, reply_last)
if not status_text and not media_ids:
raise click.ClickException("You must specify either text or media to post.")
@ -186,6 +195,8 @@ def post(
else:
click.echo(f"Toot posted: {status['url']}")
save_last_post_id(app, user, status["id"])
delete_tmp_status_file()
@ -245,6 +256,20 @@ def _get_scheduled_at(scheduled_at, scheduled_in):
return None
def _get_reply_to(app, user, reply_to, reply_last):
if reply_last and reply_to:
raise click.ClickException("--reply-last and --reply-to are mutually exclusive")
if reply_last:
last_id = get_last_post_id(app, user)
if last_id:
return last_id
else:
raise click.ClickException(f"Cannot reply-last, no previous post ID found for {user.username}@{app.instance}")
return reply_to
def _upload_media(app, user, media, descriptions, thumbnails):
# Match media to corresponding descriptions and thumbnail
media = media or []