@@ -7,6 +7,7 @@ import argparse
|
||||
import calendar
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -15,6 +16,7 @@ from datetime import datetime, timezone
|
||||
from email.utils import format_datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
from urllib.parse import parse_qsl, quote, quote_plus, urlencode, urlsplit, urlunsplit
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import feedparser
|
||||
@@ -27,6 +29,8 @@ from urllib3.util.retry import Retry
|
||||
LOG = logging.getLogger("rss-merge")
|
||||
ATOM_NS = "http://www.w3.org/2005/Atom"
|
||||
CONTENT_NS = "http://purl.org/rss/1.0/modules/content/"
|
||||
RSSHUB_HOST = "rsshub.yuzhen.pub"
|
||||
RSSHUB_SECRET_ENV = "RSSHUB_ACCESS_KEY"
|
||||
ET.register_namespace("atom", ATOM_NS)
|
||||
ET.register_namespace("content", CONTENT_NS)
|
||||
|
||||
@@ -113,13 +117,42 @@ def make_session(user_agent: str, retries: int) -> requests.Session:
|
||||
return session
|
||||
|
||||
|
||||
def is_protected_rsshub_url(url: str) -> bool:
|
||||
try:
|
||||
return (urlsplit(url).hostname or "").casefold() == RSSHUB_HOST
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def add_rsshub_access_key(url: str, access_key: str) -> str:
|
||||
"""Add or replace the key query parameter without changing the configured URL."""
|
||||
if not is_protected_rsshub_url(url):
|
||||
return url
|
||||
parsed = urlsplit(url)
|
||||
query = [(name, value) for name, value in parse_qsl(parsed.query, keep_blank_values=True) if name != "key"]
|
||||
query.append(("key", access_key))
|
||||
# Never transmit the access key over plaintext HTTP.
|
||||
return urlunsplit(("https", parsed.netloc, parsed.path, urlencode(query), parsed.fragment))
|
||||
|
||||
|
||||
def redact_secret(message: str, secret: str) -> str:
|
||||
if not secret:
|
||||
return message
|
||||
for value in {secret, quote(secret, safe=""), quote_plus(secret)}:
|
||||
if value:
|
||||
message = message.replace(value, "***")
|
||||
return message
|
||||
|
||||
|
||||
def fetch_feed(
|
||||
session: requests.Session,
|
||||
source: Source,
|
||||
timeout: float,
|
||||
max_bytes: int,
|
||||
rsshub_access_key: str,
|
||||
) -> list[FeedItem]:
|
||||
with session.get(source.url, timeout=timeout, stream=True) as response:
|
||||
request_url = add_rsshub_access_key(source.url, rsshub_access_key)
|
||||
with session.get(request_url, timeout=timeout, stream=True) as response:
|
||||
response.raise_for_status()
|
||||
chunks: list[bytes] = []
|
||||
size = 0
|
||||
@@ -297,6 +330,7 @@ def run(config_path: Path) -> int:
|
||||
)
|
||||
|
||||
groups: dict[str, dict[str, Any]] = config["groups"]
|
||||
rsshub_access_key = os.environ.get(RSSHUB_SECRET_ENV, "")
|
||||
repo_root = config_path.resolve().parent
|
||||
all_sources: dict[str, Source] = {}
|
||||
group_urls: dict[str, list[str]] = {}
|
||||
@@ -313,16 +347,22 @@ def run(config_path: Path) -> int:
|
||||
for source in sources:
|
||||
all_sources.setdefault(source.url, source)
|
||||
|
||||
if any(is_protected_rsshub_url(url) for url in all_sources) and not rsshub_access_key:
|
||||
raise ConfigError(f"配置包含 {RSSHUB_HOST},但环境变量 {RSSHUB_SECRET_ENV} 未设置")
|
||||
|
||||
session = make_session(user_agent, retries)
|
||||
# urllib3 的重试日志可能包含完整查询参数;避免将 access key 写入 Actions 日志。
|
||||
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
||||
fetched: dict[str, list[FeedItem]] = {}
|
||||
errors: dict[str, str] = {}
|
||||
for url, source in all_sources.items():
|
||||
try:
|
||||
fetched[url] = fetch_feed(session, source, timeout, max_bytes)
|
||||
fetched[url] = fetch_feed(session, source, timeout, max_bytes, rsshub_access_key)
|
||||
LOG.info("已获取 %s(%d 条)", url, len(fetched[url]))
|
||||
except Exception as exc: # One source should produce a useful aggregated error.
|
||||
errors[url] = str(exc)
|
||||
LOG.error("获取失败 %s: %s", url, exc)
|
||||
safe_error = redact_secret(str(exc), rsshub_access_key)
|
||||
errors[url] = safe_error
|
||||
LOG.error("获取失败 %s: %s", url, safe_error)
|
||||
|
||||
if errors and not allow_partial:
|
||||
LOG.error("有 %d 个源失败;为避免临时丢失条目,本次不改写输出。可设置 settings.allow_partial: true 改变此行为。", len(errors))
|
||||
|
||||
Reference in New Issue
Block a user