Initial commit: telegram bot base

This commit is contained in:
2026-02-16 10:45:01 +05:00
commit 3627117ea2
9 changed files with 809 additions and 0 deletions

195
gitea_client.py Normal file
View 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()