1use anyhow::Result;
2use awc::http::StatusCode;
3use awc::{Client, ClientResponse};
5use lazy_static::lazy_static;
6use 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 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 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 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 self.patch(&prefix, args).await
237 }
238 pub fn sanitize(source: &str) -> String {
239 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 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}