Add thread building support.
This commit is contained in:
parent
7ca62bfbb3
commit
e6260faaeb
7 changed files with 145 additions and 27 deletions
|
|
@ -51,7 +51,8 @@ class AltTextCache:
|
|||
INSERT INTO alt_text(asset_id, alt_text, updated_at)
|
||||
VALUES(?, ?, ?)
|
||||
ON CONFLICT(asset_id)
|
||||
DO UPDATE SET alt_text = excluded.alt_text, updated_at = excluded.updated_at
|
||||
DO UPDATE SET alt_text = excluded.alt_text,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(asset_id, alt_text, timestamp),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Sequence
|
||||
from typing import Any, Dict, Optional, Sequence
|
||||
|
||||
import requests
|
||||
|
||||
|
|
@ -29,6 +29,7 @@ class MastodonClient:
|
|||
"User-Agent": "ImmichAltTextHelper/0.1 (+https://photos.4angle.com)",
|
||||
}
|
||||
)
|
||||
self._account_id: Optional[str] = None
|
||||
|
||||
def _raise_for_error(self, response: requests.Response) -> None:
|
||||
try:
|
||||
|
|
@ -38,10 +39,21 @@ class MastodonClient:
|
|||
message = f"{response.status_code} {response.reason}: {payload}"
|
||||
raise MastodonError(message)
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs: Any) -> Dict[str, Any]:
|
||||
url = f"{self.base_url}{path}"
|
||||
try:
|
||||
response = self.session.request(method, url, timeout=30, **kwargs)
|
||||
except requests.RequestException as exc: # pragma: no cover - network failure
|
||||
raise MastodonError(str(exc)) from exc
|
||||
if not response.ok:
|
||||
self._raise_for_error(response)
|
||||
if response.content:
|
||||
return response.json()
|
||||
return {}
|
||||
|
||||
def upload_media(
|
||||
self, filename: str, data: bytes, mime_type: str, alt_text: str
|
||||
) -> str:
|
||||
url = f"{self.base_url}/api/v2/media"
|
||||
files = {
|
||||
"file": (
|
||||
filename or "photo.jpg",
|
||||
|
|
@ -52,37 +64,54 @@ class MastodonClient:
|
|||
form = {}
|
||||
if alt_text:
|
||||
form["description"] = alt_text
|
||||
try:
|
||||
response = self.session.post(url, files=files, data=form, timeout=30)
|
||||
except requests.RequestException as exc: # pragma: no cover - network failure
|
||||
raise MastodonError(str(exc)) from exc
|
||||
if not response.ok:
|
||||
self._raise_for_error(response)
|
||||
payload = response.json()
|
||||
payload = self._request("POST", "/api/v2/media", files=files, data=form)
|
||||
|
||||
media_id = payload.get("id")
|
||||
if not media_id:
|
||||
raise MastodonError("Mastodon response missing media id")
|
||||
return media_id
|
||||
|
||||
def create_status(self, text: str, media_ids: Sequence[str]) -> str:
|
||||
def create_status(
|
||||
self, text: str, media_ids: Sequence[str], in_reply_to_id: Optional[str] = None
|
||||
) -> Dict[str, str]:
|
||||
if not text.strip():
|
||||
raise MastodonError("Post text cannot be empty")
|
||||
url = f"{self.base_url}/api/v1/statuses"
|
||||
payload = {
|
||||
"status": text,
|
||||
"language": "en",
|
||||
"media_ids": list(media_ids),
|
||||
}
|
||||
try:
|
||||
response = self.session.post(url, json=payload, timeout=30)
|
||||
except requests.RequestException as exc: # pragma: no cover
|
||||
raise MastodonError(str(exc)) from exc
|
||||
if not response.ok:
|
||||
self._raise_for_error(response)
|
||||
data = response.json()
|
||||
if in_reply_to_id:
|
||||
payload["in_reply_to_id"] = in_reply_to_id
|
||||
data = self._request("POST", "/api/v1/statuses", json=payload)
|
||||
|
||||
status_id = data.get("id")
|
||||
status_url = data.get("url")
|
||||
if not status_id:
|
||||
raise MastodonError("Mastodon response missing status id")
|
||||
return status_id
|
||||
return {"id": status_id, "url": status_url}
|
||||
|
||||
def _get_account_id(self) -> str:
|
||||
if not self._account_id:
|
||||
data = self._request("GET", "/api/v1/accounts/verify_credentials")
|
||||
account_id = data.get("id")
|
||||
if not account_id:
|
||||
raise MastodonError("Unable to determine account id")
|
||||
self._account_id = str(account_id)
|
||||
return self._account_id
|
||||
|
||||
def get_latest_status(self) -> Optional[Dict[str, Any]]:
|
||||
account_id = self._get_account_id()
|
||||
params = {"limit": 1, "exclude_reblogs": True}
|
||||
items = self._request(
|
||||
"GET", f"/api/v1/accounts/{account_id}/statuses", params=params
|
||||
)
|
||||
if isinstance(items, list) and items:
|
||||
status = items[0]
|
||||
return {
|
||||
"id": status.get("id"),
|
||||
"content": status.get("content"),
|
||||
"created_at": status.get("created_at"),
|
||||
"url": status.get("url"),
|
||||
}
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -242,14 +242,19 @@ def compose_draft():
|
|||
error_message = None
|
||||
post_text = ""
|
||||
instructions = ""
|
||||
reply_to_latest = False
|
||||
|
||||
if request.method == "POST":
|
||||
asset_ids = _unique_asset_ids(request.form.getlist("asset_ids"))
|
||||
post_text = request.form.get("post_text", "")
|
||||
instructions = request.form.get("post_instructions", "")
|
||||
reply_values = request.form.getlist("reply_to_latest")
|
||||
reply_to_latest = any(value in {"1", "on", "true"} for value in reply_values)
|
||||
latest_status_id_form = request.form.get("latest_status_id")
|
||||
else:
|
||||
ids_param = request.args.get("ids", "")
|
||||
asset_ids = _unique_asset_ids(ids_param.split(",") if ids_param else [])
|
||||
latest_status_id_form = None
|
||||
|
||||
if not asset_ids:
|
||||
flash("Choose photos before composing a post.")
|
||||
|
|
@ -259,6 +264,14 @@ def compose_draft():
|
|||
flash(f"Select at most {MAX_MEDIA_ATTACHMENTS} photos.")
|
||||
return redirect(url_for("main.compose_select"))
|
||||
|
||||
latest_status_raw = None
|
||||
latest_status_error = None
|
||||
if mastodon_client:
|
||||
try:
|
||||
latest_status_raw = mastodon_client.get_latest_status()
|
||||
except MastodonError as exc:
|
||||
latest_status_error = str(exc)
|
||||
|
||||
assets: list[ImmichAsset] = []
|
||||
try:
|
||||
for asset_id in asset_ids:
|
||||
|
|
@ -273,6 +286,22 @@ def compose_draft():
|
|||
mastodon_ready=bool(mastodon_client),
|
||||
)
|
||||
|
||||
latest_status = None
|
||||
if latest_status_raw:
|
||||
latest_status = {
|
||||
"id": latest_status_raw.get("id"),
|
||||
"content": latest_status_raw.get("content", ""),
|
||||
"created_display": _humanize_timestamp(latest_status_raw.get("created_at")),
|
||||
"url": latest_status_raw.get("url"),
|
||||
}
|
||||
elif latest_status_id_form:
|
||||
latest_status = {
|
||||
"id": latest_status_id_form,
|
||||
"content": "",
|
||||
"created_display": None,
|
||||
"url": None,
|
||||
}
|
||||
|
||||
asset_entries = []
|
||||
for asset in assets:
|
||||
if request.method == "POST":
|
||||
|
|
@ -327,8 +356,21 @@ def compose_draft():
|
|||
asset.file_name, content, mime_type, alt_value
|
||||
)
|
||||
media_ids.append(media_id)
|
||||
mastodon_client.create_status(post_text.strip(), media_ids)
|
||||
flash("Post sent to Mastodon.")
|
||||
reply_target_id = latest_status_id_form or (
|
||||
latest_status["id"] if latest_status else None
|
||||
)
|
||||
reply_id = reply_target_id if reply_to_latest else None
|
||||
status = mastodon_client.create_status(
|
||||
post_text.strip(), media_ids, in_reply_to_id=reply_id
|
||||
)
|
||||
status_url = status.get("url")
|
||||
if status_url:
|
||||
flash(
|
||||
f'Post sent to Mastodon: <a href="{status_url}" target="_blank" rel="noopener">{status_url}</a>',
|
||||
"success",
|
||||
)
|
||||
else:
|
||||
flash("Post sent to Mastodon.", "success")
|
||||
return redirect(url_for("main.index"))
|
||||
except ValueError as exc:
|
||||
error_message = str(exc)
|
||||
|
|
@ -347,6 +389,9 @@ def compose_draft():
|
|||
instructions=instructions,
|
||||
mastodon_ready=bool(mastodon_client),
|
||||
max_photos=MAX_MEDIA_ATTACHMENTS,
|
||||
latest_status=latest_status,
|
||||
latest_status_error=latest_status_error,
|
||||
reply_to_latest=reply_to_latest,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue