Notify user about new followers
This commit is contained in:
parent
56d073e1d8
commit
c4ea2900c8
13 changed files with 222 additions and 1 deletions
8
migrations/V0008__notification.sql
Normal file
8
migrations/V0008__notification.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE notification (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sender_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
recipient_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,
|
||||
post_id UUID REFERENCES post (id) ON DELETE CASCADE,
|
||||
event_type SMALLINT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
|
@ -73,3 +73,12 @@ CREATE TABLE oauth_token (
|
|||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE notification (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sender_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
recipient_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,
|
||||
post_id UUID REFERENCES post (id) ON DELETE CASCADE,
|
||||
event_type SMALLINT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ use mitra::mastodon_api::accounts::views::account_api_scope;
|
|||
use mitra::mastodon_api::directory::views::profile_directory;
|
||||
use mitra::mastodon_api::instance::views as instance_api;
|
||||
use mitra::mastodon_api::media::views::media_api_scope;
|
||||
use mitra::mastodon_api::notifications::views::notification_api_scope;
|
||||
use mitra::mastodon_api::oauth::auth::create_auth_error_handler;
|
||||
use mitra::mastodon_api::oauth::views::oauth_api_scope;
|
||||
use mitra::mastodon_api::search::views::search;
|
||||
|
@ -77,6 +78,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(profile_directory)
|
||||
.service(account_api_scope())
|
||||
.service(media_api_scope())
|
||||
.service(notification_api_scope())
|
||||
.service(status_api_scope())
|
||||
.service(instance_api::instance)
|
||||
.service(search)
|
||||
|
|
|
@ -2,6 +2,7 @@ pub mod accounts;
|
|||
pub mod directory;
|
||||
pub mod instance;
|
||||
pub mod media;
|
||||
pub mod notifications;
|
||||
pub mod oauth;
|
||||
pub mod search;
|
||||
pub mod statuses;
|
||||
|
|
2
src/mastodon_api/notifications/mod.rs
Normal file
2
src/mastodon_api/notifications/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod types;
|
||||
pub mod views;
|
39
src/mastodon_api/notifications/types.rs
Normal file
39
src/mastodon_api/notifications/types.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::mastodon_api::accounts::types::Account;
|
||||
use crate::mastodon_api::statuses::types::Status;
|
||||
use crate::models::notifications::types::{EventType, Notification};
|
||||
|
||||
/// https://docs.joinmastodon.org/entities/notification/
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiNotification {
|
||||
pub id: String,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
pub account: Account,
|
||||
pub status: Option<Status>,
|
||||
}
|
||||
|
||||
impl ApiNotification {
|
||||
pub fn from_db(notification: Notification, instance_url: &str) -> Self {
|
||||
let account = Account::from_profile(
|
||||
notification.sender,
|
||||
instance_url,
|
||||
);
|
||||
let event_type_mastodon = match notification.event_type {
|
||||
EventType::Follow => "follow",
|
||||
};
|
||||
Self {
|
||||
id: notification.id.to_string(),
|
||||
event_type: event_type_mastodon.to_string(),
|
||||
created_at: notification.created_at,
|
||||
account,
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
}
|
33
src/mastodon_api/notifications/views.rs
Normal file
33
src/mastodon_api/notifications/views.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
/// https://docs.joinmastodon.org/methods/notifications/
|
||||
use actix_web::{get, web, HttpResponse, Scope as ActixScope};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
|
||||
use crate::config::Config;
|
||||
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;
|
||||
|
||||
#[get("")]
|
||||
async fn get_notifications_view(
|
||||
auth: BearerAuth,
|
||||
config: web::Data<Config>,
|
||||
db_pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
let notifications: Vec<ApiNotification> = get_notifications(
|
||||
db_client,
|
||||
¤t_user.id,
|
||||
).await?
|
||||
.into_iter()
|
||||
.map(|item| ApiNotification::from_db(item, &config.instance_url()))
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(notifications))
|
||||
}
|
||||
|
||||
pub fn notification_api_scope() -> ActixScope {
|
||||
web::scope("/api/v1/notifications")
|
||||
.service(get_notifications_view)
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod attachments;
|
||||
mod cleanup;
|
||||
pub mod notifications;
|
||||
pub mod oauth;
|
||||
pub mod posts;
|
||||
pub mod profiles;
|
||||
|
|
2
src/models/notifications/mod.rs
Normal file
2
src/models/notifications/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod queries;
|
||||
pub mod types;
|
47
src/models/notifications/queries.rs
Normal file
47
src/models/notifications/queries.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use tokio_postgres::GenericClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::DatabaseError;
|
||||
use super::types::{EventType, Notification};
|
||||
|
||||
pub async fn create_notification(
|
||||
db_client: &impl GenericClient,
|
||||
sender_id: &Uuid,
|
||||
recipient_id: &Uuid,
|
||||
event_type: EventType,
|
||||
) -> Result<(), DatabaseError> {
|
||||
db_client.execute(
|
||||
"
|
||||
INSERT INTO notification (
|
||||
sender_id,
|
||||
recipient_id,
|
||||
event_type
|
||||
)
|
||||
VALUES ($1, $2, $3)
|
||||
",
|
||||
&[&sender_id, &recipient_id, &i16::from(event_type)],
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_notifications(
|
||||
db_client: &impl GenericClient,
|
||||
recipient_id: &Uuid,
|
||||
) -> Result<Vec<Notification>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
SELECT notification, sender
|
||||
FROM notification
|
||||
JOIN actor_profile AS sender
|
||||
ON notification.sender_id = sender.id
|
||||
WHERE recipient_id = $1
|
||||
",
|
||||
&[&recipient_id],
|
||||
).await?;
|
||||
let notifications: Vec<Notification> = rows.iter()
|
||||
.map(|row| Notification::try_from(row))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(notifications)
|
||||
}
|
69
src/models/notifications/types.rs
Normal file
69
src/models/notifications/types.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use postgres_types::FromSql;
|
||||
use tokio_postgres::Row;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::{ConversionError, DatabaseError};
|
||||
use crate::models::profiles::types::DbActorProfile;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(FromSql)]
|
||||
#[postgres(name = "notification")]
|
||||
struct DbNotification {
|
||||
id: i32,
|
||||
sender_id: Uuid,
|
||||
recipient_id: Uuid,
|
||||
post_id: Option<Uuid>,
|
||||
event_type: i16,
|
||||
created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub enum EventType {
|
||||
Follow,
|
||||
}
|
||||
|
||||
impl From<EventType> for i16 {
|
||||
fn from(value: EventType) -> i16 {
|
||||
match value {
|
||||
EventType::Follow => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i16> for EventType {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(value: i16) -> Result<Self, Self::Error> {
|
||||
let event_type = match value {
|
||||
1 => Self::Follow,
|
||||
_ => return Err(ConversionError),
|
||||
};
|
||||
Ok(event_type)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Notification {
|
||||
pub id: i32,
|
||||
pub sender: DbActorProfile,
|
||||
pub event_type: EventType,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Row> for Notification {
|
||||
|
||||
type Error = DatabaseError;
|
||||
|
||||
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
||||
let db_notification: DbNotification = row.try_get("notification")?;
|
||||
let db_sender: DbActorProfile = row.try_get("sender")?;
|
||||
let notification = Self {
|
||||
id: db_notification.id,
|
||||
sender: db_sender,
|
||||
event_type: EventType::try_from(db_notification.event_type)?,
|
||||
created_at: db_notification.created_at,
|
||||
};
|
||||
Ok(notification)
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ impl TryFrom<&Row> for Post {
|
|||
let db_post: DbPost = row.try_get("post")?;
|
||||
let db_profile: DbActorProfile = row.try_get("actor_profile")?;
|
||||
let db_attachments: Vec<DbMediaAttachment> = row.try_get("attachments")?;
|
||||
let post = Post {
|
||||
let post = Self {
|
||||
id: db_post.id,
|
||||
author: db_profile,
|
||||
content: db_post.content,
|
||||
|
|
|
@ -4,6 +4,8 @@ use tokio_postgres::GenericClient;
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::DatabaseError;
|
||||
use crate::models::notifications::queries::create_notification;
|
||||
use crate::models::notifications::types::EventType;
|
||||
use crate::models::profiles::queries::{
|
||||
update_follower_count,
|
||||
update_following_count,
|
||||
|
@ -99,6 +101,12 @@ pub async fn follow(
|
|||
};
|
||||
update_follower_count(&transaction, target_id, 1).await?;
|
||||
update_following_count(&transaction, source_id, 1).await?;
|
||||
create_notification(
|
||||
&transaction,
|
||||
source_id,
|
||||
target_id,
|
||||
EventType::Follow,
|
||||
).await?;
|
||||
let relationship = get_relationship(&transaction, source_id, target_id).await?;
|
||||
transaction.commit().await?;
|
||||
Ok(relationship)
|
||||
|
|
Loading…
Reference in a new issue