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