dogehouse_rs/
lib.rs

1use async_trait::async_trait;
2use futures::{SinkExt, StreamExt};
3use serde::Deserialize;
4use serde_json::json;
5use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
6use url::Url;
7use uuid::Uuid;
8
9use config::{API_URL, TIME_OUT};
10use user::{TopUserWrapper, User};
11
12mod config;
13pub mod prelude;
14mod user;
15
16/// In order to use EvenHandler you must first define a struct and then
17/// implement EvenHandler for that struct.
18///
19/// Then you must pass that struct as an argument to add_event_handler
20/// which is defined on Client.
21/// ```
22/// #[async_trait]
23/// impl EventHandler for Handler {
24///   async fn on_message(&self, msg: String) {
25///     println!("{}", msg);
26///   }
27///
28///   async fn on_pong(&self) {
29///     println!("Received ping")
30///   }
31///
32///   async fn connection_closed(&self) {
33///     println!("Connection has closed");
34///     std::process::exit(1);
35///   }
36///
37///   async fn on_ready(&self, user: &User) {
38///     println!("{} is ready", user.display_name);
39///   }
40/// }
41/// ```
42#[async_trait]
43pub trait EventHandler {
44    async fn on_message(&self, _msg: String);
45    async fn on_pong(&self);
46    async fn connection_closed(&self);
47    async fn on_ready(&self, _user: &User);
48}
49
50#[derive(Clone)]
51pub struct Client<'a, T>
52where
53    T: EventHandler + Sync,
54{
55    token: String,
56    refresh_token: String,
57    bot_token: Option<String>,
58    bot_refresh_token: Option<String>,
59    room_id: Option<&'a str>,
60    event_handler: Option<T>,
61}
62
63impl<'a, T> Client<'a, T>
64where
65    T: EventHandler + Sync,
66{
67    pub fn new(token: String, refresh_token: String) -> Self {
68        Self {
69            token,
70            refresh_token,
71            bot_token: None,
72            bot_refresh_token: None,
73            room_id: None,
74            event_handler: None,
75        }
76    }
77
78    /// In order to create a bot make sure to first authenticate with your normal tokens and create a client
79    /// ```
80    /// let mut client = Client::new(
81    ///   token,
82    ///   refresh_token
83    /// ).add_event_handler(Handler);
84    /// ```
85    /// Then call use_create_bot on client with a username and a boolean value to tell dogehouse-rs
86    /// if you want your bot tokens to be shown.
87    /// use_create_bot is an async function so make sure to use await or a blocking call.
88    /// ```
89    /// client.use_create_bot("bot_name", true").await?;
90    /// ```
91    pub async fn use_create_bot(
92        &mut self,
93        username: &'a str,
94        show_bot_tokens: bool,
95    ) -> anyhow::Result<()> {
96        let url = Url::parse(API_URL)?;
97
98        let (ws_stream, _) = connect_async(url).await.expect("Failed to connect");
99        let (mut write, mut read) = ws_stream.split();
100
101        write
102            .send(Message::Text(
103                json!(
104                    {
105                        "op": "auth",
106                        "d": {
107                            "accessToken": self.token,
108                            "refreshToken": self.refresh_token,
109                            "reconnectToVoice": false,
110                            "currentRoomId": "",
111                            "muted": true,
112                            "platform": "dogehouse-rs"
113                        }
114                    }
115                )
116                .to_string(),
117            ))
118            .await?;
119
120        #[derive(Deserialize)]
121        struct CreateBotResponse {
122            op: String,
123            p: CreateBotResponseP,
124        }
125
126        #[derive(Deserialize)]
127        #[serde(rename_all = "camelCase")]
128        struct CreateBotResponseP {
129            api_key: serde_json::Value,
130            is_username_taken: serde_json::Value,
131            error: serde_json::Value,
132        }
133
134        write
135            .send(Message::Text(
136                json!(
137                    {
138                        "op": config::user::CREATE_BOT,
139                        "p": {
140                            "username": username
141                        },
142                        "ref": "[uuid]",
143                        "v": "0.2.0",
144                    }
145                )
146                .to_string(),
147            ))
148            .await?;
149
150        // Skip messages from first two requests
151        read.next().await;
152        read.next().await;
153
154        let n = read.next().await.unwrap()?.to_string();
155        let bot_response =
156            serde_json::from_str::<CreateBotResponse>(&n).expect("Error invalid bot name");
157        if bot_response.p.is_username_taken.is_boolean() {
158            return Err(anyhow::Error::msg("Bot name is taken."));
159        }
160
161        #[derive(Deserialize, Debug)]
162        #[serde(rename_all = "camelCase")]
163        struct BotAccount {
164            access_token: String,
165            refresh_token: String,
166        }
167
168        let bot_account = reqwest::Client::new()
169            .post("https://api.dogehouse.tv/bot/auth")
170            .header("content-type", "application/json")
171            .body(
172                json!(
173                    {
174                        "apiKey": bot_response.p.api_key.as_str()
175                    }
176                )
177                .to_string(),
178            )
179            .send()
180            .await?
181            .json::<BotAccount>()
182            .await?;
183
184        if show_bot_tokens {
185            println!("Bot tokens: {:?}", &bot_account);
186        }
187
188        self.bot_token = Some(bot_account.access_token);
189        self.bot_refresh_token = Some(bot_account.refresh_token);
190        Ok(())
191    }
192
193    /// Let user assign an event handler that implements EventHandler trait
194    pub fn add_event_handler(mut self, handler: T) -> Self {
195        self.event_handler = Some(handler);
196        self
197    }
198
199    pub async fn start(&mut self, room_id: &'a str) -> anyhow::Result<()> {
200        self.room_id = Some(room_id);
201
202        let token = match self.bot_token.is_some() {
203            true => &self.bot_token.as_ref().unwrap(),
204            false => &self.token,
205        };
206
207        let refresh_token = match self.bot_refresh_token.is_some() {
208            true => &self.bot_refresh_token.as_ref().unwrap(),
209            false => &self.refresh_token,
210        };
211
212        let url = Url::parse(API_URL)?;
213
214        let (ws_stream, _) = connect_async(url).await.expect("Failed to connect");
215        let (mut write, mut read) = ws_stream.split();
216
217        write
218            .send(Message::Text(
219                json!(
220                    {
221                        "op": "auth",
222                        "d": {
223                            "accessToken": token,
224                            "refreshToken": refresh_token,
225                            "reconnectToVoice": false,
226                            "currentRoomId": self.room_id.unwrap(),
227                            "muted": true,
228                            "platform": "dogehouse-rs"
229                        }
230                    }
231                )
232                .to_string(),
233            ))
234            .await?;
235
236        let n = read.next().await.unwrap()?.to_string();
237        println!("{}", &n);
238
239        let account: TopUserWrapper = serde_json::from_str(&n)?;
240        self.event_handler
241            .as_ref()
242            .unwrap()
243            .on_ready(&account.d.user)
244            .await;
245
246        let room_join_ref = Uuid::new_v4();
247
248        write
249            .send(Message::Text(
250                json!({
251                    "op": "room:join",
252                    "d": {
253                        "roomId": self.room_id.unwrap()
254                    },
255                    "ref": room_join_ref.to_string(),
256                    "v": "0.2.0",
257                })
258                .to_string(),
259            ))
260            .await?;
261
262        tokio::spawn(async move {
263            loop {
264                write.send("ping".into()).await.unwrap();
265                std::thread::sleep(std::time::Duration::new(TIME_OUT, 0));
266            }
267        });
268
269        while let Some(msg) = read.next().await {
270            let msg = msg?;
271            if msg.is_close() {
272                self.event_handler
273                    .as_ref()
274                    .unwrap()
275                    .connection_closed()
276                    .await;
277                continue;
278            } else if msg.is_binary() || msg.is_text() {
279                if msg.to_string() == "pong" {
280                    self.event_handler.as_ref().unwrap().on_pong().await;
281                    continue;
282                }
283                self.event_handler
284                    .as_ref()
285                    .unwrap()
286                    .on_message(msg.to_string())
287                    .await;
288                continue;
289            }
290        }
291
292        Ok(())
293    }
294}