From e98431df4b89b2e5cad7fbbb526122cce0e9b2ac Mon Sep 17 00:00:00 2001 From: ThePeGaSuS <25697531+TehPeGaSuS@users.noreply.github.com> Date: Tue, 5 May 2026 15:16:52 +0200 Subject: [PATCH] Fix Shlink --- requirements.txt | 1 + shlink.py | 79 +++++++++++++++++++----------------------------- 2 files changed, 32 insertions(+), 48 deletions(-) diff --git a/requirements.txt b/requirements.txt index 37dec8b..ce99f1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ aiohttp>=3.9 feedparser>=6.0 +httpx # tomllib is in stdlib for Python 3.11+ # For Python 3.9/3.10, uncomment: # tomli>=2.0 diff --git a/shlink.py b/shlink.py index 4f50621..f017bd5 100644 --- a/shlink.py +++ b/shlink.py @@ -1,76 +1,59 @@ """Shlink short-URL client. Provides a single async helper, ``shorten(url)``, that calls the Shlink -REST API and returns the shortened URL. Returns the original URL unchanged -on any error so that the bot always has *something* to post. +REST API and returns the shortened URL. Returns the original URL unchanged +on any error. -Configuration (gitbot.toml): - - [shlink] - url = "https://shlink.example.com" # base URL of your Shlink instance - api_key = "YOUR-API-KEY" # Shlink API key - # enabled = true # set false to disable (default true) - # timeout = 5 # HTTP timeout in seconds (default 5) - # domain = "s.example.com" # custom domain if you have several +Requires: httpx """ -import asyncio -import json import logging -import urllib.error -import urllib.parse -import urllib.request +import httpx log = logging.getLogger("shlink") - class ShlinkClient: def __init__(self, base_url: str, api_key: str, timeout: int = 5, domain: str | None = None): - self._base = base_url.rstrip("/") + self._base = base_url.rstrip("/") self._api_key = api_key self._timeout = timeout - self._domain = domain + self._domain = domain + self._endpoint = f"{self._base}/rest/v3/short-urls" async def shorten(self, url: str) -> str: """Return a shortened URL, or ``url`` unchanged on failure.""" + payload = { + "longUrl": url, + "findIfExists": True # Prevents creating duplicate slugs for the same URL + } + if self._domain: + payload["domain"] = self._domain + + headers = { + "X-Api-Key": self._api_key, + "Content-Type": "application/json", + } + try: - return await asyncio.get_event_loop().run_in_executor( - None, self._call, url) + # Using a context manager ensures the connection is closed immediately + async with httpx.AsyncClient(timeout=self._timeout) as client: + resp = await client.post(self._endpoint, json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + + short = data["shortUrl"] + log.debug("Shortened %s → %s", url, short) + return short except Exception as e: log.warning("Shlink error for %s: %s", url, e) return url - def _call(self, url: str) -> str: - endpoint = f"{self._base}/rest/v3/short-urls" - payload = {"longUrl": url} - if self._domain: - payload["domain"] = self._domain - body = json.dumps(payload).encode() - req = urllib.request.Request( - endpoint, - data=body, - headers={ - "Content-Type": "application/json", - "X-Api-Key": self._api_key, - }, - method="POST", - ) - with urllib.request.urlopen(req, timeout=self._timeout) as resp: - data = json.loads(resp.read()) - short = data["shortUrl"] - log.debug("Shortened %s → %s", url, short) - return short - - def from_config(cfg: dict) -> "ShlinkClient | None": - """Build a ShlinkClient from the ``[shlink]`` config section. - - Returns ``None`` when shlink is disabled or misconfigured. - """ + """Build a ShlinkClient from the [shlink] config section.""" if not cfg.get("enabled", True): return None - base = cfg.get("url", "").strip() + base = cfg.get("url", "").strip() api_key = cfg.get("api_key", "").strip() if not base or not api_key: if base or api_key: @@ -81,4 +64,4 @@ def from_config(cfg: dict) -> "ShlinkClient | None": api_key=api_key, timeout=int(cfg.get("timeout", 5)), domain=cfg.get("domain") or None, - ) + ) \ No newline at end of file