Initial commit: telegram bot base
This commit is contained in:
195
gitea_client.py
Normal file
195
gitea_client.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""Клиент Gitea API для работы с issues."""
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import httpx
|
||||
|
||||
import config
|
||||
|
||||
BASE = f"{config.GITEA_URL}/api/v1"
|
||||
HEADERS = {"Authorization": f"token {config.GITEA_TOKEN}"}
|
||||
|
||||
|
||||
def _url(path: str) -> str:
|
||||
return f"{BASE}{path}"
|
||||
|
||||
|
||||
async def get_assignees() -> list[dict]:
|
||||
"""Список пользователей, которых можно назначить на issues (username, full_name, id)."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/assignees"),
|
||||
headers=HEADERS,
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def resolve_assignee_by_name(name: str, assignees: list[dict]) -> str | None:
|
||||
"""Найти username по имени/подстроке в assignees. Возвращает login или None."""
|
||||
name = (name or "").strip().lower()
|
||||
if not name:
|
||||
return None
|
||||
for u in assignees:
|
||||
login = (u.get("login") or "").lower()
|
||||
full = (u.get("full_name") or "").lower()
|
||||
if name in login or name in full or (name and (login == name or full == name)):
|
||||
return u.get("login")
|
||||
return None
|
||||
|
||||
|
||||
def parse_deadline(s: str) -> str | None:
|
||||
"""Парсит дату из строки (завтра, ISO date). Возвращает ISO date YYYY-MM-DD или None."""
|
||||
if not s or not isinstance(s, str):
|
||||
return None
|
||||
s = s.strip().lower()
|
||||
if s == "завтра":
|
||||
d = datetime.now(timezone.utc) + timedelta(days=1)
|
||||
return d.strftime("%Y-%m-%d")
|
||||
if s == "сегодня":
|
||||
d = datetime.now(timezone.utc)
|
||||
return d.strftime("%Y-%m-%d")
|
||||
# ISO-like date
|
||||
m = re.match(r"(\d{4})-(\d{2})-(\d{2})", s)
|
||||
if m:
|
||||
return f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
|
||||
return None
|
||||
|
||||
|
||||
async def create_issue(
|
||||
title: str,
|
||||
body: str = "",
|
||||
assignees: list[str] | None = None,
|
||||
deadline: str | None = None,
|
||||
) -> dict:
|
||||
"""Создать issue. assignees — список Gitea username. deadline — ISO date YYYY-MM-DD."""
|
||||
payload = {"title": title}
|
||||
if body:
|
||||
payload["body"] = body
|
||||
if assignees:
|
||||
payload["assignees"] = assignees
|
||||
due = parse_deadline(deadline) if deadline else None
|
||||
if due:
|
||||
payload["due_date"] = due + "T12:00:00Z"
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues"),
|
||||
headers={**HEADERS, "Content-Type": "application/json"},
|
||||
json=payload,
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def get_issues(
|
||||
state: str = "open",
|
||||
assignee: str | None = None,
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
) -> list[dict]:
|
||||
"""Список issues. state: open|closed. assignee: Gitea username."""
|
||||
params = {"state": state, "page": page, "limit": limit}
|
||||
if assignee:
|
||||
params["assignee"] = assignee
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues"),
|
||||
headers=HEADERS,
|
||||
params=params,
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def add_comment(issue_number: int, body: str) -> dict:
|
||||
"""Добавить комментарий к задаче."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/comments"),
|
||||
headers={**HEADERS, "Content-Type": "application/json"},
|
||||
json={"body": body},
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def close_issue(issue_number: int) -> dict:
|
||||
"""Закрыть задачу."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.patch(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}"),
|
||||
headers={**HEADERS, "Content-Type": "application/json"},
|
||||
json={"state": "closed"},
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def start_stopwatch(issue_number: int) -> dict:
|
||||
"""Запустить таймер по задаче."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/stopwatch/start"),
|
||||
headers=HEADERS,
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def stop_stopwatch(issue_number: int) -> dict:
|
||||
"""Остановить таймер по задаче."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/stopwatch/stop"),
|
||||
headers=HEADERS,
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def add_tracked_time(issue_number: int, seconds: int) -> dict:
|
||||
"""Добавить учтённое время к задаче (в секундах)."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/times"),
|
||||
headers={**HEADERS, "Content-Type": "application/json"},
|
||||
json={"time": seconds},
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def set_issue_deadline(issue_number: int, deadline: str | None) -> dict:
|
||||
"""Установить или снять срок задачи. deadline — дата YYYY-MM-DD или None."""
|
||||
parsed = parse_deadline(deadline) if deadline else None
|
||||
due = (parsed + "T12:00:00Z") if parsed else None
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/deadline"),
|
||||
headers={**HEADERS, "Content-Type": "application/json"},
|
||||
json={"due_date": due},
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
async def set_issue_assignees(issue_number: int, assignees: list[str]) -> dict:
|
||||
"""Заменить ответственных по задаче (список Gitea username)."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.patch(
|
||||
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}"),
|
||||
headers={**HEADERS, "Content-Type": "application/json"},
|
||||
json={"assignees": assignees},
|
||||
timeout=15.0,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
Reference in New Issue
Block a user