Skip to main content

hey_sdk/services/
clearances.rs

1//! The Screener: who is waiting to be let in, and letting them in or turning them away.
2//!
3//! Clearing the Screener is the generated [`Clearances::punt`]. The work it starts is
4//! queued, so everyone waiting is still pending when it answers; they are dropped and
5//! reexamined the next time they write, so nothing is decided for them.
6
7use std::str::FromStr;
8
9use crate::error::{Error, ErrorCode};
10use crate::generated::types::{
11    BulkUpdateClearancesRequestContent, Clearance, ClearanceListResponse, ClearanceSummary,
12    UpdateClearanceRequestContent, UpdateMyClearanceRequestContent,
13};
14use crate::pagination::Page;
15
16pub use crate::generated::services::clearances::*;
17
18/// The two decisions the Screener takes.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ClearanceStatus {
21    /// Screened in: the sender's mail arrives.
22    Approved,
23    /// Screened out: the sender's mail is kept away.
24    Denied,
25}
26
27/// What to do beyond setting the status. HEY reads each of these for truthiness, so one
28/// left alone stays off the wire entirely rather than going out as a false.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30pub struct ScreenOptions {
31    /// Files everything the sender sends into that box rather than the Imbox.
32    pub designation_box_id: Option<i64>,
33    /// Marks the topics already waiting as spam and trains the filter on them.
34    pub spam: bool,
35    /// Screens the sender in without their waiting mail arriving unread.
36    pub mark_topics_as_seen: bool,
37}
38
39impl ClearanceStatus {
40    /// The decision as HEY's `status` parameter names it.
41    pub fn as_str(&self) -> &'static str {
42        match self {
43            ClearanceStatus::Approved => "approved",
44            ClearanceStatus::Denied => "denied",
45        }
46    }
47}
48
49impl Clearances<'_> {
50    /// How many senders are waiting, without fetching them.
51    ///
52    /// This is the cheap read HEY's own apps sync for the Screener badge. Use
53    /// [`Clearances::pending`] for the senders themselves.
54    pub async fn pending_count(&self) -> Result<i32, Error> {
55        Ok(self
56            .summary()
57            .await?
58            .pending_clearances_count
59            .unwrap_or_default())
60    }
61
62    /// Everything HEY says about the Screener without the queue itself: how many senders
63    /// are waiting, and the signed stream name to subscribe to on HEY's cable server to be
64    /// told when that changes.
65    pub async fn summary(&self) -> Result<ClearanceSummary, Error> {
66        self.get(&GetClearancesParams::default())
67            .await
68            .map(Page::into_inner)
69    }
70
71    /// The senders waiting to be screened, a page at a time.
72    ///
73    /// Each one carries the petitioner and the most recent entry they sent, so a caller can
74    /// show who is asking and what they wrote without a second read.
75    pub async fn pending(&self, page: Option<&str>) -> Result<ClearanceSummary, Error> {
76        self.pending_page(page).await.map(Page::into_inner)
77    }
78
79    /// The same queue as [`Clearances::pending`], keeping the cursor for the page after it
80    /// so a caller walking the queue is told when it has reached the end.
81    pub async fn pending_page(&self, page: Option<&str>) -> Result<Page<ClearanceSummary>, Error> {
82        let params = GetClearancesParams {
83            include_clearances: Some(true),
84            page: page.map(str::to_string),
85        };
86        self.get(&params).await
87    }
88
89    /// Answers the Screener for one sender.
90    pub async fn screen(
91        &self,
92        clearance_id: i64,
93        status: ClearanceStatus,
94        options: &ScreenOptions,
95    ) -> Result<Clearance, Error> {
96        let body = UpdateClearanceRequestContent {
97            status: status.as_str().to_string(),
98            designation_box_id: options.designation_box_id,
99            spam: flag(options.spam),
100            mark_topics_as_seen: flag(options.mark_topics_as_seen),
101        };
102        self.update(clearance_id, &body).await
103    }
104
105    /// Screens several senders at once and answers the clearances it changed.
106    ///
107    /// HEY answers 404 when none of the ids belong to the caller. A partial match succeeds
108    /// and answers only what it touched, so compare the answer against what was sent.
109    pub async fn screen_many(
110        &self,
111        clearance_ids: &[i64],
112        status: ClearanceStatus,
113        spam: bool,
114    ) -> Result<Vec<Clearance>, Error> {
115        if clearance_ids.is_empty() {
116            return Err(Error::usage("at least one clearance is required"));
117        }
118
119        let body = BulkUpdateClearancesRequestContent {
120            ids: join_ids(clearance_ids),
121            status: status.as_str().to_string(),
122            spam: flag(spam),
123        };
124        Ok(self
125            .bulk_update(&body)
126            .await?
127            .clearances
128            .unwrap_or_default())
129    }
130
131    /// The senders already screened in or out, newest decision first, a page at a time.
132    pub async fn screened(&self, page: Option<&str>) -> Result<Vec<Clearance>, Error> {
133        Ok(self
134            .screened_page(page)
135            .await?
136            .into_inner()
137            .clearances
138            .unwrap_or_default())
139    }
140
141    /// The same decisions as [`Clearances::screened`], keeping the cursor for the page
142    /// after it.
143    pub async fn screened_page(
144        &self,
145        page: Option<&str>,
146    ) -> Result<Page<ClearanceListResponse>, Error> {
147        let params = GetMyClearancesParams {
148            page: page.map(str::to_string),
149        };
150        self.get_my(&params).await
151    }
152
153    /// Changes its mind about a sender already screened in or out.
154    ///
155    /// This is the decided list, not the queue: [`Clearances::screen`] is what answers a
156    /// pending sender.
157    pub async fn rescreen(
158        &self,
159        clearance_id: i64,
160        status: ClearanceStatus,
161    ) -> Result<Clearance, Error> {
162        let body = UpdateMyClearanceRequestContent {
163            status: status.as_str().to_string(),
164        };
165        self.update_my(clearance_id, &body).await
166    }
167}
168
169fn flag(value: bool) -> Option<bool> {
170    if value { Some(true) } else { None }
171}
172
173fn join_ids(ids: &[i64]) -> String {
174    ids.iter()
175        .map(i64::to_string)
176        .collect::<Vec<String>>()
177        .join(",")
178}
179
180impl FromStr for ClearanceStatus {
181    type Err = Error;
182
183    fn from_str(source: &str) -> Result<ClearanceStatus, Error> {
184        match source {
185            "approved" => Ok(ClearanceStatus::Approved),
186            "denied" => Ok(ClearanceStatus::Denied),
187            _ => Err(Error::new(
188                ErrorCode::Validation,
189                format!("clearance status must be \"approved\" or \"denied\", got {source:?}"),
190            )),
191        }
192    }
193}