Add API method for retrieving tag timeline

This commit is contained in:
silverpill 2021-12-12 13:38:10 +00:00
parent a4dd06d6e9
commit da918d2296
4 changed files with 130 additions and 4 deletions

View file

@ -132,6 +132,7 @@ POST /api/v1/statuses/{status_id}/unfavourite
POST /api/v1/statuses/{status_id}/reblog
POST /api/v1/statuses/{status_id}/unreblog
GET /api/v1/timelines/home
GET /api/v1/timelines/tag/{hashtag}
```
Additional methods:

View file

@ -18,7 +18,7 @@ paths:
description: Post does not belong to user
404:
description: Post not found
/api/v1/{status_id}/make_permanent:
/api/v1/statuses/{status_id}/make_permanent:
post:
summary: Save post to IPFS
parameters:
@ -38,7 +38,7 @@ paths:
description: IPFS integration is not enabled
422:
description: Post already saved to IPFS
/api/v1/{status_id}/signature:
/api/v1/statuses/{status_id}/signature:
get:
summary: Sign post data with instance key
parameters:
@ -69,7 +69,7 @@ paths:
description: Ethereum integration is not enabled
422:
description: Post is not saved to IPFS
/api/v1/{status_id}/token_minted:
/api/v1/statuses/{status_id}/token_minted:
post:
summary: Register transaction that mints a token
parameters:
@ -95,6 +95,40 @@ paths:
description: Post not found
422:
description: Transaction already registered
/api/v1/timelines/tag/{hashtag}:
get:
summary: View public posts containing the given hashtag
parameters:
- name: hashtag
in: path
description: Hashtag name
required: true
schema:
type: string
- name: max_id
in: query
description: Return results older than this ID.
required: false
schema:
type: string
format: uuid
- name: limit
in: query
description: Maximum number of results to return.
required: false
schema:
type: integer
default: 20
responses:
200:
description: Successful operation
content:
application/json:
schema:
description: Post list
type: array
items:
$ref: '#/components/schemas/Status'
components:
parameters:
@ -113,6 +147,14 @@ components:
id:
type: string
format: uuid
content:
description: HTML-encoded status content.
type: string
tags:
description: Hashtags used within the status content.
type: array
items:
$ref: '#/components/schemas/Tag'
ipfs_cid:
type: string
nullable: true
@ -121,3 +163,11 @@ components:
type: string
nullable: true
example: '0x5fe80cdea7f...'
Tag:
type: object
properties:
name:
description: 'The value of the hashtag after the # sign.'
type: string
url:
description: A link to the hashtag on the instance.

View file

@ -11,7 +11,7 @@ use crate::models::posts::helpers::{
get_actions_for_posts,
get_reposted_posts,
};
use crate::models::posts::queries::get_home_timeline;
use crate::models::posts::queries::{get_home_timeline, get_posts_by_tag};
use super::types::TimelineQueryParams;
#[get("/home")]
@ -42,7 +42,42 @@ async fn home_timeline(
Ok(HttpResponse::Ok().json(statuses))
}
#[get("/tag/{hashtag}")]
async fn hashtag_timeline(
auth: Option<BearerAuth>,
config: web::Data<Config>,
db_pool: web::Data<Pool>,
web::Path(hashtag): web::Path<String>,
query_params: web::Query<TimelineQueryParams>,
) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?;
let maybe_current_user = match auth {
Some(auth) => Some(get_current_user(db_client, auth.token()).await?),
None => None,
};
let mut posts = get_posts_by_tag(
db_client,
&hashtag,
query_params.max_id,
query_params.limit,
).await?;
get_reposted_posts(db_client, posts.iter_mut().collect()).await?;
if let Some(user) = maybe_current_user {
get_actions_for_posts(
db_client,
&user.id,
posts.iter_mut().collect(),
).await?;
};
let statuses: Vec<Status> = posts
.into_iter()
.map(|post| Status::from_post(post, &config.instance_url()))
.collect();
Ok(HttpResponse::Ok().json(statuses))
}
pub fn timeline_api_scope() -> Scope {
web::scope("/api/v1/timelines")
.service(home_timeline)
.service(hashtag_timeline)
}

View file

@ -329,6 +329,46 @@ pub async fn get_posts_by_author(
Ok(posts)
}
pub async fn get_posts_by_tag(
db_client: &impl GenericClient,
tag_name: &str,
max_post_id: Option<Uuid>,
limit: i64,
) -> Result<Vec<Post>, DatabaseError> {
let statement = format!(
"
SELECT
post, actor_profile,
{related_attachments},
{related_mentions},
{related_tags}
FROM post
JOIN actor_profile ON post.author_id = actor_profile.id
WHERE
post.visibility = {visibility_public}
AND EXISTS (
SELECT 1 FROM post_tag JOIN tag ON post_tag.tag_id = tag.id
WHERE post_tag.post_id = post.id AND tag.tag_name = $1
)
AND ($2::uuid IS NULL OR post.id < $2)
ORDER BY post.id DESC
LIMIT $3
",
related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
related_tags=RELATED_TAGS,
visibility_public=i16::from(&Visibility::Public),
);
let rows = db_client.query(
statement.as_str(),
&[&tag_name.to_lowercase(), &max_post_id, &limit],
).await?;
let posts: Vec<Post> = rows.iter()
.map(Post::try_from)
.collect::<Result<_, _>>()?;
Ok(posts)
}
pub async fn get_post_by_id(
db_client: &impl GenericClient,
post_id: &Uuid,