You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

139 lines
5.3 KiB
Python

"""KakaoTalk self-memo notification client."""
from __future__ import annotations
import os
from typing import Optional
from loguru import logger
from tenacity import retry, stop_after_attempt, wait_exponential
KAKAO_MEMO_URL = "https://kapi.kakao.com/v2/api/talk/memo/default/send"
KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token"
MAX_TEXT_LENGTH = 9000 # KakaoTalk text message limit
class KakaoTalkClient:
def __init__(
self,
rest_api_key: Optional[str] = None,
client_secret: Optional[str] = None,
access_token: Optional[str] = None,
refresh_token: Optional[str] = None,
):
self.rest_api_key = rest_api_key or os.environ.get("KAKAO_REST_API_KEY", "")
self.client_secret = client_secret or os.environ.get("KAKAO_CLIENT_SECRET", "")
self.access_token = access_token or os.environ.get("KAKAO_ACCESS_TOKEN", "")
self.refresh_token = refresh_token or os.environ.get("KAKAO_REFRESH_TOKEN", "")
def is_configured(self) -> bool:
return bool(self.rest_api_key and self.access_token)
def send_self_memo(self, text: str) -> bool:
"""Send a self-memo to KakaoTalk. Returns True on success."""
if not self.is_configured():
logger.warning("KakaoTalk not configured (missing tokens)")
return False
# Truncate if needed
if len(text) > MAX_TEXT_LENGTH:
text = text[: MAX_TEXT_LENGTH - 20] + "\n\n[...truncated]"
try:
return self._do_send(text)
except Exception as e:
logger.error(f"KakaoTalk send failed: {e}")
# Try token refresh once
if self.refresh_token:
try:
new_token = self.refresh_access_token()
if new_token:
self.access_token = new_token
return self._do_send(text)
except Exception as e2:
logger.error(f"Token refresh also failed: {e2}")
return False
@retry(stop=stop_after_attempt(2), wait=wait_exponential(multiplier=1, min=1, max=5))
def _do_send(self, text: str) -> bool:
import httpx
from jinja2 import Environment, FileSystemLoader
from gimme_job.utils.paths import project_root
import json
# Build template object
template_path = project_root() / "gimme_job" / "templates" / "kakao_default.json.j2"
if template_path.exists():
env = Environment(loader=FileSystemLoader(str(template_path.parent)))
tmpl = env.get_template("kakao_default.json.j2")
template_object_str = tmpl.render(text=text)
template_object = json.loads(template_object_str)
else:
template_object = {"object_type": "text", "text": text, "link": {}}
response = httpx.post(
KAKAO_MEMO_URL,
headers={"Authorization": f"Bearer {self.access_token}"},
data={"template_object": json.dumps(template_object, ensure_ascii=False)},
timeout=15.0,
)
if response.status_code == 200:
result = response.json()
if result.get("result_code") == 0:
logger.info("KakaoTalk self-memo sent successfully")
return True
else:
logger.warning(f"KakaoTalk API returned: {result}")
return False
elif response.status_code == 401:
raise Exception("Unauthorized — token may be expired")
else:
logger.error(f"KakaoTalk HTTP {response.status_code}: {response.text[:200]}")
return False
def refresh_access_token(self) -> Optional[str]:
"""Refresh the access token using the refresh token."""
import httpx
data = {
"grant_type": "refresh_token",
"client_id": self.rest_api_key,
"refresh_token": self.refresh_token,
}
if self.client_secret:
data["client_secret"] = self.client_secret
response = httpx.post(KAKAO_TOKEN_URL, data=data, timeout=15.0)
response.raise_for_status()
data = response.json()
new_token = data.get("access_token")
if new_token:
logger.info("KakaoTalk access token refreshed")
# Persist to env if possible
os.environ["KAKAO_ACCESS_TOKEN"] = new_token
self._update_env_file(new_token)
return new_token
def _update_env_file(self, new_access_token: str) -> None:
"""Update the .env file with the new access token."""
from gimme_job.utils.paths import project_root
env_path = project_root() / ".env"
if not env_path.exists():
return
try:
content = env_path.read_text()
lines = content.splitlines()
updated = []
found = False
for line in lines:
if line.startswith("KAKAO_ACCESS_TOKEN="):
updated.append(f"KAKAO_ACCESS_TOKEN={new_access_token}")
found = True
else:
updated.append(line)
if not found:
updated.append(f"KAKAO_ACCESS_TOKEN={new_access_token}")
env_path.write_text("\n".join(updated) + "\n")
except Exception as e:
logger.warning(f"Could not update .env with new token: {e}")