actor_discord/
api.rs

1use anyhow::Result;
2use awc::http::StatusCode;
3//use awc::{ws, Client, ClientBuilder};
4use awc::{Client, ClientResponse};
5use lazy_static::lazy_static;
6//use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
7use crate::errors::ActorDiscordError;
8use crate::types::events::{
9    Guild, GuildChannel, GuildChannelCreate, MessageCreate, MessageObject, RetryMessage,
10    SnowflakeID,
11};
12use actix_http::encoding::Decoder;
13use actix_http::Payload;
14use regex::Regex;
15use serde::Deserialize;
16use std::str::FromStr;
17use url::Url;
18
19const API_PREFIX: &str = "/api/v9/";
20
21const GUILD_ID: &str = "guilds/";
22pub struct DiscordAPI {
23    pub client: Client,
24    pub base_url: Url,
25    pub token: String,
26}
27impl DiscordAPI {
28    pub fn create(token: &str, connect_addr: &str) -> Result<DiscordAPI> {
29        let base_url: Url = Url::from_str(connect_addr)?.join(API_PREFIX)?;
30        let client = Client::builder().finish();
31        Ok(DiscordAPI {
32            client,
33            base_url,
34            token: token.into(),
35        })
36    }
37
38    pub async fn get<T: for<'de> Deserialize<'de>>(&self, url_suffix: &str) -> anyhow::Result<T> {
39        let full_url = self.base_url.join(url_suffix)?;
40
41        let mut retries = 4;
42        while retries > 0 {
43            log::debug!("Get URL={}", full_url.as_str());
44            let response = self
45                .client
46                .get(full_url.as_str())
47                .insert_header((awc::http::header::CONTENT_TYPE, "application/json"))
48                .insert_header((awc::http::header::USER_AGENT, "PFC-Discord"))
49                .insert_header((
50                    awc::http::header::AUTHORIZATION,
51                    format!("Bot {}", self.token),
52                ))
53                .send()
54                .await
55                .map_err(|source| {
56                    eprintln!("{:#?}", source);
57                    ActorDiscordError::ResponseError()
58                })?;
59            let ok_retryable = self.handle_response::<T>(response).await?;
60            if ok_retryable.0 {
61                return Ok(ok_retryable.1.unwrap());
62            }
63            log::debug!("Retrying retries left:{}", retries);
64            retries = retries - 1;
65        }
66        Err(ActorDiscordError::RetryError.into())
67    }
68    pub async fn post<T: for<'de> Deserialize<'de>>(
69        &self,
70        url_suffix: &str,
71        args: serde_json::Value,
72    ) -> anyhow::Result<T> {
73        let full_url = self.base_url.join(url_suffix)?;
74
75        let mut retries = 2;
76        while retries > 0 {
77            log::debug!("Post URL={}", full_url.as_str());
78            let arg_json = serde_json::to_string(&args)?;
79            let response = self
80                .client
81                .post(full_url.as_str())
82                .insert_header((awc::http::header::CONTENT_TYPE, "application/json"))
83                .insert_header((awc::http::header::USER_AGENT, "PFC-Discord"))
84                .insert_header((
85                    awc::http::header::AUTHORIZATION,
86                    format!("Bot {}", self.token),
87                ))
88                .send_body(arg_json)
89                .await
90                .map_err(|source| {
91                    eprintln!("{:#?}", source);
92                    ActorDiscordError::ResponseError()
93                })?;
94            let ok_retryable = self.handle_response::<T>(response).await?;
95            if ok_retryable.0 {
96                return Ok(ok_retryable.1.unwrap());
97            }
98            log::debug!("Retrying retries left:{}", retries);
99            retries = retries - 1;
100        }
101        Err(ActorDiscordError::RetryError.into())
102    }
103    pub async fn delete<T: for<'de> Deserialize<'de>>(
104        &self,
105        url_suffix: &str,
106    ) -> anyhow::Result<T> {
107        let full_url = self.base_url.join(url_suffix)?;
108
109        let mut retries = 2;
110        while retries > 0 {
111            log::debug!("Delete URL={}", full_url.as_str());
112
113            let response = self
114                .client
115                .delete(full_url.as_str())
116                .insert_header((awc::http::header::CONTENT_TYPE, "application/json"))
117                .insert_header((awc::http::header::USER_AGENT, "PFC-Discord"))
118                .insert_header((
119                    awc::http::header::AUTHORIZATION,
120                    format!("Bot {}", self.token),
121                ))
122                .send()
123                .await
124                .map_err(|source| {
125                    eprintln!("{:#?}", source);
126                    ActorDiscordError::ResponseError()
127                })?;
128            let ok_retryable = self.handle_response::<T>(response).await?;
129            if ok_retryable.0 {
130                return Ok(ok_retryable.1.unwrap());
131            }
132            log::debug!("Retrying retries left:{}", retries);
133            retries = retries - 1;
134        }
135        Err(ActorDiscordError::RetryError.into())
136    }
137    pub async fn patch<T: for<'de> Deserialize<'de>>(
138        &self,
139        url_suffix: &str,
140        args: serde_json::Value,
141    ) -> anyhow::Result<T> {
142        let full_url = self.base_url.join(url_suffix)?;
143
144        let mut retries = 2;
145        while retries > 0 {
146            log::debug!("Patch URL={}", full_url.as_str());
147            let arg_json = serde_json::to_string(&args)?;
148            let response = self
149                .client
150                .patch(full_url.as_str())
151                .insert_header((awc::http::header::CONTENT_TYPE, "application/json"))
152                .insert_header((awc::http::header::USER_AGENT, "PFC-Discord"))
153                .insert_header((
154                    awc::http::header::AUTHORIZATION,
155                    format!("Bot {}", self.token),
156                ))
157                .send_body(arg_json)
158                .await
159                .map_err(|source| {
160                    eprintln!("{:#?}", source);
161                    ActorDiscordError::ResponseError()
162                })?;
163            let ok_retryable = self.handle_response::<T>(response).await?;
164            if ok_retryable.0 {
165                return Ok(ok_retryable.1.unwrap());
166            }
167            log::debug!("Retrying retries left:{}", retries);
168            retries = retries - 1;
169        }
170        Err(ActorDiscordError::RetryError.into())
171    }
172
173    /**
174     check response code, sleeping if required.
175       @returns ok=true/retry=false , or Error
176    if OK returns 'T' as 2nd parameter
177    */
178    async fn handle_response<T: for<'de> Deserialize<'de>>(
179        &self,
180        mut response: ClientResponse<Decoder<Payload>>,
181    ) -> Result<(bool, Option<T>)> {
182        if response.status() == StatusCode::CREATED || response.status() == StatusCode::OK {
183            let result: T = response.json::<T>().limit(1024 * 1024).await?;
184            return Ok((true, Some(result)));
185        }
186        if response.status() == StatusCode::TOO_MANY_REQUESTS {
187            let retry: RetryMessage = response.json::<RetryMessage>().await?;
188            log::debug!(
189                "Sleeping for {} seconds :{}",
190                retry.retry_after,
191                retry.message
192            );
193            tokio::time::sleep(tokio::time::Duration::from_secs_f64(retry.retry_after)).await;
194            return Ok((false, None));
195        }
196        log::error!(
197            "{} {}",
198            response.status(),
199            std::str::from_utf8(&response.body().limit(6000).await.unwrap())?
200        );
201        Err(ActorDiscordError::ResponseError().into())
202    }
203    pub async fn guild(&self, id: SnowflakeID) -> Result<Guild> {
204        let url = self.base_url.join(GUILD_ID)?.join(&id.to_string())?;
205        let guild: Guild = self.get(url.as_str()).await?;
206        Ok(guild)
207    }
208    pub async fn channels(&self, guild_id: SnowflakeID) -> Result<Vec<GuildChannel>> {
209        let prefix = format!("{}{}/channels", GUILD_ID, guild_id.to_string());
210        let url = self.base_url.join(&prefix)?;
211        let channels: Vec<GuildChannel> = self.get(url.as_str()).await?;
212        Ok(channels)
213    }
214    pub async fn create_channel(
215        &self,
216        guild_id: SnowflakeID,
217        channel_details: GuildChannelCreate,
218    ) -> Result<GuildChannel> {
219        let prefix = format!("{}{}/channels", GUILD_ID, guild_id.to_string());
220        //   let url = self.base_url.join(&prefix)?;
221        self.post(&prefix, serde_json::to_value(&channel_details)?)
222            .await
223    }
224    pub async fn delete_channel(&self, channel_id: SnowflakeID) -> Result<GuildChannel> {
225        let prefix = format!("channels/{}", channel_id.to_string());
226        //   let url = self.base_url.join(&prefix)?;
227        self.delete(&prefix).await
228    }
229    pub async fn patch_channel(
230        &self,
231        channel_id: SnowflakeID,
232        args: serde_json::Value,
233    ) -> Result<GuildChannel> {
234        let prefix = format!("channels/{}", channel_id.to_string());
235        //   let url = self.base_url.join(&prefix)?;
236        self.patch(&prefix, args).await
237    }
238    pub fn sanitize(source: &str) -> String {
239        //  let mut lowercase = source.to_ascii_lowercase();
240        lazy_static! {
241            static ref RE: Regex = Regex::new(r"[^\pN\p{Emoji}A-Za-z0-9\-]").unwrap();
242            static ref RE_HASH: Regex = Regex::new(r"#").unwrap();
243            static ref RE_DUP: Regex = Regex::new(r"-+").unwrap();
244            static ref RE_START: Regex = Regex::new(r"^-").unwrap();
245            static ref RE_END: Regex = Regex::new(r"-$").unwrap();
246        }
247
248        let sanitized = RE.replace_all(source, "-").to_string();
249        let de_hash = RE_HASH.replace_all(&sanitized, "-").to_string();
250        let de_dup: String = RE_DUP.replace_all(&de_hash, "-").to_string();
251        let trimmed_start: String = RE_START.replace_all(&de_dup, "").to_string();
252        let trimmed_end: String = RE_END.replace_all(&trimmed_start, "").to_string();
253        trimmed_end.to_lowercase()
254    }
255    pub async fn create_message(
256        &self,
257        channel_id: SnowflakeID,
258        message: MessageCreate,
259    ) -> Result<MessageObject> {
260        let prefix = format!("channels/{}/messages", channel_id.to_string());
261        //   let url = self.base_url.join(&prefix)?;
262        let args = serde_json::to_value(message)?;
263        self.post(&prefix, args).await
264    }
265}
266#[cfg(test)]
267mod tests {
268    use crate::DiscordAPI;
269
270    #[test]
271    fn sanitize() {
272        let match_tests: Vec<(&str, &str)> = vec![
273            ("a", "a"),
274            ("b b", "b-b"),
275            ("-c", "c"),
276            ("--d--", "d"),
277            ("@#$a", "a"),
278            ("TB 🚀 🌕 b 🔥 L", "tb-🚀-🌕-b-🔥-l"),
279        ];
280        for t in match_tests {
281            let result = DiscordAPI::sanitize(t.0);
282            assert_eq!(t.1, result)
283        }
284    }
285}