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