summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorColin Reeder <colin@vpzom.click>2020-08-08 10:01:39 -0600
committerColin Reeder <colin@vpzom.click>2020-08-08 10:01:39 -0600
commit319e2b6ea9ba06e6082030d8ce0b84d710738fda (patch)
tree97c6a0305e464745ff5802ef337bc9121e032491
parent3178d41ec7579e96d3d01488fd06510cdfa84b06 (diff)
parentdc5f1cb6f6c4a4ea3fc9c007d1efb7afdfc74088 (diff)
Merge branch 'api-changes' into master
-rw-r--r--openapi/openapi.json189
-rw-r--r--res/lang/en.ftl1
-rw-r--r--res/lang/eo.ftl1
-rw-r--r--src/routes/api/comments.rs11
-rw-r--r--src/routes/api/mod.rs496
-rw-r--r--src/routes/api/posts.rs11
-rw-r--r--src/routes/api/users.rs542
7 files changed, 672 insertions, 579 deletions
diff --git a/openapi/openapi.json b/openapi/openapi.json
index d4b7329..0386158 100644
--- a/openapi/openapi.json
+++ b/openapi/openapi.json
@@ -140,7 +140,7 @@
"paths": {
"/api/unstable/actors:lookup/{remoteID}": {
"get": {
- "summary": "Look up a remote community by WebFinger or ActivityPub ID",
+ "summary": "Look up a remote actor by WebFinger or ActivityPub ID",
"parameters": [
{
"name": "remoteID",
@@ -162,7 +162,8 @@
"type": "object",
"required": ["id"],
"properties": {
- "id": {"type": "integer"}
+ "id": {"type": "integer"},
+ "type": {"type": "string", "enum": ["community", "user"]}
}
}
}
@@ -248,9 +249,9 @@
"security": [{"bearer": []}]
}
},
- "/api/unstable/comments/{commentID}/like": {
+ "/api/unstable/comments/{commentID}/replies": {
"post": {
- "summary": "Like a comment",
+ "summary": "Reply to a comment",
"parameters": [
{
"name": "commentID",
@@ -259,15 +260,46 @@
"schema": {"type": "integer"}
}
],
- "responses": {
- "204": {
- "description": "Successfully liked."
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "content_text": {"type": "string"},
+ "content_markdown": {"type": "string"}
+ }
+ }
+ }
}
},
- "security": [{"bearer": []}]
+ "responses": {
+ "200": {
+ "description": "Successfully created reply.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["id", "post"],
+ "properties": {
+ "id": {"type": "integer"},
+ "post": {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": {"type": "integer"}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
},
- "/api/unstable/comments/{commentID}/likes": {
+ "/api/unstable/comments/{commentID}/votes": {
"get": {
"summary": "List likers of a comment",
"parameters": [
@@ -315,9 +347,9 @@
}
}
},
- "/api/unstable/comments/{commentID}/replies": {
- "post": {
- "summary": "Reply to a comment",
+ "/api/unstable/comments/{commentID}/your_vote": {
+ "put": {
+ "summary": "Like a comment",
"parameters": [
{
"name": "commentID",
@@ -326,47 +358,14 @@
"schema": {"type": "integer"}
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "content_text": {"type": "string"},
- "content_markdown": {"type": "string"}
- }
- }
- }
- }
- },
"responses": {
- "200": {
- "description": "Successfully created reply.",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "required": ["id", "post"],
- "properties": {
- "id": {"type": "integer"},
- "post": {
- "type": "object",
- "required": ["id"],
- "properties": {
- "id": {"type": "integer"}
- }
- }
- }
- }
- }
- }
+ "204": {
+ "description": "Successfully liked."
}
- }
- }
- },
- "/api/unstable/comments/{commentID}/unlike": {
- "post": {
+ },
+ "security": [{"bearer": []}]
+ },
+ "delete": {
"summary": "Retract a like of a comment",
"parameters": [
{
@@ -903,9 +902,9 @@
"security": [{"bearer": []}]
}
},
- "/api/unstable/posts/{postID}/like": {
+ "/api/unstable/posts/{postID}/replies": {
"post": {
- "summary": "Like a post",
+ "summary": "Reply to a post",
"parameters": [
{
"name": "postID",
@@ -914,15 +913,39 @@
"schema": {"type": "integer"}
}
],
- "responses": {
- "204": {
- "description": "Successfully liked."
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "content_text": {"type": "string"},
+ "content_markdown": {"type": "string"}
+ }
+ }
+ }
}
},
- "security": [{"bearer": []}]
+ "responses": {
+ "200": {
+ "description": "Successfully created reply.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["id", "post"],
+ "properties": {
+ "id": {"type": "integer"}
+ }
+ }
+ }
+ }
+ }
+ }
}
},
- "/api/unstable/posts/{postID}/likes": {
+ "/api/unstable/posts/{postID}/votes": {
"get": {
"summary": "List likers of a post",
"parameters": [
@@ -970,9 +993,9 @@
}
}
},
- "/api/unstable/posts/{postID}/replies": {
- "post": {
- "summary": "Reply to a post",
+ "/api/unstable/posts/{postID}/your_vote": {
+ "put": {
+ "summary": "Like a post",
"parameters": [
{
"name": "postID",
@@ -981,40 +1004,14 @@
"schema": {"type": "integer"}
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "content_text": {"type": "string"},
- "content_markdown": {"type": "string"}
- }
- }
- }
- }
- },
"responses": {
- "200": {
- "description": "Successfully created reply.",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "required": ["id", "post"],
- "properties": {
- "id": {"type": "integer"}
- }
- }
- }
- }
+ "204": {
+ "description": "Successfully liked."
}
- }
- }
- },
- "/api/unstable/posts/{postID}/unlike": {
- "post": {
+ },
+ "security": [{"bearer": []}]
+ },
+ "delete": {
"summary": "Retract a like of a post",
"parameters": [
{
@@ -1214,7 +1211,7 @@
"security": [{"bearer": []}]
}
},
- "/api/unstable/users/me": {
+ "/api/unstable/users/~me": {
"patch": {
"summary": "Edit your account settings",
"requestBody": {
@@ -1238,7 +1235,7 @@
"security": [{"bearer": []}]
}
},
- "/api/unstable/users/me/following:posts": {
+ "/api/unstable/users/~me/following:posts": {
"get": {
"summary": "Fetch posts from all the communities you follow",
"responses": {
@@ -1259,7 +1256,7 @@
"security": [{"bearer": []}]
}
},
- "/api/unstable/users/me/notifications": {
+ "/api/unstable/users/~me/notifications": {
"get": {
"summary": "Fetch your notifications. Will also clear `has_unread_notifications`.",
"responses": {
diff --git a/res/lang/en.ftl b/res/lang/en.ftl
index 809aeb5..f1f462a 100644
--- a/res/lang/en.ftl
+++ b/res/lang/en.ftl
@@ -9,7 +9,6 @@ no_such_community = No such community
no_such_local_user_by_name = No local user found by that name
no_such_post = No such post
no_such_user = No such user
-not_group = Not a group
password_incorrect = Incorrect password
post_content_conflict = content_markdown and content_text are mutually exclusive
post_href_invalid = Specified URL is not valid
diff --git a/res/lang/eo.ftl b/res/lang/eo.ftl
index 855af95..51cbdd6 100644
--- a/res/lang/eo.ftl
+++ b/res/lang/eo.ftl
@@ -9,7 +9,6 @@ no_such_community = Neniu tia komunumo
no_such_local_user_by_name = Neniu uzanto trovita per tiu nomo
no_such_post = Neniu tia poŝto
no_such_user = Neniu tia uzanto
-not_group = Ne estas grupo
password_incorrect = Pasvorto malĝustas
post_content_conflict = content_markdown kaj content_text konfliktas
post_href_invalid = URL nevalidas.
diff --git a/src/routes/api/comments.rs b/src/routes/api/comments.rs
index b608b80..f4c9fc6 100644
--- a/src/routes/api/comments.rs
+++ b/src/routes/api/comments.rs
@@ -605,6 +605,17 @@ pub fn route_comments() -> crate::RouteNode<()> {
"replies",
crate::RouteNode::new()
.with_handler_async("POST", route_unstable_comments_replies_create),
+ )
+ .with_child(
+ "votes",
+ crate::RouteNode::new()
+ .with_handler_async("GET", route_unstable_comments_likes_list),
+ )
+ .with_child(
+ "your_vote",
+ crate::RouteNode::new()
+ .with_handler_async("PUT", route_unstable_comments_like)
+ .with_handler_async("DELETE", route_unstable_comments_unlike),
),
)
}
diff --git a/src/routes/api/mod.rs b/src/routes/api/mod.rs
index 831ea5e..10a47d0 100644
--- a/src/routes/api/mod.rs
+++ b/src/routes/api/mod.rs
@@ -9,6 +9,7 @@ use std::sync::Arc;
mod comments;
mod communities;
mod posts;
+mod users;
lazy_static::lazy_static! {
static ref USERNAME_ALLOWED_CHARS: HashSet<char> = {
@@ -74,21 +75,6 @@ struct RespMinimalPostInfo<'a> {
title: &'a str,
}
-#[derive(Deserialize, Serialize)]
-struct JustContentText<'a> {
- content_text: Cow<'a, str>,
-}
-
-#[derive(Serialize)]
-struct RespUserInfo<'a> {
- #[serde(flatten)]
- base: RespMinimalAuthorInfo<'a>,
-
- description: &'a str,
- #[serde(skip_serializing_if = "Option::is_none")]
- your_note: Option<Option<JustContentText<'a>>>,
-}
-
#[derive(Serialize)]
struct RespPostListPost<'a> {
id: PostLocalID,
@@ -181,44 +167,7 @@ pub fn route_api() -> crate::RouteNode<()> {
)
.with_child("posts", posts::route_posts())
.with_child("comments", comments::route_comments())
- .with_child(
- "users",
- crate::RouteNode::new()
- .with_handler_async("POST", route_unstable_users_create)
- .with_child(
- "me",
- crate::RouteNode::new()
- .with_handler_async("PATCH", route_unstable_users_me_patch)
- .with_child(
- "following:posts",
- crate::RouteNode::new().with_handler_async(
- "GET",
- route_unstable_users_me_following_posts_list,
- ),
- )
- .with_child(
- "notifications",
- crate::RouteNode::new().with_handler_async(
- "GET",
- route_unstable_users_me_notifications_list,
- ),
- ),
- )
- .with_child_parse::<UserLocalID, _>(
- crate::RouteNode::new()
- .with_handler_async("GET", route_unstable_users_get)
- .with_child(
- "things",
- crate::RouteNode::new()
- .with_handler_async("GET", route_unstable_users_things_list),
- )
- .with_child(
- "your_note",
- crate::RouteNode::new()
- .with_handler_async("PUT", route_unstable_users_your_note_put),
- ),
- ),
- ),
+ .with_child("users", users::route_users()),
)
}
@@ -264,12 +213,11 @@ fn parse_lookup(src: &str) -> Result<Lookup, crate::Error> {
async fn route_unstable_actors_lookup(
params: (String,),
ctx: Arc<crate::RouteContext>,
- req: hyper::Request<hyper::Body>,
+ _req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
let (query,) = params;
println!("lookup {}", query);
- let lang = crate::get_lang_for_req(&req);
let db = ctx.db_pool.get().await?;
let lookup = parse_lookup(&query)?;
@@ -328,16 +276,18 @@ async fn route_unstable_actors_lookup(
let actor = crate::apub_util::fetch_actor(&uri, &db, &ctx.http_client).await?;
- if let crate::apub_util::ActorLocalInfo::Community { id, .. } = actor {
- Ok(hyper::Response::builder()
- .header(hyper::header::CONTENT_TYPE, "application/json")
- .body(serde_json::to_vec(&serde_json::json!([{ "id": id }]))?.into())?)
- } else {
- Ok(crate::simple_response(
- hyper::StatusCode::BAD_REQUEST,
- lang.tr("not_group", None).into_owned(),
- ))
- }
+ let info = match actor {
+ crate::apub_util::ActorLocalInfo::Community { id, .. } => {
+ serde_json::json!({"id": id, "type": "community"})
+ }
+ crate::apub_util::ActorLocalInfo::User { id, .. } => {
+ serde_json::json!({"id": id, "type": "user"})
+ }
+ };
+
+ Ok(hyper::Response::builder()
+ .header(hyper::header::CONTENT_TYPE, "application/json")
+ .body(serde_json::to_vec(&[info])?.into())?)
}
async fn route_unstable_logins_create(
@@ -685,422 +635,6 @@ async fn route_unstable_misc_render_markdown(
.body(output.into())?)
}
-async fn route_unstable_users_create(
- _: (),
- ctx: Arc<crate::RouteContext>,
- req: hyper::Request<hyper::Body>,
-) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let lang = crate::get_lang_for_req(&req);
- let mut db = ctx.db_pool.get().await?;
-
- let body = hyper::body::to_bytes(req.into_body()).await?;
-
- #[derive(Deserialize)]
- struct UsersCreateBody<'a> {
- username: Cow<'a, str>,
- password: String,
- #[serde(default)]
- login: bool,
- }
-
- let body: UsersCreateBody<'_> = serde_json::from_slice(&body)?;
-
- for ch in body.username.chars() {
- if !USERNAME_ALLOWED_CHARS.contains(&ch) {
- return Err(crate::Error::UserError(crate::simple_response(
- hyper::StatusCode::BAD_REQUEST,
- lang.tr("user_name_disallowed_chars", None).into_owned(),
- )));
- }
- }
-
- let req_password = body.password;
- let passhash =
- tokio::task::spawn_blocking(move || bcrypt::hash(req_password, bcrypt::DEFAULT_COST))
- .await??;
-
- let user_id = {
- let trans = db.transaction().await?;
- trans
- .execute(
- "INSERT INTO local_actor_name (name) VALUES ($1)",
- &[&body.username],
- )
- .await
- .map_err(|err| {
- if err.code() == Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION) {
- crate::Error::UserError(crate::simple_response(
- hyper::StatusCode::BAD_REQUEST,
- lang.tr("name_in_use", None).into_owned(),
- ))
- } else {
- err.into()
- }
- })?;
- let row = trans.query_one(
- "INSERT INTO person (username, local, created_local, passhash) VALUES ($1, TRUE, current_timestamp, $2) RETURNING id",
- &[&body.username, &passhash],
- ).await?;
-
- trans.commit().await?;
-
- UserLocalID(row.get(0))
- };
-
- let output = if body.login {
- let token = insert_token(user_id, &db).await?;
- serde_json::json!({"user": {"id": user_id}, "token": token.to_string()})
- } else {
- serde_json::json!({"user": {"id": user_id}})
- };
-
- Ok(hyper::Response::builder()
- .header(hyper::header::CONTENT_TYPE, "application/json")
- .body(serde_json::to_vec(&output)?.into())?)
-}
-
-async fn route_unstable_users_me_patch(
- _: (),
- ctx: Arc<crate::RouteContext>,
- req: hyper::Request<hyper::Body>,
-) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let db = ctx.db_pool.get().await?;
-
- let user = crate::require_login(&req, &db).await?;
-
- #[derive(Deserialize)]
- struct UsersEditBody<'a> {
- description: Option<Cow<'a, str>>,
- }
-
- let body = hyper::body::to_bytes(req.into_body()).await?;
- let body: UsersEditBody = serde_json::from_slice(&body)?;
-
- if let Some(description) = body.description {
- db.execute(
- "UPDATE person SET description=$1 WHERE id=$2",
- &[&description, &user],
- )
- .await?;
-
- // TODO maybe send this somewhere?
- }
-
- Ok(crate::empty_response())
-}
-
-async fn route_unstable_users_me_following_posts_list(
- _: (),
- ctx: Arc<crate::RouteContext>,
- req: hyper::Request<hyper::Body>,
-) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let db = ctx.db_pool.get().await?;
-
- let user = crate::require_login(&req, &db).await?;
-
- let limit: i64 = 30; // TODO make configurable
-
- 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",
- values.iter().map(|s| *s as _)
- ).await?;
-
- let posts = handle_common_posts_list(stream, &ctx.local_hostname).await?;
-
- let body = serde_json::to_vec(&posts)?;
-
- Ok(hyper::Response::builder()
- .header(hyper::header::CONTENT_TYPE, "application/json")
- .body(body.into())?)
-}
-
-async fn route_unstable_users_me_notifications_list(
- _: (),
- ctx: Arc<crate::RouteContext>,
- req: hyper::Request<hyper::Body>,
-) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let mut db = ctx.db_pool.get().await?;
-
- let user = crate::require_login(&req, &db).await?;
-
- let limit: i64 = 30;
-
- let rows = {
- let trans = db.transaction().await?;
-
- let rows = trans.query(
- "SELECT notification.kind, (notification.created_at > (SELECT last_checked_notifications FROM person WHERE id=$1)), reply.id, reply.content_text, reply.content_html, parent_reply.id, parent_reply_post.id, parent_reply_post.title, parent_post.id, parent_post.title FROM notification LEFT OUTER JOIN reply ON (reply.id = notification.reply) LEFT OUTER JOIN reply AS parent_reply ON (parent_reply.id = notification.parent_reply) LEFT OUTER JOIN post AS parent_reply_post ON (parent_reply_post.id = parent_reply.post) LEFT OUTER JOIN post AS parent_post ON (parent_post.id = notification.parent_post) WHERE notification.to_user = $1 AND NOT COALESCE(reply.deleted OR parent_reply.deleted OR parent_reply_post.deleted OR parent_post.deleted, FALSE) ORDER BY created_at DESC LIMIT $2",
- &[&user, &limit],
- ).await?;
- trans
- .execute(
- "UPDATE person SET last_checked_notifications=current_timestamp WHERE id=$1",
- &[&user],
- )
- .await?;
-
- trans.commit().await?;
-
- rows
- };
-
- #[derive(Serialize)]
- #[serde(tag = "type")]
- #[serde(rename_all = "snake_case")]
- enum RespNotificationInfo<'a> {
- PostReply {
- reply: RespMinimalCommentInfo<'a>,
- post: RespMinimalPostInfo<'a>,
- },
- CommentReply {
- reply: RespMinimalCommentInfo<'a>,
- comment: CommentLocalID,
- post: Option<RespMinimalPostInfo<'a>>,
- },
- }
-
- #[derive(Serialize)]
- struct RespNotification<'a> {
- #[serde(flatten)]
- info: RespNotificationInfo<'a>,
-
- unseen: bool,
- }
-
- let notifications: Vec<_> = rows
- .iter()
- .filter_map(|row| {
- let kind: &str = row.get(0);
- let unseen: bool = row.get(1);
- let info = match kind {
- "post_reply" => {
- if let Some(reply_id) = row.get(2) {
- if let Some(post_id) = row.get(8) {
- let comment = RespMinimalCommentInfo {
- id: CommentLocalID(reply_id),
- content_text: row.get::<_, Option<_>>(3).map(Cow::Borrowed),
- content_html: row.get::<_, Option<_>>(4).map(Cow::Borrowed),
- };
- let post = RespMinimalPostInfo {
- id: PostLocalID(post_id),
- title: row.get(9),
- };
-
- Some(RespNotificationInfo::PostReply {
- reply: comment,
- post,
- })
- } else {
- None
- }
- } else {
- None
- }
- }
- "reply_reply" => {
- if let Some(reply_id) = row.get(2) {
- if let Some(parent_id) = row.get(5) {
- let reply = RespMinimalCommentInfo {
- id: CommentLocalID(reply_id),
- content_text: row.get::<_, Option<_>>(3).map(Cow::Borrowed),
- content_html: row.get::<_, Option<_>>(4).map(Cow::Borrowed),
- };
- let parent_id = CommentLocalID(parent_id);
- let post =
- row.get::<_, Option<_>>(6)
- .map(|post_id| RespMinimalPostInfo {
- id: PostLocalID(post_id),
- title: row.get(7),
- });
-
- Some(RespNotificationInfo::CommentReply {
- reply,
- comment: parent_id,
- post,
- })
- } else {
- None
- }
- } else {
- None
- }
- }
- _ => None,
- };
-
- info.map(|info| RespNotification { info, unseen })
- })
- .collect();
-
- let body = serde_json::to_vec(&notifications)?;
-
- Ok(hyper::Response::builder()
- .header(hyper::header::CONTENT_TYPE, "application/json")
- .body(body.into())?)
-}
-
-async fn route_unstable_users_get(
- params: (UserLocalID,),
- ctx: Arc<crate::RouteContext>,
- req: hyper::Request<hyper::Body>,
-) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let (user_id,) = params;
-
- let query: MaybeIncludeYour = serde_urlencoded::from_str(req.uri().query().unwrap_or(""))?;
-
- let lang = crate::get_lang_for_req(&req);
- let db = ctx.db_pool.get().await?;
-
- let your_note_row;
-
- let your_note = if query.include_your {
- Some({
- let user = crate::require_login(&req, &db).await?;
-
- your_note_row = db
- .query_opt(
- "SELECT content_text FROM person_note WHERE author=$1 AND target=$2",
- &[&user, &user_id],
- )
- .await?;
-
- your_note_row.as_ref().map(|row| JustContentText {
- content_text: Cow::Borrowed(row.get(0)),
- })
- })
- } else {
- None
- };
-
- let row = db
- .query_opt(
- "SELECT username, local, ap_id, description FROM person WHERE id=$1",
- &[&user_id],
- )
- .await?;
-
- let row = row.ok_or_else(|| {
- crate::Error::UserError(crate::simple_response(
- hyper::StatusCode::NOT_FOUND,
- lang.tr("no_such_user", None).into_owned(),
- ))
- })?;
-
- let local = row.get(1);
- let ap_id = row.get(2);
-
- let info = RespMinimalAuthorInfo {
- id: user_id,
- local,
- 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),
- };
-
- let info = RespUserInfo {
- base: info,
- description: row.get(3),
- your_note,
- };
-
- let body = serde_json::to_vec(&info)?;
-
- Ok(hyper::Response::builder()
- .header(hyper::header::CONTENT_TYPE, "application/json")
- .body(body.into())?)
-}
-
-async fn route_unstable_users_your_note_put(
- params: (UserLocalID,),
- ctx: Arc<crate::RouteContext>,
- req: hyper::Request<hyper::Body>,
-) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let (user_id,) = params;
-
- let 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: JustContentText = serde_json::from_slice(&body)?;
-
- db.execute(
- "INSERT INTO person_note (author, target, content_text) VALUES ($1, $2, $3) ON CONFLICT (author, target) DO UPDATE SET content_text=$3",
- &[&user, &user_id, &body.content_text],
- ).await?;
-
- Ok(crate::empty_response())
-}
-
-async fn route_unstable_users_things_list(
- params: (UserLocalID,),
- ctx: Arc<crate::RouteContext>,
- _req: hyper::Request<hyper::Body>,
-) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let (user_id,) = params;
-
- let db = ctx.db_pool.get().await?;
-
- let limit: i64 = 30;
-
- let rows = db.query(
- "(SELECT TRUE, post.id, post.href, post.title, post.created, community.id, community.name, community.local, community.ap_id FROM post, community WHERE post.community = community.id AND post.author = $1 AND NOT post.deleted) UNION ALL (SELECT FALSE, reply.id, reply.content_text, reply.content_html, reply.created, post.id, post.title, NULL, NULL FROM reply, post WHERE post.id = reply.post AND reply.author = $1 AND NOT reply.deleted) ORDER BY created DESC LIMIT $2",
- &[&user_id, &limit],
- )
- .await?;
-
- let things: Vec<RespThingInfo> = rows
- .iter()
- .map(|row| {
- let created: chrono::DateTime<chrono::FixedOffset> = row.get(4);
- let created = created.to_rfc3339();
-
- if row.get(0) {
- let community_local = row.get(7);
- let community_ap_id = row.get(8);
-
- RespThingInfo::Post {
- id: PostLocalID(row.get(1)),
- href: row.get(2),
- title: row.get(3),
- created,
- community: RespMinimalCommunityInfo {
- id: CommunityLocalID(row.get(5)),
- name: row.get(6),
- local: community_local,
- host: crate::get_actor_host_or_unknown(
- community_local,
- community_ap_id,
- &ctx.local_hostname,
- ),
- remote_url: community_ap_id,
- },
- }
- } else {
- RespThingInfo::Comment {
- base: RespMinimalCommentInfo {
- id: CommentLocalID(row.get(1)),
- content_text: row.get::<_, Option<_>>(2).map(Cow::Borrowed),
- content_html: row.get::<_, Option<_>>(3).map(Cow::Borrowed),
- },
- created,
- post: RespMinimalPostInfo {
- id: PostLocalID(row.get(5)),
- title: row.get(6),
- },
- }
- }
- })
- .collect();
-
- let body = serde_json::to_vec(&things)?;
-
- Ok(hyper::Response::builder()
- .header(hyper::header::CONTENT_TYPE, "application/json")
- .body(body.into())?)
-}
-
async fn handle_common_posts_list(
stream: impl futures::stream::TryStream<Ok = tokio_postgres::Row, Error = tokio_postgres::Error>
+ Send,
diff --git a/src/routes/api/posts.rs b/src/routes/api/posts.rs
index c26609a..4562444 100644
--- a/src/routes/api/posts.rs
+++ b/src/routes/api/posts.rs
@@ -819,6 +819,17 @@ pub fn route_posts() -> crate::RouteNode<()> {
"replies",
crate::RouteNode::new()
.with_handler_async("POST", route_unstable_posts_replies_create),
+ )
+ .with_child(
+ "votes",
+ crate::RouteNode::new()
+ .with_handler_async("GET", route_unstable_posts_likes_list),
+ )
+ .with_child(
+ "your_vote",
+ crate::RouteNode::new()
+ .with_handler_async("PUT", route_unstable_posts_like)
+ .with_handler_async("DELETE", route_unstable_posts_unlike),
),
)
}
diff --git a/src/routes/api/users.rs b/src/routes/api/users.rs
new file mode 100644
index 0000000..c861aa6
--- /dev/null
+++ b/src/routes/api/users.rs
@@ -0,0 +1,542 @@
+use super::{
+ handle_common_posts_list, MaybeIncludeYour, RespMinimalAuthorInfo, RespMinimalCommentInfo,
+ RespMinimalCommunityInfo, RespMinimalPostInfo, RespThingInfo,
+};
+use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
+use serde_derive::{Deserialize, Serialize};
+use std::borrow::Cow;
+use std::sync::Arc;
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+enum UserIDOrMe {
+ User(UserLocalID),
+ Me,
+}
+
+impl UserIDOrMe {
+ pub fn resolve(self, me: UserLocalID) -> UserLocalID {
+ match self {
+ UserIDOrMe::User(id) => id,
+ UserIDOrMe::Me => me,
+ }
+ }
+
+ pub async fn try_resolve(
+ self,
+ req: &hyper::Request<hyper::Body>,
+ db: &tokio_postgres::Client,
+ ) -> Result<UserLocalID, crate::Error> {
+ match self {
+ UserIDOrMe::User(id) => Ok(id),
+ UserIDOrMe::Me => crate::require_login(req, db).await,
+ }
+ }
+
+ pub async fn require_me(
+ self,
+ req: &hyper::Request<hyper::Body>,
+ db: &tokio_postgres::Client,
+ ) -> Result<UserLocalID, crate::Error> {
+ let login_user = crate::require_login(req, db).await?;
+ match self {
+ UserIDOrMe::Me => Ok(login_user),
+ UserIDOrMe::User(id) => {
+ if id == login_user {
+ Ok(login_user)
+ } else {
+ Err(crate::Error::UserError(crate::simple_response(
+ hyper::StatusCode::FORBIDDEN,
+ "This endpoint is only available for the current user",
+ )))
+ }
+ }
+ }
+ }
+}
+
+impl std::str::FromStr for UserIDOrMe {
+ type Err = std::num::ParseIntError;
+
+ fn from_str(src: &str) -> Result<Self, Self::Err> {
+ if src == "~me" || src == "me"
+ /* temporary backward compat */
+ {
+ Ok(UserIDOrMe::Me)
+ } else {
+ src.parse().map(UserIDOrMe::User)
+ }
+ }
+}
+
+#[derive(Deserialize, Serialize)]
+struct JustContentText<'a> {
+ content_text: Cow<'a, str>,
+}
+
+#[derive(Serialize)]
+struct RespUserInfo<'a> {
+ #[serde(flatten)]
+ base: RespMinimalAuthorInfo<'a>,
+
+ description: &'a str,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ your_note: Option<Option<Just