lab_resource_manager/interface/slack/
commands.rs1use crate::application::usecases::grant_user_resource_access::GrantUserResourceAccessUseCase;
2use crate::domain::aggregates::identity_link::value_objects::ExternalSystem;
3use crate::domain::common::EmailAddress;
4use slack_morphism::prelude::*;
5use std::sync::Arc;
6use tokio_util::task::TaskTracker;
7use tracing::{error, info};
8
9pub struct SlackCommandHandler {
11 grant_access_usecase: Arc<GrantUserResourceAccessUseCase>,
12 task_tracker: TaskTracker,
13 http_client: reqwest::Client,
14}
15
16impl SlackCommandHandler {
17 pub fn new(grant_access_usecase: Arc<GrantUserResourceAccessUseCase>) -> Self {
18 Self {
19 grant_access_usecase,
20 task_tracker: TaskTracker::new(),
21 http_client: reqwest::Client::new(),
22 }
23 }
24
25 pub async fn shutdown(&self) {
29 self.task_tracker.close();
30 self.task_tracker.wait().await;
31 }
32
33 pub async fn route_slash_command(
35 &self,
36 event: SlackCommandEvent,
37 ) -> Result<SlackCommandEventResponse, Box<dyn std::error::Error + Send + Sync>> {
38 let command = event.command.0.as_str();
39 let text = event.text.as_deref().unwrap_or("");
40 let slack_user_id = event.user_id.to_string();
41 let response_url = event.response_url.clone();
42
43 match command {
44 "/register-calendar" => {
45 self.handle_register_calendar(text, slack_user_id, response_url)
46 .await
47 }
48 "/link-user" => self.handle_link_user(text, response_url).await,
49 _ => Ok(SlackCommandEventResponse::new(
50 SlackMessageContent::new().with_text(format!("不明なコマンド: {}", command)),
51 )),
52 }
53 }
54
55 async fn handle_register_calendar(
56 &self,
57 text: &str,
58 slack_user_id: String,
59 response_url: SlackResponseUrl,
60 ) -> Result<SlackCommandEventResponse, Box<dyn std::error::Error + Send + Sync>> {
61 if text.is_empty() {
62 return Ok(SlackCommandEventResponse::new(
63 SlackMessageContent::new()
64 .with_text("使い方: `/register-calendar <your-email@gmail.com>`".to_string()),
65 ));
66 }
67
68 let grant_access_usecase = self.grant_access_usecase.clone();
69 let email_str = text.to_string();
70
71 self.execute_with_background_response(response_url, || async move {
72 let email = EmailAddress::new(email_str.trim().to_string())
73 .map_err(|e| format!("❌ メールアドレスの形式が不正です: {}", e))?;
74
75 grant_access_usecase
76 .execute(ExternalSystem::Slack, slack_user_id, email.clone())
77 .await
78 .map_err(|e| format!("❌ カレンダー登録に失敗: {}", e))?;
79
80 Ok(format!(
81 "✅ 登録完了!カレンダーへのアクセス権を付与しました: {}",
82 email.as_str()
83 ))
84 })
85 .await
86 }
87
88 async fn handle_link_user(
89 &self,
90 text: &str,
91 response_url: SlackResponseUrl,
92 ) -> Result<SlackCommandEventResponse, Box<dyn std::error::Error + Send + Sync>> {
93 let parts: Vec<&str> = text.split_whitespace().collect();
94 if parts.len() != 2 {
95 return Ok(SlackCommandEventResponse::new(
96 SlackMessageContent::new()
97 .with_text("使い方: `/link-user <@slack_user> <email@gmail.com>`".to_string()),
98 ));
99 }
100
101 let grant_access_usecase = self.grant_access_usecase.clone();
102
103 let slack_mention = parts[0].trim();
105 let target_slack_user_id = slack_mention
106 .strip_prefix("<@")
107 .and_then(|s| s.strip_suffix(">"))
108 .filter(|id| !id.is_empty())
109 .map(|id| id.to_string());
110
111 let target_slack_user_id = match target_slack_user_id {
112 Some(id) => id,
113 None => {
114 return Ok(SlackCommandEventResponse::new(
115 SlackMessageContent::new()
116 .with_text("❌ Slackユーザーの形式が不正です。".to_string()),
117 ));
118 }
119 };
120
121 let email_str = parts[1].to_string();
122
123 self.execute_with_background_response(response_url, || async move {
124 let email = EmailAddress::new(email_str.trim().to_string())
125 .map_err(|e| format!("❌ メールアドレスの形式が不正です: {}", e))?;
126
127 grant_access_usecase
128 .execute(
129 ExternalSystem::Slack,
130 target_slack_user_id.clone(),
131 email.clone(),
132 )
133 .await
134 .map_err(|e| format!("❌ ユーザー紐付けに失敗: {}", e))?;
135
136 Ok(format!(
137 "✅ 紐付け完了!<@{}> に {} のカレンダーアクセス権を付与しました。",
138 target_slack_user_id,
139 email.as_str()
140 ))
141 })
142 .await
143 }
144
145 async fn execute_with_background_response<F, Fut>(
149 &self,
150 response_url: SlackResponseUrl,
151 operation: F,
152 ) -> Result<SlackCommandEventResponse, Box<dyn std::error::Error + Send + Sync>>
153 where
154 F: FnOnce() -> Fut + Send + 'static,
155 Fut: std::future::Future<Output = Result<String, String>> + Send + 'static,
156 {
157 let http_client = self.http_client.clone();
158 self.task_tracker.spawn(async move {
159 let message = match operation().await {
160 Ok(msg) => msg,
161 Err(err) => err,
162 };
163
164 Self::send_followup_message_static(&http_client, &response_url, message).await;
165 });
166
167 Ok(SlackCommandEventResponse::new(
168 SlackMessageContent::new().with_text("⏳ 処理中...".to_string()),
169 ))
170 }
171
172 async fn send_followup_message_static(
176 http_client: &reqwest::Client,
177 response_url: &SlackResponseUrl,
178 message: String,
179 ) {
180 let payload = serde_json::json!({
181 "text": message,
182 "response_type": "in_channel"
183 });
184
185 match http_client
186 .post(response_url.0.as_str())
187 .json(&payload)
188 .send()
189 .await
190 {
191 Ok(_) => info!("✅ フォローアップメッセージを送信しました"),
192 Err(e) => error!("フォローアップメッセージの送信に失敗: {}", e),
193 }
194 }
195}