~vpzom/lotide

d0f9369fe16ec83de034ef1aebee572b17d9f320 — Colin Reeder a month ago c9aa537
Add basic support for user avatars
A migrations/20200812151735_avatar/down.sql => migrations/20200812151735_avatar/down.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	ALTER TABLE person DROP COLUMN avatar;
COMMIT;

A migrations/20200812151735_avatar/up.sql => migrations/20200812151735_avatar/up.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	ALTER TABLE person ADD COLUMN avatar TEXT;
COMMIT;

M openapi/openapi.json => openapi/openapi.json +18 -8
@@ 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",

M src/apub_util.rs => src/apub_util.rs +21 -2
@@ 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 {

M src/routes/api/comments.rs => src/routes/api/comments.rs +8 -4
@@ 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() }),
                },
            }
        })

M src/routes/api/communities.rs => src/routes/api/communities.rs +5 -2
@@ 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() }),
                }
            });


M src/routes/api/mod.rs => src/routes/api/mod.rs +17 -2
@@ 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(),
                    }),
                }
            });


M src/routes/api/posts.rs => src/routes/api/posts.rs +15 -7
@@ 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() }),
                },
            }
        })

M src/routes/api/users.rs => src/routes/api/users.rs +6 -4
@@ 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 {