revolt_database/models/bots/
model.rs

1use revolt_result::Result;
2use ulid::Ulid;
3
4use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
5
6auto_derived_partial!(
7    /// Bot
8    pub struct Bot {
9        /// Bot Id
10        ///
11        /// This equals the associated bot user's id.
12        #[serde(rename = "_id")]
13        pub id: String,
14        /// User Id of the bot owner
15        pub owner: String,
16        /// Token used to authenticate requests for this bot
17        pub token: String,
18        /// Whether the bot is public
19        /// (may be invited by anyone)
20        pub public: bool,
21
22        /// Whether to enable analytics
23        #[serde(skip_serializing_if = "crate::if_false", default)]
24        pub analytics: bool,
25        /// Whether this bot should be publicly discoverable
26        #[serde(skip_serializing_if = "crate::if_false", default)]
27        pub discoverable: bool,
28        /// Reserved; URL for handling interactions
29        #[serde(skip_serializing_if = "String::is_empty", default)]
30        pub interactions_url: String,
31        /// URL for terms of service
32        #[serde(skip_serializing_if = "String::is_empty", default)]
33        pub terms_of_service_url: String,
34        /// URL for privacy policy
35        #[serde(skip_serializing_if = "String::is_empty", default)]
36        pub privacy_policy_url: String,
37
38        /// Enum of bot flags
39        #[serde(skip_serializing_if = "Option::is_none")]
40        pub flags: Option<i32>,
41    },
42    "PartialBot"
43);
44
45auto_derived!(
46    /// Optional fields on bot object
47    pub enum FieldsBot {
48        Token,
49        InteractionsURL,
50    }
51);
52
53#[allow(clippy::derivable_impls)]
54impl Default for Bot {
55    fn default() -> Self {
56        Self {
57            id: Default::default(),
58            owner: Default::default(),
59            token: Default::default(),
60            public: Default::default(),
61            analytics: Default::default(),
62            discoverable: Default::default(),
63            interactions_url: Default::default(),
64            terms_of_service_url: Default::default(),
65            privacy_policy_url: Default::default(),
66            flags: Default::default(),
67        }
68    }
69}
70
71#[allow(clippy::disallowed_methods)]
72impl Bot {
73    /// Create a new bot
74    pub async fn create<D>(
75        db: &Database,
76        username: String,
77        owner: &User,
78        data: D,
79    ) -> Result<(Bot, User)>
80    where
81        D: Into<Option<PartialBot>>,
82    {
83        if owner.bot.is_some() {
84            return Err(create_error!(IsBot));
85        }
86
87        if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
88            return Err(create_error!(ReachedMaximumBots));
89        }
90
91        let id = Ulid::new().to_string();
92
93        let user = User::create(
94            db,
95            username,
96            Some(id.to_string()),
97            Some(PartialUser {
98                bot: Some(BotInformation {
99                    owner: owner.id.to_string(),
100                }),
101                ..Default::default()
102            }),
103        )
104        .await?;
105
106        let mut bot = Bot {
107            id,
108            owner: owner.id.to_string(),
109            token: nanoid::nanoid!(64),
110            ..Default::default()
111        };
112
113        if let Some(data) = data.into() {
114            bot.apply_options(data);
115        }
116
117        db.insert_bot(&bot).await?;
118        Ok((bot, user))
119    }
120
121    /// Remove a field from this object
122    pub fn remove_field(&mut self, field: &FieldsBot) {
123        match field {
124            FieldsBot::Token => self.token = nanoid::nanoid!(64),
125            FieldsBot::InteractionsURL => {
126                self.interactions_url = String::new();
127            }
128        }
129    }
130
131    /// Update this bot
132    pub async fn update(
133        &mut self,
134        db: &Database,
135        mut partial: PartialBot,
136        remove: Vec<FieldsBot>,
137    ) -> Result<()> {
138        if remove.contains(&FieldsBot::Token) {
139            partial.token = Some(nanoid::nanoid!(64));
140        }
141
142        for field in &remove {
143            self.remove_field(field);
144        }
145
146        db.update_bot(&self.id, &partial, remove).await?;
147
148        if partial.token.is_some() {
149            EventV1::Logout.private(self.id.clone()).await;
150        }
151
152        self.apply_options(partial);
153        Ok(())
154    }
155
156    /// Delete this bot
157    pub async fn delete(&self, db: &Database) -> Result<()> {
158        db.fetch_user(&self.id).await?.mark_deleted(db).await?;
159        db.delete_bot(&self.id).await
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use crate::{Bot, FieldsBot, PartialBot, User};
166
167    #[async_std::test]
168    async fn crud() {
169        database_test!(|db| async move {
170            let owner = User::create(&db, "Owner".to_string(), None, None)
171                .await
172                .unwrap();
173
174            let (bot, _) = Bot::create(
175                &db,
176                "Bot Name".to_string(),
177                &owner,
178                PartialBot {
179                    token: Some("my token".to_string()),
180                    interactions_url: Some("some url".to_string()),
181                    ..Default::default()
182                },
183            )
184            .await
185            .unwrap();
186
187            assert!(!bot.interactions_url.is_empty());
188
189            let mut updated_bot = bot.clone();
190            updated_bot
191                .update(
192                    &db,
193                    PartialBot {
194                        public: Some(true),
195                        ..Default::default()
196                    },
197                    vec![FieldsBot::Token, FieldsBot::InteractionsURL],
198                )
199                .await
200                .unwrap();
201
202            let fetched_bot1 = db.fetch_bot(&bot.id).await.unwrap();
203            let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap();
204            let fetched_bots = db.fetch_bots_by_user(&owner.id).await.unwrap();
205
206            assert!(!bot.public);
207            assert!(fetched_bot1.public);
208            assert!(!bot.interactions_url.is_empty());
209            assert!(fetched_bot1.interactions_url.is_empty());
210            assert_ne!(bot.token, fetched_bot1.token);
211            assert_eq!(updated_bot, fetched_bot1);
212            assert_eq!(fetched_bot1, fetched_bot2);
213            assert_eq!(fetched_bot1, fetched_bots[0]);
214            assert_eq!(1, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
215
216            bot.delete(&db).await.unwrap();
217            assert!(db.fetch_bot(&bot.id).await.is_err());
218            assert_eq!(0, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
219            assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2))
220        });
221    }
222}