Notify user about new followers

This commit is contained in:
silverpill 2021-10-12 16:11:47 +00:00
parent 56d073e1d8
commit c4ea2900c8
13 changed files with 222 additions and 1 deletions

View 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()
);

View file

@ -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()
);

View file

@ -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)

View file

@ -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;

View file

@ -0,0 +1,2 @@
mod types;
pub mod views;

View 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,
}
}
}

View 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,
&current_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)
}

View file

@ -1,5 +1,6 @@
pub mod attachments;
mod cleanup;
pub mod notifications;
pub mod oauth;
pub mod posts;
pub mod profiles;

View file

@ -0,0 +1,2 @@
pub mod queries;
pub mod types;

View 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)
}

View 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)
}
}

View file

@ -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,

View file

@ -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)