~vpzom/lotide

3fb5dbdcf22d3ea269e93b0b17eca8cfea18f379 — Colin Reeder 7 months ago f443d25
Add API for listing and adding community moderators
3 files changed, 185 insertions(+), 1 deletions(-)

M openapi/openapi.json
M res/lang/en.ftl
M src/routes/api/communities.rs
M openapi/openapi.json => openapi/openapi.json +54 -1
@@ 2,7 2,7 @@
	"openapi": "3.0.1",
	"info": {
		"title": "lotide API",
		"version": "0.5.0-pre"
		"version": "0.6.0-pre"
	},
	"components": {
		"schemas": {


@@ 586,6 586,59 @@
				"security": [{"bearer": []}]
			}
		},
		"/api/unstable/communities/{communityID}/moderators": {
			"get": {
				"summary": "List moderators of a community",
				"parameters": [
					{
						"name": "communityID",
						"in": "path",
						"required": true,
						"schema": {"type": "integer"}
					}
				],
				"responses": {
					"200": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {
									"type": "array",
									"items": {
										"$ref": "#/components/schemas/MinimalUserInfo"
									}
								}
							}
						}
					}
				}
			}
		},
		"/api/unstable/communities/{communityID}/moderators/{userID}": {
			"put": {
				"summary": "Add a moderator to a community",
				"parameters": [
					{
						"name": "communityID",
						"in": "path",
						"required": true,
						"schema": {"type": "integer"}
					},
					{
						"name": "userID",
						"in": "path",
						"required": true,
						"schema": {"type": "integer"}
					}
				],
				"responses": {
					"204": {
						"description": "Successfully added."
					}
				},
				"security": [{"bearer": []}]
			}
		},
		"/api/unstable/communities/{communityID}/posts": {
			"get": {
				"summary": "List posts published to a community",

M res/lang/en.ftl => res/lang/en.ftl +2 -0
@@ 2,6 2,8 @@ comment_content_conflict = Exactly one of content_markdown and content_text must
comment_not_yours = That's not your comment
community_edit_denied = You are not authorized to modify this community
community_name_disallowed_chars = Community name contains disallowed characters
moderators_only_local = Only local users can be community moderators
must_be_moderator = You must be a community moderator to perform this action
name_in_use = That name is already in use
no_password = No password set for this user
no_such_comment = No such comment

M src/routes/api/communities.rs => src/routes/api/communities.rs +129 -0
@@ 340,6 340,124 @@ async fn route_unstable_communities_follow(
        .body(serde_json::to_vec(&output)?.into())?)
}

async fn route_unstable_communities_moderators_list(
    params: (CommunityLocalID,),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (community_id,) = params;

    let lang = crate::get_lang_for_req(&req);

    let db = ctx.db_pool.get().await?;

    ({
        let row = db
            .query_opt("SELECT 1 FROM community WHERE id=$1", &[&community_id])
            .await?;

        match row {
            None => Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::NOT_FOUND,
                lang.tr("no_such_community", None).into_owned(),
            ))),
            Some(_) => Ok(()),
        }
    })?;

    let rows = db.query(
        "SELECT id, username, local, ap_id, avatar FROM person WHERE id IN (SELECT person FROM community_moderator WHERE community=$1)",
        &[&community_id],
    ).await?;

    let output: Vec<_> = rows
        .iter()
        .map(|row| {
            let local = row.get(2);
            let ap_id = row.get(3);

            RespMinimalAuthorInfo {
                id: UserLocalID(row.get(0)),
                username: Cow::Borrowed(row.get(1)),
                local,
                host: crate::get_actor_host_or_unknown(local, ap_id, &ctx.local_hostname),
                remote_url: ap_id.map(|x| x.into()),
                avatar: row
                    .get::<_, Option<&str>>(4)
                    .map(|url| RespAvatarInfo { url: url.into() }),
            }
        })
        .collect();

    let output = serde_json::to_vec(&output)?;

    Ok(hyper::Response::builder()
        .header(hyper::header::CONTENT_TYPE, "application/json")
        .body(output.into())?)
}

async fn route_unstable_communities_moderators_add(
    params: (CommunityLocalID, UserLocalID),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (community_id, user_id) = params;

    let db = ctx.db_pool.get().await?;

    let lang = crate::get_lang_for_req(&req);
    let login_user = crate::require_login(&req, &db).await?;

    ({
        let row = db
            .query_opt(
                "SELECT 1 FROM community_moderator WHERE community=$1 AND person=$2",
                &[&community_id, &login_user],
            )
            .await?;
        match row {
            None => Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::FORBIDDEN,
                lang.tr("must_be_moderator", None).into_owned(),
            ))),
            Some(_) => Ok(()),
        }
    })?;

    ({
        let row = db
            .query_opt("SELECT local FROM person WHERE id=$1", &[&user_id])
            .await?;

        match row {
            None => Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::FORBIDDEN,
                lang.tr("no_such_user", None).into_owned(),
            ))),
            Some(row) => {
                let local: bool = row.get(0);

                if local {
                    Ok(())
                } else {
                    Err(crate::Error::UserError(crate::simple_response(
                        hyper::StatusCode::FORBIDDEN,
                        lang.tr("moderators_only_local", None).into_owned(),
                    )))
                }
            }
        }
    })?;

    db.execute(
        "INSERT INTO community_moderator (community, person) VALUES ($1, $2)",
        &[&community_id, &user_id],
    )
    .await?;

    Ok(crate::empty_response())
}

async fn route_unstable_communities_unfollow(
    params: (CommunityLocalID,),
    ctx: Arc<crate::RouteContext>,


@@ 613,6 731,17 @@ pub fn route_communities() -> crate::RouteNode<()> {
                        .with_handler_async("POST", route_unstable_communities_follow),
                )
                .with_child(
                    "moderators",
                    crate::RouteNode::new()
                        .with_handler_async("GET", route_unstable_communities_moderators_list)
                        .with_child_parse::<UserLocalID, _>(
                            crate::RouteNode::new().with_handler_async(
                                "PUT",
                                route_unstable_communities_moderators_add,
                            ),
                        ),
                )
                .with_child(
                    "unfollow",
                    crate::RouteNode::new()
                        .with_handler_async("POST", route_unstable_communities_unfollow),