]> Untitled Git - lemmy.git/blob - lemmy_db/src/user.rs
Merge branch 'main' into move_views_to_diesel
[lemmy.git] / lemmy_db / src / user.rs
1 use crate::{
2   is_email_regex,
3   naive_now,
4   schema::{user_, user_::dsl::*},
5   ApubObject,
6   Crud,
7 };
8 use bcrypt::{hash, DEFAULT_COST};
9 use diesel::{dsl::*, result::Error, *};
10 use lemmy_utils::settings::Settings;
11 use serde::Serialize;
12
13 #[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
14 #[table_name = "user_"]
15 pub struct User_ {
16   pub id: i32,
17   pub name: String,
18   pub preferred_username: Option<String>,
19   pub password_encrypted: String,
20   pub email: Option<String>,
21   pub avatar: Option<String>,
22   pub admin: bool,
23   pub banned: bool,
24   pub published: chrono::NaiveDateTime,
25   pub updated: Option<chrono::NaiveDateTime>,
26   pub show_nsfw: bool,
27   pub theme: String,
28   pub default_sort_type: i16,
29   pub default_listing_type: i16,
30   pub lang: String,
31   pub show_avatars: bool,
32   pub send_notifications_to_email: bool,
33   pub matrix_user_id: Option<String>,
34   pub actor_id: String,
35   pub bio: Option<String>,
36   pub local: bool,
37   pub private_key: Option<String>,
38   pub public_key: Option<String>,
39   pub last_refreshed_at: chrono::NaiveDateTime,
40   pub banner: Option<String>,
41   pub deleted: bool,
42 }
43
44 /// A safe representation of user, without the sensitive info
45 #[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
46 #[table_name = "user_"]
47 pub struct UserSafe {
48   pub id: i32,
49   pub name: String,
50   pub preferred_username: Option<String>,
51   pub avatar: Option<String>,
52   pub admin: bool,
53   pub banned: bool,
54   pub published: chrono::NaiveDateTime,
55   pub updated: Option<chrono::NaiveDateTime>,
56   pub matrix_user_id: Option<String>,
57   pub actor_id: String,
58   pub bio: Option<String>,
59   pub local: bool,
60   pub banner: Option<String>,
61   pub deleted: bool,
62 }
63
64 mod safe_type {
65   use crate::{schema::user_::columns::*, user::User_, ToSafe};
66   type Columns = (
67     id,
68     name,
69     preferred_username,
70     avatar,
71     admin,
72     banned,
73     published,
74     updated,
75     matrix_user_id,
76     actor_id,
77     bio,
78     local,
79     banner,
80     deleted,
81   );
82
83   impl ToSafe for User_ {
84     type SafeColumns = Columns;
85     fn safe_columns_tuple() -> Self::SafeColumns {
86       (
87         id,
88         name,
89         preferred_username,
90         avatar,
91         admin,
92         banned,
93         published,
94         updated,
95         matrix_user_id,
96         actor_id,
97         bio,
98         local,
99         banner,
100         deleted,
101       )
102     }
103   }
104 }
105
106 #[derive(Insertable, AsChangeset, Clone)]
107 #[table_name = "user_"]
108 pub struct UserForm {
109   pub name: String,
110   pub preferred_username: Option<Option<String>>,
111   pub password_encrypted: String,
112   pub admin: bool,
113   pub banned: Option<bool>,
114   pub email: Option<Option<String>>,
115   pub avatar: Option<Option<String>>,
116   pub published: Option<chrono::NaiveDateTime>,
117   pub updated: Option<chrono::NaiveDateTime>,
118   pub show_nsfw: bool,
119   pub theme: String,
120   pub default_sort_type: i16,
121   pub default_listing_type: i16,
122   pub lang: String,
123   pub show_avatars: bool,
124   pub send_notifications_to_email: bool,
125   pub matrix_user_id: Option<Option<String>>,
126   pub actor_id: Option<String>,
127   pub bio: Option<Option<String>>,
128   pub local: bool,
129   pub private_key: Option<String>,
130   pub public_key: Option<String>,
131   pub last_refreshed_at: Option<chrono::NaiveDateTime>,
132   pub banner: Option<Option<String>>,
133 }
134
135 impl Crud<UserForm> for User_ {
136   fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
137     user_
138       .filter(deleted.eq(false))
139       .find(user_id)
140       .first::<Self>(conn)
141   }
142   fn delete(conn: &PgConnection, user_id: i32) -> Result<usize, Error> {
143     diesel::delete(user_.find(user_id)).execute(conn)
144   }
145   fn create(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
146     insert_into(user_).values(form).get_result::<Self>(conn)
147   }
148   fn update(conn: &PgConnection, user_id: i32, form: &UserForm) -> Result<Self, Error> {
149     diesel::update(user_.find(user_id))
150       .set(form)
151       .get_result::<Self>(conn)
152   }
153 }
154
155 impl ApubObject<UserForm> for User_ {
156   fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
157     use crate::schema::user_::dsl::*;
158     user_
159       .filter(deleted.eq(false))
160       .filter(actor_id.eq(object_id))
161       .first::<Self>(conn)
162   }
163
164   fn upsert(conn: &PgConnection, user_form: &UserForm) -> Result<User_, Error> {
165     insert_into(user_)
166       .values(user_form)
167       .on_conflict(actor_id)
168       .do_update()
169       .set(user_form)
170       .get_result::<Self>(conn)
171   }
172 }
173
174 impl User_ {
175   pub fn register(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
176     let mut edited_user = form.clone();
177     let password_hash =
178       hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
179     edited_user.password_encrypted = password_hash;
180
181     Self::create(&conn, &edited_user)
182   }
183
184   // TODO do more individual updates like these
185   pub fn update_password(
186     conn: &PgConnection,
187     user_id: i32,
188     new_password: &str,
189   ) -> Result<Self, Error> {
190     let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
191
192     diesel::update(user_.find(user_id))
193       .set((
194         password_encrypted.eq(password_hash),
195         updated.eq(naive_now()),
196       ))
197       .get_result::<Self>(conn)
198   }
199
200   pub fn read_from_name(conn: &PgConnection, from_user_name: &str) -> Result<Self, Error> {
201     user_
202       .filter(local.eq(true))
203       .filter(deleted.eq(false))
204       .filter(name.eq(from_user_name))
205       .first::<Self>(conn)
206   }
207
208   pub fn add_admin(conn: &PgConnection, user_id: i32, added: bool) -> Result<Self, Error> {
209     diesel::update(user_.find(user_id))
210       .set(admin.eq(added))
211       .get_result::<Self>(conn)
212   }
213
214   pub fn ban_user(conn: &PgConnection, user_id: i32, ban: bool) -> Result<Self, Error> {
215     diesel::update(user_.find(user_id))
216       .set(banned.eq(ban))
217       .get_result::<Self>(conn)
218   }
219
220   pub fn find_by_email_or_username(
221     conn: &PgConnection,
222     username_or_email: &str,
223   ) -> Result<Self, Error> {
224     if is_email_regex(username_or_email) {
225       Self::find_by_email(conn, username_or_email)
226     } else {
227       Self::find_by_username(conn, username_or_email)
228     }
229   }
230
231   pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
232     user_
233       .filter(deleted.eq(false))
234       .filter(local.eq(true))
235       .filter(name.ilike(username))
236       .first::<User_>(conn)
237   }
238
239   pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<User_, Error> {
240     user_
241       .filter(deleted.eq(false))
242       .filter(local.eq(true))
243       .filter(email.eq(from_email))
244       .first::<User_>(conn)
245   }
246
247   pub fn get_profile_url(&self, hostname: &str) -> String {
248     format!(
249       "{}://{}/u/{}",
250       Settings::get().get_protocol_string(),
251       hostname,
252       self.name
253     )
254   }
255
256   pub fn mark_as_updated(conn: &PgConnection, user_id: i32) -> Result<User_, Error> {
257     diesel::update(user_.find(user_id))
258       .set((last_refreshed_at.eq(naive_now()),))
259       .get_result::<Self>(conn)
260   }
261
262   pub fn delete_account(conn: &PgConnection, user_id: i32) -> Result<User_, Error> {
263     diesel::update(user_.find(user_id))
264       .set((
265         preferred_username.eq::<Option<String>>(None),
266         email.eq::<Option<String>>(None),
267         matrix_user_id.eq::<Option<String>>(None),
268         bio.eq::<Option<String>>(None),
269         deleted.eq(true),
270         updated.eq(naive_now()),
271       ))
272       .get_result::<Self>(conn)
273   }
274 }
275
276 #[cfg(test)]
277 mod tests {
278   use crate::{tests::establish_unpooled_connection, user::*, ListingType, SortType};
279
280   #[test]
281   fn test_crud() {
282     let conn = establish_unpooled_connection();
283
284     let new_user = UserForm {
285       name: "thommy".into(),
286       preferred_username: None,
287       password_encrypted: "nope".into(),
288       email: None,
289       matrix_user_id: None,
290       avatar: None,
291       banner: None,
292       admin: false,
293       banned: Some(false),
294       published: None,
295       updated: None,
296       show_nsfw: false,
297       theme: "browser".into(),
298       default_sort_type: SortType::Hot as i16,
299       default_listing_type: ListingType::Subscribed as i16,
300       lang: "browser".into(),
301       show_avatars: true,
302       send_notifications_to_email: false,
303       actor_id: None,
304       bio: None,
305       local: true,
306       private_key: None,
307       public_key: None,
308       last_refreshed_at: None,
309     };
310
311     let inserted_user = User_::create(&conn, &new_user).unwrap();
312
313     let expected_user = User_ {
314       id: inserted_user.id,
315       name: "thommy".into(),
316       preferred_username: None,
317       password_encrypted: "nope".into(),
318       email: None,
319       matrix_user_id: None,
320       avatar: None,
321       banner: None,
322       admin: false,
323       banned: false,
324       published: inserted_user.published,
325       updated: None,
326       show_nsfw: false,
327       theme: "browser".into(),
328       default_sort_type: SortType::Hot as i16,
329       default_listing_type: ListingType::Subscribed as i16,
330       lang: "browser".into(),
331       show_avatars: true,
332       send_notifications_to_email: false,
333       actor_id: inserted_user.actor_id.to_owned(),
334       bio: None,
335       local: true,
336       private_key: None,
337       public_key: None,
338       last_refreshed_at: inserted_user.published,
339       deleted: false,
340     };
341
342     let read_user = User_::read(&conn, inserted_user.id).unwrap();
343     let updated_user = User_::update(&conn, inserted_user.id, &new_user).unwrap();
344     let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
345
346     assert_eq!(expected_user, read_user);
347     assert_eq!(expected_user, inserted_user);
348     assert_eq!(expected_user, updated_user);
349     assert_eq!(1, num_deleted);
350   }
351 }