From d3edd6420c1acdb12cf16b6d6ca4ffadf1903577 Mon Sep 17 00:00:00 2001 From: nose Date: Fri, 19 Dec 2025 03:25:52 -0800 Subject: [PATCH] dfd --- .gitignore | 4 +- CLI.py | 16 +++++++- Log/medeia_macina/telegram.session | Bin 28672 -> 28672 bytes Provider/matrix.py | 60 ++++++++++++++++++++++++++++- Provider/telegram.py | 47 +++++++++++++++++++++- Provider/zeroxzero.py | 12 +++++- Store/registry.py | 41 ++++++++++++++++++++ cmdlet/add_file.py | 30 +-------------- cmdnat/matrix.py | 46 +++++++++++++++++++++- 9 files changed, 221 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 258aedc..8545a64 100644 --- a/.gitignore +++ b/.gitignore @@ -221,4 +221,6 @@ luac.out config.conf config.d/ MPV/ffmpeg/* -MPV/portable_config/* \ No newline at end of file +MPV/portable_config/* +Log/ +Log/medeia_macina/telegram.session diff --git a/CLI.py b/CLI.py index ab8bdec..2360f0d 100644 --- a/CLI.py +++ b/CLI.py @@ -1088,10 +1088,12 @@ def _create_cmdlet_cli(): if isinstance(provider_cfg, dict) and provider_cfg: try: from ProviderCore.registry import ( + list_providers, list_search_providers, list_file_providers, ) except Exception: + list_providers = None # type: ignore list_search_providers = None # type: ignore list_file_providers = None # type: ignore @@ -1103,6 +1105,13 @@ def _create_cmdlet_cli(): search_availability = {} file_availability = {} meta_availability = {} + provider_availability = {} + + try: + if list_providers is not None: + provider_availability = list_providers(config) or {} + except Exception: + provider_availability = {} try: if list_search_providers is not None: @@ -1192,7 +1201,12 @@ def _create_cmdlet_cli(): is_known = False ok = None - if prov in search_availability: + # Prefer unified provider registry for availability (covers providers that + # implement download-only behavior, like Telegram). + if prov in provider_availability: + is_known = True + ok = bool(provider_availability.get(prov)) + elif prov in search_availability: is_known = True ok = bool(search_availability.get(prov)) elif prov in file_availability: diff --git a/Log/medeia_macina/telegram.session b/Log/medeia_macina/telegram.session index 4e18901498e5f43ef643022348436833ada24268..4e3915a9c1796e7ae1ca699c01ba624a2852fc6e 100644 GIT binary patch delta 470 zcmZp8z}WDBae_3X)I=F)MyZVnOZ3Hfcq?@^ol4u1Dia9BFlVUY3rxmPqt?zCTFMSlw=lrB_?MpxPn-juEr&jAobh+ zKA+#p&cMdNAjwkJypwySol}Rmqko8}bEc~SyDCV@_KvNarm{1zsWM2ioWA6Ay0a{H zdPZhSN@`wlYFUo^qyto(TEJ39lLGJ`V9 zyGb?9?ZNLH%M*)o^Yb!G@{5YY6N?mlA?!?7&7Fb_g6s^e3=9k`42=Ks+{`jvjb~40 Yv)5oUE}3j#e~!txWRt@`eJ0~#07Oc@82|tP delta 80 zcmZp8z}WDBae_3Xz(g5mMuCk9OZ53z`93r78}QfgedcH5Kebs<;31!|AOkxCD+2=q l3j^c7JU6pUSB>t;Z1x&V8tsz}?9VZ2v~P0wr_ZF(1^|aQ7IOdq diff --git a/Provider/matrix.py b/Provider/matrix.py index f991099..a95784a 100644 --- a/Provider/matrix.py +++ b/Provider/matrix.py @@ -144,7 +144,7 @@ class Matrix(Provider): out.append({"room_id": room_id, "name": name}) return out - def upload_to_room(self, file_path: str, room_id: str) -> str: + def upload_to_room(self, file_path: str, room_id: str, **kwargs: Any) -> str: """Upload a file and send it to a specific room.""" path = Path(file_path) if not path.exists(): @@ -174,6 +174,22 @@ class Matrix(Provider): if not content_uri: raise Exception("No content_uri returned") + # Build a fragment-free URL suitable for storage backends. + # `matrix.to` links use fragments (`#/...`) which some backends normalize away. + download_url_for_store = "" + try: + curi = str(content_uri or "").strip() + if curi.startswith("mxc://"): + rest = curi[len("mxc://"):] + if "/" in rest: + server_name, media_id = rest.split("/", 1) + server_name = str(server_name).strip() + media_id = str(media_id).strip() + if server_name and media_id: + download_url_for_store = f"{base}/_matrix/media/v3/download/{quote(server_name, safe='')}/{quote(media_id, safe='')}" + except Exception: + download_url_for_store = "" + # Determine message type msgtype = "m.file" ext = path.suffix.lower() @@ -199,6 +215,44 @@ class Matrix(Provider): if send_resp.status_code != 200: raise Exception(f"Matrix send message failed: {send_resp.text}") + event_id = (send_resp.json() or {}).get("event_id") + link = f"https://matrix.to/#/{room_id}/{event_id}" if event_id else f"https://matrix.to/#/{room_id}" + + # Optional: if a PipeObject is provided and it already has store+hash, + # attach the uploaded URL back to the stored file. + try: + pipe_obj = kwargs.get("pipe_obj") + if pipe_obj is not None: + from Store import Store + + # Prefer the direct media download URL for storage backends. + Store(self.config, suppress_debug=True).try_add_url_for_pipe_object( + pipe_obj, + download_url_for_store or link, + ) + except Exception: + pass + + return link + + def send_text_to_room(self, text: str, room_id: str) -> str: + """Send a plain text message to a specific room.""" + message = str(text or "").strip() + if not message: + return "" + if not room_id: + raise Exception("Matrix room_id missing") + + base, token = self._get_homeserver_and_token() + encoded_room = quote(str(room_id), safe="") + txn_id = f"mm_{int(time.time())}_{uuid.uuid4().hex[:8]}" + send_url = f"{base}/_matrix/client/v3/rooms/{encoded_room}/send/m.room.message/{txn_id}" + send_headers = {"Authorization": f"Bearer {token}"} + payload = {"msgtype": "m.text", "body": message} + send_resp = requests.put(send_url, headers=send_headers, json=payload) + if send_resp.status_code != 200: + raise Exception(f"Matrix send text failed: {send_resp.text}") + event_id = (send_resp.json() or {}).get("event_id") return f"https://matrix.to/#/{room_id}/{event_id}" if event_id else f"https://matrix.to/#/{room_id}" @@ -247,9 +301,11 @@ class Matrix(Provider): try: file_path = '' delete_after = False + pipe_obj = None if isinstance(payload, dict): file_path = str(payload.get('path') or '') delete_after = bool(payload.get('delete_after', False)) + pipe_obj = payload.get('pipe_obj') else: file_path = str(getattr(payload, 'path', '') or '') if not file_path: @@ -262,7 +318,7 @@ class Matrix(Provider): print(f"Matrix upload file missing: {file_path}") continue - link = self.upload_to_room(str(media_path), str(room_id)) + link = self.upload_to_room(str(media_path), str(room_id), pipe_obj=pipe_obj) if link: print(link) diff --git a/Provider/telegram.py b/Provider/telegram.py index 140311b..76ce6b3 100644 --- a/Provider/telegram.py +++ b/Provider/telegram.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio import re +import sys +import time from pathlib import Path from typing import Any, Dict, Optional, Tuple from urllib.parse import urlparse @@ -143,6 +145,21 @@ class Telegram(Provider): session_base = self._session_base_path() chat, message_id = _parse_telegram_message_url(url) + def _format_bytes(num: Optional[int]) -> str: + try: + if num is None: + return "?B" + n = float(num) + suffixes = ["B", "KB", "MB", "GB", "TB"] + for s in suffixes: + if n < 1024 or s == suffixes[-1]: + if s == "B": + return f"{int(n)}{s}" + return f"{n:.1f}{s}" + n /= 1024 + except Exception: + return "?B" + client = TelegramClient(str(session_base), app_id, api_hash) try: # This prompts on first run for phone/code and persists the session. @@ -226,7 +243,35 @@ class Telegram(Provider): except Exception: pass - downloaded = _resolve(client.download_media(message, file=str(output_dir))) + # Progress callback: prints to stderr so it doesn't interfere with pipeline stdout. + last_print = {"t": 0.0} + def _progress(current: int, total: int) -> None: + try: + now = time.monotonic() + # Throttle to avoid spamming. + if now - float(last_print.get("t", 0.0)) < 0.25 and current < total: + return + last_print["t"] = now + + pct = "" + try: + if total and total > 0: + pct = f" {min(100.0, (current / total) * 100.0):5.1f}%" + except Exception: + pct = "" + + line = f"[telegram] Downloading{pct} ({_format_bytes(current)}/{_format_bytes(total)})" + sys.stderr.write("\r" + line) + sys.stderr.flush() + except Exception: + return + + downloaded = _resolve(client.download_media(message, file=str(output_dir), progress_callback=_progress)) + try: + sys.stderr.write("\n") + sys.stderr.flush() + except Exception: + pass if not downloaded: raise Exception("Telegram download returned no file") downloaded_path = Path(str(downloaded)) diff --git a/Provider/zeroxzero.py b/Provider/zeroxzero.py index 4d11375..dc1251c 100644 --- a/Provider/zeroxzero.py +++ b/Provider/zeroxzero.py @@ -24,7 +24,17 @@ class ZeroXZero(Provider): response = client.post("https://0x0.st", files={"file": handle}) if response.status_code == 200: - return response.text.strip() + uploaded_url = response.text.strip() + try: + pipe_obj = kwargs.get("pipe_obj") + if pipe_obj is not None: + from Store import Store + + Store(self.config, suppress_debug=True).try_add_url_for_pipe_object(pipe_obj, uploaded_url) + except Exception: + pass + + return uploaded_url raise Exception(f"Upload failed: {response.status_code} - {response.text}") diff --git a/Store/registry.py b/Store/registry.py index e377a99..09e67b5 100644 --- a/Store/registry.py +++ b/Store/registry.py @@ -14,6 +14,7 @@ from __future__ import annotations import importlib import inspect import pkgutil +import re from pathlib import Path from typing import Any, Dict, Iterable, Optional, Type @@ -22,6 +23,9 @@ from SYS.logger import debug from Store._base import Store as BaseStore +_SHA256_HEX_RE = re.compile(r"^[0-9a-fA-F]{64}$") + + # Backends that failed to initialize earlier in the current process. # Keyed by (store_type, instance_key) where instance_key is the name used under config.store... _FAILED_BACKEND_CACHE: Dict[tuple[str, str], str] = {} @@ -237,3 +241,40 @@ class Store: def is_available(self, backend_name: str) -> bool: return backend_name in self._backends + + def try_add_url_for_pipe_object(self, pipe_obj: Any, url: str) -> bool: + """Best-effort helper: if `pipe_obj` contains `store` + `hash`, add `url` to that store backend. + + Intended for providers to attach newly generated/hosted URLs back to an existing stored file. + """ + try: + url_text = str(url or "").strip() + if not url_text: + return False + + store_name = None + file_hash = None + if isinstance(pipe_obj, dict): + store_name = pipe_obj.get("store") + file_hash = pipe_obj.get("hash") + else: + store_name = getattr(pipe_obj, "store", None) + file_hash = getattr(pipe_obj, "hash", None) + + store_name = str(store_name).strip() if store_name is not None else "" + file_hash = str(file_hash).strip() if file_hash is not None else "" + if not store_name or not file_hash: + return False + + if not _SHA256_HEX_RE.fullmatch(file_hash): + return False + + backend = self[store_name] + add_url = getattr(backend, "add_url", None) + if not callable(add_url): + return False + + ok = add_url(file_hash.lower(), [url_text]) + return bool(ok) if ok is not None else True + except Exception: + return False diff --git a/cmdlet/add_file.py b/cmdlet/add_file.py index 9679ae3..0f75b53 100644 --- a/cmdlet/add_file.py +++ b/cmdlet/add_file.py @@ -1554,22 +1554,10 @@ class Add_File(Cmdlet): return 1 try: - hoster_url = provider.upload_to_room(str(media_path), str(room_id)) + hoster_url = provider.upload_to_room(str(media_path), str(room_id), pipe_obj=pipe_obj) log(f"File uploaded: {hoster_url}", file=sys.stderr) - # Associate URL with Hydrus if possible f_hash = Add_File._resolve_file_hash(None, media_path, pipe_obj, None) - if f_hash: - try: - store_name = getattr(pipe_obj, "store", None) - if store_name: - store = Store(config) - backend = store[str(store_name)] - client = getattr(backend, "_client", None) - if client is not None and hasattr(client, "associate_url"): - client.associate_url(str(f_hash), hoster_url) - except Exception: - pass except Exception as exc: log(f"Upload failed: {exc}", file=sys.stderr) @@ -1676,24 +1664,10 @@ class Add_File(Cmdlet): log(f"File provider '{provider_name}' not available", file=sys.stderr) return 1 - hoster_url = file_provider.upload(str(media_path)) + hoster_url = file_provider.upload(str(media_path), pipe_obj=pipe_obj) log(f"File uploaded: {hoster_url}", file=sys.stderr) - # Associate URL with Hydrus if possible f_hash = Add_File._resolve_file_hash(None, media_path, pipe_obj, None) - if f_hash: - try: - # Only associate when we can target an explicit Hydrus store backend. - # Do not fall back to a global/default Hydrus client. - store_name = getattr(pipe_obj, "store", None) - if store_name: - store = Store(config) - backend = store[str(store_name)] - client = getattr(backend, "_client", None) - if client is not None and hasattr(client, "associate_url"): - client.associate_url(str(f_hash), hoster_url) - except Exception: - pass except Exception as exc: log(f"Upload failed: {exc}", file=sys.stderr) diff --git a/cmdnat/matrix.py b/cmdnat/matrix.py index 0eab195..a9880a7 100644 --- a/cmdnat/matrix.py +++ b/cmdnat/matrix.py @@ -15,6 +15,24 @@ import pipeline as ctx _MATRIX_PENDING_ITEMS_KEY = "matrix_pending_items" +_MATRIX_PENDING_TEXT_KEY = "matrix_pending_text" + + +def _extract_text_arg(args: Sequence[str]) -> str: + """Extract a `-text ` argument from a cmdnat args list.""" + if not args: + return "" + try: + tokens = list(args) + except Exception: + return "" + for i, tok in enumerate(tokens): + try: + if str(tok).lower() == "-text" and i + 1 < len(tokens): + return str(tokens[i + 1] or "").strip() + except Exception: + continue + return "" def _normalize_to_list(value: Any) -> List[Any]: @@ -341,8 +359,16 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: log(f"Matrix not available: {exc}", file=sys.stderr) return 1 + text_value = _extract_text_arg(args) + if not text_value: + try: + text_value = str(ctx.load_value(_MATRIX_PENDING_TEXT_KEY, default="") or "").strip() + except Exception: + text_value = "" + any_failed = False for rid in room_ids: + sent_any_for_room = False for item in items: file_path = _resolve_upload_path(item, config) if not file_path: @@ -350,16 +376,29 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: log("Matrix upload requires a local file (path) or a direct URL on the selected item", file=sys.stderr) continue try: - link = provider.upload_to_room(file_path, rid) + link = provider.upload_to_room(file_path, rid, pipe_obj=item) debug(f"✓ Sent {Path(file_path).name} -> {rid}") if link: log(link) + sent_any_for_room = True except Exception as exc: any_failed = True log(f"Matrix send failed for {Path(file_path).name}: {exc}", file=sys.stderr) + # Optional caption-like follow-up message (sent once per room). + if text_value and sent_any_for_room: + try: + provider.send_text_to_room(text_value, rid) + except Exception as exc: + any_failed = True + log(f"Matrix text send failed: {exc}", file=sys.stderr) + # Clear pending items once we've attempted to send. ctx.store_value(_MATRIX_PENDING_ITEMS_KEY, []) + try: + ctx.store_value(_MATRIX_PENDING_TEXT_KEY, "") + except Exception: + pass return 1 if any_failed else 0 # Default stage: show rooms, then wait for @N selection to resume sending. @@ -369,6 +408,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: return 1 ctx.store_value(_MATRIX_PENDING_ITEMS_KEY, selected_items) + try: + ctx.store_value(_MATRIX_PENDING_TEXT_KEY, _extract_text_arg(args)) + except Exception: + pass from Provider.matrix import Matrix try: @@ -431,6 +474,7 @@ CMDLET = Cmdlet( usage="@N | .matrix", arg=[ CmdletArg(name="send", type="bool", description="(internal) Send to selected room(s)", required=False), + CmdletArg(name="text", type="string", description="Send a follow-up text message after each upload (caption-like)", required=False), ], exec=_run )