Переглянути джерело

Allow acknowledging all notifications (#871)

Now you can send `ack all` or `ack *` to triagebot and all your
notifications will *poof* disappear!
Camelid 4 роки тому
батько
коміт
21661d3272
3 змінених файлів з 63 додано та 25 видалено
  1. 29 3
      src/db/notifications.rs
  2. 3 2
      src/notification_listing.rs
  3. 31 20
      src/zulip.rs

+ 29 - 3
src/db/notifications.rs

@@ -40,6 +40,8 @@ pub async fn record_ping(db: &DbClient, notification: &Notification) -> anyhow::
 pub enum Identifier<'a> {
 pub enum Identifier<'a> {
     Url(&'a str),
     Url(&'a str),
     Index(std::num::NonZeroUsize),
     Index(std::num::NonZeroUsize),
+    /// Glob identifier (`all` or `*`).
+    All,
 }
 }
 
 
 pub async fn delete_ping(
 pub async fn delete_ping(
@@ -57,9 +59,6 @@ pub async fn delete_ping(
                 )
                 )
                 .await
                 .await
                 .context("delete notification query")?;
                 .context("delete notification query")?;
-            if rows.is_empty() {
-                anyhow::bail!("Did not delete any notifications");
-            }
             Ok(rows
             Ok(rows
                 .into_iter()
                 .into_iter()
                 .map(|row| {
                 .map(|row| {
@@ -139,6 +138,33 @@ pub async fn delete_ping(
                 return Ok(vec![deleted_notification]);
                 return Ok(vec![deleted_notification]);
             }
             }
         },
         },
+        Identifier::All => {
+            let rows = db
+                .query(
+                    "DELETE FROM notifications WHERE user_id = $1
+                        RETURNING origin_url, origin_html, time, short_description, metadata",
+                    &[&user_id],
+                )
+                .await
+                .context("delete all notifications query")?;
+            Ok(rows
+                .into_iter()
+                .map(|row| {
+                    let origin_url: String = row.get(0);
+                    let origin_text: String = row.get(1);
+                    let time: DateTime<FixedOffset> = row.get(2);
+                    let short_description: Option<String> = row.get(3);
+                    let metadata: Option<String> = row.get(4);
+                    NotificationData {
+                        origin_url,
+                        origin_text,
+                        time,
+                        short_description,
+                        metadata,
+                    }
+                })
+                .collect())
+        }
     }
     }
 }
 }
 
 

+ 3 - 2
src/notification_listing.rs

@@ -54,8 +54,9 @@ pub async fn render(db: &DbClient, user: &str) -> String {
         out.push_str("</ol>");
         out.push_str("</ol>");
 
 
         out.push_str(
         out.push_str(
-            "<p><em>You can acknowledge notifications by sending </em><code>ack &lt;idx&gt;</code><em> \
-            to </em><strong><code>@triagebot</code></strong><em> on Zulip. Read about the other notification commands \
+            "<p><em>You can acknowledge a notification by sending </em><code>ack &lt;idx&gt;</code><em> \
+            to </em><strong><code>@triagebot</code></strong><em> on Zulip, or you can acknowledge \
+            <em>all</em> notifications by sending <em>ack all</em>. Read about the other notification commands \
             <a href=\"https://forge.rust-lang.org/platforms/zulip/triagebot.html#issue-notifications\">here</a>.</em></p>"
             <a href=\"https://forge.rust-lang.org/platforms/zulip/triagebot.html#issue-notifications\">here</a>.</em></p>"
         );
         );
     }
     }

+ 31 - 20
src/zulip.rs

@@ -470,43 +470,54 @@ impl<'a> MessageApiRequest<'a> {
 }
 }
 
 
 async fn acknowledge(gh_id: i64, mut words: impl Iterator<Item = &str>) -> anyhow::Result<String> {
 async fn acknowledge(gh_id: i64, mut words: impl Iterator<Item = &str>) -> anyhow::Result<String> {
-    let url = match words.next() {
-        Some(url) => {
+    let filter = match words.next() {
+        Some(filter) => {
             if words.next().is_some() {
             if words.next().is_some() {
                 anyhow::bail!("too many words");
                 anyhow::bail!("too many words");
             }
             }
-            url
+            filter
         }
         }
         None => anyhow::bail!("not enough words"),
         None => anyhow::bail!("not enough words"),
     };
     };
-    let ident = if let Ok(number) = url.parse::<usize>() {
+    let ident = if let Ok(number) = filter.parse::<usize>() {
         Identifier::Index(
         Identifier::Index(
             std::num::NonZeroUsize::new(number)
             std::num::NonZeroUsize::new(number)
                 .ok_or_else(|| anyhow::anyhow!("index must be at least 1"))?,
                 .ok_or_else(|| anyhow::anyhow!("index must be at least 1"))?,
         )
         )
+    } else if filter == "all" || filter == "*" {
+        Identifier::All
     } else {
     } else {
-        Identifier::Url(url)
+        Identifier::Url(filter)
     };
     };
     match delete_ping(&mut crate::db::make_client().await?, gh_id, ident).await {
     match delete_ping(&mut crate::db::make_client().await?, gh_id, ident).await {
         Ok(deleted) => {
         Ok(deleted) => {
-            let mut resp = format!("Acknowledged:\n");
-            for deleted in deleted {
-                resp.push_str(&format!(
-                    " * [{}]({}){}\n",
-                    deleted
-                        .short_description
-                        .as_deref()
-                        .unwrap_or(&deleted.origin_url),
-                    deleted.origin_url,
-                    deleted
-                        .metadata
-                        .map_or(String::new(), |m| format!(" ({})", m)),
-                ));
-            }
+            let resp = if deleted.is_empty() {
+                format!(
+                    "No notifications matched `{}`, so none were deleted.",
+                    filter
+                )
+            } else {
+                let mut resp = String::from("Acknowledged:\n");
+                for deleted in deleted {
+                    resp.push_str(&format!(
+                        " * [{}]({}){}\n",
+                        deleted
+                            .short_description
+                            .as_deref()
+                            .unwrap_or(&deleted.origin_url),
+                        deleted.origin_url,
+                        deleted
+                            .metadata
+                            .map_or(String::new(), |m| format!(" ({})", m)),
+                    ));
+                }
+                resp
+            };
+
             Ok(serde_json::to_string(&Response { content: &resp }).unwrap())
             Ok(serde_json::to_string(&Response { content: &resp }).unwrap())
         }
         }
         Err(e) => Ok(serde_json::to_string(&Response {
         Err(e) => Ok(serde_json::to_string(&Response {
-            content: &format!("Failed to acknowledge {}: {:?}.", url, e),
+            content: &format!("Failed to acknowledge {}: {:?}.", filter, e),
         })
         })
         .unwrap()),
         .unwrap()),
     }
     }