Skip to main content

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}