diff --git a/.gitignore b/.gitignore index 6ae0ae193..c3a8bd70e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,6 @@ ui/src/translations # ide config .idea/ +.vscode/ target diff --git a/docs/src/administration_backup_and_restore.md b/docs/src/administration_backup_and_restore.md index fe97cf880..633c687fb 100644 --- a/docs/src/administration_backup_and_restore.md +++ b/docs/src/administration_backup_and_restore.md @@ -9,14 +9,14 @@ When using docker or ansible, there should be a `volumes` folder, which contains To incrementally backup the DB to an `.sql` file, you can run: ```bash -docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy > lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql +docker-compose exec postgres pg_dumpall -c -U lemmy > lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql ``` ### A Sample backup script ```bash #!/bin/sh # DB Backup -ssh MY_USER@MY_IP "docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy" > ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql +ssh MY_USER@MY_IP "docker-compose exec postgres pg_dumpall -c -U lemmy" > ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql # Volumes folder Backup rsync -avP -zz --rsync-path="sudo rsync" MY_USER@MY_IP:/LEMMY_LOCATION/volumes ~/BACKUP_LOCATION/FOLDERNAME @@ -37,6 +37,45 @@ cat db_dump.sql | docker exec -i FOLDERNAME_postgres_1 psql -U lemmy # restore docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "alter user lemmy with password 'bleh'" ``` +### Changing your domain name + +If you haven't federated yet, you can change your domain name in the DB. **Warning: do not do this after you've federated, or it will break federation.** + +Get into `psql` for your docker: + +`docker-compose exec postgres psql -U lemmy` + +``` +-- Post +update post set ap_id = replace (ap_id, 'old_domain', 'new_domain'); +update post set url = replace (url, 'old_domain', 'new_domain'); +update post set body = replace (body, 'old_domain', 'new_domain'); +update post set thumbnail_url = replace (thumbnail_url, 'old_domain', 'new_domain'); + +delete from post_aggregates_fast; +insert into post_aggregates_fast select * from post_aggregates_view; + +-- Comments +update comment set ap_id = replace (ap_id, 'old_domain', 'new_domain'); +update comment set content = replace (content, 'old_domain', 'new_domain'); + +delete from comment_aggregates_fast; +insert into comment_aggregates_fast select * from comment_aggregates_view; + +-- User +update user_ set actor_id = replace (actor_id, 'old_domain', 'new_domain'); +update user_ set avatar = replace (avatar, 'old_domain', 'new_domain'); + +delete from user_fast; +insert into user_fast select * from user_view; + +-- Community +update community set actor_id = replace (actor_id, 'old_domain', 'new_domain'); + +delete from community_aggregates_fast; +insert into community_aggregates_fast select * from community_aggregates_view; +``` + ## More resources - https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database diff --git a/server/Cargo.lock b/server/Cargo.lock index 90687982a..eb29e4b1d 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -408,6 +408,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "anyhow" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" + [[package]] name = "arc-swap" version = "0.4.7" @@ -1196,28 +1202,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "fake-simd" version = "0.1.2" @@ -1735,6 +1719,7 @@ dependencies = [ "actix-rt", "actix-web", "actix-web-actors", + "anyhow", "async-trait", "awc", "base64 0.12.3", @@ -1745,7 +1730,6 @@ dependencies = [ "diesel_migrations", "dotenv", "env_logger", - "failure", "futures", "http", "http-signature-normalization-actix", @@ -1764,6 +1748,7 @@ dependencies = [ "sha2", "strum", "strum_macros", + "thiserror", "tokio", "url", "uuid 0.8.1", @@ -3118,18 +3103,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "synstructure" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "tempfile" version = "3.1.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index d245cf4d4..dba0fee6f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -23,7 +23,6 @@ activitystreams-ext = "0.1.0-alpha.2" bcrypt = "0.8.0" chrono = { version = "0.4.7", features = ["serde"] } serde_json = { version = "1.0.52", features = ["preserve_order"]} -failure = "0.1.8" serde = { version = "1.0.105", features = ["derive"] } actix = "0.10.0-alpha.2" actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] } @@ -52,3 +51,5 @@ uuid = { version = "0.8", features = ["serde", "v4"] } sha2 = "0.9" async-trait = "0.1.36" captcha = "0.0.7" +anyhow = "1.0.32" +thiserror = "1.0.20" diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index 901172601..11f958f08 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -9,6 +9,7 @@ use lemmy_db::{ user_view::*, Crud, }; +use thiserror::Error; pub mod claims; pub mod comment; @@ -17,8 +18,8 @@ pub mod post; pub mod site; pub mod user; -#[derive(Fail, Debug)] -#[fail(display = "{{\"error\":\"{}\"}}", message)] +#[derive(Debug, Error)] +#[error("{{\"error\":\"{message}\"}}")] pub struct APIError { pub message: String, } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index c2b6955b5..f9a92cd39 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -97,6 +97,7 @@ pub struct SaveUserSettings { lang: String, avatar: Option, email: Option, + bio: Option, matrix_user_id: Option, new_password: Option, new_password_verify: Option, @@ -557,6 +558,17 @@ impl Perform for Oper { None => read_user.email, }; + let bio = match &data.bio { + Some(bio) => { + if bio.chars().count() <= 300 { + Some(bio.to_owned()) + } else { + return Err(APIError::err("bio_length_overflow").into()); + } + } + None => read_user.bio, + }; + let avatar = match &data.avatar { Some(avatar) => Some(avatar.to_owned()), None => read_user.avatar, @@ -613,7 +625,7 @@ impl Perform for Oper { show_avatars: data.show_avatars, send_notifications_to_email: data.send_notifications_to_email, actor_id: read_user.actor_id, - bio: read_user.bio, + bio, local: read_user.local, private_key: read_user.private_key, public_key: read_user.public_key, diff --git a/server/src/apub/extensions/signatures.rs b/server/src/apub/extensions/signatures.rs index e37bde250..efc8dccfc 100644 --- a/server/src/apub/extensions/signatures.rs +++ b/server/src/apub/extensions/signatures.rs @@ -2,6 +2,7 @@ use crate::{apub::ActorType, LemmyError}; use activitystreams::unparsed::UnparsedMutExt; use activitystreams_ext::UnparsedExtension; use actix_web::{client::ClientRequest, HttpRequest}; +use anyhow::anyhow; use http_signature_normalization_actix::{ digest::{DigestClient, SignExt}, Config, @@ -70,7 +71,7 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyE debug!("verified signature for {}", &request.uri()); Ok(()) } else { - Err(format_err!("Invalid signature on request: {}", &request.uri()).into()) + Err(anyhow!("Invalid signature on request: {}", &request.uri()).into()) } } diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index 51f3a50b4..4425757de 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -17,6 +17,7 @@ use crate::{ }; use activitystreams::{base::BaseExt, collection::OrderedCollection, object::Note, prelude::*}; use actix_web::client::Client; +use anyhow::anyhow; use chrono::NaiveDateTime; use diesel::{result::Error::NotFound, PgConnection}; use lemmy_db::{ @@ -66,7 +67,7 @@ where Response: for<'de> Deserialize<'de>, { if !is_apub_id_valid(&url) { - return Err(format_err!("Activitypub uri invalid or blocked: {}", url).into()); + return Err(anyhow!("Activitypub uri invalid or blocked: {}", url).into()); } let timeout = Duration::from_secs(60); @@ -125,10 +126,10 @@ pub async fn search_by_apub_id( let split2 = split[0].split('!').collect::>(); (format!("/c/{}", split2[1]), split[1]) } else { - return Err(format_err!("Invalid search query: {}", query).into()); + return Err(anyhow!("Invalid search query: {}", query).into()); } } else { - return Err(format_err!("Invalid search query: {}", query).into()); + return Err(anyhow!("Invalid search query: {}", query).into()); }; let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name); diff --git a/server/src/apub/inbox/activities/undo.rs b/server/src/apub/inbox/activities/undo.rs index 6c90fc814..edfcf372a 100644 --- a/server/src/apub/inbox/activities/undo.rs +++ b/server/src/apub/inbox/activities/undo.rs @@ -22,6 +22,7 @@ use crate::{ }; use activitystreams::{activity::*, base::AnyBase, object::Note, prelude::*}; use actix_web::{client::Client, HttpResponse}; +use anyhow::anyhow; use lemmy_db::{ comment::{Comment, CommentForm, CommentLike, CommentLikeForm}, comment_view::CommentView, @@ -63,7 +64,7 @@ async fn receive_undo_delete( "Note" => receive_undo_delete_comment(undo, &delete, client, pool, chat_server).await, "Page" => receive_undo_delete_post(undo, &delete, client, pool, chat_server).await, "Group" => receive_undo_delete_community(undo, &delete, client, pool, chat_server).await, - d => Err(format_err!("Undo Delete type {} not supported", d).into()), + d => Err(anyhow!("Undo Delete type {} not supported", d).into()), } } @@ -80,7 +81,7 @@ async fn receive_undo_remove( "Note" => receive_undo_remove_comment(undo, &remove, client, pool, chat_server).await, "Page" => receive_undo_remove_post(undo, &remove, client, pool, chat_server).await, "Group" => receive_undo_remove_community(undo, &remove, client, pool, chat_server).await, - d => Err(format_err!("Undo Delete type {} not supported", d).into()), + d => Err(anyhow!("Undo Delete type {} not supported", d).into()), } } @@ -96,7 +97,7 @@ async fn receive_undo_like( match type_ { "Note" => receive_undo_like_comment(undo, &like, client, pool, chat_server).await, "Page" => receive_undo_like_post(undo, &like, client, pool, chat_server).await, - d => Err(format_err!("Undo Delete type {} not supported", d).into()), + d => Err(anyhow!("Undo Delete type {} not supported", d).into()), } } @@ -109,7 +110,7 @@ async fn receive_undo_dislike( let dislike = Dislike::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap(); let type_ = dislike.object().as_single_kind_str().unwrap(); - Err(format_err!("Undo Delete type {} not supported", type_).into()) + Err(anyhow!("Undo Delete type {} not supported", type_).into()) } async fn receive_undo_delete_comment( diff --git a/server/src/apub/inbox/community_inbox.rs b/server/src/apub/inbox/community_inbox.rs index 337a89900..69dd2cdf7 100644 --- a/server/src/apub/inbox/community_inbox.rs +++ b/server/src/apub/inbox/community_inbox.rs @@ -14,6 +14,7 @@ use activitystreams::{ prelude::*, }; use actix_web::{client::Client, web, HttpRequest, HttpResponse}; +use anyhow::anyhow; use lemmy_db::{ community::{Community, CommunityFollower, CommunityFollowerForm}, user::User_, @@ -57,7 +58,7 @@ pub async fn community_inbox( if !community.local { return Err( - format_err!( + anyhow!( "Received activity is addressed to remote community {}", &community.actor_id ) diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 0af19dcd8..e86032f61 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -28,8 +28,8 @@ use activitystreams::{ }; use activitystreams_ext::{Ext1, Ext2}; use actix_web::{body::Body, client::Client, HttpResponse}; +use anyhow::anyhow; use chrono::NaiveDateTime; -use failure::_core::fmt::Debug; use lemmy_db::{activity::do_insert_activity, user::User_}; use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData}; use log::debug; @@ -118,10 +118,10 @@ where tombstone.set_deleted(convert_datetime(updated)); Ok(tombstone) } else { - Err(format_err!("Cant convert to tombstone because updated time was None.").into()) + Err(anyhow!("Cant convert to tombstone because updated time was None.").into()) } } else { - Err(format_err!("Cant convert object to tombstone if it wasnt deleted").into()) + Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into()) } } @@ -339,13 +339,13 @@ pub async fn fetch_webfinger_url( .links .iter() .find(|l| l.type_.eq(&Some("application/activity+json".to_string()))) - .ok_or_else(|| format_err!("No application/activity+json link found."))?; + .ok_or_else(|| anyhow!("No application/activity+json link found."))?; link .href .to_owned() .map(|u| Url::parse(&u)) .transpose()? - .ok_or_else(|| format_err!("No href found.").into()) + .ok_or_else(|| anyhow!("No href found.").into()) } pub async fn insert_activity( @@ -355,7 +355,7 @@ pub async fn insert_activity( pool: &DbPool, ) -> Result<(), LemmyError> where - T: Serialize + Debug + Send + 'static, + T: Serialize + std::fmt::Debug + Send + 'static, { blocking(pool, move |conn| { do_insert_activity(conn, user_id, &data, local) diff --git a/server/src/lib.rs b/server/src/lib.rs index 682efc774..ace843818 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -3,8 +3,6 @@ pub extern crate strum_macros; #[macro_use] pub extern crate lazy_static; -#[macro_use] -pub extern crate failure; pub extern crate actix; pub extern crate actix_web; pub extern crate base64; @@ -33,6 +31,7 @@ pub mod websocket; use crate::request::{retry, RecvError}; use actix_web::{client::Client, dev::ConnectionInfo}; +use anyhow::anyhow; use lemmy_utils::{get_apub_protocol_string, settings::Settings}; use log::error; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; @@ -48,12 +47,12 @@ pub type IPAddr = String; #[derive(Debug)] pub struct LemmyError { - inner: failure::Error, + inner: anyhow::Error, } impl From for LemmyError where - T: Into, + T: Into, { fn from(t: T) -> Self { LemmyError { inner: t.into() } @@ -118,7 +117,7 @@ pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result Result<(), Le if response .headers() .get("Content-Type") - .ok_or_else(|| format_err!("No Content-Type header"))? + .ok_or_else(|| anyhow!("No Content-Type header"))? .to_str()? .starts_with("image/") { Ok(()) } else { - Err(format_err!("Not an image type.").into()) + Err(anyhow!("Not an image type.").into()) } } diff --git a/server/src/request.rs b/server/src/request.rs index 7d09b60df..70a2b6933 100644 --- a/server/src/request.rs +++ b/server/src/request.rs @@ -1,12 +1,14 @@ use crate::LemmyError; +use anyhow::anyhow; use std::future::Future; +use thiserror::Error; -#[derive(Clone, Debug, Fail)] -#[fail(display = "Error sending request, {}", _0)] +#[derive(Clone, Debug, Error)] +#[error("Error sending request, {0}")] struct SendError(pub String); -#[derive(Clone, Debug, Fail)] -#[fail(display = "Error receiving response, {}", _0)] +#[derive(Clone, Debug, Error)] +#[error("Error receiving response, {0}")] pub struct RecvError(pub String); pub async fn retry(f: F) -> Result @@ -22,7 +24,7 @@ where F: Fn() -> Fut, Fut: Future, LemmyError>>, { - let mut response = Err(format_err!("connect timeout").into()); + let mut response = Err(anyhow!("connect timeout").into()); for _ in 0u8..3 { match (f)().await? { diff --git a/server/src/routes/feeds.rs b/server/src/routes/feeds.rs index 1322feb44..40e0ab658 100644 --- a/server/src/routes/feeds.rs +++ b/server/src/routes/feeds.rs @@ -1,5 +1,6 @@ use crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError}; use actix_web::{error::ErrorBadRequest, *}; +use anyhow::anyhow; use chrono::{DateTime, NaiveDateTime, Utc}; use diesel::{ r2d2::{ConnectionManager, Pool}, @@ -88,7 +89,7 @@ async fn get_feed( "c" => RequestType::Community, "front" => RequestType::Front, "inbox" => RequestType::Inbox, - _ => return Err(ErrorBadRequest(LemmyError::from(format_err!("wrong_type")))), + _ => return Err(ErrorBadRequest(LemmyError::from(anyhow!("wrong_type")))), }; let param = path.1.to_owned(); diff --git a/server/src/routes/nodeinfo.rs b/server/src/routes/nodeinfo.rs index 5094c2f15..a18d06ea2 100644 --- a/server/src/routes/nodeinfo.rs +++ b/server/src/routes/nodeinfo.rs @@ -1,5 +1,6 @@ use crate::{blocking, routes::DbPoolParam, version, LemmyError}; use actix_web::{body::Body, error::ErrorBadRequest, *}; +use anyhow::anyhow; use lemmy_db::site_view::SiteView; use lemmy_utils::{get_apub_protocol_string, settings::Settings}; use serde::{Deserialize, Serialize}; @@ -28,7 +29,7 @@ async fn node_info_well_known() -> Result, LemmyError> { async fn node_info(db: DbPoolParam) -> Result { let site_view = blocking(&db, SiteView::read) .await? - .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?; + .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?; let protocols = if Settings::get().federation.enabled { vec!["activitypub".to_string()] diff --git a/server/src/routes/webfinger.rs b/server/src/routes/webfinger.rs index e616de0e8..81fad6110 100644 --- a/server/src/routes/webfinger.rs +++ b/server/src/routes/webfinger.rs @@ -1,5 +1,6 @@ use crate::{blocking, routes::DbPoolParam, LemmyError}; use actix_web::{error::ErrorBadRequest, web::Query, *}; +use anyhow::anyhow; use lemmy_db::{community::Community, user::User_}; use lemmy_utils::{settings::Settings, WEBFINGER_COMMUNITY_REGEX, WEBFINGER_USER_REGEX}; use serde::{Deserialize, Serialize}; @@ -62,17 +63,17 @@ async fn get_webfinger_response( Community::read_from_name(conn, &community_name) }) .await? - .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))? + .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))? .actor_id } else if let Some(user_name) = user_regex_parsed { let user_name = user_name.as_str().to_owned(); // Make sure the requested user exists. blocking(&db, move |conn| User_::read_from_name(conn, &user_name)) .await? - .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))? + .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))? .actor_id } else { - return Err(ErrorBadRequest(LemmyError::from(format_err!("not_found")))); + return Err(ErrorBadRequest(LemmyError::from(anyhow!("not_found")))); }; let json = WebFingerResponse { diff --git a/ui/package.json b/ui/package.json index f5ba9c8d4..aa803aa4a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -33,7 +33,7 @@ "i18next": "^19.4.1", "inferno": "^7.4.2", "inferno-helmet": "^5.2.1", - "inferno-i18next": "nimbusec-oss/inferno-i18next", + "inferno-i18next": "github:nimbusec-oss/inferno-i18next#semver:^7.4.2", "inferno-router": "^7.4.2", "js-cookie": "^2.2.0", "jwt-decode": "^2.2.0", diff --git a/ui/src/components/markdown-textarea.tsx b/ui/src/components/markdown-textarea.tsx index 237ef9ff3..002d7c86b 100644 --- a/ui/src/components/markdown-textarea.tsx +++ b/ui/src/components/markdown-textarea.tsx @@ -21,6 +21,7 @@ interface MarkdownTextAreaProps { replyType?: boolean; focus?: boolean; disabled?: boolean; + maxLength?: number; onSubmit?(msg: { val: string; formId: string }): any; onContentChange?(val: string): any; onReplyCancel?(): any; @@ -121,7 +122,7 @@ export class MarkdownTextArea extends Component< required disabled={this.props.disabled} rows={2} - maxLength={10000} + maxLength={this.props.maxLength || 10000} /> {this.state.previewMode && (
{ show_avatars: null, send_notifications_to_email: null, auth: null, + bio: null, }, userSettingsLoading: null, deleteAccountLoading: null, @@ -149,7 +152,13 @@ export class User extends Component { this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind( this ); + this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind( + this + ); this.handlePageChange = this.handlePageChange.bind(this); + this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind( + this + ); this.state.user_id = Number(this.props.match.params.id) || null; this.state.username = this.props.match.params.username; @@ -375,6 +384,12 @@ export class User extends Component { )} +
+
+
@@ -570,6 +585,18 @@ export class User extends Component { />
+
+ +
+ +
+