Skip to content

Commit ea05c71

Browse files
committed
feat(slack): add schedule draft flow via dedicated modal
- Add btn_schedule, schedule_modal_* keys to lexicon - Add build_schedule_modal with datetimepicker to block_builder - Add Schedule button to build_draft_card - Handle action_schedule_draft (opens modal) in feedback router - Handle modal_schedule_draft submission: save scheduled_at + SCHEDULED status
1 parent cfe3a19 commit ea05c71

File tree

3 files changed

+132
-5
lines changed

3 files changed

+132
-5
lines changed

backend/api/routes/feedback.py

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from datetime import datetime, timezone
23

34
import httpx
45
import structlog
@@ -21,6 +22,7 @@
2122
build_approval_modal,
2223
build_draft_card,
2324
build_generation_modal,
25+
build_schedule_modal,
2426
build_upload_modal,
2527
)
2628

@@ -117,8 +119,31 @@ async def slack_interactions(
117119
draft=db_draft.content or "",
118120
platform=db_draft.platform,
119121
draft_id=str(db_draft.id),
120-
channel_id="", # Порожньо, бо відкриваємо не з чату
121-
message_ts="", # Порожньо, бо відкриваємо не з чату
122+
channel_id="",
123+
message_ts="",
124+
)
125+
async with httpx.AsyncClient() as client:
126+
await client.post(
127+
"https://slack.com/api/views.open",
128+
headers=headers,
129+
json={"trigger_id": trigger_id, "view": modal_view},
130+
)
131+
return Response(status_code=200)
132+
133+
if action_id == "action_schedule_draft":
134+
if draft_id.isdigit():
135+
repo = DraftRepository(session)
136+
db_draft = await repo.get_by_id(int(draft_id))
137+
if db_draft:
138+
sched_ts = (
139+
int(db_draft.scheduled_at.timestamp())
140+
if db_draft.scheduled_at
141+
else None
142+
)
143+
modal_view = build_schedule_modal(
144+
draft_id=draft_id,
145+
platform=platform,
146+
scheduled_at=sched_ts,
122147
)
123148
async with httpx.AsyncClient() as client:
124149
await client.post(
@@ -363,7 +388,51 @@ async def slack_interactions(
363388
status_code=200,
364389
)
365390

366-
# --- СЦЕНАРІЙ 3: Завантаження гайдлайну ---
391+
# --- СЦЕНАРІЙ 3: Планування публікації ---
392+
elif callback_id == "modal_schedule_draft":
393+
metadata_parts = view.get("private_metadata", "").split("|")
394+
draft_id = metadata_parts[0] if len(metadata_parts) > 0 else ""
395+
platform = metadata_parts[1] if len(metadata_parts) > 1 else "telegram"
396+
397+
schedule_timestamp = (
398+
state_values.get("block_schedule_time", {})
399+
.get("input_schedule_time", {})
400+
.get("selected_date_time")
401+
)
402+
403+
if not schedule_timestamp or not draft_id.isdigit():
404+
return Response(
405+
content=json.dumps({
406+
"response_action": "errors",
407+
"errors": {
408+
"block_schedule_time": SLACK_UI["schedule_no_time_error"]
409+
},
410+
}),
411+
media_type="application/json",
412+
status_code=200,
413+
)
414+
415+
scheduled_at = datetime.fromtimestamp(int(schedule_timestamp), tz=timezone.utc)
416+
repo = DraftRepository(session)
417+
await repo.update(
418+
int(draft_id),
419+
DraftUpdate(status=DraftStatus.SCHEDULED, scheduled_at=scheduled_at),
420+
)
421+
422+
logger.info(
423+
"draft_scheduled",
424+
draft_id=draft_id,
425+
platform=platform,
426+
scheduled_at=scheduled_at.isoformat(),
427+
)
428+
429+
return Response(
430+
content=json.dumps({"response_action": "clear"}),
431+
media_type="application/json",
432+
status_code=200,
433+
)
434+
435+
# --- СЦЕНАРІЙ 4: Завантаження гайдлайну ---
367436
elif callback_id == "modal_upload_guideline":
368437
files = (
369438
state_values.get("block_file_upload", {})
@@ -406,22 +475,32 @@ async def slack_events(
406475
event = data.get("event", {})
407476
user_id = event.get("user")
408477

478+
logger.info("slack_event_received", event_type=event.get("type"), user_id=user_id)
479+
409480
if event.get("type") == "app_home_opened":
410481
# 1. Витягуємо останні 10 драфтів
411482
repo = DraftRepository(session)
412483
recent_drafts = await repo.get_recent_drafts(limit=10)
413484

485+
logger.info("slack_home_opened", user_id=user_id, drafts_count=len(recent_drafts))
486+
414487
# 2. Рендеримо дашборд
415488
slack_token = (
416489
settings.SLACK_BOT_TOKEN.get_secret_value()
417490
if hasattr(settings.SLACK_BOT_TOKEN, "get_secret_value")
418491
else settings.SLACK_BOT_TOKEN
419492
)
493+
home_view = build_app_home(drafts=recent_drafts)
420494
async with httpx.AsyncClient() as client:
421-
await client.post(
495+
res = await client.post(
422496
"https://slack.com/api/views.publish",
423497
headers={"Authorization": f"Bearer {slack_token}"},
424-
json={"user_id": user_id, "view": build_app_home(drafts=recent_drafts)},
498+
json={"user_id": user_id, "view": home_view},
425499
)
500+
resp_data = res.json()
501+
if not resp_data.get("ok"):
502+
logger.error("slack_home_publish_error", error=resp_data)
503+
else:
504+
logger.info("slack_home_published", user_id=user_id)
426505

427506
return Response(status_code=200)

backend/config/lexicon.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,11 @@
5757
# --- Upload Notifications ---
5858
"upload_success": "✅ Гайдлайн *{file_name}* успішно завантажено та векторизовано у базу знань!",
5959
"upload_failure": "❌ *Помилка* обробки гайдлайну *{file_name}*.\n\nДеталі:\n```{error_msg}```",
60+
# --- Schedule ---
61+
"btn_schedule": "🕒 Запланувати",
62+
"schedule_modal_title": "Планування публікації",
63+
"schedule_modal_submit": "Запланувати",
64+
"schedule_modal_label": "📅 Час публікації (UTC)",
65+
"schedule_success": "🕒 Пост успішно заплановано на *{scheduled_at}* UTC!\n\n_Ви можете побачити його у вкладці Home._",
66+
"schedule_no_time_error": "⚠️ Вкажіть час публікації у формі нижче.",
6067
}

slack_app/utils/block_builder.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ def build_draft_card(
8484
"value": f"{draft_id}|{platform}",
8585
"action_id": "action_publish_draft",
8686
},
87+
{
88+
"type": "button",
89+
"text": {
90+
"type": "plain_text",
91+
"text": SLACK_UI["btn_schedule"],
92+
"emoji": True,
93+
},
94+
"value": f"{draft_id}|{platform}",
95+
"action_id": "action_schedule_draft",
96+
},
8797
{
8898
"type": "button",
8999
"text": {
@@ -197,6 +207,37 @@ def build_approval_modal(
197207
}
198208

199209

210+
def build_schedule_modal(draft_id: str, platform: str, scheduled_at: int | None = None) -> dict[str, Any]:
211+
"""Генерує мінімальну модалку для вибору часу планування публікації."""
212+
element: dict[str, Any] = {
213+
"type": "datetimepicker",
214+
"action_id": "input_schedule_time",
215+
}
216+
if scheduled_at is not None:
217+
element["initial_date_time"] = scheduled_at
218+
219+
return {
220+
"type": "modal",
221+
"callback_id": "modal_schedule_draft",
222+
"private_metadata": f"{draft_id}|{platform}",
223+
"title": {"type": "plain_text", "text": SLACK_UI["schedule_modal_title"], "emoji": True},
224+
"submit": {"type": "plain_text", "text": SLACK_UI["schedule_modal_submit"], "emoji": True},
225+
"close": {"type": "plain_text", "text": SLACK_UI["modal_cancel"], "emoji": True},
226+
"blocks": [
227+
{
228+
"type": "input",
229+
"block_id": "block_schedule_time",
230+
"element": element,
231+
"label": {
232+
"type": "plain_text",
233+
"text": SLACK_UI["schedule_modal_label"],
234+
"emoji": True,
235+
},
236+
}
237+
],
238+
}
239+
240+
200241
def build_generation_modal(channel_id: str) -> dict[str, Any]:
201242
"""Генерує стартове модальне вікно для вводу теми та платформи."""
202243
return {

0 commit comments

Comments
 (0)