@@ -172,103 +172,162 @@ 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
+ let impersonated_gh_id = match get_id_for_username ( & ctx. github , username)
191
+ . await
192
+ . context ( "getting ID of github user" ) ?
193
+ {
194
+ Some ( id) => id. try_into ( ) ?,
195
+ None => anyhow:: bail!( "Can only authorize for other GitHub users." ) ,
196
+ } ;
197
+
198
+ // Impersonate => change actual gh_id
199
+ if impersonated_gh_id != gh_id {
200
+ impersonated = true ;
201
+ gh_id = impersonated_gh_id;
202
+ }
203
+ } else {
204
+ return Err ( anyhow:: anyhow!(
205
+ "Failed to parse command; expected `as <username> <command...>`."
206
+ ) ) ;
207
+ }
192
208
}
193
209
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 ,
210
+ let cmd = parse_cli :: < ChatCommand , _ > ( words. into_iter ( ) ) ?;
211
+ let output = match & cmd {
212
+ ChatCommand :: Acknowledge { identifier } => {
213
+ acknowledge ( & ctx, gh_id, identifier. into ( ) ) . await
214
+ }
215
+ ChatCommand :: Add { url, description } => {
216
+ add_notification ( & ctx, gh_id, & url, & description. join ( " " ) ) . await
211
217
}
212
- } else {
213
- // We are in a stream, where someone wrote `@**triagebot** <command(s)>`
214
- let cmd_index = words
218
+ ChatCommand :: Move { from, to } => move_notification ( & ctx, gh_id, * from, * to) . await ,
219
+ ChatCommand :: Meta { index, description } => {
220
+ add_meta_notification ( & ctx, gh_id, * index, & description. join ( " " ) ) . await
221
+ }
222
+ ChatCommand :: Whoami => whoami_cmd ( & ctx, gh_id) . await ,
223
+ ChatCommand :: Lookup ( cmd) => lookup_cmd ( & ctx, cmd) . await ,
224
+ ChatCommand :: Work ( cmd) => workqueue_commands ( ctx, gh_id, cmd) . await ,
225
+ } ;
226
+
227
+ let output = output?;
228
+
229
+ // Let the impersonated person know about the impersonation if the command was sensitive
230
+ if impersonated && is_sensitive_command ( & cmd) {
231
+ let impersonated_zulip_id = to_zulip_id ( & ctx. github , gh_id)
232
+ . await ?
233
+ . ok_or_else ( || anyhow:: anyhow!( "Zulip user for GitHub ID {gh_id} was not found" ) ) ?;
234
+ let users = ctx. zulip . get_zulip_users ( ) . await ?;
235
+ let user = users
215
236
. 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 ( ) ) ) ;
237
+ . find ( |m| m. user_id == impersonated_zulip_id)
238
+ . ok_or_else ( || format_err ! ( "Could not find Zulip user email." ) ) ?;
239
+
240
+ let sender = & message_data. sender_full_name ;
241
+ let message = format ! (
242
+ "{sender} ran `{command}` on your behalf. Output:\n {}" ,
243
+ output. as_deref( ) . unwrap_or( "<empty>" )
244
+ ) ;
245
+
246
+ MessageApiRequest {
247
+ recipient : Recipient :: Private {
248
+ id : user. user_id ,
249
+ email : & user. email ,
250
+ } ,
251
+ content : & message,
221
252
}
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 ! (
253
+ . send ( & ctx. zulip )
254
+ . await ?;
255
+ }
256
+
257
+ Ok ( output)
258
+ } else {
259
+ // We are in a stream, where someone wrote `@**triagebot** <command(s)>`
260
+ let cmd_index = words
261
+ . iter ( )
262
+ . position ( |w| * w == "@**triagebot**" )
263
+ . unwrap_or ( words. len ( ) ) ;
264
+ let cmd_index = cmd_index + 1 ;
265
+ if cmd_index >= words. len ( ) {
266
+ return Ok ( Some ( "Unknown command" . to_string ( ) ) ) ;
267
+ }
268
+ let cmd = parse_cli :: < StreamCommand , _ > ( words[ cmd_index..] . into_iter ( ) . copied ( ) ) ?;
269
+ match cmd {
270
+ StreamCommand :: EndTopic => post_waiter ( & ctx, message_data, WaitingMessage :: end_topic ( ) )
271
+ . await
272
+ . map_err ( |e| format_err ! ( "Failed to await at this time: {e:?}" ) ) ,
273
+ StreamCommand :: EndMeeting => {
274
+ post_waiter ( & ctx, message_data, WaitingMessage :: end_meeting ( ) )
275
+ . await
276
+ . map_err ( |e| format_err ! ( "Failed to await at this time: {e:?}" ) )
277
+ }
278
+ StreamCommand :: Read => post_waiter ( & ctx, message_data, WaitingMessage :: start_reading ( ) )
279
+ . await
280
+ . map_err ( |e| format_err ! ( "Failed to await at this time: {e:?}" ) ) ,
281
+ StreamCommand :: PingGoals {
282
+ threshold,
283
+ next_update,
284
+ } => {
285
+ if project_goals:: check_project_goal_acl ( & ctx. github , gh_id) . await ? {
286
+ ping_project_goals_owners (
287
+ & ctx. github ,
288
+ & ctx. zulip ,
289
+ false ,
290
+ threshold as i64 ,
291
+ & format ! ( "on {next_update}" ) ,
292
+ )
293
+ . await
294
+ . map_err ( |e| format_err ! ( "Failed to await at this time: {e:?}" ) ) ?;
295
+ Ok ( None )
296
+ } else {
297
+ Err ( format_err ! (
256
298
"That command is only permitted for those running the project-goal program." ,
257
299
) )
258
- }
259
300
}
260
- StreamCommand :: DocsUpdate => trigger_docs_update ( message_data, & ctx. zulip ) ,
261
301
}
302
+ StreamCommand :: DocsUpdate => trigger_docs_update ( message_data, & ctx. zulip ) ,
262
303
}
263
- } )
304
+ }
305
+ }
306
+
307
+ /// Returns true if we should notify user who was impersonated by someone who executed this command.
308
+ /// More or less, the following holds: `sensitive` == `not read-only`.
309
+ fn is_sensitive_command ( cmd : & ChatCommand ) -> bool {
310
+ match cmd {
311
+ ChatCommand :: Acknowledge { .. }
312
+ | ChatCommand :: Add { .. }
313
+ | ChatCommand :: Move { .. }
314
+ | ChatCommand :: Meta { .. } => true ,
315
+ ChatCommand :: Whoami => false ,
316
+ ChatCommand :: Lookup ( _) => false ,
317
+ ChatCommand :: Work ( cmd) => match cmd {
318
+ WorkqueueCmd :: Show => false ,
319
+ WorkqueueCmd :: SetPrLimit { .. } => true ,
320
+ WorkqueueCmd :: SetRotationMode { .. } => true ,
321
+ } ,
322
+ }
264
323
}
265
324
266
325
/// Commands for working with the workqueue, e.g. showing how many PRs are assigned
267
326
/// or modifying the PR review assignment limit.
268
327
async fn workqueue_commands (
269
328
ctx : & Context ,
270
329
gh_id : u64 ,
271
- cmd : WorkqueueCmd ,
330
+ cmd : & WorkqueueCmd ,
272
331
) -> anyhow:: Result < Option < String > > {
273
332
let db_client = ctx. db . get ( ) . await ;
274
333
@@ -326,7 +385,7 @@ async fn workqueue_commands(
326
385
WorkqueueCmd :: SetPrLimit { limit } => {
327
386
let max_assigned_prs = match limit {
328
387
WorkqueueLimit :: Unlimited => None ,
329
- WorkqueueLimit :: Limit ( limit) => Some ( limit) ,
388
+ WorkqueueLimit :: Limit ( limit) => Some ( * limit) ,
330
389
} ;
331
390
upsert_review_prefs (
332
391
& db_client,
@@ -419,7 +478,7 @@ async fn whoami_cmd(ctx: &Context, gh_id: u64) -> anyhow::Result<Option<String>>
419
478
Ok ( Some ( output) )
420
479
}
421
480
422
- async fn lookup_cmd ( ctx : & Context , cmd : LookupCmd ) -> anyhow:: Result < Option < String > > {
481
+ async fn lookup_cmd ( ctx : & Context , cmd : & LookupCmd ) -> anyhow:: Result < Option < String > > {
423
482
let username = match & cmd {
424
483
LookupCmd :: Zulip { github_username } => github_username. clone ( ) ,
425
484
// Usernames could contain spaces, so rejoin everything to serve as the username.
@@ -545,82 +604,6 @@ async fn lookup_zulip_username(ctx: &Context, gh_username: &str) -> anyhow::Resu
545
604
) )
546
605
}
547
606
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
607
#[ derive( serde:: Serialize ) ]
625
608
pub ( crate ) struct MessageApiRequest < ' a > {
626
609
pub ( crate ) recipient : Recipient < ' a > ,
0 commit comments