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
use std::sync::atomic::Ordering;
use std::sync::Arc;

use anyhow::Result;
use tokio::sync::RwLock;

use crate::context::Context;

/// Manages subscription to Apple Push Notification services.
///
/// This structure is created by account manager and is shared between accounts.
/// To enable notifications, application should request the device token as described in
/// <https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns>
/// and give it to the account manager, which will forward the token in this structure.
///
/// Each account (context) can then retrieve device token
/// from this structure and give it to the email server.
/// If email server does not support push notifications,
/// account can call `subscribe` method
/// to register device token with the heartbeat
/// notification provider server as a fallback.
#[derive(Debug, Clone, Default)]
pub struct PushSubscriber {
    inner: Arc<RwLock<PushSubscriberState>>,
}

impl PushSubscriber {
    /// Creates new push notification subscriber.
    pub(crate) fn new() -> Self {
        Default::default()
    }

    /// Sets device token for Apple Push Notification service.
    pub(crate) async fn set_device_token(&mut self, token: &str) {
        self.inner.write().await.device_token = Some(token.to_string());
    }

    /// Retrieves device token.
    ///
    /// Token may be not available if application is not running on Apple platform,
    /// failed to register for remote notifications or is in the process of registering.
    ///
    /// IMAP loop should periodically check if device token is available
    /// and send the token to the email server if it supports push notifications.
    pub(crate) async fn device_token(&self) -> Option<String> {
        self.inner.read().await.device_token.clone()
    }

    /// Subscribes for heartbeat notifications with previously set device token.
    #[cfg(target_os = "ios")]
    pub(crate) async fn subscribe(&self) -> Result<()> {
        use crate::net::http;

        let mut state = self.inner.write().await;

        if state.heartbeat_subscribed {
            return Ok(());
        }

        let Some(ref token) = state.device_token else {
            return Ok(());
        };

        let socks5_config = None;
        let response = http::get_client(socks5_config)?
            .post("https://notifications.delta.chat/register")
            .body(format!("{{\"token\":\"{token}\"}}"))
            .send()
            .await?;

        let response_status = response.status();
        if response_status.is_success() {
            state.heartbeat_subscribed = true;
        }
        Ok(())
    }

    /// Placeholder to skip subscribing to heartbeat notifications outside iOS.
    #[cfg(not(target_os = "ios"))]
    pub(crate) async fn subscribe(&self) -> Result<()> {
        let mut state = self.inner.write().await;
        state.heartbeat_subscribed = true;
        Ok(())
    }

    pub(crate) async fn heartbeat_subscribed(&self) -> bool {
        self.inner.read().await.heartbeat_subscribed
    }
}

#[derive(Debug, Default)]
pub(crate) struct PushSubscriberState {
    /// Device token.
    device_token: Option<String>,

    /// If subscribed to heartbeat push notifications.
    heartbeat_subscribed: bool,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i8)]
pub enum NotifyState {
    /// Not subscribed to push notifications.
    #[default]
    NotConnected = 0,

    /// Subscribed to heartbeat push notifications.
    Heartbeat = 1,

    /// Subscribed to push notifications for new messages.
    Connected = 2,
}

impl Context {
    /// Returns push notification subscriber state.
    pub async fn push_state(&self) -> NotifyState {
        if self.push_subscribed.load(Ordering::Relaxed) {
            NotifyState::Connected
        } else if self.push_subscriber.heartbeat_subscribed().await {
            NotifyState::Heartbeat
        } else {
            NotifyState::NotConnected
        }
    }
}