users.rs

  1use anyhow::Context as _;
  2use chrono::NaiveDateTime;
  3
  4use super::*;
  5
  6impl Database {
  7    /// Creates a new user.
  8    pub async fn create_user(
  9        &self,
 10        email_address: &str,
 11        name: Option<&str>,
 12        admin: bool,
 13        params: NewUserParams,
 14    ) -> Result<NewUserResult> {
 15        self.transaction(|tx| async {
 16            let tx = tx;
 17            let user = user::Entity::insert(user::ActiveModel {
 18                email_address: ActiveValue::set(Some(email_address.into())),
 19                name: ActiveValue::set(name.map(|s| s.into())),
 20                github_login: ActiveValue::set(params.github_login.clone()),
 21                github_user_id: ActiveValue::set(params.github_user_id),
 22                admin: ActiveValue::set(admin),
 23                metrics_id: ActiveValue::set(Uuid::new_v4()),
 24                ..Default::default()
 25            })
 26            .on_conflict(
 27                OnConflict::column(user::Column::GithubUserId)
 28                    .update_columns([
 29                        user::Column::Admin,
 30                        user::Column::EmailAddress,
 31                        user::Column::GithubLogin,
 32                    ])
 33                    .to_owned(),
 34            )
 35            .exec_with_returning(&*tx)
 36            .await?;
 37
 38            Ok(NewUserResult {
 39                user_id: user.id,
 40                metrics_id: user.metrics_id.to_string(),
 41                signup_device_id: None,
 42                inviting_user_id: None,
 43            })
 44        })
 45        .await
 46    }
 47
 48    /// Returns a user by ID. There are no access checks here, so this should only be used internally.
 49    pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
 50        self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
 51            .await
 52    }
 53
 54    /// Returns all users by ID. There are no access checks here, so this should only be used internally.
 55    pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
 56        if ids.len() >= 10000_usize {
 57            return Err(anyhow!("too many users"))?;
 58        }
 59        self.transaction(|tx| async {
 60            let tx = tx;
 61            Ok(user::Entity::find()
 62                .filter(user::Column::Id.is_in(ids.iter().copied()))
 63                .all(&*tx)
 64                .await?)
 65        })
 66        .await
 67    }
 68
 69    /// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
 70    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
 71        self.transaction(|tx| async move {
 72            Ok(user::Entity::find()
 73                .filter(user::Column::GithubLogin.eq(github_login))
 74                .one(&*tx)
 75                .await?)
 76        })
 77        .await
 78    }
 79
 80    pub async fn update_or_create_user_by_github_account(
 81        &self,
 82        github_login: &str,
 83        github_user_id: i32,
 84        github_email: Option<&str>,
 85        github_name: Option<&str>,
 86        github_user_created_at: DateTimeUtc,
 87        initial_channel_id: Option<ChannelId>,
 88    ) -> Result<User> {
 89        self.transaction(|tx| async move {
 90            self.update_or_create_user_by_github_account_tx(
 91                github_login,
 92                github_user_id,
 93                github_email,
 94                github_name,
 95                github_user_created_at.naive_utc(),
 96                initial_channel_id,
 97                &tx,
 98            )
 99            .await
100        })
101        .await
102    }
103
104    pub async fn update_or_create_user_by_github_account_tx(
105        &self,
106        github_login: &str,
107        github_user_id: i32,
108        github_email: Option<&str>,
109        github_name: Option<&str>,
110        github_user_created_at: NaiveDateTime,
111        initial_channel_id: Option<ChannelId>,
112        tx: &DatabaseTransaction,
113    ) -> Result<User> {
114        if let Some(existing_user) = self
115            .get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
116            .await?
117        {
118            let mut existing_user = existing_user.into_active_model();
119            existing_user.github_login = ActiveValue::set(github_login.into());
120            existing_user.github_user_created_at = ActiveValue::set(Some(github_user_created_at));
121
122            if let Some(github_email) = github_email {
123                existing_user.email_address = ActiveValue::set(Some(github_email.into()));
124            }
125
126            if let Some(github_name) = github_name {
127                existing_user.name = ActiveValue::set(Some(github_name.into()));
128            }
129
130            Ok(existing_user.update(tx).await?)
131        } else {
132            let user = user::Entity::insert(user::ActiveModel {
133                email_address: ActiveValue::set(github_email.map(|email| email.into())),
134                name: ActiveValue::set(github_name.map(|name| name.into())),
135                github_login: ActiveValue::set(github_login.into()),
136                github_user_id: ActiveValue::set(github_user_id),
137                github_user_created_at: ActiveValue::set(Some(github_user_created_at)),
138                admin: ActiveValue::set(false),
139                invite_count: ActiveValue::set(0),
140                invite_code: ActiveValue::set(None),
141                metrics_id: ActiveValue::set(Uuid::new_v4()),
142                ..Default::default()
143            })
144            .exec_with_returning(tx)
145            .await?;
146            if let Some(channel_id) = initial_channel_id {
147                channel_member::Entity::insert(channel_member::ActiveModel {
148                    id: ActiveValue::NotSet,
149                    channel_id: ActiveValue::Set(channel_id),
150                    user_id: ActiveValue::Set(user.id),
151                    accepted: ActiveValue::Set(true),
152                    role: ActiveValue::Set(ChannelRole::Guest),
153                })
154                .exec(tx)
155                .await?;
156            }
157            Ok(user)
158        }
159    }
160
161    /// Tries to retrieve a user, first by their GitHub user ID, and then by their GitHub login.
162    ///
163    /// Returns `None` if a user is not found with this GitHub user ID or GitHub login.
164    pub async fn get_user_by_github_user_id_or_github_login(
165        &self,
166        github_user_id: i32,
167        github_login: &str,
168        tx: &DatabaseTransaction,
169    ) -> Result<Option<User>> {
170        if let Some(user_by_github_user_id) = user::Entity::find()
171            .filter(user::Column::GithubUserId.eq(github_user_id))
172            .one(tx)
173            .await?
174        {
175            return Ok(Some(user_by_github_user_id));
176        }
177
178        if let Some(user_by_github_login) = user::Entity::find()
179            .filter(user::Column::GithubLogin.eq(github_login))
180            .one(tx)
181            .await?
182        {
183            return Ok(Some(user_by_github_login));
184        }
185
186        Ok(None)
187    }
188
189    /// get_all_users returns the next page of users. To get more call again with
190    /// the same limit and the page incremented by 1.
191    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
192        self.transaction(|tx| async move {
193            Ok(user::Entity::find()
194                .order_by_asc(user::Column::GithubLogin)
195                .limit(limit as u64)
196                .offset(page as u64 * limit as u64)
197                .all(&*tx)
198                .await?)
199        })
200        .await
201    }
202
203    /// Returns the metrics id for the user.
204    pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
205        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
206        enum QueryAs {
207            MetricsId,
208        }
209
210        self.transaction(|tx| async move {
211            let metrics_id: Uuid = user::Entity::find_by_id(id)
212                .select_only()
213                .column(user::Column::MetricsId)
214                .into_values::<_, QueryAs>()
215                .one(&*tx)
216                .await?
217                .context("could not find user")?;
218            Ok(metrics_id.to_string())
219        })
220        .await
221    }
222
223    /// Sets "connected_once" on the user for analytics.
224    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
225        self.transaction(|tx| async move {
226            user::Entity::update_many()
227                .filter(user::Column::Id.eq(id))
228                .set(user::ActiveModel {
229                    connected_once: ActiveValue::set(connected_once),
230                    ..Default::default()
231                })
232                .exec(&*tx)
233                .await?;
234            Ok(())
235        })
236        .await
237    }
238
239    /// Find users where github_login ILIKE name_query.
240    pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
241        self.transaction(|tx| async {
242            let tx = tx;
243            let like_string = Self::fuzzy_like_string(name_query);
244            let query = "
245                SELECT users.*
246                FROM users
247                WHERE github_login ILIKE $1
248                ORDER BY github_login <-> $2
249                LIMIT $3
250            ";
251
252            Ok(user::Entity::find()
253                .from_raw_sql(Statement::from_sql_and_values(
254                    self.pool.get_database_backend(),
255                    query,
256                    vec![like_string.into(), name_query.into(), limit.into()],
257                ))
258                .all(&*tx)
259                .await?)
260        })
261        .await
262    }
263
264    /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
265    /// e.g. "cir" would become "%c%i%r%"
266    pub fn fuzzy_like_string(string: &str) -> String {
267        let mut result = String::with_capacity(string.len() * 2 + 1);
268        for c in string.chars() {
269            if c.is_alphanumeric() {
270                result.push('%');
271                result.push(c);
272            }
273        }
274        result.push('%');
275        result
276    }
277}