]> Untitled Git - lemmy.git/blob - crates/db_schema/src/impls/actor_language.rs
Cache & Optimize Woodpecker CI (#3450)
[lemmy.git] / crates / db_schema / src / impls / actor_language.rs
1 use crate::{
2   diesel::JoinOnDsl,
3   newtypes::{CommunityId, InstanceId, LanguageId, LocalUserId, SiteId},
4   schema::{local_site, site, site_language},
5   source::{
6     actor_language::{
7       CommunityLanguage,
8       CommunityLanguageForm,
9       LocalUserLanguage,
10       LocalUserLanguageForm,
11       SiteLanguage,
12       SiteLanguageForm,
13     },
14     language::Language,
15     site::Site,
16   },
17   utils::{get_conn, DbPool},
18 };
19 use diesel::{
20   delete,
21   dsl::{count, exists},
22   insert_into,
23   result::Error,
24   select,
25   ExpressionMethods,
26   QueryDsl,
27 };
28 use diesel_async::{AsyncPgConnection, RunQueryDsl};
29 use lemmy_utils::error::{LemmyError, LemmyErrorType};
30 use tokio::sync::OnceCell;
31
32 pub const UNDETERMINED_ID: LanguageId = LanguageId(0);
33
34 impl LocalUserLanguage {
35   pub async fn read(
36     pool: &mut DbPool<'_>,
37     for_local_user_id: LocalUserId,
38   ) -> Result<Vec<LanguageId>, Error> {
39     use crate::schema::local_user_language::dsl::{
40       language_id,
41       local_user_id,
42       local_user_language,
43     };
44     let conn = &mut get_conn(pool).await?;
45
46     conn
47       .build_transaction()
48       .run(|conn| {
49         Box::pin(async move {
50           let langs = local_user_language
51             .filter(local_user_id.eq(for_local_user_id))
52             .order(language_id)
53             .select(language_id)
54             .get_results(conn)
55             .await?;
56           convert_read_languages(conn, langs).await
57         }) as _
58       })
59       .await
60   }
61
62   /// Update the user's languages.
63   ///
64   /// If no language_id vector is given, it will show all languages
65   pub async fn update(
66     pool: &mut DbPool<'_>,
67     language_ids: Vec<LanguageId>,
68     for_local_user_id: LocalUserId,
69   ) -> Result<(), Error> {
70     let conn = &mut get_conn(pool).await?;
71     let mut lang_ids = convert_update_languages(conn, language_ids).await?;
72
73     // No need to update if languages are unchanged
74     let current = LocalUserLanguage::read(&mut conn.into(), for_local_user_id).await?;
75     if current == lang_ids {
76       return Ok(());
77     }
78
79     // TODO: Force enable undetermined language for all users. This is necessary because many posts
80     //       don't have a language tag (e.g. those from other federated platforms), so Lemmy users
81     //       won't see them if undetermined language is disabled.
82     //       This hack can be removed once a majority of posts have language tags, or when it is
83     //       clearer for new users that they need to enable undetermined language.
84     //       See https://github.com/LemmyNet/lemmy-ui/issues/999
85     if !lang_ids.contains(&UNDETERMINED_ID) {
86       lang_ids.push(UNDETERMINED_ID);
87     }
88
89     conn
90       .build_transaction()
91       .run(|conn| {
92         Box::pin(async move {
93           use crate::schema::local_user_language::dsl::{local_user_id, local_user_language};
94           // Clear the current user languages
95           delete(local_user_language.filter(local_user_id.eq(for_local_user_id)))
96             .execute(conn)
97             .await?;
98
99           for l in lang_ids {
100             let form = LocalUserLanguageForm {
101               local_user_id: for_local_user_id,
102               language_id: l,
103             };
104             insert_into(local_user_language)
105               .values(form)
106               .get_result::<Self>(conn)
107               .await?;
108           }
109           Ok(())
110         }) as _
111       })
112       .await
113   }
114 }
115
116 impl SiteLanguage {
117   pub async fn read_local_raw(pool: &mut DbPool<'_>) -> Result<Vec<LanguageId>, Error> {
118     let conn = &mut get_conn(pool).await?;
119     site::table
120       .inner_join(local_site::table)
121       .inner_join(site_language::table)
122       .order(site_language::id)
123       .select(site_language::language_id)
124       .load(conn)
125       .await
126   }
127
128   pub async fn read(pool: &mut DbPool<'_>, for_site_id: SiteId) -> Result<Vec<LanguageId>, Error> {
129     let conn = &mut get_conn(pool).await?;
130     let langs = site_language::table
131       .filter(site_language::site_id.eq(for_site_id))
132       .order(site_language::language_id)
133       .select(site_language::language_id)
134       .load(conn)
135       .await?;
136
137     convert_read_languages(conn, langs).await
138   }
139
140   pub async fn update(
141     pool: &mut DbPool<'_>,
142     language_ids: Vec<LanguageId>,
143     site: &Site,
144   ) -> Result<(), Error> {
145     let conn = &mut get_conn(pool).await?;
146     let for_site_id = site.id;
147     let instance_id = site.instance_id;
148     let lang_ids = convert_update_languages(conn, language_ids).await?;
149
150     // No need to update if languages are unchanged
151     let current = SiteLanguage::read(&mut conn.into(), site.id).await?;
152     if current == lang_ids {
153       return Ok(());
154     }
155
156     conn
157       .build_transaction()
158       .run(|conn| {
159         Box::pin(async move {
160           use crate::schema::site_language::dsl::{site_id, site_language};
161
162           // Clear the current languages
163           delete(site_language.filter(site_id.eq(for_site_id)))
164             .execute(conn)
165             .await?;
166
167           for l in lang_ids {
168             let form = SiteLanguageForm {
169               site_id: for_site_id,
170               language_id: l,
171             };
172             insert_into(site_language)
173               .values(form)
174               .get_result::<Self>(conn)
175               .await?;
176           }
177
178           CommunityLanguage::limit_languages(conn, instance_id).await?;
179
180           Ok(())
181         }) as _
182       })
183       .await
184   }
185 }
186
187 impl CommunityLanguage {
188   /// Returns true if the given language is one of configured languages for given community
189   pub async fn is_allowed_community_language(
190     pool: &mut DbPool<'_>,
191     for_language_id: Option<LanguageId>,
192     for_community_id: CommunityId,
193   ) -> Result<(), LemmyError> {
194     use crate::schema::community_language::dsl::{community_id, community_language, language_id};
195     let conn = &mut get_conn(pool).await?;
196
197     if let Some(for_language_id) = for_language_id {
198       let is_allowed = select(exists(
199         community_language
200           .filter(language_id.eq(for_language_id))
201           .filter(community_id.eq(for_community_id)),
202       ))
203       .get_result(conn)
204       .await?;
205
206       if is_allowed {
207         Ok(())
208       } else {
209         Err(LemmyErrorType::LanguageNotAllowed)?
210       }
211     } else {
212       Ok(())
213     }
214   }
215
216   /// When site languages are updated, delete all languages of local communities which are not
217   /// also part of site languages. This is because post/comment language is only checked against
218   /// community language, and it shouldnt be possible to post content in languages which are not
219   /// allowed by local site.
220   async fn limit_languages(
221     conn: &mut AsyncPgConnection,
222     for_instance_id: InstanceId,
223   ) -> Result<(), Error> {
224     use crate::schema::{
225       community::dsl as c,
226       community_language::dsl as cl,
227       site_language::dsl as sl,
228     };
229     let community_languages: Vec<LanguageId> = cl::community_language
230       .left_outer_join(sl::site_language.on(cl::language_id.eq(sl::language_id)))
231       .inner_join(c::community)
232       .filter(c::instance_id.eq(for_instance_id))
233       .filter(sl::language_id.is_null())
234       .select(cl::language_id)
235       .get_results(conn)
236       .await?;
237
238     for c in community_languages {
239       delete(cl::community_language.filter(cl::language_id.eq(c)))
240         .execute(conn)
241         .await?;
242     }
243     Ok(())
244   }
245
246   pub async fn read(
247     pool: &mut DbPool<'_>,
248     for_community_id: CommunityId,
249   ) -> Result<Vec<LanguageId>, Error> {
250     use crate::schema::community_language::dsl::{community_id, community_language, language_id};
251     let conn = &mut get_conn(pool).await?;
252     let langs = community_language
253       .filter(community_id.eq(for_community_id))
254       .order(language_id)
255       .select(language_id)
256       .get_results(conn)
257       .await?;
258     convert_read_languages(conn, langs).await
259   }
260
261   pub async fn update(
262     pool: &mut DbPool<'_>,
263     mut language_ids: Vec<LanguageId>,
264     for_community_id: CommunityId,
265   ) -> Result<(), Error> {
266     if language_ids.is_empty() {
267       language_ids = SiteLanguage::read_local_raw(pool).await?;
268     }
269     let conn = &mut get_conn(pool).await?;
270     let lang_ids = convert_update_languages(conn, language_ids).await?;
271
272     // No need to update if languages are unchanged
273     let current = CommunityLanguage::read(&mut conn.into(), for_community_id).await?;
274     if current == lang_ids {
275       return Ok(());
276     }
277
278     let form = lang_ids
279       .into_iter()
280       .map(|language_id| CommunityLanguageForm {
281         community_id: for_community_id,
282         language_id,
283       })
284       .collect::<Vec<_>>();
285
286     conn
287       .build_transaction()
288       .run(|conn| {
289         Box::pin(async move {
290           use crate::schema::community_language::dsl::{community_id, community_language};
291           use diesel::result::DatabaseErrorKind::UniqueViolation;
292           // Clear the current languages
293           delete(community_language.filter(community_id.eq(for_community_id)))
294             .execute(conn)
295             .await?;
296
297           let insert_res = insert_into(community_language)
298             .values(form)
299             .get_result::<Self>(conn)
300             .await;
301
302           if let Err(Error::DatabaseError(UniqueViolation, _info)) = insert_res {
303             // race condition: this function was probably called simultaneously from another caller. ignore error
304             // tracing::warn!("unique error: {_info:#?}");
305             // _info.constraint_name() should be = "community_language_community_id_language_id_key"
306             return Ok(());
307           } else {
308             insert_res?;
309           }
310           Ok(())
311         }) as _
312       })
313       .await
314   }
315 }
316
317 pub async fn default_post_language(
318   pool: &mut DbPool<'_>,
319   community_id: CommunityId,
320   local_user_id: LocalUserId,
321 ) -> Result<Option<LanguageId>, Error> {
322   use crate::schema::{community_language::dsl as cl, local_user_language::dsl as ul};
323   let conn = &mut get_conn(pool).await?;
324   let mut intersection = ul::local_user_language
325     .inner_join(cl::community_language.on(ul::language_id.eq(cl::language_id)))
326     .filter(ul::local_user_id.eq(local_user_id))
327     .filter(cl::community_id.eq(community_id))
328     .select(cl::language_id)
329     .get_results::<LanguageId>(conn)
330     .await?;
331
332   if intersection.len() == 1 {
333     Ok(intersection.pop())
334   } else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) {
335     intersection.retain(|i| i != &UNDETERMINED_ID);
336     Ok(intersection.pop())
337   } else {
338     Ok(None)
339   }
340 }
341
342 /// If no language is given, set all languages
343 async fn convert_update_languages(
344   conn: &mut AsyncPgConnection,
345   language_ids: Vec<LanguageId>,
346 ) -> Result<Vec<LanguageId>, Error> {
347   if language_ids.is_empty() {
348     Ok(
349       Language::read_all(&mut conn.into())
350         .await?
351         .into_iter()
352         .map(|l| l.id)
353         .collect(),
354     )
355   } else {
356     Ok(language_ids)
357   }
358 }
359
360 /// If all languages are returned, return empty vec instead
361 async fn convert_read_languages(
362   conn: &mut AsyncPgConnection,
363   language_ids: Vec<LanguageId>,
364 ) -> Result<Vec<LanguageId>, Error> {
365   static ALL_LANGUAGES_COUNT: OnceCell<usize> = OnceCell::const_new();
366   let count = ALL_LANGUAGES_COUNT
367     .get_or_init(|| async {
368       use crate::schema::language::dsl::{id, language};
369       let count: i64 = language
370         .select(count(id))
371         .first(conn)
372         .await
373         .expect("read number of languages");
374       count as usize
375     })
376     .await;
377
378   if &language_ids.len() == count {
379     Ok(vec![])
380   } else {
381     Ok(language_ids)
382   }
383 }
384
385 #[cfg(test)]
386 mod tests {
387   #![allow(clippy::unwrap_used)]
388   #![allow(clippy::indexing_slicing)]
389
390   use super::*;
391   use crate::{
392     impls::actor_language::{
393       convert_read_languages,
394       convert_update_languages,
395       default_post_language,
396       get_conn,
397       CommunityLanguage,
398       DbPool,
399       Language,
400       LanguageId,
401       LocalUserLanguage,
402       QueryDsl,
403       RunQueryDsl,
404       SiteLanguage,
405     },
406     source::{
407       community::{Community, CommunityInsertForm},
408       instance::Instance,
409       local_site::{LocalSite, LocalSiteInsertForm},
410       local_user::{LocalUser, LocalUserInsertForm},
411       person::{Person, PersonInsertForm},
412       site::{Site, SiteInsertForm},
413     },
414     traits::Crud,
415     utils::build_db_pool_for_tests,
416   };
417   use serial_test::serial;
418
419   async fn test_langs1(pool: &mut DbPool<'_>) -> Vec<LanguageId> {
420     vec![
421       Language::read_id_from_code(pool, Some("en"))
422         .await
423         .unwrap()
424         .unwrap(),
425       Language::read_id_from_code(pool, Some("fr"))
426         .await
427         .unwrap()
428         .unwrap(),
429       Language::read_id_from_code(pool, Some("ru"))
430         .await
431         .unwrap()
432         .unwrap(),
433     ]
434   }
435   async fn test_langs2(pool: &mut DbPool<'_>) -> Vec<LanguageId> {
436     vec![
437       Language::read_id_from_code(pool, Some("fi"))
438         .await
439         .unwrap()
440         .unwrap(),
441       Language::read_id_from_code(pool, Some("se"))
442         .await
443         .unwrap()
444         .unwrap(),
445     ]
446   }
447
448   async fn create_test_site(pool: &mut DbPool<'_>) -> (Site, Instance) {
449     let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
450       .await
451       .unwrap();
452
453     let site_form = SiteInsertForm::builder()
454       .name("test site".to_string())
455       .instance_id(inserted_instance.id)
456       .build();
457     let site = Site::create(pool, &site_form).await.unwrap();
458
459     // Create a local site, since this is necessary for local languages
460     let local_site_form = LocalSiteInsertForm::builder().site_id(site.id).build();
461     LocalSite::create(pool, &local_site_form).await.unwrap();
462
463     (site, inserted_instance)
464   }
465
466   #[tokio::test]
467   #[serial]
468   async fn test_convert_update_languages() {
469     let pool = &build_db_pool_for_tests().await;
470     let pool = &mut pool.into();
471
472     // call with empty vec, returns all languages
473     let conn = &mut get_conn(pool).await.unwrap();
474     let converted1 = convert_update_languages(conn, vec![]).await.unwrap();
475     assert_eq!(184, converted1.len());
476
477     // call with nonempty vec, returns same vec
478     let test_langs = test_langs1(&mut conn.into()).await;
479     let converted2 = convert_update_languages(conn, test_langs.clone())
480       .await
481       .unwrap();
482     assert_eq!(test_langs, converted2);
483   }
484   #[tokio::test]
485   #[serial]
486   async fn test_convert_read_languages() {
487     use crate::schema::language::dsl::{id, language};
488     let pool = &build_db_pool_for_tests().await;
489     let pool = &mut pool.into();
490
491     // call with all languages, returns empty vec
492     let conn = &mut get_conn(pool).await.unwrap();
493     let all_langs = language.select(id).get_results(conn).await.unwrap();
494     let converted1: Vec<LanguageId> = convert_read_languages(conn, all_langs).await.unwrap();
495     assert_eq!(0, converted1.len());
496
497     // call with nonempty vec, returns same vec
498     let test_langs = test_langs1(&mut conn.into()).await;
499     let converted2 = convert_read_languages(conn, test_langs.clone())
500       .await
501       .unwrap();
502     assert_eq!(test_langs, converted2);
503   }
504
505   #[tokio::test]
506   #[serial]
507   async fn test_site_languages() {
508     let pool = &build_db_pool_for_tests().await;
509     let pool = &mut pool.into();
510
511     let (site, instance) = create_test_site(pool).await;
512     let site_languages1 = SiteLanguage::read_local_raw(pool).await.unwrap();
513     // site is created with all languages
514     assert_eq!(184, site_languages1.len());
515
516     let test_langs = test_langs1(pool).await;
517     SiteLanguage::update(pool, test_langs.clone(), &site)
518       .await
519       .unwrap();
520
521     let site_languages2 = SiteLanguage::read_local_raw(pool).await.unwrap();
522     // after update, site only has new languages
523     assert_eq!(test_langs, site_languages2);
524
525     Site::delete(pool, site.id).await.unwrap();
526     Instance::delete(pool, instance.id).await.unwrap();
527     LocalSite::delete(pool).await.unwrap();
528   }
529
530   #[tokio::test]
531   #[serial]
532   async fn test_user_languages() {
533     let pool = &build_db_pool_for_tests().await;
534     let pool = &mut pool.into();
535
536     let (site, instance) = create_test_site(pool).await;
537     let mut test_langs = test_langs1(pool).await;
538     SiteLanguage::update(pool, test_langs.clone(), &site)
539       .await
540       .unwrap();
541
542     let person_form = PersonInsertForm::builder()
543       .name("my test person".to_string())
544       .public_key("pubkey".to_string())
545       .instance_id(instance.id)
546       .build();
547     let person = Person::create(pool, &person_form).await.unwrap();
548     let local_user_form = LocalUserInsertForm::builder()
549       .person_id(person.id)
550       .password_encrypted("my_pw".to_string())
551       .build();
552
553     let local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
554     let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await.unwrap();
555
556     // new user should be initialized with site languages and undetermined
557     //test_langs.push(UNDETERMINED_ID);
558     //test_langs.sort();
559     test_langs.insert(0, UNDETERMINED_ID);
560     assert_eq!(test_langs, local_user_langs1);
561
562     // update user languages
563     let test_langs2 = test_langs2(pool).await;
564     LocalUserLanguage::update(pool, test_langs2, local_user.id)
565       .await
566       .unwrap();
567     let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await.unwrap();
568     assert_eq!(3, local_user_langs2.len());
569
570     Person::delete(pool, person.id).await.unwrap();
571     LocalUser::delete(pool, local_user.id).await.unwrap();
572     Site::delete(pool, site.id).await.unwrap();
573     LocalSite::delete(pool).await.unwrap();
574     Instance::delete(pool, instance.id).await.unwrap();
575   }
576
577   #[tokio::test]
578   #[serial]
579   async fn test_community_languages() {
580     let pool = &build_db_pool_for_tests().await;
581     let pool = &mut pool.into();
582     let (site, instance) = create_test_site(pool).await;
583     let test_langs = test_langs1(pool).await;
584     SiteLanguage::update(pool, test_langs.clone(), &site)
585       .await
586       .unwrap();
587
588     let read_site_langs = SiteLanguage::read(pool, site.id).await.unwrap();
589     assert_eq!(test_langs, read_site_langs);
590
591     // Test the local ones are the same
592     let read_local_site_langs = SiteLanguage::read_local_raw(pool).await.unwrap();
593     assert_eq!(test_langs, read_local_site_langs);
594
595     let community_form = CommunityInsertForm::builder()
596       .name("test community".to_string())
597       .title("test community".to_string())
598       .public_key("pubkey".to_string())
599       .instance_id(instance.id)
600       .build();
601     let community = Community::create(pool, &community_form).await.unwrap();
602     let community_langs1 = CommunityLanguage::read(pool, community.id).await.unwrap();
603
604     // community is initialized with site languages
605     assert_eq!(test_langs, community_langs1);
606
607     let allowed_lang1 =
608       CommunityLanguage::is_allowed_community_language(pool, Some(test_langs[0]), community.id)
609         .await;
610     assert!(allowed_lang1.is_ok());
611
612     let test_langs2 = test_langs2(pool).await;
613     let allowed_lang2 =
614       CommunityLanguage::is_allowed_community_language(pool, Some(test_langs2[0]), community.id)
615         .await;
616     assert!(allowed_lang2.is_err());
617
618     // limit site languages to en, fi. after this, community languages should be updated to
619     // intersection of old languages (en, fr, ru) and (en, fi), which is only fi.
620     SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site)
621       .await
622       .unwrap();
623     let community_langs2 = CommunityLanguage::read(pool, community.id).await.unwrap();
624     assert_eq!(vec![test_langs[0]], community_langs2);
625
626     // update community languages to different ones
627     CommunityLanguage::update(pool, test_langs2.clone(), community.id)
628       .await
629       .unwrap();
630     let community_langs3 = CommunityLanguage::read(pool, community.id).await.unwrap();
631     assert_eq!(test_langs2, community_langs3);
632
633     Community::delete(pool, community.id).await.unwrap();
634     Site::delete(pool, site.id).await.unwrap();
635     LocalSite::delete(pool).await.unwrap();
636     Instance::delete(pool, instance.id).await.unwrap();
637   }
638
639   #[tokio::test]
640   #[serial]
641   async fn test_default_post_language() {
642     let pool = &build_db_pool_for_tests().await;
643     let pool = &mut pool.into();
644     let (site, instance) = create_test_site(pool).await;
645     let test_langs = test_langs1(pool).await;
646     let test_langs2 = test_langs2(pool).await;
647
648     let community_form = CommunityInsertForm::builder()
649       .name("test community".to_string())
650       .title("test community".to_string())
651       .public_key("pubkey".to_string())
652       .instance_id(instance.id)
653       .build();
654     let community = Community::create(pool, &community_form).await.unwrap();
655     CommunityLanguage::update(pool, test_langs, community.id)
656       .await
657       .unwrap();
658
659     let person_form = PersonInsertForm::builder()
660       .name("my test person".to_string())
661       .public_key("pubkey".to_string())
662       .instance_id(instance.id)
663       .build();
664     let person = Person::create(pool, &person_form).await.unwrap();
665     let local_user_form = LocalUserInsertForm::builder()
666       .person_id(person.id)
667       .password_encrypted("my_pw".to_string())
668       .build();
669     let local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
670     LocalUserLanguage::update(pool, test_langs2, local_user.id)
671       .await
672       .unwrap();
673
674     // no overlap in user/community languages, so defaults to undetermined
675     let def1 = default_post_language(pool, community.id, local_user.id)
676       .await
677       .unwrap();
678     assert_eq!(None, def1);
679
680     let ru = Language::read_id_from_code(pool, Some("ru"))
681       .await
682       .unwrap()
683       .unwrap();
684     let test_langs3 = vec![
685       ru,
686       Language::read_id_from_code(pool, Some("fi"))
687         .await
688         .unwrap()
689         .unwrap(),
690       Language::read_id_from_code(pool, Some("se"))
691         .await
692         .unwrap()
693         .unwrap(),
694       UNDETERMINED_ID,
695     ];
696     LocalUserLanguage::update(pool, test_langs3, local_user.id)
697       .await
698       .unwrap();
699
700     // this time, both have ru as common lang
701     let def2 = default_post_language(pool, community.id, local_user.id)
702       .await
703       .unwrap();
704     assert_eq!(Some(ru), def2);
705
706     Person::delete(pool, person.id).await.unwrap();
707     Community::delete(pool, community.id).await.unwrap();
708     LocalUser::delete(pool, local_user.id).await.unwrap();
709     Site::delete(pool, site.id).await.unwrap();
710     LocalSite::delete(pool).await.unwrap();
711     Instance::delete(pool, instance.id).await.unwrap();
712   }
713 }