Merge remote-tracking branch 'github/main' into main

This commit is contained in:
Felix Ableitner 2020-08-01 17:42:33 +02:00
commit e4d5614f3f
22 changed files with 165 additions and 92 deletions

1
.gitignore vendored
View File

@ -16,5 +16,6 @@ ui/src/translations
# ide config # ide config
.idea/ .idea/
.vscode/
target target

View File

@ -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: To incrementally backup the DB to an `.sql` file, you can run:
```bash ```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 ### A Sample backup script
```bash ```bash
#!/bin/sh #!/bin/sh
# DB Backup # 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 # Volumes folder Backup
rsync -avP -zz --rsync-path="sudo rsync" MY_USER@MY_IP:/LEMMY_LOCATION/volumes ~/BACKUP_LOCATION/FOLDERNAME 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'" 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 ## More resources
- https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database - https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database

43
server/Cargo.lock generated vendored
View File

@ -408,6 +408,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "anyhow"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "0.4.7" version = "0.4.7"
@ -1196,28 +1202,6 @@ dependencies = [
"termcolor", "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]] [[package]]
name = "fake-simd" name = "fake-simd"
version = "0.1.2" version = "0.1.2"
@ -1735,6 +1719,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-web", "actix-web",
"actix-web-actors", "actix-web-actors",
"anyhow",
"async-trait", "async-trait",
"awc", "awc",
"base64 0.12.3", "base64 0.12.3",
@ -1745,7 +1730,6 @@ dependencies = [
"diesel_migrations", "diesel_migrations",
"dotenv", "dotenv",
"env_logger", "env_logger",
"failure",
"futures", "futures",
"http", "http",
"http-signature-normalization-actix", "http-signature-normalization-actix",
@ -1764,6 +1748,7 @@ dependencies = [
"sha2", "sha2",
"strum", "strum",
"strum_macros", "strum_macros",
"thiserror",
"tokio", "tokio",
"url", "url",
"uuid 0.8.1", "uuid 0.8.1",
@ -3118,18 +3103,6 @@ dependencies = [
"unicode-xid", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.1.0" version = "3.1.0"

3
server/Cargo.toml vendored
View File

@ -23,7 +23,6 @@ activitystreams-ext = "0.1.0-alpha.2"
bcrypt = "0.8.0" bcrypt = "0.8.0"
chrono = { version = "0.4.7", features = ["serde"] } chrono = { version = "0.4.7", features = ["serde"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]} serde_json = { version = "1.0.52", features = ["preserve_order"]}
failure = "0.1.8"
serde = { version = "1.0.105", features = ["derive"] } serde = { version = "1.0.105", features = ["derive"] }
actix = "0.10.0-alpha.2" actix = "0.10.0-alpha.2"
actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] } actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] }
@ -52,3 +51,5 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
sha2 = "0.9" sha2 = "0.9"
async-trait = "0.1.36" async-trait = "0.1.36"
captcha = "0.0.7" captcha = "0.0.7"
anyhow = "1.0.32"
thiserror = "1.0.20"

View File

@ -9,6 +9,7 @@ use lemmy_db::{
user_view::*, user_view::*,
Crud, Crud,
}; };
use thiserror::Error;
pub mod claims; pub mod claims;
pub mod comment; pub mod comment;
@ -17,8 +18,8 @@ pub mod post;
pub mod site; pub mod site;
pub mod user; pub mod user;
#[derive(Fail, Debug)] #[derive(Debug, Error)]
#[fail(display = "{{\"error\":\"{}\"}}", message)] #[error("{{\"error\":\"{message}\"}}")]
pub struct APIError { pub struct APIError {
pub message: String, pub message: String,
} }

View File

@ -97,6 +97,7 @@ pub struct SaveUserSettings {
lang: String, lang: String,
avatar: Option<String>, avatar: Option<String>,
email: Option<String>, email: Option<String>,
bio: Option<String>,
matrix_user_id: Option<String>, matrix_user_id: Option<String>,
new_password: Option<String>, new_password: Option<String>,
new_password_verify: Option<String>, new_password_verify: Option<String>,
@ -557,6 +558,17 @@ impl Perform for Oper<SaveUserSettings> {
None => read_user.email, 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 { let avatar = match &data.avatar {
Some(avatar) => Some(avatar.to_owned()), Some(avatar) => Some(avatar.to_owned()),
None => read_user.avatar, None => read_user.avatar,
@ -613,7 +625,7 @@ impl Perform for Oper<SaveUserSettings> {
show_avatars: data.show_avatars, show_avatars: data.show_avatars,
send_notifications_to_email: data.send_notifications_to_email, send_notifications_to_email: data.send_notifications_to_email,
actor_id: read_user.actor_id, actor_id: read_user.actor_id,
bio: read_user.bio, bio,
local: read_user.local, local: read_user.local,
private_key: read_user.private_key, private_key: read_user.private_key,
public_key: read_user.public_key, public_key: read_user.public_key,

View File

@ -2,6 +2,7 @@ use crate::{apub::ActorType, LemmyError};
use activitystreams::unparsed::UnparsedMutExt; use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension; use activitystreams_ext::UnparsedExtension;
use actix_web::{client::ClientRequest, HttpRequest}; use actix_web::{client::ClientRequest, HttpRequest};
use anyhow::anyhow;
use http_signature_normalization_actix::{ use http_signature_normalization_actix::{
digest::{DigestClient, SignExt}, digest::{DigestClient, SignExt},
Config, Config,
@ -70,7 +71,7 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyE
debug!("verified signature for {}", &request.uri()); debug!("verified signature for {}", &request.uri());
Ok(()) Ok(())
} else { } else {
Err(format_err!("Invalid signature on request: {}", &request.uri()).into()) Err(anyhow!("Invalid signature on request: {}", &request.uri()).into())
} }
} }

View File

@ -17,6 +17,7 @@ use crate::{
}; };
use activitystreams::{base::BaseExt, collection::OrderedCollection, object::Note, prelude::*}; use activitystreams::{base::BaseExt, collection::OrderedCollection, object::Note, prelude::*};
use actix_web::client::Client; use actix_web::client::Client;
use anyhow::anyhow;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{result::Error::NotFound, PgConnection}; use diesel::{result::Error::NotFound, PgConnection};
use lemmy_db::{ use lemmy_db::{
@ -66,7 +67,7 @@ where
Response: for<'de> Deserialize<'de>, Response: for<'de> Deserialize<'de>,
{ {
if !is_apub_id_valid(&url) { 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); let timeout = Duration::from_secs(60);
@ -125,10 +126,10 @@ pub async fn search_by_apub_id(
let split2 = split[0].split('!').collect::<Vec<&str>>(); let split2 = split[0].split('!').collect::<Vec<&str>>();
(format!("/c/{}", split2[1]), split[1]) (format!("/c/{}", split2[1]), split[1])
} else { } else {
return Err(format_err!("Invalid search query: {}", query).into()); return Err(anyhow!("Invalid search query: {}", query).into());
} }
} else { } 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); let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);

View File

@ -22,6 +22,7 @@ use crate::{
}; };
use activitystreams::{activity::*, base::AnyBase, object::Note, prelude::*}; use activitystreams::{activity::*, base::AnyBase, object::Note, prelude::*};
use actix_web::{client::Client, HttpResponse}; use actix_web::{client::Client, HttpResponse};
use anyhow::anyhow;
use lemmy_db::{ use lemmy_db::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm}, comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
comment_view::CommentView, comment_view::CommentView,
@ -63,7 +64,7 @@ async fn receive_undo_delete(
"Note" => receive_undo_delete_comment(undo, &delete, client, pool, chat_server).await, "Note" => receive_undo_delete_comment(undo, &delete, client, pool, chat_server).await,
"Page" => receive_undo_delete_post(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, "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, "Note" => receive_undo_remove_comment(undo, &remove, client, pool, chat_server).await,
"Page" => receive_undo_remove_post(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, "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_ { match type_ {
"Note" => receive_undo_like_comment(undo, &like, client, pool, chat_server).await, "Note" => receive_undo_like_comment(undo, &like, client, pool, chat_server).await,
"Page" => receive_undo_like_post(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 dislike = Dislike::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
let type_ = dislike.object().as_single_kind_str().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( async fn receive_undo_delete_comment(

View File

@ -14,6 +14,7 @@ use activitystreams::{
prelude::*, prelude::*,
}; };
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use anyhow::anyhow;
use lemmy_db::{ use lemmy_db::{
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_, user::User_,
@ -57,7 +58,7 @@ pub async fn community_inbox(
if !community.local { if !community.local {
return Err( return Err(
format_err!( anyhow!(
"Received activity is addressed to remote community {}", "Received activity is addressed to remote community {}",
&community.actor_id &community.actor_id
) )

View File

@ -28,8 +28,8 @@ use activitystreams::{
}; };
use activitystreams_ext::{Ext1, Ext2}; use activitystreams_ext::{Ext1, Ext2};
use actix_web::{body::Body, client::Client, HttpResponse}; use actix_web::{body::Body, client::Client, HttpResponse};
use anyhow::anyhow;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use failure::_core::fmt::Debug;
use lemmy_db::{activity::do_insert_activity, user::User_}; use lemmy_db::{activity::do_insert_activity, user::User_};
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData}; use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData};
use log::debug; use log::debug;
@ -118,10 +118,10 @@ where
tombstone.set_deleted(convert_datetime(updated)); tombstone.set_deleted(convert_datetime(updated));
Ok(tombstone) Ok(tombstone)
} else { } 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 { } 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 .links
.iter() .iter()
.find(|l| l.type_.eq(&Some("application/activity+json".to_string()))) .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 link
.href .href
.to_owned() .to_owned()
.map(|u| Url::parse(&u)) .map(|u| Url::parse(&u))
.transpose()? .transpose()?
.ok_or_else(|| format_err!("No href found.").into()) .ok_or_else(|| anyhow!("No href found.").into())
} }
pub async fn insert_activity<T>( pub async fn insert_activity<T>(
@ -355,7 +355,7 @@ pub async fn insert_activity<T>(
pool: &DbPool, pool: &DbPool,
) -> Result<(), LemmyError> ) -> Result<(), LemmyError>
where where
T: Serialize + Debug + Send + 'static, T: Serialize + std::fmt::Debug + Send + 'static,
{ {
blocking(pool, move |conn| { blocking(pool, move |conn| {
do_insert_activity(conn, user_id, &data, local) do_insert_activity(conn, user_id, &data, local)

View File

@ -3,8 +3,6 @@
pub extern crate strum_macros; pub extern crate strum_macros;
#[macro_use] #[macro_use]
pub extern crate lazy_static; pub extern crate lazy_static;
#[macro_use]
pub extern crate failure;
pub extern crate actix; pub extern crate actix;
pub extern crate actix_web; pub extern crate actix_web;
pub extern crate base64; pub extern crate base64;
@ -33,6 +31,7 @@ pub mod websocket;
use crate::request::{retry, RecvError}; use crate::request::{retry, RecvError};
use actix_web::{client::Client, dev::ConnectionInfo}; use actix_web::{client::Client, dev::ConnectionInfo};
use anyhow::anyhow;
use lemmy_utils::{get_apub_protocol_string, settings::Settings}; use lemmy_utils::{get_apub_protocol_string, settings::Settings};
use log::error; use log::error;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
@ -48,12 +47,12 @@ pub type IPAddr = String;
#[derive(Debug)] #[derive(Debug)]
pub struct LemmyError { pub struct LemmyError {
inner: failure::Error, inner: anyhow::Error,
} }
impl<T> From<T> for LemmyError impl<T> From<T> for LemmyError
where where
T: Into<failure::Error>, T: Into<anyhow::Error>,
{ {
fn from(t: T) -> Self { fn from(t: T) -> Self {
LemmyError { inner: t.into() } LemmyError { inner: t.into() }
@ -118,7 +117,7 @@ pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResp
if response.msg == "ok" { if response.msg == "ok" {
Ok(response) Ok(response)
} else { } else {
Err(format_err!("{}", &response.msg).into()) Err(anyhow!("{}", &response.msg).into())
} }
} }
@ -191,13 +190,13 @@ pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), Le
if response if response
.headers() .headers()
.get("Content-Type") .get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))? .ok_or_else(|| anyhow!("No Content-Type header"))?
.to_str()? .to_str()?
.starts_with("image/") .starts_with("image/")
{ {
Ok(()) Ok(())
} else { } else {
Err(format_err!("Not an image type.").into()) Err(anyhow!("Not an image type.").into())
} }
} }

View File

@ -1,12 +1,14 @@
use crate::LemmyError; use crate::LemmyError;
use anyhow::anyhow;
use std::future::Future; use std::future::Future;
use thiserror::Error;
#[derive(Clone, Debug, Fail)] #[derive(Clone, Debug, Error)]
#[fail(display = "Error sending request, {}", _0)] #[error("Error sending request, {0}")]
struct SendError(pub String); struct SendError(pub String);
#[derive(Clone, Debug, Fail)] #[derive(Clone, Debug, Error)]
#[fail(display = "Error receiving response, {}", _0)] #[error("Error receiving response, {0}")]
pub struct RecvError(pub String); pub struct RecvError(pub String);
pub async fn retry<F, Fut, T>(f: F) -> Result<T, LemmyError> pub async fn retry<F, Fut, T>(f: F) -> Result<T, LemmyError>
@ -22,7 +24,7 @@ where
F: Fn() -> Fut, F: Fn() -> Fut,
Fut: Future<Output = Result<Result<T, actix_web::client::SendRequestError>, LemmyError>>, Fut: Future<Output = Result<Result<T, actix_web::client::SendRequestError>, LemmyError>>,
{ {
let mut response = Err(format_err!("connect timeout").into()); let mut response = Err(anyhow!("connect timeout").into());
for _ in 0u8..3 { for _ in 0u8..3 {
match (f)().await? { match (f)().await? {

View File

@ -1,5 +1,6 @@
use crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError}; use crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError};
use actix_web::{error::ErrorBadRequest, *}; use actix_web::{error::ErrorBadRequest, *};
use anyhow::anyhow;
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use diesel::{ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
@ -88,7 +89,7 @@ async fn get_feed(
"c" => RequestType::Community, "c" => RequestType::Community,
"front" => RequestType::Front, "front" => RequestType::Front,
"inbox" => RequestType::Inbox, "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(); let param = path.1.to_owned();

View File

@ -1,5 +1,6 @@
use crate::{blocking, routes::DbPoolParam, version, LemmyError}; use crate::{blocking, routes::DbPoolParam, version, LemmyError};
use actix_web::{body::Body, error::ErrorBadRequest, *}; use actix_web::{body::Body, error::ErrorBadRequest, *};
use anyhow::anyhow;
use lemmy_db::site_view::SiteView; use lemmy_db::site_view::SiteView;
use lemmy_utils::{get_apub_protocol_string, settings::Settings}; use lemmy_utils::{get_apub_protocol_string, settings::Settings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -28,7 +29,7 @@ async fn node_info_well_known() -> Result<HttpResponse<Body>, LemmyError> {
async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> { async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
let site_view = blocking(&db, SiteView::read) let site_view = blocking(&db, SiteView::read)
.await? .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 { let protocols = if Settings::get().federation.enabled {
vec!["activitypub".to_string()] vec!["activitypub".to_string()]

View File

@ -1,5 +1,6 @@
use crate::{blocking, routes::DbPoolParam, LemmyError}; use crate::{blocking, routes::DbPoolParam, LemmyError};
use actix_web::{error::ErrorBadRequest, web::Query, *}; use actix_web::{error::ErrorBadRequest, web::Query, *};
use anyhow::anyhow;
use lemmy_db::{community::Community, user::User_}; use lemmy_db::{community::Community, user::User_};
use lemmy_utils::{settings::Settings, WEBFINGER_COMMUNITY_REGEX, WEBFINGER_USER_REGEX}; use lemmy_utils::{settings::Settings, WEBFINGER_COMMUNITY_REGEX, WEBFINGER_USER_REGEX};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -62,17 +63,17 @@ async fn get_webfinger_response(
Community::read_from_name(conn, &community_name) Community::read_from_name(conn, &community_name)
}) })
.await? .await?
.map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))? .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?
.actor_id .actor_id
} else if let Some(user_name) = user_regex_parsed { } else if let Some(user_name) = user_regex_parsed {
let user_name = user_name.as_str().to_owned(); let user_name = user_name.as_str().to_owned();
// Make sure the requested user exists. // Make sure the requested user exists.
blocking(&db, move |conn| User_::read_from_name(conn, &user_name)) blocking(&db, move |conn| User_::read_from_name(conn, &user_name))
.await? .await?
.map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))? .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?
.actor_id .actor_id
} else { } else {
return Err(ErrorBadRequest(LemmyError::from(format_err!("not_found")))); return Err(ErrorBadRequest(LemmyError::from(anyhow!("not_found"))));
}; };
let json = WebFingerResponse { let json = WebFingerResponse {

2
ui/package.json vendored
View File

@ -33,7 +33,7 @@
"i18next": "^19.4.1", "i18next": "^19.4.1",
"inferno": "^7.4.2", "inferno": "^7.4.2",
"inferno-helmet": "^5.2.1", "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", "inferno-router": "^7.4.2",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",

View File

@ -21,6 +21,7 @@ interface MarkdownTextAreaProps {
replyType?: boolean; replyType?: boolean;
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
maxLength?: number;
onSubmit?(msg: { val: string; formId: string }): any; onSubmit?(msg: { val: string; formId: string }): any;
onContentChange?(val: string): any; onContentChange?(val: string): any;
onReplyCancel?(): any; onReplyCancel?(): any;
@ -121,7 +122,7 @@ export class MarkdownTextArea extends Component<
required required
disabled={this.props.disabled} disabled={this.props.disabled}
rows={2} rows={2}
maxLength={10000} maxLength={this.props.maxLength || 10000}
/> />
{this.state.previewMode && ( {this.state.previewMode && (
<div <div

View File

@ -31,6 +31,7 @@ import {
toast, toast,
setupTippy, setupTippy,
getLanguage, getLanguage,
mdToHtml,
} from '../utils'; } from '../utils';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
import { SortSelect } from './sort-select'; import { SortSelect } from './sort-select';
@ -39,6 +40,7 @@ import { MomentTime } from './moment-time';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import moment from 'moment'; import moment from 'moment';
import { UserDetails } from './user-details'; import { UserDetails } from './user-details';
import { MarkdownTextArea } from './markdown-textarea';
interface UserState { interface UserState {
user: UserView; user: UserView;
@ -109,6 +111,7 @@ export class User extends Component<any, UserState> {
show_avatars: null, show_avatars: null,
send_notifications_to_email: null, send_notifications_to_email: null,
auth: null, auth: null,
bio: null,
}, },
userSettingsLoading: null, userSettingsLoading: null,
deleteAccountLoading: null, deleteAccountLoading: null,
@ -149,7 +152,13 @@ export class User extends Component<any, UserState> {
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind( this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this this
); );
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this
);
this.handlePageChange = this.handlePageChange.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.user_id = Number(this.props.match.params.id) || null;
this.state.username = this.props.match.params.username; this.state.username = this.props.match.params.username;
@ -375,6 +384,12 @@ export class User extends Component<any, UserState> {
)} )}
</ul> </ul>
</h5> </h5>
<div className="d-flex align-items-center mb-2">
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(user.bio)}
/>
</div>
<div className="d-flex align-items-center mb-2"> <div className="d-flex align-items-center mb-2">
<svg class="icon"> <svg class="icon">
<use xlinkHref="#icon-cake"></use> <use xlinkHref="#icon-cake"></use>
@ -570,6 +585,18 @@ export class User extends Component<any, UserState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row">
<label class="col-lg-3 col-form-label" htmlFor="user-bio">
{i18n.t('bio')}
</label>
<div class="col-lg-9">
<MarkdownTextArea
initialContent={this.state.userSettingsForm.bio}
onContentChange={this.handleUserSettingsBioChange}
maxLength={300}
/>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col-lg-5 col-form-label"> <label class="col-lg-5 col-form-label">
<a <a
@ -900,6 +927,11 @@ export class User extends Component<any, UserState> {
i.setState(i.state); i.setState(i.state);
} }
handleUserSettingsBioChange(val: string) {
this.state.userSettingsForm.bio = val;
this.setState(this.state);
}
handleUserSettingsMatrixUserIdChange(i: User, event: any) { handleUserSettingsMatrixUserIdChange(i: User, event: any) {
i.state.userSettingsForm.matrix_user_id = event.target.value; i.state.userSettingsForm.matrix_user_id = event.target.value;
if ( if (
@ -1057,6 +1089,7 @@ export class User extends Component<any, UserState> {
this.state.userSettingsForm.lang = UserService.Instance.user.lang; this.state.userSettingsForm.lang = UserService.Instance.user.lang;
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar; this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
this.state.userSettingsForm.email = this.state.user.email; this.state.userSettingsForm.email = this.state.user.email;
this.state.userSettingsForm.bio = this.state.user.bio;
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email; this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
this.state.userSettingsForm.show_avatars = this.state.userSettingsForm.show_avatars =
UserService.Instance.user.show_avatars; UserService.Instance.user.show_avatars;
@ -1068,9 +1101,10 @@ export class User extends Component<any, UserState> {
} else if (res.op == UserOperation.SaveUserSettings) { } else if (res.op == UserOperation.SaveUserSettings) {
const data = res.data as LoginResponse; const data = res.data as LoginResponse;
UserService.Instance.login(data); UserService.Instance.login(data);
this.setState({ this.state.user.bio = this.state.userSettingsForm.bio;
userSettingsLoading: false, this.state.userSettingsLoading = false;
}); this.setState(this.state);
window.scrollTo(0, 0); window.scrollTo(0, 0);
} else if (res.op == UserOperation.DeleteAccount) { } else if (res.op == UserOperation.DeleteAccount) {
this.setState({ this.setState({

View File

@ -597,6 +597,7 @@ export interface UserSettingsForm {
lang: string; lang: string;
avatar?: string; avatar?: string;
email?: string; email?: string;
bio?: string;
matrix_user_id?: string; matrix_user_id?: string;
new_password?: string; new_password?: string;
new_password_verify?: string; new_password_verify?: string;

View File

@ -228,6 +228,7 @@
"landing_0": "landing_0":
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.", "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
"not_logged_in": "Not logged in.", "not_logged_in": "Not logged in.",
"bio_length_overflow": "User bio cannot exceed 300 characters!",
"logged_in": "Logged in.", "logged_in": "Logged in.",
"must_login": "You must <1>log in or register</1> to comment.", "must_login": "You must <1>log in or register</1> to comment.",
"site_saved": "Site Saved.", "site_saved": "Site Saved.",
@ -284,5 +285,6 @@
"cake_day_info": "It's {{ creator_name }}'s cake day today!", "cake_day_info": "It's {{ creator_name }}'s cake day today!",
"invalid_post_title": "Invalid post title", "invalid_post_title": "Invalid post title",
"invalid_url": "Invalid URL.", "invalid_url": "Invalid URL.",
"play_captcha_audio": "Play Captcha Audio" "play_captcha_audio": "Play Captcha Audio",
"bio": "Bio"
} }

26
ui/yarn.lock vendored
View File

@ -3813,14 +3813,14 @@ infer-owner@^1.0.4:
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
inferno-clone-vnode@^7.1.12: inferno-clone-vnode@^7.4.2:
version "7.4.2" version "7.4.2"
resolved "https://registry.yarnpkg.com/inferno-clone-vnode/-/inferno-clone-vnode-7.4.2.tgz#071577098fd8ffdffd41cf81819207effa520bc1" resolved "https://registry.yarnpkg.com/inferno-clone-vnode/-/inferno-clone-vnode-7.4.2.tgz#071577098fd8ffdffd41cf81819207effa520bc1"
integrity sha512-pX5agEWfU+w6vYaVyKtzgFT4jlMK+eOEKL/LkyWu2dtOy0DXx174AFm6GSgOt66W6xmxn58sbWI7ngsWmp4f2w== integrity sha512-pX5agEWfU+w6vYaVyKtzgFT4jlMK+eOEKL/LkyWu2dtOy0DXx174AFm6GSgOt66W6xmxn58sbWI7ngsWmp4f2w==
dependencies: dependencies:
inferno "7.4.2" inferno "7.4.2"
inferno-create-element@^7.1.12: inferno-create-element@^7.4.2:
version "7.4.2" version "7.4.2"
resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.2.tgz#d3ac6c64c792f8d3c4784279825418ebd73cfe1c" resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.2.tgz#d3ac6c64c792f8d3c4784279825418ebd73cfe1c"
integrity sha512-FHNca1NR/9SRtaifr4DdAuA6dKBeWkCrYVVkTazMadJyjMtpPN3d+sKlkmdvorAjhUvdMMNcRQStrgRTpLcZyg== integrity sha512-FHNca1NR/9SRtaifr4DdAuA6dKBeWkCrYVVkTazMadJyjMtpPN3d+sKlkmdvorAjhUvdMMNcRQStrgRTpLcZyg==
@ -3836,16 +3836,16 @@ inferno-helmet@^5.2.1:
inferno-side-effect "^1.1.5" inferno-side-effect "^1.1.5"
object-assign "^4.1.1" object-assign "^4.1.1"
inferno-i18next@nimbusec-oss/inferno-i18next: "inferno-i18next@github:nimbusec-oss/inferno-i18next#semver:^7.4.2":
version "7.1.12" version "7.4.2"
resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/f8c1403e60be70141c558e36f12f22c106cb7463" resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/54b9be591ccd62c53799ad23e35f17144a62f909"
dependencies: dependencies:
html-parse-stringify2 "^2.0.1" html-parse-stringify2 "^2.0.1"
inferno "^7.1.12" inferno "^7.4.2"
inferno-clone-vnode "^7.1.12" inferno-clone-vnode "^7.4.2"
inferno-create-element "^7.1.12" inferno-create-element "^7.4.2"
inferno-shared "^7.1.12" inferno-shared "^7.4.2"
inferno-vnode-flags "^7.1.12" inferno-vnode-flags "^7.4.2"
inferno-router@^7.4.2: inferno-router@^7.4.2:
version "7.4.2" version "7.4.2"
@ -3857,7 +3857,7 @@ inferno-router@^7.4.2:
inferno "7.4.2" inferno "7.4.2"
path-to-regexp-es6 "1.7.0" path-to-regexp-es6 "1.7.0"
inferno-shared@7.4.2, inferno-shared@^7.1.12: inferno-shared@7.4.2, inferno-shared@^7.4.2:
version "7.4.2" version "7.4.2"
resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.2.tgz#400cf6d19a077af9e5e852e1f189726391efa273" resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.2.tgz#400cf6d19a077af9e5e852e1f189726391efa273"
integrity sha512-SULfgzj/PuyMd3rHThEXkeEdZorjcr/q+kaqbwr7C2XhIPCk4e5jcRKZujI6YCSahGA9WFwP2Mft1v5PsaaNeg== integrity sha512-SULfgzj/PuyMd3rHThEXkeEdZorjcr/q+kaqbwr7C2XhIPCk4e5jcRKZujI6YCSahGA9WFwP2Mft1v5PsaaNeg==
@ -3871,12 +3871,12 @@ inferno-side-effect@^1.1.5:
npm "^5.8.0" npm "^5.8.0"
shallowequal "^1.0.1" shallowequal "^1.0.1"
inferno-vnode-flags@7.4.2, inferno-vnode-flags@^7.1.12: inferno-vnode-flags@7.4.2, inferno-vnode-flags@^7.4.2:
version "7.4.2" version "7.4.2"
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.2.tgz#54982dabe34f308853ba17de7de4241e23769135" resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.2.tgz#54982dabe34f308853ba17de7de4241e23769135"
integrity sha512-sV4KqqvZH4MW9/dNbC9blHInnpQSqMWouU5VlanbJ+NhJ8ufPwsDy0/+jiA2aODpg2HFHwVMJFF1fsewPqNtLQ== integrity sha512-sV4KqqvZH4MW9/dNbC9blHInnpQSqMWouU5VlanbJ+NhJ8ufPwsDy0/+jiA2aODpg2HFHwVMJFF1fsewPqNtLQ==
inferno@7.4.2, inferno@^7.1.12, inferno@^7.4.2: inferno@7.4.2, inferno@^7.4.2:
version "7.4.2" version "7.4.2"
resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.4.2.tgz#833cc423ee7b939fad705c59ea41924f31c92453" resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.4.2.tgz#833cc423ee7b939fad705c59ea41924f31c92453"
integrity sha512-RsmuN8F7lAWTTuy2juf3Tqn/BihkRNdy0WZN+vuyryuEySKHBA1fruyq6K0covF3Ja8mAdha5NNISZz9ltgcug== integrity sha512-RsmuN8F7lAWTTuy2juf3Tqn/BihkRNdy0WZN+vuyryuEySKHBA1fruyq6K0covF3Ja8mAdha5NNISZz9ltgcug==