users.rs

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