diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 1175322..3e7a170 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -238,6 +238,33 @@ paths: type: array items: $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: post: summary: Create new post. @@ -446,6 +473,31 @@ components: description: Ethereum wallet address. type: string 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: type: object properties: diff --git a/src/mastodon_api/notifications/types.rs b/src/mastodon_api/notifications/types.rs index c404ac6..cd9fe28 100644 --- a/src/mastodon_api/notifications/types.rs +++ b/src/mastodon_api/notifications/types.rs @@ -1,10 +1,21 @@ use chrono::{DateTime, Utc}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use crate::mastodon_api::accounts::types::Account; use crate::mastodon_api::statuses::types::Status; 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, + + #[serde(default = "default_page_size")] + pub limit: u8, +} + /// https://docs.joinmastodon.org/entities/notification/ #[derive(Serialize)] pub struct ApiNotification { diff --git a/src/mastodon_api/notifications/views.rs b/src/mastodon_api/notifications/views.rs index 85f27ab..c06fd27 100644 --- a/src/mastodon_api/notifications/views.rs +++ b/src/mastodon_api/notifications/views.rs @@ -7,27 +7,71 @@ use crate::database::{Pool, get_database_client}; use crate::errors::HttpError; use crate::mastodon_api::oauth::auth::get_current_user; 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("")] async fn get_notifications_view( auth: BearerAuth, config: web::Data, db_pool: web::Data, + query_params: web::Query, ) -> Result { let db_client = &**get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; let notifications: Vec = get_notifications( db_client, ¤t_user.id, + query_params.max_id, + query_params.limit.into(), ).await? .into_iter() .map(|item| ApiNotification::from_db(item, &config.instance_url())) .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 { web::scope("/api/v1/notifications") .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#"; rel="next""#, + ); + } +} diff --git a/src/models/notifications/queries.rs b/src/models/notifications/queries.rs index f151b6f..c881d3f 100644 --- a/src/models/notifications/queries.rs +++ b/src/models/notifications/queries.rs @@ -96,6 +96,8 @@ pub async fn create_repost_notification( pub async fn get_notifications( db_client: &impl GenericClient, recipient_id: &Uuid, + max_id: Option, + limit: i64, ) -> Result, DatabaseError> { let statement = format!( " @@ -111,8 +113,11 @@ pub async fn get_notifications( ON notification.post_id = post.id LEFT JOIN actor_profile AS post_author ON post.author_id = post_author.id - WHERE recipient_id = $1 - ORDER BY notification.created_at DESC + WHERE + recipient_id = $1 + AND ($2::integer IS NULL OR notification.id < $2) + ORDER BY notification.id DESC + LIMIT $3 ", related_attachments=RELATED_ATTACHMENTS, related_mentions=RELATED_MENTIONS, @@ -120,7 +125,7 @@ pub async fn get_notifications( ); let rows = db_client.query( statement.as_str(), - &[&recipient_id], + &[&recipient_id, &max_id, &limit], ).await?; let mut notifications: Vec = rows.iter() .map(Notification::try_from)