From f926d6507a8d91191c240e3f18b34fafd5723e4f Mon Sep 17 00:00:00 2001 From: zipee Date: Thu, 9 Apr 2026 08:40:07 +0800 Subject: [PATCH 1/3] docs: point skill metadata at public repo --- SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SKILL.md b/SKILL.md index a349841..2935957 100644 --- a/SKILL.md +++ b/SKILL.md @@ -4,8 +4,8 @@ version: "3.0.0" description: "Multi-query social search with intelligent planning. Agent plans queries when possible, falls back to Gemini/OpenAI when not. Research any topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web." argument-hint: 'last30days-3 AI video tools, last30days-3 best noise cancelling headphones' allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch -homepage: https://github.com/mvanhorn/last30days-skill-private -repository: https://github.com/mvanhorn/last30days-skill-private +homepage: https://github.com/mvanhorn/last30days-skill +repository: https://github.com/mvanhorn/last30days-skill author: mvanhorn license: MIT user-invocable: true From 20201565915fc9554d4c55ed0ebf78dd9772593a Mon Sep 17 00:00:00 2001 From: ziperlee Date: Thu, 9 Apr 2026 23:01:17 +0800 Subject: [PATCH 2/3] fix: write briefing files as utf-8 --- scripts/briefing.py | 4 ++-- tests/test_briefing_v3.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/briefing.py b/scripts/briefing.py index 84b250d..ac8f611 100644 --- a/scripts/briefing.py +++ b/scripts/briefing.py @@ -216,7 +216,7 @@ def show_briefing(date: str = None) -> dict: if not path.exists(): return {"status": "not_found", "message": f"No briefing found for {date}."} - with open(path) as f: + with open(path, encoding="utf-8") as f: return json.load(f) @@ -225,7 +225,7 @@ def _save_briefing(data: dict, suffix: str = ""): BRIEFS_DIR.mkdir(parents=True, exist_ok=True) date = datetime.now().strftime("%Y-%m-%d") path = BRIEFS_DIR / f"{date}{suffix}.json" - with open(path, "w") as f: + with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, default=str) diff --git a/tests/test_briefing_v3.py b/tests/test_briefing_v3.py index cd1cec4..2d4f4f4 100644 --- a/tests/test_briefing_v3.py +++ b/tests/test_briefing_v3.py @@ -2,6 +2,7 @@ import sys import tempfile import unittest from pathlib import Path +from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) @@ -30,6 +31,27 @@ class BriefingV3Tests(unittest.TestCase): store._db_override = old_db_override briefing.BRIEFS_DIR = old_briefs_dir + def test_save_briefing_uses_utf8_encoding(self): + with tempfile.TemporaryDirectory() as tmpdir: + old_briefs_dir = briefing.BRIEFS_DIR + try: + briefing.BRIEFS_DIR = Path(tmpdir) / "briefs" + payload = {"status": "ok", "message": "emoji 💬 and accents café"} + with mock.patch("briefing.open", create=True) as mock_open: + handle = mock.Mock() + handle.__enter__ = mock.Mock(return_value=handle) + handle.__exit__ = mock.Mock(return_value=False) + mock_open.return_value = handle + + briefing._save_briefing(payload) + + mock_open.assert_called_once() + _, kwargs = mock_open.call_args + self.assertEqual("w", kwargs["mode"] if "mode" in kwargs else mock_open.call_args.args[1]) + self.assertEqual("utf-8", kwargs["encoding"]) + finally: + briefing.BRIEFS_DIR = old_briefs_dir + if __name__ == "__main__": unittest.main() From 9405aa3fb440439c6cf8d2f3d6a21e6ecfa657b1 Mon Sep 17 00:00:00 2001 From: ziperlee Date: Thu, 9 Apr 2026 23:03:47 +0800 Subject: [PATCH 3/3] fix: refresh expired bluesky sessions --- scripts/lib/bluesky.py | 58 +++++++++++++++++++++++++++--------------- tests/test_bluesky.py | 16 ++++++++++++ 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/scripts/lib/bluesky.py b/scripts/lib/bluesky.py index 4b65a42..07ef4e8 100644 --- a/scripts/lib/bluesky.py +++ b/scripts/lib/bluesky.py @@ -75,6 +75,12 @@ def _create_session(handle: str, app_password: str) -> Optional[str]: return None +def _reset_session_cache() -> None: + global _cached_token, _session_error + _cached_token = None + _session_error = None + + def _extract_core_subject(topic: str) -> str: """Extract core subject from verbose query for Bluesky search.""" from .query import extract_core_subject @@ -129,12 +135,6 @@ def search_bluesky( if not handle or not app_password: return {"posts": [], "error": "Bluesky credentials not configured"} - # Authenticate - token = _create_session(handle, app_password) - if not token: - error_msg = _session_error or "Bluesky session creation failed (unknown error)" - return {"posts": [], "error": error_msg} - count = DEPTH_CONFIG.get(depth, DEPTH_CONFIG["default"]) core_topic = _extract_core_subject(topic) @@ -148,20 +148,38 @@ def search_bluesky( } url = f"{BSKY_SEARCH_URL}?{urlencode(params)}" - try: - response = http.request( - "GET", url, - headers={"Authorization": f"Bearer {token}"}, - timeout=30, - ) - except http.HTTPError as e: - _log(f"Search failed: {e}") - if e.status_code == 403 and e.body and "cloudflare" in e.body.lower(): - return {"posts": [], "error": "Bluesky search blocked by Cloudflare (403). This is a network-level block - try a different network or VPN."} - return {"posts": [], "error": f"Bluesky search failed: {e}"} - except Exception as e: - _log(f"Search failed: {e}") - return {"posts": [], "error": f"Bluesky search failed: {type(e).__name__}: {e}"} + def _auth_and_search() -> tuple[Optional[Dict[str, Any]], Optional[str]]: + token = _create_session(handle, app_password) + if not token: + error_msg = _session_error or "Bluesky session creation failed (unknown error)" + return None, error_msg + try: + response = http.request( + "GET", url, + headers={"Authorization": f"Bearer {token}"}, + timeout=30, + ) + return response, None + except http.HTTPError as e: + _log(f"Search failed: {e}") + if e.status_code == 401: + _reset_session_cache() + return None, "refresh" + if e.status_code == 403 and e.body and "cloudflare" in e.body.lower(): + return None, "Bluesky search blocked by Cloudflare (403). This is a network-level block - try a different network or VPN." + return None, f"Bluesky search failed: {e}" + except Exception as e: + _log(f"Search failed: {e}") + return None, f"Bluesky search failed: {type(e).__name__}: {e}" + + response, error_msg = _auth_and_search() + if error_msg == "refresh": + _log("Session expired; recreating token and retrying once") + response, error_msg = _auth_and_search() + if error_msg: + return {"posts": [], "error": error_msg} + if response is None: + return {"posts": [], "error": "Bluesky search failed (unknown error)"} posts = response.get("posts", []) _log(f"Found {len(posts)} posts") diff --git a/tests/test_bluesky.py b/tests/test_bluesky.py index 01174f0..59bb096 100644 --- a/tests/test_bluesky.py +++ b/tests/test_bluesky.py @@ -194,6 +194,22 @@ class TestSearchBlueskyAuth(unittest.TestCase): search_call = mock_request.call_args_list[1] self.assertEqual(search_call.kwargs.get("headers", {}), {"Authorization": "Bearer tok123"}) + @patch("lib.bluesky.http.request") + def test_401_search_refreshes_session_once(self, mock_request): + from lib.http import HTTPError + + mock_request.side_effect = [ + {"accessJwt": "tok-old", "refreshJwt": "ref-old"}, + HTTPError("HTTP 401: Unauthorized", 401, ""), + {"accessJwt": "tok-new", "refreshJwt": "ref-new"}, + {"posts": [{"uri": "at://did/app.bsky.feed.post/abc", "author": {"handle": "u1"}, "record": {"text": "hi"}}]}, + ] + config = {"BSKY_HANDLE": "user.bsky.social", "BSKY_APP_PASSWORD": "pw"} + result = bluesky.search_bluesky("test", "2026-01-01", "2026-03-09", config=config) + self.assertEqual(len(result["posts"]), 1) + self.assertEqual(mock_request.call_count, 4) + self.assertEqual(mock_request.call_args_list[3].kwargs.get("headers", {}), {"Authorization": "Bearer tok-new"}) + if __name__ == "__main__": unittest.main()