hey_sdk/services/boxes.rs
1//! Resolving a box by its kind, and gathering a selection of postings into a Set Aside
2//! group.
3//!
4//! [`Boxes::get_imbox_seen`] answers the Imbox's Previously Seen postings, ordered by when
5//! they were seen. Its `next_history_url` names the `/imbox` route, but the cursor in it
6//! belongs to the seen scope: feed that cursor back to [`Boxes::get_imbox_seen`], never to
7//! [`Boxes::get_imbox`].
8
9use std::collections::HashMap;
10use std::fmt;
11use std::str::FromStr;
12
13use crate::error::Error;
14use crate::generated::types::{CreateBoxGroupRequestContent, CreateBoxGroupResponseContent};
15
16pub use crate::generated::services::boxes::*;
17
18/// The kinds of box a HEY account has, as [`Boxes::list`] reports them.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[non_exhaustive]
21pub enum BoxKind {
22 /// The Imbox, where screened-in mail lands.
23 Imbox,
24 /// The Feed, for newsletters and the like. HEY's `feedbox`.
25 Feed,
26 /// Set Aside, for threads kept close to hand. HEY's `asidebox`.
27 SetAside,
28 /// Reply Later, for threads waiting on an answer. HEY's `laterbox`.
29 ReplyLater,
30 /// The Paper Trail, for receipts and confirmations. HEY's `trailbox`.
31 PaperTrail,
32 /// Bubble Up, holding postings until the day they resurface. HEY's `bubblebox`.
33 BubbleUp,
34}
35
36impl BoxKind {
37 /// The kind as the box index names it — the `kind` a listed box carries.
38 pub fn as_str(&self) -> &'static str {
39 match self {
40 BoxKind::Imbox => "imbox",
41 BoxKind::Feed => "feedbox",
42 BoxKind::SetAside => "asidebox",
43 BoxKind::ReplyLater => "laterbox",
44 BoxKind::PaperTrail => "trailbox",
45 BoxKind::BubbleUp => "bubblebox",
46 }
47 }
48}
49
50/// The caller's boxes by kind, as one [`Boxes::list`] read answered them. Hold on to it and
51/// a kind resolves without reading the index again.
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
53pub struct BoxKinds(HashMap<String, i64>);
54
55impl BoxKinds {
56 /// The id of the box of a kind, or a failure when the account has none of that kind.
57 pub fn id(&self, kind: BoxKind) -> Result<i64, Error> {
58 match self.0.get(kind.as_str()) {
59 Some(id) => Ok(*id),
60 None => Err(Error::api(0, format!("no box of kind {:?}", kind.as_str()))),
61 }
62 }
63}
64
65impl Boxes<'_> {
66 /// The id of the box of a kind.
67 ///
68 /// The client reads the box index once and answers every kind from that reading for as
69 /// long as it lives — a box's kind does not change, and the ids do not either. A client
70 /// derived with [`Client::for_account`](crate::Client::for_account) reads it again for
71 /// the account it presents. Use [`Boxes::kinds`] to read the index afresh.
72 ///
73 /// A kind the account has no box for is still a failure, and the index is not read
74 /// again to be sure. Go re-reads it on every such miss, which is a read per call for a
75 /// kind that will never be there.
76 pub async fn id_by_kind(&self, kind: BoxKind) -> Result<i64, Error> {
77 // The lock is held across the index read on purpose: it makes concurrent callers
78 // share one read rather than each starting their own.
79 let mut cached = self.client().scope.box_kinds.lock().await;
80 if let Some(kinds) = &*cached {
81 kinds.id(kind)
82 } else {
83 let kinds = self.kinds().await?;
84 let id = kinds.id(kind);
85 *cached = Some(kinds);
86 id
87 }
88 }
89
90 /// Reads the box index and maps every box's kind to its id. This is the read itself,
91 /// so it goes to HEY however many times it is called.
92 pub async fn kinds(&self) -> Result<BoxKinds, Error> {
93 let boxes = self.list().await?;
94 Ok(BoxKinds(
95 boxes
96 .iter()
97 .filter(|mailbox| !mailbox.kind.is_empty())
98 .map(|mailbox| (mailbox.kind.clone(), mailbox.id))
99 .collect(),
100 ))
101 }
102
103 /// Gathers a selection of postings into a new Set Aside group. The generated
104 /// [`Boxes::create_group`] takes the same request as a body.
105 pub async fn create_box_group(
106 &self,
107 box_id: i64,
108 posting_ids: &[i64],
109 ) -> Result<CreateBoxGroupResponseContent, Error> {
110 let body = CreateBoxGroupRequestContent {
111 posting_ids: posting_ids.to_vec(),
112 };
113 self.create_group(box_id, &body).await
114 }
115}
116
117impl fmt::Display for BoxKind {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 f.write_str(self.as_str())
120 }
121}
122
123impl FromStr for BoxKind {
124 type Err = Error;
125
126 fn from_str(source: &str) -> Result<BoxKind, Error> {
127 match source {
128 "imbox" => Ok(BoxKind::Imbox),
129 "feedbox" => Ok(BoxKind::Feed),
130 "asidebox" => Ok(BoxKind::SetAside),
131 "laterbox" => Ok(BoxKind::ReplyLater),
132 "trailbox" => Ok(BoxKind::PaperTrail),
133 "bubblebox" => Ok(BoxKind::BubbleUp),
134 _ => Err(Error::usage(format!(
135 "box kind {source:?} is none of imbox, feedbox, asidebox, laterbox, trailbox, bubblebox"
136 ))),
137 }
138 }
139}