bark_dev/
bark.rs

1// MIT License
2//
3// Copyright (c) 2025 66f94eae
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23use std::time::Duration;
24
25use crate::msg::Msg;
26
27
28const TOKEN_OFFSET: u64 = 2700;
29const TEAM_ID: &str = "5U8LBRXG3A";
30const AUTH_KEY_ID: &str = "LH4T9V5U4R";
31const TOPIC: &str = "me.fin.bark";
32
33const KEY: &str = r#"-----BEGIN PRIVATE KEY----- 
34MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg4vtC3g5L5HgKGJ2+ 
35T1eA0tOivREvEAY2g+juRXJkYL2gCgYIKoZIzj0DAQehRANCAASmOs3JkSyoGEWZ 
36sUGxFs/4pw1rIlSV2IC19M8u3G5kq36upOwyFWj9Gi3Ejc9d3sC7+SHRqXrEAJow 
378/7tRpV+ 
38-----END PRIVATE KEY-----
39"#;
40pub struct Bark {
41    team_id: String,
42    auth_key_id: String,
43    topic: String,
44    key: String,
45    token: String
46}
47
48
49impl Bark {
50    pub fn new() -> Self {
51        Self {
52            team_id : TEAM_ID.to_string(),
53            auth_key_id : AUTH_KEY_ID.to_string(),
54            topic : TOPIC.to_string(),
55            key : KEY.to_string(),
56            token : ".".to_string(),
57        }
58    }
59
60    pub fn born(timestamp: u64, token: String) -> Self {
61        if timestamp + TOKEN_OFFSET <= Self::ts() {
62            println!("warning: token expired, bark will new one");
63            return Self::new();
64        }
65        Self {
66            team_id : TEAM_ID.to_string(),
67            auth_key_id : AUTH_KEY_ID.to_string(),
68            topic: TOPIC.to_string(),
69            key: KEY.to_string(),
70            token: format!("{}.{}", timestamp, token),
71        }
72    }
73
74    fn ts() -> u64 {
75        std::time::SystemTime::now()
76            .duration_since(std::time::UNIX_EPOCH)
77            .unwrap_or_else(|_e| Duration::from_secs(0))
78            .as_secs()
79    }
80
81    /// get apns token
82    /// 
83    /// return (create_timestamp, token)
84    pub fn token(&mut self) -> (u64, String) {
85        let token = self.token.split_once(".").unwrap();
86        (token.0.parse::<u64>().unwrap_or_else(|_e| 0), token.1.to_string())
87    }
88
89    /// force refresh apns token
90    /// 
91    /// return (create_timestamp, token)
92    pub fn force_refresh_token(&mut self) -> (u64, String) {
93        self.get_token();
94        self.token()
95    }
96    /// send msg to devices
97    /// 
98    /// return : None if success, or a vector of failed devices and error messages
99    pub fn send<T>(&mut self, msg: &Msg, devices: T) -> Option<Vec<String>> 
100    where
101        T: IntoIterator<Item = String>
102    {
103        crate::apns::send(&msg, self.topic.clone().as_str(), &self.get_token(), devices)
104    }
105
106    /// async send to devices
107    /// 
108    /// return : None if success, or a vector of failed devices and error messages
109    pub async fn async_send<T>(&mut self, msg: &Msg, devices: T) -> Option<Vec<String>>
110    where
111        T: IntoIterator<Item = String>
112    {
113        crate::apns::async_send(&msg, self.topic.clone().as_str(), &self.get_token(), devices).await
114    }
115
116    fn get_token(&mut self) -> String {
117        let time_stamp: u64 = Self::ts(); 
118
119        if let Some((ts, token)) = self.token.split_once(".") {
120            // cache the token in memory for TOKEN_OFFSET[default is 2700] seconds
121            if ts.parse::<u64>().unwrap_or_else(|_e| 0) + TOKEN_OFFSET >= time_stamp {
122                return token.to_string();
123            }
124        }
125        
126        let jwt_header: String = Self::clean_str(
127            openssl::base64::encode_block(
128                format!("{{ \"alg\": \"ES256\", \"kid\": \"{}\" }}", self.auth_key_id)
129                .as_bytes()
130            )
131        );
132
133        let jwt_claims: String = Self::clean_str(
134            openssl::base64::encode_block(
135                format!("{{ \"iss\": \"{}\", \"iat\": {} }}", 
136                        self.team_id, time_stamp
137                    )
138                .as_bytes()
139            )
140        );
141
142        let mut singer: openssl::sign::Signer<'_> = openssl::sign::Signer::new(
143                                openssl::hash::MessageDigest::sha256(),
144                                &openssl::pkey::PKey::from_ec_key(
145                                                openssl::ec::EcKey::private_key_from_pem(self.key.as_bytes()).expect("init key data failed")
146                                            ).expect("generate private key failed")
147                                ).expect("init signer failed");
148
149        let jwt_header: String = format!("{}.{}", jwt_header, jwt_claims);
150        singer.update(jwt_header.as_bytes()).expect("fill sign data failed");
151        let sign: Vec<u8> = singer.sign_to_vec().expect("sign failed");
152        let jwt_signature: String = Self::clean_str(openssl::base64::encode_block(&sign));
153        let token: String= format!("{}.{}", jwt_header, jwt_signature);
154
155        self.token = format!("{}.{}", time_stamp, token);
156        token
157    }
158    
159    fn clean_str(str: String) -> String {
160        str.replace("+", "-")
161            .replace("/", "_")
162            .replace("=", "")
163    }
164}