summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorColin Reeder <colin@vpzom.click>2020-08-12 09:46:07 -0600
committerColin Reeder <colin@vpzom.click>2020-08-12 09:47:58 -0600
commitd0f9369fe16ec83de034ef1aebee572b17d9f320 (patch)
treed8e664bfe13e6fd8c56478163ba52d6add4a755c
parentc9aa53785ef33f529e99363e1e9293bcafb3047d (diff)
Add basic support for user avatars
-rw-r--r--migrations/20200812151735_avatar/down.sql3
-rw-r--r--migrations/20200812151735_avatar/up.sql3
-rw-r--r--openapi/openapi.json26
-rw-r--r--src/apub_util.rs23
-rw-r--r--src/routes/api/comments.rs12
-rw-r--r--src/routes/api/communities.rs7
-rw-r--r--src/routes/api/mod.rs19
-rw-r--r--src/routes/api/posts.rs22
-rw-r--r--src/routes/api/users.rs10
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 {