mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-09-03 11:43:51 +00:00
Adding GDPR-style data export (#5801)
* Adding GDPR-style data export - Fixes #4540 Still need to: - [ ] Figure out limits * Fixing format * Adding no_limit overrides. * Slimming down export. * Cleaning up types some more. * Addressing PR comments. * Embedding settings in backup. * Fixing comment.
This commit is contained in:
parent
e3d36d4f9a
commit
899c87f21f
20 changed files with 293 additions and 79 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3149,6 +3149,7 @@ dependencies = [
|
|||
"lemmy_db_views_local_user",
|
||||
"lemmy_db_views_modlog_combined",
|
||||
"lemmy_db_views_person",
|
||||
"lemmy_db_views_person_content_combined",
|
||||
"lemmy_db_views_person_liked_combined",
|
||||
"lemmy_db_views_person_saved_combined",
|
||||
"lemmy_db_views_post",
|
||||
|
|
|
@ -34,6 +34,9 @@ lemmy_db_views_inbox_combined = { workspace = true, features = ["full"] }
|
|||
lemmy_db_views_modlog_combined = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views_person_saved_combined = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views_person_liked_combined = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views_person_content_combined = { workspace = true, features = [
|
||||
"full",
|
||||
] }
|
||||
lemmy_db_views_report_combined = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views_site = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views_registration_applications = { workspace = true, features = [
|
||||
|
|
103
crates/api/api/src/local_user/export_data.rs
Normal file
103
crates/api/api/src/local_user/export_data.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_utils::context::LemmyContext;
|
||||
use lemmy_db_schema::source::local_user::LocalUser;
|
||||
use lemmy_db_views_community_moderator::CommunityModeratorView;
|
||||
use lemmy_db_views_inbox_combined::{impls::InboxCombinedQuery, InboxCombinedView};
|
||||
use lemmy_db_views_local_user::LocalUserView;
|
||||
use lemmy_db_views_person_content_combined::{
|
||||
impls::PersonContentCombinedQuery,
|
||||
PersonContentCombinedView,
|
||||
};
|
||||
use lemmy_db_views_person_liked_combined::{
|
||||
impls::PersonLikedCombinedQuery,
|
||||
PersonLikedCombinedView,
|
||||
};
|
||||
use lemmy_db_views_post::PostView;
|
||||
use lemmy_db_views_site::{
|
||||
api::{ExportDataResponse, PostOrCommentOrPrivateMessage},
|
||||
impls::user_backup_list_to_user_settings_backup,
|
||||
};
|
||||
use lemmy_utils::{self, error::LemmyResult};
|
||||
|
||||
pub async fn export_data(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<ExportDataResponse>> {
|
||||
use PostOrCommentOrPrivateMessage::*;
|
||||
|
||||
let local_instance_id = local_user_view.person.instance_id;
|
||||
let my_person_id = local_user_view.person.id;
|
||||
let my_person = &local_user_view.person;
|
||||
let local_user = &local_user_view.local_user;
|
||||
|
||||
let pool = &mut context.pool();
|
||||
|
||||
let content = PersonContentCombinedQuery {
|
||||
no_limit: Some(true),
|
||||
..PersonContentCombinedQuery::new(my_person_id)
|
||||
}
|
||||
.list(pool, Some(&local_user_view), local_instance_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| match u {
|
||||
PersonContentCombinedView::Post(pv) => Post(pv.post),
|
||||
PersonContentCombinedView::Comment(cv) => Comment(cv.comment),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let inbox = InboxCombinedQuery {
|
||||
no_limit: Some(true),
|
||||
..InboxCombinedQuery::default()
|
||||
}
|
||||
.list(pool, my_person_id, local_instance_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| match u {
|
||||
InboxCombinedView::CommentReply(cr) => Comment(cr.comment),
|
||||
InboxCombinedView::CommentMention(cm) => Comment(cm.comment),
|
||||
InboxCombinedView::PostMention(pm) => Post(pm.post),
|
||||
InboxCombinedView::PrivateMessage(pm) => PrivateMessage(pm.private_message),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let liked = PersonLikedCombinedQuery {
|
||||
no_limit: Some(true),
|
||||
..PersonLikedCombinedQuery::default()
|
||||
}
|
||||
.list(pool, &local_user_view)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| {
|
||||
match u {
|
||||
PersonLikedCombinedView::Post(pv) => pv.post.ap_id,
|
||||
PersonLikedCombinedView::Comment(cv) => cv.comment.ap_id,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let read_posts = PostView::list_read(pool, my_person, None, None, None, Some(true))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|pv| pv.post.ap_id.into())
|
||||
.collect();
|
||||
|
||||
let moderates = CommunityModeratorView::for_person(pool, my_person_id, Some(local_user))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|cv| cv.community.ap_id.into())
|
||||
.collect();
|
||||
|
||||
let lists = LocalUser::export_backup(pool, local_user_view.person.id).await?;
|
||||
let settings = user_backup_list_to_user_settings_backup(local_user_view, lists);
|
||||
|
||||
Ok(Json(ExportDataResponse {
|
||||
inbox,
|
||||
content,
|
||||
liked,
|
||||
read_posts,
|
||||
moderates,
|
||||
settings,
|
||||
}))
|
||||
}
|
|
@ -24,6 +24,7 @@ pub async fn list_person_hidden(
|
|||
cursor_data,
|
||||
data.page_back,
|
||||
data.limit,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ pub async fn list_person_liked(
|
|||
cursor_data,
|
||||
page_back: data.page_back,
|
||||
limit: data.limit,
|
||||
no_limit: None,
|
||||
}
|
||||
.list(&mut context.pool(), &local_user_view)
|
||||
.await?;
|
||||
|
|
|
@ -24,6 +24,7 @@ pub async fn list_person_read(
|
|||
cursor_data,
|
||||
data.page_back,
|
||||
data.limit,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ pub async fn list_person_saved(
|
|||
cursor_data,
|
||||
page_back: data.page_back,
|
||||
limit: data.limit,
|
||||
no_limit: None,
|
||||
}
|
||||
.list(&mut context.pool(), &local_user_view)
|
||||
.await?;
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod block;
|
|||
pub mod change_password;
|
||||
pub mod change_password_after_reset;
|
||||
pub mod donation_dialog_shown;
|
||||
pub mod export_data;
|
||||
pub mod generate_totp_secret;
|
||||
pub mod get_captcha;
|
||||
pub mod list_hidden;
|
||||
|
|
|
@ -31,6 +31,7 @@ pub async fn list_inbox(
|
|||
cursor_data,
|
||||
page_back: data.page_back,
|
||||
limit: data.limit,
|
||||
no_limit: None,
|
||||
}
|
||||
.list(&mut context.pool(), person_id, local_instance_id)
|
||||
.await?;
|
||||
|
|
|
@ -44,8 +44,13 @@ pub async fn list_person_content(
|
|||
cursor_data,
|
||||
page_back: data.page_back,
|
||||
limit: data.limit,
|
||||
no_limit: None,
|
||||
}
|
||||
.list(&mut context.pool(), &local_user_view, local_instance_id)
|
||||
.list(
|
||||
&mut context.pool(),
|
||||
local_user_view.as_ref(),
|
||||
local_instance_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let next_page = content.last().map(PaginationCursorBuilder::to_cursor);
|
||||
|
|
|
@ -10,7 +10,6 @@ use lemmy_apub_objects::objects::{
|
|||
post::ApubPost,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::DbUrl,
|
||||
source::{
|
||||
comment::{CommentActions, CommentSavedForm},
|
||||
community::{CommunityActions, CommunityBlockForm, CommunityFollowerForm},
|
||||
|
@ -23,73 +22,29 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_schema_file::enums::CommunityFollowerState;
|
||||
use lemmy_db_views_local_user::LocalUserView;
|
||||
use lemmy_db_views_site::api::SuccessResponse;
|
||||
use lemmy_db_views_site::{
|
||||
api::{SuccessResponse, UserSettingsBackup},
|
||||
impls::user_backup_list_to_user_settings_backup,
|
||||
};
|
||||
use lemmy_utils::{
|
||||
error::LemmyResult,
|
||||
spawn_try_task,
|
||||
utils::validation::check_api_elements_count,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use std::future::Future;
|
||||
use tracing::info;
|
||||
|
||||
const PARALLELISM: usize = 10;
|
||||
|
||||
/// Backup of user data. This struct should never be changed so that the data can be used as a
|
||||
/// long-term backup in case the instance goes down unexpectedly. All fields are optional to allow
|
||||
/// importing partial backups.
|
||||
///
|
||||
/// This data should not be parsed by apps/clients, but directly downloaded as a file.
|
||||
///
|
||||
/// Be careful with any changes to this struct, to avoid breaking changes which could prevent
|
||||
/// importing older backups.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct UserSettingsBackup {
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub avatar: Option<DbUrl>,
|
||||
pub banner: Option<DbUrl>,
|
||||
pub matrix_id: Option<String>,
|
||||
pub bot_account: Option<bool>,
|
||||
// TODO: might be worth making a separate struct for settings backup, to avoid breakage in case
|
||||
// fields are renamed, and to avoid storing unnecessary fields like person_id or email
|
||||
pub settings: Option<LocalUser>,
|
||||
#[serde(default)]
|
||||
pub followed_communities: Vec<ObjectId<ApubCommunity>>,
|
||||
#[serde(default)]
|
||||
pub saved_posts: Vec<ObjectId<ApubPost>>,
|
||||
#[serde(default)]
|
||||
pub saved_comments: Vec<ObjectId<ApubComment>>,
|
||||
#[serde(default)]
|
||||
pub blocked_communities: Vec<ObjectId<ApubCommunity>>,
|
||||
#[serde(default)]
|
||||
pub blocked_users: Vec<ObjectId<ApubPerson>>,
|
||||
#[serde(default)]
|
||||
pub blocked_instances: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn export_settings(
|
||||
local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<UserSettingsBackup>> {
|
||||
let lists = LocalUser::export_backup(&mut context.pool(), local_user_view.person.id).await?;
|
||||
let settings = user_backup_list_to_user_settings_backup(local_user_view, lists);
|
||||
|
||||
let vec_into = |vec: Vec<_>| vec.into_iter().map(Into::into).collect();
|
||||
Ok(Json(UserSettingsBackup {
|
||||
display_name: local_user_view.person.display_name,
|
||||
bio: local_user_view.person.bio,
|
||||
avatar: local_user_view.person.avatar,
|
||||
banner: local_user_view.person.banner,
|
||||
matrix_id: local_user_view.person.matrix_user_id,
|
||||
bot_account: local_user_view.person.bot_account.into(),
|
||||
settings: Some(local_user_view.local_user),
|
||||
followed_communities: vec_into(lists.followed_communities),
|
||||
blocked_communities: vec_into(lists.blocked_communities),
|
||||
blocked_instances: lists.blocked_instances,
|
||||
blocked_users: lists.blocked_users.into_iter().map(Into::into).collect(),
|
||||
saved_posts: lists.saved_posts.into_iter().map(Into::into).collect(),
|
||||
saved_comments: lists.saved_comments.into_iter().map(Into::into).collect(),
|
||||
}))
|
||||
Ok(Json(settings))
|
||||
}
|
||||
|
||||
pub async fn import_settings(
|
||||
|
@ -157,7 +112,12 @@ pub async fn import_settings(
|
|||
);
|
||||
|
||||
let failed_followed_communities = fetch_and_import(
|
||||
data.followed_communities.clone(),
|
||||
data
|
||||
.followed_communities
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<ObjectId<ApubCommunity>>>(),
|
||||
&context,
|
||||
|(followed, context)| async move {
|
||||
let community = followed.dereference(&context).await?;
|
||||
|
@ -170,7 +130,12 @@ pub async fn import_settings(
|
|||
.await?;
|
||||
|
||||
let failed_saved_posts = fetch_and_import(
|
||||
data.saved_posts.clone(),
|
||||
data
|
||||
.saved_posts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<ObjectId<ApubPost>>>(),
|
||||
&context,
|
||||
|(saved, context)| async move {
|
||||
let post = saved.dereference(&context).await?;
|
||||
|
@ -182,7 +147,12 @@ pub async fn import_settings(
|
|||
.await?;
|
||||
|
||||
let failed_saved_comments = fetch_and_import(
|
||||
data.saved_comments.clone(),
|
||||
data
|
||||
.saved_comments
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<ObjectId<ApubComment>>>(),
|
||||
&context,
|
||||
|(saved, context)| async move {
|
||||
let comment = saved.dereference(&context).await?;
|
||||
|
@ -194,7 +164,12 @@ pub async fn import_settings(
|
|||
.await?;
|
||||
|
||||
let failed_community_blocks = fetch_and_import(
|
||||
data.blocked_communities.clone(),
|
||||
data
|
||||
.blocked_communities
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<ObjectId<ApubCommunity>>>(),
|
||||
&context,
|
||||
|(blocked, context)| async move {
|
||||
let community = blocked.dereference(&context).await?;
|
||||
|
@ -206,7 +181,12 @@ pub async fn import_settings(
|
|||
.await?;
|
||||
|
||||
let failed_user_blocks = fetch_and_import(
|
||||
data.blocked_users.clone(),
|
||||
data
|
||||
.blocked_users
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<ObjectId<ApubPerson>>>(),
|
||||
&context,
|
||||
|(blocked, context)| async move {
|
||||
let target = blocked.dereference(&context).await?;
|
||||
|
|
|
@ -240,6 +240,7 @@ pub struct InboxCombinedQuery {
|
|||
pub cursor_data: Option<InboxCombined>,
|
||||
pub page_back: Option<bool>,
|
||||
pub limit: Option<i64>,
|
||||
pub no_limit: Option<bool>,
|
||||
}
|
||||
|
||||
impl InboxCombinedQuery {
|
||||
|
@ -250,16 +251,19 @@ impl InboxCombinedQuery {
|
|||
local_instance_id: InstanceId,
|
||||
) -> LemmyResult<Vec<InboxCombinedView>> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
|
||||
let item_creator = person::id;
|
||||
let recipient_person = aliases::person1.field(person::id);
|
||||
|
||||
let mut query = InboxCombinedViewInternal::joins(my_person_id, local_instance_id)
|
||||
.select(InboxCombinedViewInternal::as_select())
|
||||
.limit(limit)
|
||||
.into_boxed();
|
||||
|
||||
if !self.no_limit.unwrap_or_default() {
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
// Filters
|
||||
if self.unread_only.unwrap_or_default() {
|
||||
query = query
|
||||
|
|
|
@ -160,20 +160,21 @@ pub struct PersonContentCombinedQuery {
|
|||
pub page_back: Option<bool>,
|
||||
#[new(default)]
|
||||
pub limit: Option<i64>,
|
||||
#[new(default)]
|
||||
pub no_limit: Option<bool>,
|
||||
}
|
||||
|
||||
impl PersonContentCombinedQuery {
|
||||
pub async fn list(
|
||||
self,
|
||||
pool: &mut DbPool<'_>,
|
||||
user: &Option<LocalUserView>,
|
||||
user: Option<&LocalUserView>,
|
||||
local_instance_id: InstanceId,
|
||||
) -> LemmyResult<Vec<PersonContentCombinedView>> {
|
||||
let my_person_id = user.as_ref().map(|u| u.local_user.person_id);
|
||||
let item_creator = person::id;
|
||||
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
|
||||
// Notes: since the post_id and comment_id are optional columns,
|
||||
// many joins must use an OR condition.
|
||||
|
@ -184,9 +185,13 @@ impl PersonContentCombinedQuery {
|
|||
// The creator id filter
|
||||
.filter(item_creator.eq(self.creator_id))
|
||||
.select(PersonContentCombinedViewInternal::as_select())
|
||||
.limit(limit)
|
||||
.into_boxed();
|
||||
|
||||
if !self.no_limit.unwrap_or_default() {
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
if let Some(type_) = self.type_ {
|
||||
query = match type_ {
|
||||
PersonContentType::All => query,
|
||||
|
@ -366,7 +371,7 @@ mod tests {
|
|||
|
||||
// Do a batch read of timmy
|
||||
let timmy_content = PersonContentCombinedQuery::new(data.timmy.id)
|
||||
.list(pool, &None, data.instance.id)
|
||||
.list(pool, None, data.instance.id)
|
||||
.await?;
|
||||
assert_eq!(3, timmy_content.len());
|
||||
|
||||
|
@ -392,7 +397,7 @@ mod tests {
|
|||
|
||||
// Do a batch read of sara
|
||||
let sara_content = PersonContentCombinedQuery::new(data.sara.id)
|
||||
.list(pool, &None, data.instance.id)
|
||||
.list(pool, None, data.instance.id)
|
||||
.await?;
|
||||
assert_eq!(3, sara_content.len());
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ pub struct PersonLikedCombinedQuery {
|
|||
pub cursor_data: Option<PersonLikedCombined>,
|
||||
pub page_back: Option<bool>,
|
||||
pub limit: Option<i64>,
|
||||
pub no_limit: Option<bool>,
|
||||
}
|
||||
|
||||
impl PaginationCursorBuilder for PersonLikedCombinedView {
|
||||
|
@ -161,14 +162,17 @@ impl PersonLikedCombinedQuery {
|
|||
let local_instance_id = user.person.instance_id;
|
||||
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
|
||||
let mut query = PersonLikedCombinedViewInternal::joins(my_person_id, local_instance_id)
|
||||
.filter(person_liked_combined::person_id.eq(my_person_id))
|
||||
.select(PersonLikedCombinedViewInternal::as_select())
|
||||
.limit(limit)
|
||||
.into_boxed();
|
||||
|
||||
if !self.no_limit.unwrap_or_default() {
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
if let Some(type_) = self.type_ {
|
||||
query = match type_ {
|
||||
PersonContentType::All => query,
|
||||
|
|
|
@ -51,6 +51,7 @@ pub struct PersonSavedCombinedQuery {
|
|||
pub cursor_data: Option<PersonSavedCombined>,
|
||||
pub page_back: Option<bool>,
|
||||
pub limit: Option<i64>,
|
||||
pub no_limit: Option<bool>,
|
||||
}
|
||||
|
||||
impl PaginationCursorBuilder for PersonSavedCombinedView {
|
||||
|
@ -159,14 +160,17 @@ impl PersonSavedCombinedQuery {
|
|||
let local_instance_id = user.person.instance_id;
|
||||
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
|
||||
let mut query = PersonSavedCombinedViewInternal::joins(my_person_id, local_instance_id)
|
||||
.filter(person_saved_combined::person_id.eq(my_person_id))
|
||||
.select(PersonSavedCombinedViewInternal::as_select())
|
||||
.limit(limit)
|
||||
.into_boxed();
|
||||
|
||||
if !self.no_limit.unwrap_or_default() {
|
||||
let limit = limit_fetch(self.limit)?;
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
if let Some(type_) = self.type_ {
|
||||
query = match type_ {
|
||||
PersonContentType::All => query,
|
||||
|
|
|
@ -182,18 +182,22 @@ impl PostView {
|
|||
cursor_data: Option<PostActions>,
|
||||
page_back: Option<bool>,
|
||||
limit: Option<i64>,
|
||||
no_limit: Option<bool>,
|
||||
) -> LemmyResult<Vec<PostView>> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let limit = limit_fetch(limit)?;
|
||||
|
||||
let query = PostView::joins(Some(my_person.id), my_person.instance_id)
|
||||
let mut query = PostView::joins(Some(my_person.id), my_person.instance_id)
|
||||
.filter(post_actions::person_id.eq(my_person.id))
|
||||
.filter(post_actions::read_at.is_not_null())
|
||||
.filter(filter_blocked())
|
||||
.select(PostView::as_select())
|
||||
.limit(limit)
|
||||
.into_boxed();
|
||||
|
||||
if !no_limit.unwrap_or_default() {
|
||||
let limit = limit_fetch(limit)?;
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
// Sorting by the read date
|
||||
let paginated_query = paginate(query, SortDirection::Desc, cursor_data, None, page_back)
|
||||
.then_order_by(pa_key::read_at)
|
||||
|
@ -213,18 +217,22 @@ impl PostView {
|
|||
cursor_data: Option<PostActions>,
|
||||
page_back: Option<bool>,
|
||||
limit: Option<i64>,
|
||||
no_limit: Option<bool>,
|
||||
) -> LemmyResult<Vec<PostView>> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let limit = limit_fetch(limit)?;
|
||||
|
||||
let query = PostView::joins(Some(my_person.id), my_person.instance_id)
|
||||
let mut query = PostView::joins(Some(my_person.id), my_person.instance_id)
|
||||
.filter(post_actions::person_id.eq(my_person.id))
|
||||
.filter(post_actions::hidden_at.is_not_null())
|
||||
.filter(filter_blocked())
|
||||
.select(PostView::as_select())
|
||||
.limit(limit)
|
||||
.into_boxed();
|
||||
|
||||
if !no_limit.unwrap_or_default() {
|
||||
let limit = limit_fetch(limit)?;
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
// Sorting by the hidden date
|
||||
let paginated_query = paginate(query, SortDirection::Desc, cursor_data, None, page_back)
|
||||
.then_order_by(pa_key::hidden_at)
|
||||
|
@ -1223,7 +1231,7 @@ mod tests {
|
|||
PostActions::mark_as_read(pool, &tag_post_read_form).await?;
|
||||
|
||||
let read_read_post_listing =
|
||||
PostView::list_read(pool, &data.tegan.person, None, None, None).await?;
|
||||
PostView::list_read(pool, &data.tegan.person, None, None, None, None).await?;
|
||||
|
||||
// This should be ordered from most recently read
|
||||
assert_eq!(
|
||||
|
@ -1802,7 +1810,8 @@ mod tests {
|
|||
.is_some_and(|a| a.hidden_at.is_some())));
|
||||
|
||||
// Make sure only that one comes back for list_hidden
|
||||
let list_hidden = PostView::list_hidden(pool, &data.tegan.person, None, None, None).await?;
|
||||
let list_hidden =
|
||||
PostView::list_hidden(pool, &data.tegan.person, None, None, None, None).await?;
|
||||
assert_eq!(vec![POST_BY_BOT], names(&list_hidden));
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -11,13 +11,17 @@ use lemmy_db_schema::{
|
|||
},
|
||||
sensitive::SensitiveString,
|
||||
source::{
|
||||
comment::Comment,
|
||||
community::Community,
|
||||
instance::Instance,
|
||||
language::Language,
|
||||
local_site_url_blocklist::LocalSiteUrlBlocklist,
|
||||
local_user::LocalUser,
|
||||
login_token::LoginToken,
|
||||
oauth_provider::{OAuthProvider, PublicOAuthProvider},
|
||||
person::Person,
|
||||
post::Post,
|
||||
private_message::PrivateMessage,
|
||||
tagline::Tagline,
|
||||
},
|
||||
};
|
||||
|
@ -691,6 +695,60 @@ pub struct ResolveObject {
|
|||
pub q: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
pub enum PostOrCommentOrPrivateMessage {
|
||||
Post(Post),
|
||||
Comment(Comment),
|
||||
PrivateMessage(PrivateMessage),
|
||||
}
|
||||
|
||||
/// Backup of user data. This struct should never be changed so that the data can be used as a
|
||||
/// long-term backup in case the instance goes down unexpectedly. All fields are optional to allow
|
||||
/// importing partial backups.
|
||||
///
|
||||
/// This data should not be parsed by apps/clients, but directly downloaded as a file.
|
||||
///
|
||||
/// Be careful with any changes to this struct, to avoid breaking changes which could prevent
|
||||
/// importing older backups.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct UserSettingsBackup {
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub avatar: Option<Url>,
|
||||
pub banner: Option<Url>,
|
||||
pub matrix_id: Option<String>,
|
||||
pub bot_account: Option<bool>,
|
||||
// TODO: might be worth making a separate struct for settings backup, to avoid breakage in case
|
||||
// fields are renamed, and to avoid storing unnecessary fields like person_id or email
|
||||
pub settings: Option<LocalUser>,
|
||||
#[serde(default)]
|
||||
pub followed_communities: Vec<Url>,
|
||||
#[serde(default)]
|
||||
pub saved_posts: Vec<Url>,
|
||||
#[serde(default)]
|
||||
pub saved_comments: Vec<Url>,
|
||||
#[serde(default)]
|
||||
pub blocked_communities: Vec<Url>,
|
||||
#[serde(default)]
|
||||
pub blocked_users: Vec<Url>,
|
||||
#[serde(default)]
|
||||
pub blocked_instances: Vec<String>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
/// Your exported data.
|
||||
pub struct ExportDataResponse {
|
||||
pub inbox: Vec<PostOrCommentOrPrivateMessage>,
|
||||
pub content: Vec<PostOrCommentOrPrivateMessage>,
|
||||
pub read_posts: Vec<Url>,
|
||||
pub liked: Vec<Url>,
|
||||
pub moderates: Vec<Url>,
|
||||
pub settings: UserSettingsBackup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use crate::SiteView;
|
||||
use crate::{api::UserSettingsBackup, SiteView};
|
||||
use diesel::{ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl, SelectableHelper};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema::{
|
||||
impls::local_user::UserBackupLists,
|
||||
source::person::Person,
|
||||
traits::Crud,
|
||||
utils::{get_conn, DbPool},
|
||||
};
|
||||
use lemmy_db_schema_file::schema::{instance, local_site, local_site_rate_limit, site};
|
||||
use lemmy_db_views_local_user::LocalUserView;
|
||||
use lemmy_utils::{
|
||||
build_cache,
|
||||
error::{LemmyError, LemmyErrorType, LemmyResult},
|
||||
|
@ -43,3 +45,26 @@ impl SiteView {
|
|||
Person::read(pool, site_view.local_site.multi_comm_follower).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_backup_list_to_user_settings_backup(
|
||||
local_user_view: LocalUserView,
|
||||
lists: UserBackupLists,
|
||||
) -> UserSettingsBackup {
|
||||
let vec_into = |vec: Vec<_>| vec.into_iter().map(Into::into).collect();
|
||||
|
||||
UserSettingsBackup {
|
||||
display_name: local_user_view.person.display_name,
|
||||
bio: local_user_view.person.bio,
|
||||
avatar: local_user_view.person.avatar.map(Into::into),
|
||||
banner: local_user_view.person.banner.map(Into::into),
|
||||
matrix_id: local_user_view.person.matrix_user_id,
|
||||
bot_account: local_user_view.person.bot_account.into(),
|
||||
settings: Some(local_user_view.local_user),
|
||||
followed_communities: vec_into(lists.followed_communities),
|
||||
blocked_communities: vec_into(lists.blocked_communities),
|
||||
blocked_instances: lists.blocked_instances,
|
||||
blocked_users: lists.blocked_users.into_iter().map(Into::into).collect(),
|
||||
saved_posts: lists.saved_posts.into_iter().map(Into::into).collect(),
|
||||
saved_comments: lists.saved_comments.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -215,8 +215,9 @@ async fn get_feed_user(
|
|||
cursor_data: None,
|
||||
page_back: None,
|
||||
limit: (Some(*limit)),
|
||||
no_limit: None,
|
||||
}
|
||||
.list(&mut context.pool(), &None, site_view.site.instance_id)
|
||||
.list(&mut context.pool(), None, site_view.site.instance_id)
|
||||
.await?;
|
||||
|
||||
let posts = content
|
||||
|
|
|
@ -28,6 +28,7 @@ use lemmy_api::{
|
|||
change_password::change_password,
|
||||
change_password_after_reset::change_password_after_reset,
|
||||
donation_dialog_shown::donation_dialog_shown,
|
||||
export_data::export_data,
|
||||
generate_totp_secret::generate_totp_secret,
|
||||
get_captcha::get_captcha,
|
||||
list_hidden::list_person_hidden,
|
||||
|
@ -400,6 +401,11 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
|
|||
.wrap(rate_limit.import_user_settings())
|
||||
.route("/export", get().to(export_settings))
|
||||
.route("/import", post().to(import_settings)),
|
||||
)
|
||||
.service(
|
||||
resource("/data/export")
|
||||
.wrap(rate_limit.import_user_settings())
|
||||
.route(get().to(export_data)),
|
||||
),
|
||||
)
|
||||
// User actions
|
||||
|
|
Loading…
Reference in a new issue