~vpzom/lotide

8f6a821df74f710dbb3340e51a3b00553d6ae01e — Colin Reeder 3 months ago f5c4a65
Add API for sending poll votes (untested)
3 files changed, 220 insertions(+), 4 deletions(-)

M src/apub_util/mod.rs
M src/routes/api/posts.rs
M types/src/lib.rs
M src/apub_util/mod.rs => src/apub_util/mod.rs +73 -2
@@ 1,6 1,6 @@
use crate::types::{
    ActorLocalRef, CommentLocalID, CommunityLocalID, FlagLocalID, PostLocalID, ThingLocalRef,
    UserLocalID,
    ActorLocalRef, CommentLocalID, CommunityLocalID, FlagLocalID, PollLocalID, PollOptionLocalID,
    PostLocalID, ThingLocalRef, UserLocalID,
};
use crate::BaseURL;
use activitystreams::prelude::*;


@@ 242,6 242,24 @@ pub fn get_local_comment_like_apub_id(
    res
}

pub fn get_local_poll_vote_apub_id(
    poll_id: PollLocalID,
    user: UserLocalID,
    option_id: PollOptionLocalID,
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = host_url_apub.clone();
    res.path_segments_mut().extend(&[
        "polls",
        &poll_id.to_string(),
        "voters",
        &user.to_string(),
        "votes",
        &option_id.to_string(),
    ]);
    res
}

pub fn get_local_person_apub_id(person: UserLocalID, host_url_apub: &BaseURL) -> BaseURL {
    let mut res = host_url_apub.clone();
    res.path_segments_mut()


@@ 1693,6 1711,59 @@ pub fn local_comment_like_undo_to_ap(
    Ok(undo)
}

pub fn local_poll_vote_to_ap(
    poll_id: PollLocalID,
    poll_ap_id: BaseURL,
    user: UserLocalID,
    option_id: PollOptionLocalID,
    option_name: String,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Create, crate::Error> {
    let id = get_local_poll_vote_apub_id(poll_id, user, option_id, host_url_apub);
    let note_id = {
        let mut res = id.clone();
        res.path_segments_mut().push("note");
        res
    };

    let mut note = activitystreams::object::Note::new();
    note.set_id(note_id.into())
        .set_in_reply_to(poll_ap_id)
        .set_name(option_name);

    let mut create = activitystreams::activity::Create::new(
        crate::apub_util::get_local_person_apub_id(user, host_url_apub),
        note.into_any_base()?,
    );
    create
        .set_context(activitystreams::context())
        .set_id(id.into());

    Ok(create)
}

pub fn local_poll_vote_undo_to_ap(
    poll_id: PollLocalID,
    user: UserLocalID,
    option_id: PollOptionLocalID,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Undo, crate::Error> {
    let undo_id = uuid::Uuid::new_v4(); // activity is temporary

    let mut undo = activitystreams::activity::Undo::new(
        get_local_person_apub_id(user, host_url_apub),
        get_local_poll_vote_apub_id(poll_id, user, option_id, host_url_apub),
    );
    undo.set_context(activitystreams::context()).set_id({
        let mut res = host_url_apub.clone();
        res.path_segments_mut()
            .extend(&["tmp_objects", &undo_id.to_string()]);
        res.into()
    });

    Ok(undo)
}

pub fn spawn_enqueue_send_comment(
    inboxes: HashSet<url::Url>,
    comment: crate::CommentInfo,

M src/routes/api/posts.rs => src/routes/api/posts.rs +138 -2
@@ 4,9 4,11 @@ use super::{
};
use crate::lang;
use crate::types::{
    ActorLocalRef, CommentLocalID, CommunityLocalID, FlagLocalID, JustUser, PostLocalID,
    RespPollInfo, RespPollOption, RespPostInfo, UserLocalID,
    ActorLocalRef, CommentLocalID, CommunityLocalID, FlagLocalID, JustUser, PollLocalID,
    PollOptionLocalID, PollVoteBody, PostLocalID, RespPollInfo, RespPollOption, RespPostInfo,
    UserLocalID,
};
use crate::BaseURL;
use serde_derive::Deserialize;
use std::borrow::Cow;
use std::collections::HashSet;


@@ 728,6 730,130 @@ async fn route_unstable_posts_flags_create(
    crate::json_response(&crate::types::Empty {})
}

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

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

    let user = crate::require_login(&req, &db).await?;

    let body = hyper::body::to_bytes(req.into_body()).await?;
    let body: PollVoteBody = serde_json::from_slice(&body)?;

    let row = db.query_opt("SELECT poll.multiple, poll.id, author.local, COALESCE(author.ap_inbox, author.ap_shared_inbox), post.ap_id FROM post INNER JOIN poll ON (poll.id = post.poll_id) LEFT OUTER JOIN person AS author ON (author.id = post.author) WHERE post.id = $1", &[&post_id]).await?.ok_or_else(|| crate::Error::UserError(crate::simple_response(hyper::StatusCode::BAD_REQUEST, "No such poll")))?;

    let multiple: bool = row.get(0);
    let poll_id = PollLocalID(row.get(1));

    let tmp;
    let options: Result<&[PollOptionLocalID], _> = if multiple {
        match &body {
            PollVoteBody::Multiple { options } => Ok(&options),
            PollVoteBody::Single { .. } => Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::BAD_REQUEST,
                "Cannot use `option` for multiple-choice poll",
            ))),
        }
    } else {
        match &body {
            PollVoteBody::Single { option } => {
                tmp = [*option];
                Ok(&tmp[..])
            }
            PollVoteBody::Multiple { .. } => Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::BAD_REQUEST,
                "Cannot use `options` for single-choice poll",
            ))),
        }
    };
    let options = options?;

    let (removed, added) = {
        let trans = db.transaction().await?;

        let removed: Vec<PollOptionLocalID> = {
            let rows = trans.query("DELETE FROM poll_vote WHERE poll_id=$1 AND (NOT option_id = ANY($2::BIGINT[])) AND person=$3 RETURNING option_id", &[&poll_id, &options, &user]).await?;

            rows.into_iter()
                .map(|row| PollOptionLocalID(row.get(0)))
                .collect()
        };

        let added: Vec<(PollOptionLocalID, String)> = {
            let added_rows = trans.query("INSERT INTO poll_vote (poll_id, person, option_id) SELECT $1, $3, * FROM UNNEST($2::BIGINT[]) RETURNING option_id, (SELECT name FROM poll_option WHERE id=option_id)", &[&poll_id, &options, &user]).await?;

            added_rows
                .into_iter()
                .map(|row| (PollOptionLocalID(row.get(0)), row.get(1)))
                .collect()
        };

        trans.commit().await?;

        (removed, added)
    };

    if !removed.is_empty() || !added.is_empty() {
        let author_local: bool = row.get(2);
        if !author_local {
            let inbox: Option<&str> = row.get(3);
            let post_ap_id: Option<String> = row.get(4);
            if let (Some(inbox), Some(post_ap_id)) = (inbox, post_ap_id) {
                let inbox = inbox.parse();
                let post_ap_id: Result<BaseURL, _> = post_ap_id.parse();
                crate::spawn_task(async move {
                    let inbox = inbox?;
                    let post_ap_id = post_ap_id?;

                    for option_id in removed {
                        let activity = crate::apub_util::local_poll_vote_undo_to_ap(
                            poll_id,
                            user,
                            option_id,
                            &ctx.host_url_apub,
                        )?;
                        let body = serde_json::to_string(&activity)?;

                        ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                            inbox: Cow::Borrowed(&inbox),
                            sign_as: Some(ActorLocalRef::Person(user)),
                            object: (&body).into(),
                        })
                        .await?;
                    }

                    for (option_id, name) in added {
                        let activity = crate::apub_util::local_poll_vote_to_ap(
                            poll_id,
                            post_ap_id.clone(),
                            user,
                            option_id,
                            name,
                            &ctx.host_url_apub,
                        )?;
                        let body = serde_json::to_string(&activity)?;

                        ctx.enqueue_task(&crate::tasks::DeliverToInbox {
                            inbox: Cow::Borrowed(&inbox),
                            sign_as: Some(ActorLocalRef::Person(user)),
                            object: (&body).into(),
                        })
                        .await?;
                    }

                    Ok(())
                })
            }
        }
    }

    Ok(crate::empty_response())
}

async fn route_unstable_posts_replies_list(
    params: (PostLocalID,),
    ctx: Arc<crate::RouteContext>,


@@ 1602,6 1728,16 @@ pub fn route_posts() -> crate::RouteNode<()> {
                        .with_handler_async(hyper::Method::POST, route_unstable_posts_flags_create),
                )
                .with_child(
                    "poll",
                    crate::RouteNode::new().with_child(
                        "your_vote",
                        crate::RouteNode::new().with_handler_async(
                            hyper::Method::PUT,
                            route_unstable_posts_poll_your_vote_set,
                        ),
                    ),
                )
                .with_child(
                    "replies",
                    crate::RouteNode::new()
                        .with_handler_async(hyper::Method::GET, route_unstable_posts_replies_list)

M types/src/lib.rs => types/src/lib.rs +9 -0
@@ 46,6 46,8 @@ macro_rules! id_wrapper {

id_wrapper!(CommentLocalID);
id_wrapper!(CommunityLocalID);
id_wrapper!(PollLocalID);
id_wrapper!(PollOptionLocalID);
id_wrapper!(PostLocalID);
id_wrapper!(UserLocalID);
id_wrapper!(NotificationID);


@@ 385,3 387,10 @@ pub struct NotificationSubscriptionCreateQuery<'a> {
    pub p256dh_key: Cow<'a, str>,
    pub auth_key: Cow<'a, str>,
}

#[derive(Deserialize)]
#[serde(untagged)]
pub enum PollVoteBody {
    Multiple { options: Vec<PollOptionLocalID> },
    Single { option: PollOptionLocalID },
}