|
1 | 1 | pub mod api; |
2 | 2 | pub mod client; |
3 | | -pub mod commands; |
| 3 | +mod commands; |
4 | 4 |
|
5 | 5 | use crate::db::notifications::add_metadata; |
6 | 6 | use crate::db::notifications::{self, delete_ping, move_indices, record_ping, Identifier}; |
@@ -172,95 +172,133 @@ async fn process_zulip_request(ctx: &Context, req: Request) -> anyhow::Result<Op |
172 | 172 | handle_command(ctx, gh_id, &req.data, &req.message).await |
173 | 173 | } |
174 | 174 |
|
175 | | -fn handle_command<'a>( |
| 175 | +async fn handle_command<'a>( |
176 | 176 | ctx: &'a Context, |
177 | | - gh_id: u64, |
| 177 | + mut gh_id: u64, |
178 | 178 | command: &'a str, |
179 | 179 | message_data: &'a Message, |
180 | | -) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<Option<String>>> + Send + 'a>> |
181 | | -{ |
182 | | - Box::pin(async move { |
183 | | - log::trace!("handling zulip command {:?}", command); |
184 | | - let words: Vec<&str> = command.split_whitespace().collect(); |
| 180 | +) -> anyhow::Result<Option<String>> { |
| 181 | + log::trace!("handling zulip command {:?}", command); |
| 182 | + let words: Vec<&str> = command.split_whitespace().collect(); |
185 | 183 |
|
| 184 | + // Missing stream means that this is a direct message |
| 185 | + if message_data.stream_id.is_none() { |
| 186 | + // Handle impersonation |
| 187 | + let mut impersonated = false; |
186 | 188 | if let Some(&"as") = words.get(0) { |
187 | | - return execute_for_other_user(&ctx, words.iter().skip(1).copied(), message_data) |
188 | | - .await |
189 | | - .map_err(|e| { |
190 | | - format_err!("Failed to parse; expected `as <username> <command...>`: {e:?}.") |
191 | | - }); |
| 189 | + if let Some(username) = words.get(1) { |
| 190 | + impersonated = true; |
| 191 | + |
| 192 | + // Impersonate => change actual gh_id |
| 193 | + gh_id = match get_id_for_username(&ctx.github, username) |
| 194 | + .await |
| 195 | + .context("getting ID of github user")? |
| 196 | + { |
| 197 | + Some(id) => id.try_into().unwrap(), |
| 198 | + None => anyhow::bail!("Can only authorize for other GitHub users."), |
| 199 | + }; |
| 200 | + } else { |
| 201 | + return Err(anyhow::anyhow!( |
| 202 | + "Failed to parse command; expected `as <username> <command...>`." |
| 203 | + )); |
| 204 | + } |
192 | 205 | } |
193 | 206 |
|
194 | | - // Missing stream means that this is a direct message |
195 | | - if message_data.stream_id.is_none() { |
196 | | - let cmd = parse_no_color::<ChatCommand, _>(words.into_iter())?; |
197 | | - match cmd { |
198 | | - ChatCommand::Acknowledge { identifier } => { |
199 | | - acknowledge(&ctx, gh_id, (&identifier).into()).await |
200 | | - } |
201 | | - ChatCommand::Add { url, description } => { |
202 | | - add_notification(&ctx, gh_id, &url, &description.join(" ")).await |
203 | | - } |
204 | | - ChatCommand::Move { from, to } => move_notification(&ctx, gh_id, from, to).await, |
205 | | - ChatCommand::Meta { index, description } => { |
206 | | - add_meta_notification(&ctx, gh_id, index, &description.join(" ")).await |
207 | | - } |
208 | | - ChatCommand::Whoami => whoami_cmd(&ctx, gh_id).await, |
209 | | - ChatCommand::Lookup(cmd) => lookup_cmd(&ctx, cmd).await, |
210 | | - ChatCommand::Work(cmd) => workqueue_commands(ctx, gh_id, cmd).await, |
| 207 | + let cmd = parse_no_color::<ChatCommand, _>(words.into_iter())?; |
| 208 | + let output = match cmd { |
| 209 | + ChatCommand::Acknowledge { identifier } => { |
| 210 | + acknowledge(&ctx, gh_id, (&identifier).into()).await |
211 | 211 | } |
212 | | - } else { |
213 | | - // We are in a stream, where someone wrote `@**triagebot** <command(s)>` |
214 | | - let cmd_index = words |
| 212 | + ChatCommand::Add { url, description } => { |
| 213 | + add_notification(&ctx, gh_id, &url, &description.join(" ")).await |
| 214 | + } |
| 215 | + ChatCommand::Move { from, to } => move_notification(&ctx, gh_id, from, to).await, |
| 216 | + ChatCommand::Meta { index, description } => { |
| 217 | + add_meta_notification(&ctx, gh_id, index, &description.join(" ")).await |
| 218 | + } |
| 219 | + ChatCommand::Whoami => whoami_cmd(&ctx, gh_id).await, |
| 220 | + ChatCommand::Lookup(cmd) => lookup_cmd(&ctx, cmd).await, |
| 221 | + ChatCommand::Work(cmd) => workqueue_commands(ctx, gh_id, cmd).await, |
| 222 | + }; |
| 223 | + |
| 224 | + let output = output?; |
| 225 | + |
| 226 | + // Let the impersonated person know about the impersonation |
| 227 | + if impersonated { |
| 228 | + let impersonated_zulip_id = to_zulip_id(&ctx.github, gh_id) |
| 229 | + .await? |
| 230 | + .ok_or_else(|| anyhow::anyhow!("Zulip user for GitHub ID {gh_id} was not found"))?; |
| 231 | + let users = ctx.zulip.get_zulip_users().await?; |
| 232 | + let user = users |
215 | 233 | .iter() |
216 | | - .position(|w| *w == "@**triagebot**") |
217 | | - .unwrap_or(words.len()); |
218 | | - let cmd_index = cmd_index + 1; |
219 | | - if cmd_index >= words.len() { |
220 | | - return Ok(Some("Unknown command".to_string())); |
| 234 | + .find(|m| m.user_id == impersonated_zulip_id) |
| 235 | + .ok_or_else(|| format_err!("Could not find Zulip user email."))?; |
| 236 | + |
| 237 | + let sender = &message_data.sender_full_name; |
| 238 | + let message = format!( |
| 239 | + "{sender} ran `{command}` on your behalf. Output:\n{}", |
| 240 | + output.as_deref().unwrap_or("<empty>") |
| 241 | + ); |
| 242 | + |
| 243 | + MessageApiRequest { |
| 244 | + recipient: Recipient::Private { |
| 245 | + id: user.user_id, |
| 246 | + email: &user.email, |
| 247 | + }, |
| 248 | + content: &message, |
221 | 249 | } |
222 | | - let cmd = parse_no_color::<StreamCommand, _>(words[cmd_index..].into_iter().copied())?; |
223 | | - match cmd { |
224 | | - StreamCommand::EndTopic => { |
225 | | - post_waiter(&ctx, message_data, WaitingMessage::end_topic()) |
226 | | - .await |
227 | | - .map_err(|e| format_err!("Failed to await at this time: {e:?}")) |
228 | | - } |
229 | | - StreamCommand::EndMeeting => { |
230 | | - post_waiter(&ctx, message_data, WaitingMessage::end_meeting()) |
231 | | - .await |
232 | | - .map_err(|e| format_err!("Failed to await at this time: {e:?}")) |
233 | | - } |
234 | | - StreamCommand::Read => { |
235 | | - post_waiter(&ctx, message_data, WaitingMessage::start_reading()) |
236 | | - .await |
237 | | - .map_err(|e| format_err!("Failed to await at this time: {e:?}")) |
238 | | - } |
239 | | - StreamCommand::PingGoals { |
240 | | - threshold, |
241 | | - next_update, |
242 | | - } => { |
243 | | - if project_goals::check_project_goal_acl(&ctx.github, gh_id).await? { |
244 | | - ping_project_goals_owners( |
245 | | - &ctx.github, |
246 | | - &ctx.zulip, |
247 | | - false, |
248 | | - threshold as i64, |
249 | | - &format!("on {next_update}"), |
250 | | - ) |
251 | | - .await |
252 | | - .map_err(|e| format_err!("Failed to await at this time: {e:?}"))?; |
253 | | - Ok(None) |
254 | | - } else { |
255 | | - Err(format_err!( |
| 250 | + .send(&ctx.zulip) |
| 251 | + .await?; |
| 252 | + } |
| 253 | + |
| 254 | + Ok(output) |
| 255 | + } else { |
| 256 | + // We are in a stream, where someone wrote `@**triagebot** <command(s)>` |
| 257 | + let cmd_index = words |
| 258 | + .iter() |
| 259 | + .position(|w| *w == "@**triagebot**") |
| 260 | + .unwrap_or(words.len()); |
| 261 | + let cmd_index = cmd_index + 1; |
| 262 | + if cmd_index >= words.len() { |
| 263 | + return Ok(Some("Unknown command".to_string())); |
| 264 | + } |
| 265 | + let cmd = parse_no_color::<StreamCommand, _>(words[cmd_index..].into_iter().copied())?; |
| 266 | + match cmd { |
| 267 | + StreamCommand::EndTopic => post_waiter(&ctx, message_data, WaitingMessage::end_topic()) |
| 268 | + .await |
| 269 | + .map_err(|e| format_err!("Failed to await at this time: {e:?}")), |
| 270 | + StreamCommand::EndMeeting => { |
| 271 | + post_waiter(&ctx, message_data, WaitingMessage::end_meeting()) |
| 272 | + .await |
| 273 | + .map_err(|e| format_err!("Failed to await at this time: {e:?}")) |
| 274 | + } |
| 275 | + StreamCommand::Read => post_waiter(&ctx, message_data, WaitingMessage::start_reading()) |
| 276 | + .await |
| 277 | + .map_err(|e| format_err!("Failed to await at this time: {e:?}")), |
| 278 | + StreamCommand::PingGoals { |
| 279 | + threshold, |
| 280 | + next_update, |
| 281 | + } => { |
| 282 | + if project_goals::check_project_goal_acl(&ctx.github, gh_id).await? { |
| 283 | + ping_project_goals_owners( |
| 284 | + &ctx.github, |
| 285 | + &ctx.zulip, |
| 286 | + false, |
| 287 | + threshold as i64, |
| 288 | + &format!("on {next_update}"), |
| 289 | + ) |
| 290 | + .await |
| 291 | + .map_err(|e| format_err!("Failed to await at this time: {e:?}"))?; |
| 292 | + Ok(None) |
| 293 | + } else { |
| 294 | + Err(format_err!( |
256 | 295 | "That command is only permitted for those running the project-goal program.", |
257 | 296 | )) |
258 | | - } |
259 | 297 | } |
260 | | - StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip), |
261 | 298 | } |
| 299 | + StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip), |
262 | 300 | } |
263 | | - }) |
| 301 | + } |
264 | 302 | } |
265 | 303 |
|
266 | 304 | /// Commands for working with the workqueue, e.g. showing how many PRs are assigned |
@@ -545,82 +583,6 @@ async fn lookup_zulip_username(ctx: &Context, gh_username: &str) -> anyhow::Resu |
545 | 583 | )) |
546 | 584 | } |
547 | 585 |
|
548 | | -// This does two things: |
549 | | -// * execute the command for the other user |
550 | | -// * tell the user executed for that a command was run as them by the user |
551 | | -// given. |
552 | | -async fn execute_for_other_user( |
553 | | - ctx: &Context, |
554 | | - mut words: impl Iterator<Item = &str>, |
555 | | - message_data: &Message, |
556 | | -) -> anyhow::Result<Option<String>> { |
557 | | - // username is a GitHub username, not a Zulip username |
558 | | - let username = match words.next() { |
559 | | - Some(username) => username, |
560 | | - None => anyhow::bail!("no username provided"), |
561 | | - }; |
562 | | - let user_id = match get_id_for_username(&ctx.github, username) |
563 | | - .await |
564 | | - .context("getting ID of github user")? |
565 | | - { |
566 | | - Some(id) => id.try_into().unwrap(), |
567 | | - None => anyhow::bail!("Can only authorize for other GitHub users."), |
568 | | - }; |
569 | | - let mut command = words.fold(String::new(), |mut acc, piece| { |
570 | | - acc.push_str(piece); |
571 | | - acc.push(' '); |
572 | | - acc |
573 | | - }); |
574 | | - let command = if command.is_empty() { |
575 | | - anyhow::bail!("no command provided") |
576 | | - } else { |
577 | | - assert_eq!(command.pop(), Some(' ')); // pop trailing space |
578 | | - command |
579 | | - }; |
580 | | - |
581 | | - let members = ctx |
582 | | - .zulip |
583 | | - .get_zulip_users() |
584 | | - .await |
585 | | - .map_err(|e| format_err!("Failed to get list of zulip users: {e:?}."))?; |
586 | | - |
587 | | - // Map GitHub `user_id` to `zulip_user_id`. |
588 | | - let zulip_user_id = match to_zulip_id(&ctx.github, user_id).await { |
589 | | - Ok(Some(id)) => id as u64, |
590 | | - Ok(None) => anyhow::bail!("Could not find Zulip ID for GitHub ID: {user_id}"), |
591 | | - Err(e) => anyhow::bail!("Could not find Zulip ID for GitHub id {user_id}: {e:?}"), |
592 | | - }; |
593 | | - |
594 | | - let user = members |
595 | | - .iter() |
596 | | - .find(|m| m.user_id == zulip_user_id) |
597 | | - .ok_or_else(|| format_err!("Could not find Zulip user email."))?; |
598 | | - |
599 | | - let output = handle_command(ctx, user_id, &command, message_data) |
600 | | - .await? |
601 | | - .unwrap_or_default(); |
602 | | - |
603 | | - // At this point, the command has been run. |
604 | | - let sender = &message_data.sender_full_name; |
605 | | - let message = format!("{sender} ran `{command}` with output `{output}` as you."); |
606 | | - |
607 | | - let res = MessageApiRequest { |
608 | | - recipient: Recipient::Private { |
609 | | - id: user.user_id, |
610 | | - email: &user.email, |
611 | | - }, |
612 | | - content: &message, |
613 | | - } |
614 | | - .send(&ctx.zulip) |
615 | | - .await; |
616 | | - |
617 | | - if let Err(err) = res { |
618 | | - log::error!("Failed to notify real user about command: {:?}", err); |
619 | | - } |
620 | | - |
621 | | - Ok(Some(output)) |
622 | | -} |
623 | | - |
624 | 586 | #[derive(serde::Serialize)] |
625 | 587 | pub(crate) struct MessageApiRequest<'a> { |
626 | 588 | pub(crate) recipient: Recipient<'a>, |
|
0 commit comments