3 newtypes::{CommunityId, InstanceId, LanguageId, LocalUserId, SiteId},
4 schema::{local_site, site, site_language},
10 LocalUserLanguageForm,
17 utils::{get_conn, DbPool},
28 use diesel_async::{AsyncPgConnection, RunQueryDsl};
29 use lemmy_utils::error::{LemmyError, LemmyErrorType};
30 use tokio::sync::OnceCell;
32 pub const UNDETERMINED_ID: LanguageId = LanguageId(0);
34 impl LocalUserLanguage {
36 pool: &mut DbPool<'_>,
37 for_local_user_id: LocalUserId,
38 ) -> Result<Vec<LanguageId>, Error> {
39 use crate::schema::local_user_language::dsl::{
44 let conn = &mut get_conn(pool).await?;
50 let langs = local_user_language
51 .filter(local_user_id.eq(for_local_user_id))
56 convert_read_languages(conn, langs).await
62 /// Update the user's languages.
64 /// If no language_id vector is given, it will show all languages
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?;
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 {
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);
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)))
100 let form = LocalUserLanguageForm {
101 local_user_id: for_local_user_id,
104 insert_into(local_user_language)
106 .get_result::<Self>(conn)
117 pub async fn read_local_raw(pool: &mut DbPool<'_>) -> Result<Vec<LanguageId>, Error> {
118 let conn = &mut get_conn(pool).await?;
120 .inner_join(local_site::table)
121 .inner_join(site_language::table)
122 .order(site_language::id)
123 .select(site_language::language_id)
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)
137 convert_read_languages(conn, langs).await
141 pool: &mut DbPool<'_>,
142 language_ids: Vec<LanguageId>,
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?;
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 {
159 Box::pin(async move {
160 use crate::schema::site_language::dsl::{site_id, site_language};
162 // Clear the current languages
163 delete(site_language.filter(site_id.eq(for_site_id)))
168 let form = SiteLanguageForm {
169 site_id: for_site_id,
172 insert_into(site_language)
174 .get_result::<Self>(conn)
178 CommunityLanguage::limit_languages(conn, instance_id).await?;
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?;
197 if let Some(for_language_id) = for_language_id {
198 let is_allowed = select(exists(
200 .filter(language_id.eq(for_language_id))
201 .filter(community_id.eq(for_community_id)),
209 Err(LemmyErrorType::LanguageNotAllowed)?
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> {
226 community_language::dsl as cl,
227 site_language::dsl as sl,
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)
238 for c in community_languages {
239 delete(cl::community_language.filter(cl::language_id.eq(c)))
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))
258 convert_read_languages(conn, langs).await
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?;
269 let conn = &mut get_conn(pool).await?;
270 let lang_ids = convert_update_languages(conn, language_ids).await?;
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 {
280 .map(|language_id| CommunityLanguageForm {
281 community_id: for_community_id,
284 .collect::<Vec<_>>();
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)))
297 let insert_res = insert_into(community_language)
299 .get_result::<Self>(conn)
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"
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)
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())
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() {
349 Language::read_all(&mut conn.into())
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
373 .expect("read number of languages");
378 if &language_ids.len() == count {
387 #![allow(clippy::unwrap_used)]
388 #![allow(clippy::indexing_slicing)]
392 impls::actor_language::{
393 convert_read_languages,
394 convert_update_languages,
395 default_post_language,
407 community::{Community, CommunityInsertForm},
409 local_site::{LocalSite, LocalSiteInsertForm},
410 local_user::{LocalUser, LocalUserInsertForm},
411 person::{Person, PersonInsertForm},
412 site::{Site, SiteInsertForm},
415 utils::build_db_pool_for_tests,
417 use serial_test::serial;
419 async fn test_langs1(pool: &mut DbPool<'_>) -> Vec<LanguageId> {
421 Language::read_id_from_code(pool, Some("en"))
425 Language::read_id_from_code(pool, Some("fr"))
429 Language::read_id_from_code(pool, Some("ru"))
435 async fn test_langs2(pool: &mut DbPool<'_>) -> Vec<LanguageId> {
437 Language::read_id_from_code(pool, Some("fi"))
441 Language::read_id_from_code(pool, Some("se"))
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())
453 let site_form = SiteInsertForm::builder()
454 .name("test site".to_string())
455 .instance_id(inserted_instance.id)
457 let site = Site::create(pool, &site_form).await.unwrap();
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();
463 (site, inserted_instance)
468 async fn test_convert_update_languages() {
469 let pool = &build_db_pool_for_tests().await;
470 let pool = &mut pool.into();
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());
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())
482 assert_eq!(test_langs, converted2);
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();
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());
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())
502 assert_eq!(test_langs, converted2);
507 async fn test_site_languages() {
508 let pool = &build_db_pool_for_tests().await;
509 let pool = &mut pool.into();
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());
516 let test_langs = test_langs1(pool).await;
517 SiteLanguage::update(pool, test_langs.clone(), &site)
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);
525 Site::delete(pool, site.id).await.unwrap();
526 Instance::delete(pool, instance.id).await.unwrap();
527 LocalSite::delete(pool).await.unwrap();
532 async fn test_user_languages() {
533 let pool = &build_db_pool_for_tests().await;
534 let pool = &mut pool.into();
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)
542 let person_form = PersonInsertForm::builder()
543 .name("my test person".to_string())
544 .public_key("pubkey".to_string())
545 .instance_id(instance.id)
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())
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();
556 // new user should be initialized with site languages and undetermined
557 //test_langs.push(UNDETERMINED_ID);
559 test_langs.insert(0, UNDETERMINED_ID);
560 assert_eq!(test_langs, local_user_langs1);
562 // update user languages
563 let test_langs2 = test_langs2(pool).await;
564 LocalUserLanguage::update(pool, test_langs2, local_user.id)
567 let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await.unwrap();
568 assert_eq!(3, local_user_langs2.len());
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();
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)
588 let read_site_langs = SiteLanguage::read(pool, site.id).await.unwrap();
589 assert_eq!(test_langs, read_site_langs);
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);
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)
601 let community = Community::create(pool, &community_form).await.unwrap();
602 let community_langs1 = CommunityLanguage::read(pool, community.id).await.unwrap();
604 // community is initialized with site languages
605 assert_eq!(test_langs, community_langs1);
608 CommunityLanguage::is_allowed_community_language(pool, Some(test_langs[0]), community.id)
610 assert!(allowed_lang1.is_ok());
612 let test_langs2 = test_langs2(pool).await;
614 CommunityLanguage::is_allowed_community_language(pool, Some(test_langs2[0]), community.id)
616 assert!(allowed_lang2.is_err());
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)
623 let community_langs2 = CommunityLanguage::read(pool, community.id).await.unwrap();
624 assert_eq!(vec![test_langs[0]], community_langs2);
626 // update community languages to different ones
627 CommunityLanguage::update(pool, test_langs2.clone(), community.id)
630 let community_langs3 = CommunityLanguage::read(pool, community.id).await.unwrap();
631 assert_eq!(test_langs2, community_langs3);
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();
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;
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)
654 let community = Community::create(pool, &community_form).await.unwrap();
655 CommunityLanguage::update(pool, test_langs, community.id)
659 let person_form = PersonInsertForm::builder()
660 .name("my test person".to_string())
661 .public_key("pubkey".to_string())
662 .instance_id(instance.id)
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())
669 let local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
670 LocalUserLanguage::update(pool, test_langs2, local_user.id)
674 // no overlap in user/community languages, so defaults to undetermined
675 let def1 = default_post_language(pool, community.id, local_user.id)
678 assert_eq!(None, def1);
680 let ru = Language::read_id_from_code(pool, Some("ru"))
684 let test_langs3 = vec![
686 Language::read_id_from_code(pool, Some("fi"))
690 Language::read_id_from_code(pool, Some("se"))
696 LocalUserLanguage::update(pool, test_langs3, local_user.id)
700 // this time, both have ru as common lang
701 let def2 = default_post_language(pool, community.id, local_user.id)
704 assert_eq!(Some(ru), def2);
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();