summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorColin Reeder <colin@vpzom.click>2020-10-24 21:12:12 -0600
committerColin Reeder <colin@vpzom.click>2020-10-24 21:23:02 -0600
commitf00efb1a32e6bd8df7a6ba4d20b3ddfad930d789 (patch)
tree138d507c511e464539f7b639bee20767b4c189e4
parentd98b7e2445485398d6c1b96b2439db7c71686452 (diff)
Allow local users to set avatars
-rw-r--r--res/lang/en.ftl1
-rw-r--r--src/main.rs12
-rw-r--r--src/routes/api/users.rs102
-rw-r--r--src/routes/apub/mod.rs11
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(),