@@ -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_cli :: < 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_cli :: < 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_cli :: < 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_cli :: < 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