Enable pagination for notification list

This commit is contained in:
silverpill 2022-01-14 20:43:04 +00:00
parent aea6db5acb
commit f1f3829b8d
4 changed files with 118 additions and 6 deletions

View file

@ -238,6 +238,33 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/Account' $ref: '#/components/schemas/Account'
/api/v1/notifications:
get:
summary: Notifications concerning the user.
parameters:
- name: max_id
in: query
description: Return results older than this ID.
required: false
schema:
type: integer
- 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: Notification list
type: array
items:
$ref: '#/components/schemas/Notification'
/api/v1/statuses: /api/v1/statuses:
post: post:
summary: Create new post. summary: Create new post.
@ -446,6 +473,31 @@ components:
description: Ethereum wallet address. description: Ethereum wallet address.
type: string type: string
example: '0xd8da6bf...' example: '0xd8da6bf...'
Notification:
type: object
properties:
id:
description: The id of the notification in the database.
type: string
type:
description: The type of event that resulted in the notification.
type: string
enum:
- follow
- follow_request
- reply
- favourite
- mention
- reblog
example: reply
created_at:
description: The timestamp of the notification.
type: string
format: dateTime
account:
$ref: '#/components/schemas/Account'
status:
$ref: '#/components/schemas/Account'
Relationship: Relationship:
type: object type: object
properties: properties:

View file

@ -1,10 +1,21 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::Serialize; use serde::{Deserialize, Serialize};
use crate::mastodon_api::accounts::types::Account; use crate::mastodon_api::accounts::types::Account;
use crate::mastodon_api::statuses::types::Status; use crate::mastodon_api::statuses::types::Status;
use crate::models::notifications::types::{EventType, Notification}; use crate::models::notifications::types::{EventType, Notification};
fn default_page_size() -> u8 { 20 }
/// https://docs.joinmastodon.org/methods/notifications/
#[derive(Deserialize)]
pub struct NotificationQueryParams {
pub max_id: Option<i32>,
#[serde(default = "default_page_size")]
pub limit: u8,
}
/// https://docs.joinmastodon.org/entities/notification/ /// https://docs.joinmastodon.org/entities/notification/
#[derive(Serialize)] #[derive(Serialize)]
pub struct ApiNotification { pub struct ApiNotification {

View file

@ -7,27 +7,71 @@ use crate::database::{Pool, get_database_client};
use crate::errors::HttpError; use crate::errors::HttpError;
use crate::mastodon_api::oauth::auth::get_current_user; use crate::mastodon_api::oauth::auth::get_current_user;
use crate::models::notifications::queries::get_notifications; use crate::models::notifications::queries::get_notifications;
use super::types::ApiNotification; use super::types::{ApiNotification, NotificationQueryParams};
fn get_pagination_header(
instance_url: &str,
last_id: &str,
) -> String {
let next_page_url = format!(
"{}/api/v1/notifications?max_id={}",
instance_url,
last_id
);
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
format!(r#"<{}>; rel="next""#, next_page_url)
}
#[get("")] #[get("")]
async fn get_notifications_view( async fn get_notifications_view(
auth: BearerAuth, auth: BearerAuth,
config: web::Data<Config>, config: web::Data<Config>,
db_pool: web::Data<Pool>, db_pool: web::Data<Pool>,
query_params: web::Query<NotificationQueryParams>,
) -> Result<HttpResponse, HttpError> { ) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?; let db_client = &**get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).await?; let current_user = get_current_user(db_client, auth.token()).await?;
let notifications: Vec<ApiNotification> = get_notifications( let notifications: Vec<ApiNotification> = get_notifications(
db_client, db_client,
&current_user.id, &current_user.id,
query_params.max_id,
query_params.limit.into(),
).await? ).await?
.into_iter() .into_iter()
.map(|item| ApiNotification::from_db(item, &config.instance_url())) .map(|item| ApiNotification::from_db(item, &config.instance_url()))
.collect(); .collect();
Ok(HttpResponse::Ok().json(notifications)) let max_index = usize::from(query_params.limit - 1);
let response = if let Some(item) = notifications.get(max_index) {
let pagination_header = get_pagination_header(&config.instance_url(), &item.id);
HttpResponse::Ok()
.header("Link", pagination_header)
// Link header needs to be exposed
// https://github.com/actix/actix-extras/issues/192
.header("Access-Control-Expose-Headers", "Link")
.json(notifications)
} else {
HttpResponse::Ok().json(notifications)
};
Ok(response)
} }
pub fn notification_api_scope() -> ActixScope { pub fn notification_api_scope() -> ActixScope {
web::scope("/api/v1/notifications") web::scope("/api/v1/notifications")
.service(get_notifications_view) .service(get_notifications_view)
} }
#[cfg(test)]
mod tests {
use super::*;
const INSTANCE_URL: &str = "https://example.org";
#[test]
fn test_get_next_page_link() {
let result = get_pagination_header(INSTANCE_URL, "123");
assert_eq!(
result,
r#"<https://example.org/api/v1/notifications?max_id=123>; rel="next""#,
);
}
}

View file

@ -96,6 +96,8 @@ pub async fn create_repost_notification(
pub async fn get_notifications( pub async fn get_notifications(
db_client: &impl GenericClient, db_client: &impl GenericClient,
recipient_id: &Uuid, recipient_id: &Uuid,
max_id: Option<i32>,
limit: i64,
) -> Result<Vec<Notification>, DatabaseError> { ) -> Result<Vec<Notification>, DatabaseError> {
let statement = format!( let statement = format!(
" "
@ -111,8 +113,11 @@ pub async fn get_notifications(
ON notification.post_id = post.id ON notification.post_id = post.id
LEFT JOIN actor_profile AS post_author LEFT JOIN actor_profile AS post_author
ON post.author_id = post_author.id ON post.author_id = post_author.id
WHERE recipient_id = $1 WHERE
ORDER BY notification.created_at DESC recipient_id = $1
AND ($2::integer IS NULL OR notification.id < $2)
ORDER BY notification.id DESC
LIMIT $3
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS, related_mentions=RELATED_MENTIONS,
@ -120,7 +125,7 @@ pub async fn get_notifications(
); );
let rows = db_client.query( let rows = db_client.query(
statement.as_str(), statement.as_str(),
&[&recipient_id], &[&recipient_id, &max_id, &limit],
).await?; ).await?;
let mut notifications: Vec<Notification> = rows.iter() let mut notifications: Vec<Notification> = rows.iter()
.map(Notification::try_from) .map(Notification::try_from)