diff options
author | Colin Reeder <colin@vpzom.click> | 2020-08-12 09:46:07 -0600 |
---|---|---|
committer | Colin Reeder <colin@vpzom.click> | 2020-08-12 09:47:58 -0600 |
commit | d0f9369fe16ec83de034ef1aebee572b17d9f320 (patch) | |
tree | d8e664bfe13e6fd8c56478163ba52d6add4a755c | |
parent | c9aa53785ef33f529e99363e1e9293bcafb3047d (diff) |
Add basic support for user avatars
-rw-r--r-- | migrations/20200812151735_avatar/down.sql | 3 | ||||
-rw-r--r-- | migrations/20200812151735_avatar/up.sql | 3 | ||||
-rw-r--r-- | openapi/openapi.json | 26 | ||||
-rw-r--r-- | src/apub_util.rs | 23 | ||||
-rw-r--r-- | src/routes/api/comments.rs | 12 | ||||
-rw-r--r-- | src/routes/api/communities.rs | 7 | ||||
-rw-r--r-- | src/routes/api/mod.rs | 19 | ||||
-rw-r--r-- | src/routes/api/posts.rs | 22 | ||||
-rw-r--r-- | src/routes/api/users.rs | 10 |
9 files changed, 96 insertions, 29 deletions
diff --git a/migrations/20200812151735_avatar/down.sql b/migrations/20200812151735_avatar/down.sql new file mode 100644 index 0000000..8e9563a --- /dev/null +++ b/migrations/20200812151735_avatar/down.sql @@ -0,0 +1,3 @@ +BEGIN; + ALTER TABLE person DROP COLUMN avatar; +COMMIT; diff --git a/migrations/20200812151735_avatar/up.sql b/migrations/20200812151735_avatar/up.sql new file mode 100644 index 0000000..5d5745c --- /dev/null +++ b/migrations/20200812151735_avatar/up.sql @@ -0,0 +1,3 @@ +BEGIN; + ALTER TABLE person ADD COLUMN avatar TEXT; +COMMIT; diff --git a/openapi/openapi.json b/openapi/openapi.json index bb57dc3..fe7cd68 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -61,7 +61,14 @@ "username": {"type": "string"}, "local": {"type": "boolean"}, "host": {"type": "string"}, - "remote_url": {"type": "string", "nullable": true} + "remote_url": {"type": "string", "nullable": true}, + "avatar": { + "type": "object", + "required": ["url"], + "properties": { + "url": {"type": "string"} + } + } } }, "NullableMinimalUserInfo": { @@ -73,7 +80,14 @@ "username": {"type": "string"}, "local": {"type": "boolean"}, "host": {"type": "string"}, - "remote_url": {"type": "string", "nullable": true} + "remote_url": {"type": "string", "nullable": true}, + "avatar": { + "type": "object", + "required": ["url"], + "properties": { + "url": {"type": "string"} + } + } } }, "PostListPost": { @@ -1108,14 +1122,10 @@ "content": { "application/json": { "schema": { + "allOf": [{"$ref": "#/components/schemas/MinimalUserInfo"}], "type": "object", - "required": ["id", "local", "username", "host", "remote_url", "description"], + "required": ["description"], "properties": { - "id": {"type": "integer"}, - "local": {"type": "boolean"}, - "username": {"type": "string"}, - "host": {"type": "string"}, - "remote_url": {"type": "string", "nullable": true}, "description": {"type": "string"}, "your_note": { "type": "string", diff --git a/src/apub_util.rs b/src/apub_util.rs index a3d7ca6..47e0b5f 100644 --- a/src/apub_util.rs +++ b/src/apub_util.rs @@ -394,9 +394,28 @@ pub async fn fetch_actor( .and_then(|maybe| maybe.iter().filter_map(|x| x.as_xsd_string()).next()) .unwrap_or(""); + let avatar = person.icon().and_then(|icon| { + icon.iter() + .filter_map(|icon| { + if icon.kind_str() == Some("Image") { + match activitystreams::object::Image::from_any_base(icon.clone()) { + Err(_) | Ok(None) => None, + Ok(Some(icon)) => Some(icon), + } + } else { + None + } + }) + .next() + }); + let avatar = avatar + .as_ref() + .and_then(|icon| icon.url().and_then(|url| url.as_single_id())) + .map(|x| x.as_str()); + let id = UserLocalID(db.query_one( - "INSERT INTO person (username, local, created_local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description) VALUES ($1, FALSE, localtimestamp, $2, $3, $4, $5, $6, $7) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description=$7 RETURNING id", - &[&username, &ap_id.as_str(), &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description], + "INSERT INTO person (username, local, created_local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description, avatar) VALUES ($1, FALSE, localtimestamp, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description=$7, avatar=$8 RETURNING id", + &[&username, &ap_id.as_str(), &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description, &avatar], ).await?.get(0)); Ok(ActorLocalInfo::User { diff --git a/src/routes/api/comments.rs b/src/routes/api/comments.rs index f4c9fc6..546289d 100644 --- a/src/routes/api/comments.rs +++ b/src/routes/api/comments.rs @@ -1,6 +1,6 @@ use super::{ - MaybeIncludeYour, RespMinimalAuthorInfo, RespMinimalCommentInfo, RespMinimalPostInfo, - RespPostCommentInfo, + MaybeIncludeYour, RespAvatarInfo, RespMinimalAuthorInfo, RespMinimalCommentInfo, + RespMinimalPostInfo, RespPostCommentInfo, }; use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID}; use serde_derive::{Deserialize, Serialize}; @@ -39,7 +39,7 @@ async fn route_unstable_comments_get( let (row, your_vote) = futures::future::try_join( db.query_opt( - "SELECT reply.author, reply.post, reply.content_text, reply.created, reply.local, reply.content_html, person.username, person.local, person.ap_id, post.title, reply.deleted, reply.parent FROM reply INNER JOIN post ON (reply.post = post.id) LEFT OUTER JOIN person ON (reply.author = person.id) WHERE reply.id = $1", + "SELECT reply.author, reply.post, reply.content_text, reply.created, reply.local, reply.content_html, person.username, person.local, person.ap_id, post.title, reply.deleted, reply.parent, person.avatar FROM reply INNER JOIN post ON (reply.post = post.id) LEFT OUTER JOIN person ON (reply.author = person.id) WHERE reply.id = $1", &[&comment_id], ) .map_err(crate::Error::from), @@ -68,6 +68,7 @@ async fn route_unstable_comments_get( Some(author_username) => { let author_local = row.get(7); let author_ap_id = row.get(8); + let author_avatar: Option<&str> = row.get(12); Some(RespMinimalAuthorInfo { id: UserLocalID(row.get(0)), username: Cow::Borrowed(author_username), @@ -78,6 +79,7 @@ async fn route_unstable_comments_get( &ctx.local_hostname, ), remote_url: author_ap_id.map(From::from), + avatar: author_avatar.map(|url| RespAvatarInfo { url: url.into() }), }) } None => None, @@ -352,7 +354,7 @@ async fn route_unstable_comments_likes_list( None => "", }; - let sql: &str = &format!("SELECT person.id, person.username, person.local, person.ap_id, reply_like.created_local FROM reply_like, person WHERE person.id = reply_like.person AND reply_like.reply = $1{} ORDER BY reply_like.created_local DESC, reply_like.person DESC LIMIT $2", page_conditions); + let sql: &str = &format!("SELECT person.id, person.username, person.local, person.ap_id, reply_like.created_local, person.avatar FROM reply_like, person WHERE person.id = reply_like.person AND reply_like.reply = $1{} ORDER BY reply_like.created_local DESC, reply_like.person DESC LIMIT $2", page_conditions); let mut rows = db.query(sql, &values).await?; @@ -376,6 +378,7 @@ async fn route_unstable_comments_likes_list( let username: &str = row.get(1); let local: bool = row.get(2); let ap_id: Option<&str> = row.get(3); + let avatar: Option<&str> = row.get(5); super::JustUser { user: RespMinimalAuthorInfo { @@ -384,6 +387,7 @@ async fn route_unstable_comments_likes_list( local, 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() }), }, } }) diff --git a/src/routes/api/communities.rs b/src/routes/api/communities.rs index d494cb3..8f9c71f 100644 --- a/src/routes/api/communities.rs +++ b/src/routes/api/communities.rs @@ -1,5 +1,6 @@ use crate::routes::api::{ - MaybeIncludeYour, RespMinimalAuthorInfo, RespMinimalCommunityInfo, RespPostListPost, + MaybeIncludeYour, RespAvatarInfo, RespMinimalAuthorInfo, RespMinimalCommunityInfo, + RespPostListPost, }; use crate::{CommunityLocalID, PostLocalID, UserLocalID}; use serde_derive::{Deserialize, Serialize}; @@ -443,7 +444,7 @@ async fn route_unstable_communities_posts_list( let values: &[&(dyn tokio_postgres::types::ToSql + Sync)] = &[&community_id, &limit]; let sql: &str = &format!( - "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, person.username, person.local, person.ap_id FROM post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = $1 AND post.approved=TRUE AND post.deleted=FALSE ORDER BY {} LIMIT $2", + "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, person.username, person.local, person.ap_id, person.avatar FROM post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = $1 AND post.approved=TRUE AND post.deleted=FALSE ORDER BY {} LIMIT $2", query.sort.post_sort_sql(), ); @@ -464,6 +465,7 @@ async fn route_unstable_communities_posts_list( let author_name: &str = row.get(7); let author_local: bool = row.get(8); let author_ap_id: Option<&str> = row.get(9); + let author_avatar: Option<&str> = row.get(10); RespMinimalAuthorInfo { id, username: author_name.into(), @@ -477,6 +479,7 @@ async fn route_unstable_communities_posts_list( } }, remote_url: author_ap_id.map(From::from), + avatar: author_avatar.map(|url| RespAvatarInfo { url: url.into() }), } }); diff --git a/src/routes/api/mod.rs b/src/routes/api/mod.rs index 75039db..ca3e7e9 100644 --- a/src/routes/api/mod.rs +++ b/src/routes/api/mod.rs @@ -47,12 +47,19 @@ struct MaybeIncludeYour { } #[derive(Serialize)] +struct RespAvatarInfo<'a> { + url: Cow<'a, str>, +} + +#[derive(Serialize)] struct RespMinimalAuthorInfo<'a> { id: UserLocalID, username: Cow<'a, str>, local: bool, host: Cow<'a, str>, remote_url: Option<Cow<'a, str>>, + #[serde(skip_serializing_if = "Option::is_none")] + avatar: Option<RespAvatarInfo<'a>>, } #[derive(Serialize)] @@ -545,7 +552,7 @@ async fn get_comments_replies<'a>( ) -> Result<HashMap<CommentLocalID, Vec<RespPostCommentInfo<'a>>>, crate::Error> { use futures::TryStreamExt; - let sql1 = "SELECT reply.id, reply.author, reply.content_text, reply.created, reply.parent, reply.content_html, person.username, person.local, person.ap_id, reply.deleted"; + let sql1 = "SELECT reply.id, reply.author, reply.content_text, reply.created, reply.parent, reply.content_html, person.username, person.local, person.ap_id, reply.deleted, person.avatar"; let (sql2, values): (_, Vec<&(dyn tokio_postgres::types::ToSql + Sync)>) = if include_your_for.is_some() { ( @@ -575,6 +582,7 @@ async fn get_comments_replies<'a>( let author_id = UserLocalID(row.get(1)); let author_local: bool = row.get(7); let author_ap_id: Option<&str> = row.get(8); + let author_avatar: Option<&str> = row.get(10); RespMinimalAuthorInfo { id: author_id, @@ -586,6 +594,9 @@ async fn get_comments_replies<'a>( &local_hostname, ), remote_url: author_ap_id.map(|x| x.to_owned().into()), + avatar: author_avatar.map(|url| RespAvatarInfo { + url: url.to_owned().into(), + }), } }); @@ -605,7 +616,7 @@ async fn get_comments_replies<'a>( has_replies: false, your_vote: match include_your_for { None => None, - Some(_) => Some(if row.get(10) { + Some(_) => Some(if row.get(11) { Some(crate::Empty {}) } else { None @@ -677,6 +688,7 @@ async fn handle_common_posts_list( let author_name: &str = row.get(11); let author_local: bool = row.get(12); let author_ap_id: Option<&str> = row.get(13); + let author_avatar: Option<&str> = row.get(14); RespMinimalAuthorInfo { id, username: author_name.into(), @@ -687,6 +699,9 @@ async fn handle_common_posts_list( &local_hostname, ), remote_url: author_ap_id.map(|x| x.to_owned().into()), + avatar: author_avatar.map(|url| RespAvatarInfo { + url: url.to_owned().into(), + }), } }); diff --git a/src/routes/api/posts.rs b/src/routes/api/posts.rs index 4562444..89957e2 100644 --- a/src/routes/api/posts.rs +++ b/src/routes/api/posts.rs @@ -1,6 +1,6 @@ use super::{ - MaybeIncludeYour, RespMinimalAuthorInfo, RespMinimalCommentInfo, RespMinimalCommunityInfo, - RespPostCommentInfo, RespPostListPost, + MaybeIncludeYour, RespAvatarInfo, RespMinimalAuthorInfo, RespMinimalCommentInfo, + RespMinimalCommunityInfo, RespPostCommentInfo, RespPostListPost, }; use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID}; use serde_derive::{Deserialize, Serialize}; @@ -16,7 +16,7 @@ async fn get_post_comments<'a>( ) -> Result<Vec<RespPostCommentInfo<'a>>, crate::Error> { use futures::TryStreamExt; - let sql1 = "SELECT reply.id, reply.author, reply.content_text, reply.created, reply.content_html, person.username, person.local, person.ap_id, reply.deleted"; + let sql1 = "SELECT reply.id, reply.author, reply.content_text, reply.created, reply.content_html, person.username, person.local, person.ap_id, reply.deleted, person.avatar"; let (sql2, values): (_, Vec<&(dyn tokio_postgres::types::ToSql + Sync)>) = if include_your_for.is_some() { ( @@ -45,6 +45,7 @@ async fn get_post_comments<'a>( let author_id = UserLocalID(row.get(1)); let author_local: bool = row.get(6); let author_ap_id: Option<&str> = row.get(7); + let author_avatar: Option<&str> = row.get(9); RespMinimalAuthorInfo { id: author_id, @@ -56,6 +57,9 @@ async fn get_post_comments<'a>( &local_hostname, ), remote_url: author_ap_id.map(|x| x.to_owned().into()), + avatar: author_avatar.map(|url| RespAvatarInfo { + url: url.to_owned().into(), + }), } }); @@ -75,7 +79,7 @@ async fn get_post_comments<'a>( has_replies: false, your_vote: match include_your_for { None => None, - Some(_) => Some(if row.get(9) { + Some(_) => Some(if row.get(10) { Some(crate::Empty {}) } else { None @@ -102,7 +106,7 @@ async fn route_unstable_posts_list( let limit: i64 = 30; let stream = db.query_raw( - "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND deleted=FALSE ORDER BY hot_rank((SELECT COUNT(*) FROM post_like WHERE post = post.id AND person != post.author), post.created) DESC LIMIT $1", + "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, person.avatar FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND deleted=FALSE ORDER BY hot_rank((SELECT COUNT(*) FROM post_like WHERE post = post.id AND person != post.author), post.created) DESC LIMIT $1", ([limit]).iter().map(|x| x as _), ).await?; @@ -267,7 +271,7 @@ async fn route_unstable_posts_get( let (row, comments, your_vote) = futures::future::try_join3( db.query_opt( - "SELECT post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, (SELECT COUNT(*) FROM post_like WHERE post_like.post = $1), post.approved FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND post.id = $1", + "SELECT post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, (SELECT COUNT(*) FROM post_like WHERE post_like.post = $1), post.approved, person.avatar FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND post.id = $1", &[&post_id], ) .map_err(crate::Error::from), @@ -306,6 +310,7 @@ async fn route_unstable_posts_get( Some(author_username) => { let author_local = row.get(11); let author_ap_id = row.get(12); + let author_avatar: Option<&str> = row.get(15); Some(RespMinimalAuthorInfo { id: UserLocalID(row.get(0)), username: Cow::Borrowed(author_username), @@ -316,6 +321,7 @@ async fn route_unstable_posts_get( &ctx.local_hostname, ), remote_url: author_ap_id.map(From::from), + avatar: author_avatar.map(|url| RespAvatarInfo { url: url.into() }), }) } None => None, @@ -576,7 +582,7 @@ async fn route_unstable_posts_likes_list( None => "", }; - let sql: &str = &format!("SELECT person.id, person.username, person.local, person.ap_id, post_like.created_local FROM post_like, person WHERE person.id = post_like.person AND post_like.post = $1{} ORDER BY post_like.created_local DESC, post_like.person DESC LIMIT $2", page_conditions); + let sql: &str = &format!("SELECT person.id, person.username, person.local, person.ap_id, post_like.created_local, person.avatar FROM post_like, person WHERE person.id = post_like.person AND post_like.post = $1{} ORDER BY post_like.created_local DESC, post_like.person DESC LIMIT $2", page_conditions); let mut rows = db.query(sql, &values).await?; @@ -600,6 +606,7 @@ async fn route_unstable_posts_likes_list( let username: &str = row.get(1); let local: bool = row.get(2); let ap_id: Option<&str> = row.get(3); + let avatar: Option<&str> = row.get(5); super::JustUser { user: RespMinimalAuthorInfo { @@ -608,6 +615,7 @@ async fn route_unstable_posts_likes_list( local, 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() }), }, } }) diff --git a/src/routes/api/users.rs b/src/routes/api/users.rs index c861aa6..57101f3 100644 --- a/src/routes/api/users.rs +++ b/src/routes/api/users.rs @@ -1,6 +1,6 @@ use super::{ - handle_common_posts_list, MaybeIncludeYour, RespMinimalAuthorInfo, RespMinimalCommentInfo, - RespMinimalCommunityInfo, RespMinimalPostInfo, RespThingInfo, + handle_common_posts_list, MaybeIncludeYour, RespAvatarInfo, RespMinimalAuthorInfo, + RespMinimalCommentInfo, RespMinimalCommunityInfo, RespMinimalPostInfo, RespThingInfo, }; use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID}; use serde_derive::{Deserialize, Serialize}; @@ -201,7 +201,7 @@ async fn route_unstable_users_following_posts_list( let values: &[&(dyn tokio_postgres::types::ToSql + Sync)] = &[&user, &limit]; let stream = db.query_raw( - "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND post.approved AND post.deleted=FALSE AND community.id IN (SELECT community FROM community_follow WHERE follower=$1 AND accepted) ORDER BY hot_rank((SELECT COUNT(*) FROM post_like WHERE post = post.id AND person != post.author), post.created) DESC LIMIT $2", + "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, person.avatar FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND post.approved AND post.deleted=FALSE AND community.id IN (SELECT community FROM community_follow WHERE follower=$1 AND accepted) ORDER BY hot_rank((SELECT COUNT(*) FROM post_like WHERE post = post.id AND person != post.author), post.created) DESC LIMIT $2", values.iter().map(|s| *s as _) ).await?; @@ -382,7 +382,7 @@ async fn route_unstable_users_get( let row = db .query_opt( - "SELECT username, local, ap_id, description FROM person WHERE id=$1", + "SELECT username, local, ap_id, description, avatar FROM person WHERE id=$1", &[&user_id], ) .await?; @@ -396,6 +396,7 @@ async fn route_unstable_users_get( let local = row.get(1); let ap_id = row.get(2); + let avatar: Option<&str> = row.get(4); let info = RespMinimalAuthorInfo { id: user_id, @@ -403,6 +404,7 @@ 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() }), }; let info = RespUserInfo { |