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}