Add a Blogger delivery channel with one-time OAuth setup

BloggerDeliverer posts each briefing (markdown -> HTML, H1 becomes the
post title with the report headline) via the Blogger API v3 using a
long-lived refresh token; `us2kr blogger-auth` runs the loopback OAuth
flow once and prints the BLOGGER_* lines for .env. Enabled only when
all four BLOGGER_* settings are present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jsahn
2026-09-01 12:04:49 +09:00
co-authored by Claude Fable 5
parent fecc767a5c
commit 12d7b47f46
8 changed files with 521 additions and 4 deletions
+21 -1
View File
@@ -35,6 +35,15 @@ class Settings(BaseSettings):
telegram_bot_token: str | None = Field(default=None, alias="TELEGRAM_BOT_TOKEN")
telegram_chat_id: str | None = Field(default=None, alias="TELEGRAM_CHAT_ID")
# --- Blogger (optional; issue the refresh token once with `us2kr blogger-auth`) ---
blogger_client_id: str | None = Field(default=None, alias="BLOGGER_CLIENT_ID")
blogger_client_secret: str | None = Field(default=None, alias="BLOGGER_CLIENT_SECRET")
blogger_refresh_token: str | None = Field(default=None, alias="BLOGGER_REFRESH_TOKEN")
blogger_blog_id: str | None = Field(default=None, alias="BLOGGER_BLOG_ID")
blogger_labels: Annotated[list[str], NoDecode] = Field(
default_factory=lambda: ["미국증시", "마감브리핑"], alias="BLOGGER_LABELS"
)
# --- Optional data keys ---
fred_api_key: str | None = Field(default=None, alias="FRED_API_KEY")
@@ -70,7 +79,9 @@ class Settings(BaseSettings):
log_level: str = Field(default="INFO", alias="US2KR_LOG_LEVEL")
@field_validator("minimax_fallback_models", "krx_extra_holidays", "searxng_queries", mode="before")
@field_validator(
"minimax_fallback_models", "krx_extra_holidays", "searxng_queries", "blogger_labels", mode="before"
)
@classmethod
def _split_csv(cls, v):
if isinstance(v, str):
@@ -85,6 +96,15 @@ class Settings(BaseSettings):
def telegram_enabled(self) -> bool:
return bool(self.telegram_bot_token and self.telegram_chat_id)
@property
def blogger_enabled(self) -> bool:
return bool(
self.blogger_client_id
and self.blogger_client_secret
and self.blogger_refresh_token
and self.blogger_blog_id
)
def get_settings() -> Settings:
"""Load settings from environment / .env (called once per process by the CLI)."""