diff options
author | Colin Reeder <colin@vpzom.click> | 2020-10-24 21:12:12 -0600 |
---|---|---|
committer | Colin Reeder <colin@vpzom.click> | 2020-10-24 21:23:02 -0600 |
commit | f00efb1a32e6bd8df7a6ba4d20b3ddfad930d789 (patch) | |
tree | 138d507c511e464539f7b639bee20767b4c189e4 | |
parent | d98b7e2445485398d6c1b96b2439db7c71686452 (diff) |
Allow local users to set avatars
-rw-r--r-- | res/lang/en.ftl | 1 | ||||
-rw-r--r-- | src/main.rs | 12 | ||||
-rw-r--r-- | src/routes/api/users.rs | 102 | ||||
-rw-r--r-- | src/routes/apub/mod.rs | 11 |
4 files changed, 124 insertions, 2 deletions
diff --git a/res/lang/en.ftl b/res/lang/en.ftl index 3970c9c..55f3f14 100644 --- a/res/lang/en.ftl +++ b/res/lang/en.ftl @@ -29,3 +29,4 @@ post_not_yours = That's not your post root = lotide is running. Note that lotide itself does not include a frontend, and you'll need to install one separately. user_email_invalid = Specified email address is invalid user_name_disallowed_chars = Username contains disallowed characters +user_no_avatar = That user does not have an avatar diff --git a/src/main.rs b/src/main.rs index 569beb0..afd7101 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,6 +182,18 @@ impl BaseContext { None => None, } } + + pub fn process_avatar_href<'a>(&self, href: &'a str, user_id: UserLocalID) -> Cow<'a, str> { + if href.starts_with("local-media://") { + format!( + "{}/unstable/users/{}/avatar/href", + self.host_url_api, user_id, + ) + .into() + } else { + href.into() + } + } } pub struct RouteContext { diff --git a/src/routes/api/users.rs b/src/routes/api/users.rs index e9421a7..2bc2eac 100644 --- a/src/routes/api/users.rs +++ b/src/routes/api/users.rs @@ -187,6 +187,7 @@ async fn route_unstable_users_patch( description: Option<Cow<'a, str>>, email_address: Option<Cow<'a, str>>, password: Option<String>, + avatar: Option<Cow<'a, str>>, } let body = hyper::body::to_bytes(req.into_body()).await?; @@ -216,6 +217,16 @@ async fn route_unstable_users_patch( changes.push(("passhash", arena.alloc(passhash))); } + if let Some(avatar) = &body.avatar { + if !avatar.starts_with("local-media://") { + return Err(crate::Error::UserError(crate::simple_response( + hyper::StatusCode::BAD_REQUEST, + "Avatar must be local media", + ))); + } + + changes.push(("avatar", avatar)); + } if !changes.is_empty() { use std::fmt::Write; @@ -248,6 +259,88 @@ async fn route_unstable_users_patch( Ok(crate::empty_response()) } +async fn route_unstable_users_avatar_href_get( + params: (UserIDOrMe,), + ctx: Arc<crate::RouteContext>, + req: hyper::Request<hyper::Body>, +) -> Result<hyper::Response<hyper::Body>, crate::Error> { + let (user_id,) = params; + + let lang = crate::get_lang_for_req(&req); + let db = ctx.db_pool.get().await?; + + let user_id = user_id.try_resolve(&req, &db).await?; + + let row = db + .query_opt("SELECT avatar FROM person WHERE id=$1", &[&user_id]) + .await?; + match row { + None => Ok(crate::simple_response( + hyper::StatusCode::NOT_FOUND, + lang.tr("no_such_user", None).into_owned(), + )), + Some(row) => { + let href: Option<String> = row.get(0); + match href { + None => Ok(crate::simple_response( + hyper::StatusCode::NOT_FOUND, + lang.tr("user_no_avatar", None).into_owned(), + )), + Some(href) => { + if href.starts_with("local-media://") { + // local media, serve file content + + let media_id: crate::Pineapple = (&href[14..]).parse()?; + + let media_row = db + .query_opt( + "SELECT path, mime FROM media WHERE id=$1", + &[&media_id.as_int()], + ) + .await?; + match media_row { + None => Ok(crate::simple_response( + hyper::StatusCode::NOT_FOUND, + lang.tr("media_upload_missing", None).into_owned(), + )), + Some(media_row) => { + let path: &str = media_row.get(0); + let mime: &str = media_row.get(1); + + if let Some(media_location) = &ctx.media_location { + let path = media_location.join(path); + + let file = tokio::fs::File::open(path).await?; + let body = hyper::Body::wrap_stream( + tokio_util::codec::FramedRead::new( + file, + tokio_util::codec::BytesCodec::new(), + ), + ); + + Ok(crate::common_response_builder() + .header(hyper::header::CONTENT_TYPE, mime) + .body(body)?) + } else { + Ok(crate::simple_response( + hyper::StatusCode::NOT_FOUND, + lang.tr("media_upload_missing", None).into_owned(), + )) + } + } + } + } else { + Ok(crate::common_response_builder() + .status(hyper::StatusCode::FOUND) + .header(hyper::header::LOCATION, &href) + .body(href.into())?) + } + } + } + } + } +} + async fn route_unstable_users_following_posts_list( params: (UserIDOrMe,), ctx: Arc<crate::RouteContext>, @@ -461,7 +554,9 @@ async fn route_unstable_users_get( username: Cow::Borrowed(row.get(0)), host: crate::get_actor_host_or_unknown(local, ap_id, &ctx.local_hostname), remote_url: ap_id.map(From::from), - avatar: avatar.map(|url| RespAvatarInfo { url: url.into() }), + avatar: avatar.map(|url| RespAvatarInfo { + url: ctx.process_avatar_href(url, user_id), + }), }; let info = RespUserInfo { @@ -572,6 +667,11 @@ pub fn route_users() -> crate::RouteNode<()> { .with_handler_async("GET", route_unstable_users_get) .with_handler_async("PATCH", route_unstable_users_patch) .with_child( + "avatar/href", + crate::RouteNode::new() + .with_handler_async("GET", route_unstable_users_avatar_href_get), + ) + .with_child( "following:posts", crate::RouteNode::new() .with_handler_async("GET", route_unstable_users_following_posts_list), diff --git a/src/routes/apub/mod.rs b/src/routes/apub/mod.rs index d6a3b36..277065f 100644 --- a/src/routes/apub/mod.rs +++ b/src/routes/apub/mod.rs @@ -118,7 +118,7 @@ async fn handler_users_get( match db .query_opt( - "SELECT username, local, public_key, description FROM person WHERE id=$1", + "SELECT username, local, public_key, description, avatar FROM person WHERE id=$1", &[&user_id.raw()], ) .await? @@ -150,6 +150,8 @@ async fn handler_users_get( let description: &str = row.get(3); + let avatar: Option<&str> = row.get(4); + let user_ap_id = crate::apub_util::get_local_person_apub_id(user_id, &ctx.host_url_apub); @@ -162,6 +164,13 @@ async fn handler_users_get( .set_name(username.as_ref()) .set_summary(description); + if let Some(avatar) = avatar { + let mut attachment = activitystreams::object::Image::new(); + attachment.set_url(ctx.process_avatar_href(avatar, user_id).into_owned()); + + info.set_icon(attachment.into_any_base()?); + } + let endpoints = activitystreams::actor::Endpoints { shared_inbox: Some( crate::apub_util::get_local_shared_inbox(&ctx.host_url_apub).into(), |