assign.rs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. //! Permit assignment of any user to issues, without requiring "write" access to the repository.
  2. //!
  3. //! We need to fake-assign ourselves and add a 'claimed by' section to the top-level comment.
  4. //!
  5. //! Such assigned issues should also be placed in a queue to ensure that the user remains
  6. //! active; the assigned user will be asked for a status report every 2 weeks (XXX: timing).
  7. //!
  8. //! If we're intending to ask for a status report but no comments from the assigned user have
  9. //! been given for the past 2 weeks, the bot will de-assign the user. They can once more claim
  10. //! the issue if necessary.
  11. //!
  12. //! Assign users with `@rustbot assign @gh-user` or `@rustbot claim` (self-claim).
  13. use crate::{
  14. config::AssignConfig,
  15. github::{self, Event, Selection},
  16. handlers::{Context, Handler},
  17. interactions::EditIssueBody,
  18. };
  19. use failure::{Error, ResultExt};
  20. use parser::command::assign::AssignCommand;
  21. use parser::command::{Command, Input};
  22. use futures::future::{FutureExt, BoxFuture};
  23. pub(super) struct AssignmentHandler;
  24. #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
  25. struct AssignData {
  26. user: Option<String>,
  27. }
  28. impl Handler for AssignmentHandler {
  29. type Input = AssignCommand;
  30. type Config = AssignConfig;
  31. fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, Error> {
  32. let body = if let Some(b) = event.comment_body() {
  33. b
  34. } else {
  35. // not interested in other events
  36. return Ok(None);
  37. };
  38. if let Event::Issue(e) = event {
  39. if e.action != github::IssuesAction::Opened {
  40. // skip events other than opening the issue to avoid retriggering commands in the
  41. // issue body
  42. return Ok(None);
  43. }
  44. }
  45. let mut input = Input::new(&body, &ctx.username);
  46. match input.parse_command() {
  47. Command::Assign(Ok(command)) => Ok(Some(command)),
  48. Command::Assign(Err(err)) => {
  49. failure::bail!(
  50. "Parsing assign command in [comment]({}) failed: {}",
  51. event.html_url().expect("has html url"),
  52. err
  53. );
  54. }
  55. _ => Ok(None),
  56. }
  57. }
  58. fn handle_input<'a>(
  59. &self,
  60. ctx: &'a Context,
  61. _config: &'a AssignConfig,
  62. event: &'a Event,
  63. cmd: AssignCommand,
  64. ) -> BoxFuture<'a, Result<(), Error>> {
  65. handle_input(ctx, event, cmd).boxed()
  66. }
  67. }
  68. async fn handle_input(ctx: &Context, event: &Event, cmd: AssignCommand) -> Result<(), Error> {
  69. let is_team_member = if let Err(_) | Ok(false) = event.user().is_team_member(&ctx.github).await {
  70. false
  71. } else {
  72. true
  73. };
  74. let e = EditIssueBody::new(&event.issue().unwrap(), "ASSIGN");
  75. let to_assign = match cmd {
  76. AssignCommand::Own => event.user().login.clone(),
  77. AssignCommand::User { username } => {
  78. if !is_team_member && username != event.user().login {
  79. failure::bail!("Only Rust team members can assign other users");
  80. }
  81. username.clone()
  82. }
  83. AssignCommand::Release => {
  84. if let Some(AssignData {
  85. user: Some(current),
  86. }) = e.current_data()
  87. {
  88. if current == event.user().login || is_team_member {
  89. event
  90. .issue()
  91. .unwrap()
  92. .remove_assignees(&ctx.github, Selection::All).await?;
  93. e.apply(&ctx.github, String::new(), AssignData { user: None }).await?;
  94. return Ok(());
  95. } else {
  96. failure::bail!("Cannot release another user's assignment");
  97. }
  98. } else {
  99. let current = &event.user();
  100. if event.issue().unwrap().contain_assignee(current) {
  101. event
  102. .issue()
  103. .unwrap()
  104. .remove_assignees(&ctx.github, Selection::One(&current)).await?;
  105. e.apply(&ctx.github, String::new(), AssignData { user: None }).await?;
  106. return Ok(());
  107. } else {
  108. failure::bail!("Cannot release unassigned issue");
  109. }
  110. };
  111. }
  112. };
  113. let data = AssignData {
  114. user: Some(to_assign.clone()),
  115. };
  116. e.apply(&ctx.github, String::new(), &data).await?;
  117. match event.issue().unwrap().set_assignee(&ctx.github, &to_assign).await {
  118. Ok(()) => return Ok(()), // we are done
  119. Err(github::AssignmentError::InvalidAssignee) => {
  120. event
  121. .issue()
  122. .unwrap()
  123. .set_assignee(&ctx.github, &ctx.username).await
  124. .context("self-assignment failed")?;
  125. e.apply(
  126. &ctx.github,
  127. format!(
  128. "This issue has been assigned to @{} via [this comment]({}).",
  129. to_assign,
  130. event.html_url().unwrap()
  131. ),
  132. &data,
  133. ).await?;
  134. }
  135. Err(e) => return Err(e.into()),
  136. }
  137. Ok(())
  138. }