Skip to content

Commit 61ca84c

Browse files
committed
feat(slack): implement App Home, document upload and event verification
- Added /events endpoint for URL verification and Slack event handling. - Implemented App Home interface with quick action buttons. - Added modal views for document uploads and post generation. - Updated app manifest and UI lexicon. - Fixed critical indentation errors in interaction routes.
1 parent 463be1a commit 61ca84c

4 files changed

Lines changed: 112 additions & 17 deletions

File tree

backend/api/routes/feedback.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
build_app_home,
1313
build_approval_modal,
1414
build_generation_modal,
15+
build_upload_modal,
1516
)
1617

1718
logger = structlog.get_logger()
@@ -57,7 +58,7 @@ async def slack_interactions(request: Request):
5758
raise HTTPException(status_code=400, detail="Missing payload")
5859

5960
payload = json.loads(str(payload_str))
60-
user_id = payload.get("user", {}).get("id")
61+
user_id = str(payload.get("user", {}).get("id", "")) # Cast to str for Pylance
6162
interaction_type = payload.get("type")
6263

6364
slack_token = (
@@ -66,8 +67,8 @@ async def slack_interactions(request: Request):
6667
headers = {"Authorization": f"Bearer {slack_token}"}
6768

6869
if interaction_type == "block_actions":
69-
response_url = payload.get("response_url")
70-
trigger_id = payload.get("trigger_id")
70+
response_url = str(payload.get("response_url", ""))
71+
trigger_id = str(payload.get("trigger_id", ""))
7172
action = payload.get("actions", [])[0]
7273
action_id = action.get("action_id")
7374

@@ -76,12 +77,24 @@ async def slack_interactions(request: Request):
7677
draft_text = raw_draft.replace("```", "").strip()
7778
topic = "Медичний пост"
7879

79-
if action_id == "action_publish_draft":
80+
# УСІ перевірки action_id мають бути всередині цього блоку
81+
if action_id == "action_open_upload_modal":
82+
logger.info("slack_home_upload_btn_clicked", user_id=user_id)
83+
modal_view = build_upload_modal()
84+
async with httpx.AsyncClient() as client:
85+
res = await client.post(
86+
"[https://slack.com/api/views.open](https://slack.com/api/views.open)",
87+
headers=headers,
88+
json={"trigger_id": trigger_id, "view": modal_view},
89+
)
90+
if not res.json().get("ok"):
91+
logger.error("slack_modal_error", error=res.json())
92+
93+
elif action_id == "action_publish_draft":
8094
logger.info("slack_draft_approved", user_id=user_id)
8195
await publish_post_task.kiq(
8296
post_id="temp_id", platform="telegram", content=draft_text
8397
)
84-
8598
async with httpx.AsyncClient() as client:
8699
await client.post(
87100
response_url,
@@ -126,9 +139,8 @@ async def slack_interactions(request: Request):
126139
topic=topic, draft=draft_text, platform="telegram"
127140
)
128141
async with httpx.AsyncClient() as client:
129-
# ВИПРАВЛЕНО URL
130142
res = await client.post(
131-
"https://slack.com/api/views.open",
143+
"[https://slack.com/api/views.open](https://slack.com/api/views.open)",
132144
headers=headers,
133145
json={"trigger_id": trigger_id, "view": modal_view},
134146
)
@@ -137,12 +149,10 @@ async def slack_interactions(request: Request):
137149

138150
elif action_id == "action_regenerate_draft":
139151
logger.info("slack_draft_regenerate", user_id=user_id)
140-
channel_id = payload.get("channel", {}).get("id") # ДОДАНО
141-
142-
await generate_draft_task.kiq( # type: ignore[call-overload]
152+
channel_id = str(payload.get("channel", {}).get("id", ""))
153+
await generate_draft_task.kiq(
143154
topic=topic, platform="telegram", user_id=user_id, channel_id=channel_id
144-
)
145-
155+
) # type: ignore
146156
async with httpx.AsyncClient() as client:
147157
await client.post(
148158
response_url,
@@ -166,12 +176,13 @@ async def slack_interactions(request: Request):
166176
modal_view = build_generation_modal(channel_id=user_id)
167177
async with httpx.AsyncClient() as client:
168178
res = await client.post(
169-
"https://slack.com/api/views.open",
179+
"[https://slack.com/api/views.open](https://slack.com/api/views.open)",
170180
headers=headers,
171181
json={"trigger_id": trigger_id, "view": modal_view},
172182
)
173183
if not res.json().get("ok"):
174184
logger.error("slack_modal_error", error=res.json())
185+
175186
return Response(status_code=200)
176187

177188
elif interaction_type == "view_submission":
@@ -252,7 +263,34 @@ async def slack_interactions(request: Request):
252263
media_type="application/json",
253264
status_code=200,
254265
)
266+
# --- СЦЕНАРІЙ 3: Завантаження гайдлайну ---
267+
elif callback_id == "modal_upload_guideline":
268+
# Slack повертає масив об'єктів файлів
269+
files = (
270+
state_values.get("block_file_upload", {})
271+
.get("input_file", {})
272+
.get("files", [])
273+
)
274+
if not files:
275+
return Response(status_code=400)
276+
277+
file_info = files[0]
278+
# file_url = file_info.get("url_private_download")
279+
file_name = file_info.get("name")
280+
281+
logger.info("slack_file_uploaded", user_id=user_id, file_name=file_name)
282+
283+
# TODO: Тут ми створимо таск `ingest_document_task`, який буде
284+
# скачувати файл за file_url (використовуючи Slack Token),
285+
# парсити PDF/TXT та векторизувати його у Qdrant.
286+
# await ingest_document_task.kiq(file_url=file_url, file_name=file_name)
255287

288+
# Закриваємо модалку
289+
return Response(
290+
content=json.dumps({"response_action": "clear"}),
291+
media_type="application/json",
292+
status_code=200,
293+
)
256294
return Response(status_code=200)
257295

258296

backend/config/lexicon.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@
4242
"home_welcome": "Вітаю у Seratonin Script! 🧠",
4343
"home_description": "Це твій центр керування медичним контентом. Натисни кнопку нижче, щоб почати.",
4444
"home_btn_create": "✨ Створити новий пост",
45+
"home_btn_upload": "📚 Завантажити гайдлайн",
46+
# --- Upload Modal ---
47+
"upload_modal_title": "База знань",
48+
"upload_modal_submit": "Завантажити",
49+
"upload_modal_input_label": "Оберіть файл (PDF/TXT)",
4550
}

slack_app/manifest.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ oauth_config:
2222
- commands
2323
- chat:write
2424
- users:read
25+
- files:read
2526
settings:
2627
interactivity:
2728
is_enabled: true
2829
request_url: https://optifood-ai.duckdns.org/api/v1/slack/interactions
2930
org_deploy_enabled: false
3031
socket_mode_enabled: false
31-
token_rotation_enabled: false
32+
token_rotation_enabled: false
33+
event_subscriptions:
34+
request_url: https://optifood-ai.duckdns.org/api/v1/slack/events
35+
bot_events:
36+
- app_home_opened

slack_app/utils/block_builder.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,6 @@ def build_generation_modal(channel_id: str) -> dict[str, Any]:
224224

225225

226226
def build_app_home() -> dict[str, Any]:
227-
"""Генерує інтерфейс вкладки Home."""
228227
return {
229228
"type": "home",
230229
"blocks": [
@@ -252,9 +251,57 @@ def build_app_home() -> dict[str, Any]:
252251
"emoji": True,
253252
},
254253
"style": "primary",
255-
"action_id": "action_open_generation_modal", # Та сама логіка, що й для команди
256-
}
254+
"action_id": "action_open_generation_modal",
255+
},
256+
{
257+
"type": "button",
258+
"text": {
259+
"type": "plain_text",
260+
"text": SLACK_UI["home_btn_upload"],
261+
"emoji": True,
262+
},
263+
"action_id": "action_open_upload_modal",
264+
},
257265
],
258266
},
259267
],
260268
}
269+
270+
271+
def build_upload_modal() -> dict[str, Any]:
272+
"""Генерує модальне вікно для завантаження файлу в базу знань."""
273+
return {
274+
"type": "modal",
275+
"callback_id": "modal_upload_guideline",
276+
"title": {
277+
"type": "plain_text",
278+
"text": SLACK_UI["upload_modal_title"],
279+
"emoji": True,
280+
},
281+
"submit": {
282+
"type": "plain_text",
283+
"text": SLACK_UI["upload_modal_submit"],
284+
"emoji": True,
285+
},
286+
"close": {
287+
"type": "plain_text",
288+
"text": SLACK_UI["modal_cancel"],
289+
"emoji": True,
290+
},
291+
"blocks": [
292+
{
293+
"type": "input",
294+
"block_id": "block_file_upload",
295+
"label": {
296+
"type": "plain_text",
297+
"text": SLACK_UI["upload_modal_input_label"],
298+
},
299+
"element": {
300+
"type": "file_input",
301+
"action_id": "input_file",
302+
"filetypes": ["pdf", "txt"], # Обмежуємо формати
303+
"max_files": 1,
304+
},
305+
}
306+
],
307+
}

0 commit comments

Comments
 (0)