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}