Skip to content

Commit 51fb040

Browse files
committed
feat(workers): implement slack block kit callbacks and isolate ui text to lexicon
1 parent 0e92534 commit 51fb040

4 files changed

Lines changed: 199 additions & 2 deletions

File tree

backend/config/lexicon.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SLACK_UI = {
2+
# Success Notifications
3+
"draft_ready_fallback": "✅ Драфт готовий: {topic}",
4+
"draft_ready_header": "📝 Новий драфт: {topic}",
5+
"ordered_by": "👤 Замовлено: <@{user_id}>",
6+
"btn_publish": "Опублікувати",
7+
"btn_reject": "Відхилити",
8+
# Error Notifications
9+
"error_fallback": "❌ Помилка задачі: {topic}",
10+
"error_header": "🚨 Сталася помилка",
11+
"error_details": "*Тема/Задача:* {topic}\n*Замовник:* <@{user_id}>\n\n*Деталі помилки:*\n```{error_msg}```",
12+
}

backend/config/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Settings(BaseSettings):
4747
QDRANT_PORT: int = 6333
4848
EXTERNAL_QDRANT_PORT: int = 6333
4949

50+
SLACK_BOT_TOKEN: SecretStr = Field(default=...)
5051
# Конфігурація Pydantic
5152
model_config = SettingsConfigDict(
5253
env_file=".env", env_file_encoding="utf-8", extra="ignore"

backend/workers/callbacks.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,135 @@
1-
# backend/workers/callbacks.py
1+
from typing import Any
2+
3+
import httpx
4+
import structlog
5+
6+
from backend.config.lexicon import SLACK_UI
7+
from backend.config.settings import settings
8+
9+
logger = structlog.get_logger()
10+
11+
SLACK_API_URL = "https://slack.com/api/chat.postMessage"
12+
13+
14+
async def _send_slack_message(payload: dict[str, Any]) -> None:
15+
token = (
16+
settings.SLACK_BOT_TOKEN.get_secret_value()
17+
if settings.SLACK_BOT_TOKEN
18+
else None
19+
)
20+
if not token:
21+
logger.error("slack_token_missing", action="aborting_notification")
22+
return
23+
24+
headers = {
25+
"Authorization": f"Bearer {token}",
26+
"Content-Type": "application/json; charset=utf-8",
27+
}
28+
29+
async with httpx.AsyncClient(timeout=10.0) as client:
30+
try:
31+
response = await client.post(SLACK_API_URL, headers=headers, json=payload)
32+
response.raise_for_status()
33+
data = response.json()
34+
if not data.get("ok"):
35+
logger.error(
36+
"slack_api_error", error=data.get("error"), payload=payload
37+
)
38+
except Exception as e:
39+
logger.error(
40+
"slack_network_error", error=str(e), error_type=type(e).__name__
41+
)
42+
43+
44+
async def notify_slack_on_complete(
45+
user_id: str, channel_id: str, draft: str, topic: str
46+
) -> None:
47+
logger.info(
48+
"sending_slack_completion_notification", user_id=user_id, channel_id=channel_id
49+
)
50+
51+
payload = {
52+
"channel": channel_id,
53+
"text": SLACK_UI["draft_ready_fallback"].format(topic=topic),
54+
"blocks": [
55+
{
56+
"type": "header",
57+
"text": {
58+
"type": "plain_text",
59+
"text": SLACK_UI["draft_ready_header"].format(topic=topic),
60+
"emoji": True,
61+
},
62+
},
63+
{"type": "section", "text": {"type": "mrkdwn", "text": f"```{draft}```"}},
64+
{
65+
"type": "context",
66+
"elements": [
67+
{
68+
"type": "mrkdwn",
69+
"text": SLACK_UI["ordered_by"].format(user_id=user_id),
70+
}
71+
],
72+
},
73+
{
74+
"type": "actions",
75+
"elements": [
76+
{
77+
"type": "button",
78+
"text": {
79+
"type": "plain_text",
80+
"text": SLACK_UI["btn_publish"],
81+
"emoji": True,
82+
},
83+
"style": "primary",
84+
"value": "publish",
85+
"action_id": "action_publish_draft",
86+
},
87+
{
88+
"type": "button",
89+
"text": {
90+
"type": "plain_text",
91+
"text": SLACK_UI["btn_reject"],
92+
"emoji": True,
93+
},
94+
"style": "danger",
95+
"value": "reject",
96+
"action_id": "action_reject_draft",
97+
},
98+
],
99+
},
100+
],
101+
}
102+
await _send_slack_message(payload)
103+
104+
105+
async def notify_slack_on_failure(
106+
user_id: str, channel_id: str, error_msg: str, topic: str
107+
) -> None:
108+
logger.info(
109+
"sending_slack_failure_notification", user_id=user_id, channel_id=channel_id
110+
)
111+
112+
payload = {
113+
"channel": channel_id,
114+
"text": SLACK_UI["error_fallback"].format(topic=topic),
115+
"blocks": [
116+
{
117+
"type": "header",
118+
"text": {
119+
"type": "plain_text",
120+
"text": SLACK_UI["error_header"],
121+
"emoji": True,
122+
},
123+
},
124+
{
125+
"type": "section",
126+
"text": {
127+
"type": "mrkdwn",
128+
"text": SLACK_UI["error_details"].format(
129+
topic=topic, user_id=user_id, error_msg=error_msg
130+
),
131+
},
132+
},
133+
],
134+
}
135+
await _send_slack_message(payload)
Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,51 @@
1-
# backend/workers/tasks/publish_post.py
1+
import structlog
2+
from taskiq import TaskiqDepends # type: ignore
3+
4+
from backend.workers.broker import broker
5+
6+
# TODO: Розкоментувати та реалізувати після створення PublisherService та DB сесій
7+
# from backend.workers.dependencies import get_publisher_service, get_db_session
8+
# from backend.services.publisher import PublisherService, PlatformAPIError
9+
10+
logger = structlog.get_logger()
11+
12+
13+
@broker.task(task_name="publish_post", timeout=30, labels={"priority": "medium"})
14+
async def publish_post_task(
15+
post_id: str,
16+
platform: str,
17+
content: str,
18+
# publisher: PublisherService = TaskiqDepends(get_publisher_service),
19+
# db_session = TaskiqDepends(get_db_session)
20+
) -> dict[str, str]:
21+
"""
22+
Фонова задача для публікації контенту в цільову соцмережу.
23+
"""
24+
logger.info("publish_task_started", post_id=post_id, platform=platform)
25+
26+
try:
27+
# TODO: Реальна логіка виклику API платформи
28+
# publish_result = await publisher.publish(platform=platform, content=content)
29+
30+
# TODO: Оновлення статусу в БД
31+
# await db_session.execute(
32+
# update(Post).where(Post.id == post_id).values(status="PUBLISHED", url=publish_result.url)
33+
# )
34+
# await db_session.commit()
35+
36+
logger.info("publish_task_success", post_id=post_id, platform=platform)
37+
return {"status": "success", "post_id": post_id, "platform": platform}
38+
39+
except Exception as e: # Замінити на PlatformAPIError при реалізації
40+
logger.error(
41+
"publish_task_platform_error",
42+
post_id=post_id,
43+
platform=platform,
44+
error=str(e),
45+
)
46+
# TODO: Оновлення статусу в БД на "FAILED"
47+
# await db_session.execute(
48+
# update(Post).where(Post.id == post_id).values(status="FAILED", error_log=str(e))
49+
# )
50+
# await db_session.commit()
51+
raise

0 commit comments

Comments
 (0)