From 3edae49c9db52d3b955fe17b27cc7d04c4dfd6b5 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Wed, 1 Nov 2023 19:17:51 -0700 Subject: [PATCH] adds version create and project update --- src/auth/checks.rs | 2 +- src/database/models/creator_follows.rs | 2 + src/database/models/event_item.rs | 90 +++++++++++++-- src/database/models/ids.rs | 2 + src/models/feeds.rs | 13 ++- src/routes/updates.rs | 2 +- src/routes/v2/analytics_get.rs | 2 +- src/routes/v2/projects.rs | 41 ++++++- src/routes/v2/version_creation.rs | 33 +++++- src/routes/v2/version_file.rs | 2 +- src/routes/v2/versions.rs | 4 +- src/routes/v3/organizations.rs | 2 +- src/routes/v3/users.rs | 153 +++++++++++++++++++++---- tests/feed.rs | 59 ++++++++++ tests/version.rs | 4 +- 15 files changed, 367 insertions(+), 44 deletions(-) diff --git a/src/auth/checks.rs b/src/auth/checks.rs index 769ffcee..9f60cf7d 100644 --- a/src/auth/checks.rs +++ b/src/auth/checks.rs @@ -191,7 +191,7 @@ impl ValidateAuthorized for crate::database::models::OAuthClient { pub async fn filter_authorized_versions( versions: Vec, - user_option: &Option, + user_option: Option<&User>, pool: &web::Data, ) -> Result, ApiError> { let mut return_versions = Vec::new(); diff --git a/src/database/models/creator_follows.rs b/src/database/models/creator_follows.rs index 15fc9e2d..28f9c360 100644 --- a/src/database/models/creator_follows.rs +++ b/src/database/models/creator_follows.rs @@ -3,11 +3,13 @@ use itertools::Itertools; use super::{OrganizationId, UserId}; use crate::database::models::DatabaseError; +#[derive(Copy, Clone, Debug)] pub struct UserFollow { pub follower_id: UserId, pub target_id: UserId, } +#[derive(Copy, Clone, Debug)] pub struct OrganizationFollow { pub follower_id: UserId, pub target_id: OrganizationId, diff --git a/src/database/models/event_item.rs b/src/database/models/event_item.rs index 32ebc281..caf0c51e 100644 --- a/src/database/models/event_item.rs +++ b/src/database/models/event_item.rs @@ -1,6 +1,6 @@ use super::{ dynamic::{DynamicId, IdType}, - generate_event_id, DatabaseError, EventId, OrganizationId, ProjectId, UserId, + generate_event_id, DatabaseError, EventId, OrganizationId, ProjectId, UserId, VersionId, }; use chrono::{DateTime, Utc}; use itertools::Itertools; @@ -12,6 +12,8 @@ use std::convert::{TryFrom, TryInto}; #[sqlx(rename_all = "snake_case")] pub enum EventType { ProjectCreated, + VersionCreated, + ProjectUpdated, } impl PgHasArrayType for EventType { @@ -20,18 +22,29 @@ impl PgHasArrayType for EventType { } } +#[derive(Debug)] pub enum CreatorId { User(UserId), Organization(OrganizationId), } +#[derive(Debug)] pub enum EventData { ProjectCreated { project_id: ProjectId, creator_id: CreatorId, }, + VersionCreated { + version_id: VersionId, + creator_id: CreatorId, + }, + ProjectUpdated { + project_id: ProjectId, + updater_id: CreatorId, + }, } +#[derive(Debug)] pub struct Event { pub id: EventId, pub event_data: EventData, @@ -108,6 +121,40 @@ impl From for RawEvent { created: None, } } + EventData::VersionCreated { + version_id, + creator_id, + } => { + let target_id = DynamicId::from(version_id); + let triggerer_id = DynamicId::from(creator_id); + RawEvent { + id: value.id, + target_id: target_id.id, + target_id_type: target_id.id_type, + triggerer_id: Some(triggerer_id.id), + triggerer_id_type: Some(triggerer_id.id_type), + event_type: EventType::VersionCreated, + metadata: None, + created: None, + } + } + EventData::ProjectUpdated { + project_id, + updater_id, + } => { + let target_id = DynamicId::from(project_id); + let triggerer_id = DynamicId::from(updater_id); + RawEvent { + id: value.id, + target_id: target_id.id, + target_id_type: target_id.id_type, + triggerer_id: Some(triggerer_id.id), + triggerer_id_type: Some(triggerer_id.id_type), + event_type: EventType::ProjectUpdated, + metadata: None, + created: None, + } + } } } } @@ -124,10 +171,11 @@ impl TryFrom for Event { (Some(id), Some(id_type)) => Some(DynamicId { id, id_type }), _ => None, }; - Ok(match value.event_type { - EventType::ProjectCreated => Event { - id: value.id, - event_data: EventData::ProjectCreated { + + let event = Event { + id : value.id, + event_data : match value.event_type { + EventType::ProjectCreated => EventData::ProjectCreated { project_id: target_id.try_into()?, creator_id: triggerer_id.map_or_else(|| { Err(DatabaseError::UnexpectedNull( @@ -135,16 +183,34 @@ impl TryFrom for Event { )) }, |v| v.try_into())?, }, - time: value.created.map_or_else( - || { + EventType::VersionCreated => EventData::VersionCreated { + version_id: target_id.try_into()?, + creator_id: triggerer_id.map_or_else(|| { Err(DatabaseError::UnexpectedNull( - "the value of created should not be null".to_string(), + "Neither triggerer_id nor triggerer_id_type should be null for version creation".to_string(), )) - }, - Ok, - )?, + }, |v| v.try_into())?, + }, + EventType::ProjectUpdated => EventData::ProjectUpdated { + project_id: target_id.try_into()?, + updater_id: triggerer_id.map_or_else(|| { + Err(DatabaseError::UnexpectedNull( + "Neither triggerer_id nor triggerer_id_type should be null for project update".to_string(), + )) + }, |v| v.try_into())?, + }, }, - }) + time : value.created.map_or_else( + || { + Err(DatabaseError::UnexpectedNull( + "the value of created should not be null".to_string(), + )) + }, + Ok, + )?, + }; + + Ok(event) } } diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 4f740ec8..c07fc38d 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -463,6 +463,7 @@ pub mod dynamic { ProjectId, UserId, OrganizationId, + VersionId, } impl PgHasArrayType for IdType { @@ -506,6 +507,7 @@ pub mod dynamic { } from_static_impl!(ProjectId, IdType::ProjectId); + from_static_impl!(VersionId, IdType::VersionId); from_static_impl!(UserId, IdType::UserId); from_static_impl!(OrganizationId, IdType::OrganizationId); } diff --git a/src/models/feeds.rs b/src/models/feeds.rs index 0820b888..1d3fa387 100644 --- a/src/models/feeds.rs +++ b/src/models/feeds.rs @@ -1,7 +1,7 @@ use super::ids::Base62Id; use super::ids::OrganizationId; use super::users::UserId; -use crate::models::ids::ProjectId; +use crate::models::ids::{ProjectId, VersionId}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -34,6 +34,17 @@ pub enum FeedItemBody { creator_id: CreatorId, project_title: String, }, + ProjectUpdated { + project_id: ProjectId, + updater_id: CreatorId, + project_title: String, + }, + VersionCreated { + project_id: ProjectId, + version_id: VersionId, + creator_id: CreatorId, + project_title: String, + }, } impl From for CreatorId { diff --git a/src/routes/updates.rs b/src/routes/updates.rs index 004621a9..eaf8eced 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -72,7 +72,7 @@ pub async fn forge_updates( .into_iter() .filter(|x| x.loaders.iter().any(loaders)) .collect(), - &user_option, + user_option.as_ref(), &pool, ) .await?; diff --git a/src/routes/v2/analytics_get.rs b/src/routes/v2/analytics_get.rs index d6ab2c21..8e907ffb 100644 --- a/src/routes/v2/analytics_get.rs +++ b/src/routes/v2/analytics_get.rs @@ -590,7 +590,7 @@ async fn filter_allowed_ids( .map(|id| Ok(VersionId(parse_base62(id)?).into())) .collect::, ApiError>>()?; let versions = version_item::Version::get_many(&ids, &***pool, redis).await?; - let ids: Vec = filter_authorized_versions(versions, &Some(user), pool) + let ids: Vec = filter_authorized_versions(versions, Some(&user), pool) .await? .into_iter() .map(|x| x.id) diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index d1883798..876a7e1a 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -1,9 +1,10 @@ use crate::auth::{filter_authorized_projects, get_user_from_headers, is_authorized}; use crate::database; -use crate::database::models::image_item; +use crate::database::models::event_item::{CreatorId, EventData}; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::project_item::{GalleryItem, ModCategory}; use crate::database::models::thread_item::ThreadMessageBuilder; +use crate::database::models::{image_item, Event}; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models; @@ -24,6 +25,7 @@ use crate::util::routes::read_from_payload; use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; +use db_ids::OrganizationId; use futures::TryStreamExt; use meilisearch_sdk::indexes::IndexesResults; use serde::{Deserialize, Serialize}; @@ -1118,6 +1120,14 @@ pub async fn project_edit( ) .await?; + insert_project_update_event( + id.into(), + project_item.inner.organization_id, + &user, + &mut transaction, + ) + .await?; + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -1467,6 +1477,14 @@ pub async fn projects_edit( .await?; } + insert_project_update_event( + project.inner.id.into(), + project.inner.organization_id, + &user, + &mut transaction, + ) + .await?; + db_models::Project::clear_cache(project.inner.id, project.inner.slug, None, &redis).await?; } @@ -2558,3 +2576,24 @@ pub async fn delete_from_index( Ok(()) } + +async fn insert_project_update_event( + project_id: ProjectId, + organization_id: Option, + current_user: &crate::models::users::User, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result<(), ApiError> { + let event = Event::new( + EventData::ProjectUpdated { + project_id: project_id.into(), + updater_id: organization_id.map_or_else( + || CreatorId::User(current_user.id.into()), + CreatorId::Organization, + ), + }, + transaction, + ) + .await?; + event.insert(transaction).await?; + Ok(()) +} diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index e94284cf..83da5a0b 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -1,10 +1,11 @@ use super::project_creation::{CreateError, UploadedFile}; use crate::auth::get_user_from_headers; +use crate::database::models::event_item::{CreatorId, EventData}; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::version_item::{ DependencyBuilder, VersionBuilder, VersionFileBuilder, }; -use crate::database::models::{self, image_item, Organization}; +use crate::database::models::{self, image_item, Event, Organization}; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::images::{Image, ImageContext, ImageId}; @@ -304,6 +305,14 @@ async fn version_create_inner( }) .collect::>(); + insert_version_create_event( + version_id, + organization.map(|o| o.id), + &user, + transaction, + ) + .await?; + version_builder = Some(VersionBuilder { version_id: version_id.into(), project_id, @@ -961,3 +970,25 @@ pub fn get_name_ext( }; Ok((file_name, file_extension)) } + +async fn insert_version_create_event( + version_id: VersionId, + organization_id: Option, + current_user: &crate::models::users::User, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result<(), CreateError> { + println!("Adding version create event"); + let event = Event::new( + EventData::VersionCreated { + version_id: version_id.into(), + creator_id: organization_id.map_or_else( + || CreatorId::User(current_user.id.into()), + CreatorId::Organization, + ), + }, + transaction, + ) + .await?; + event.insert(transaction).await?; + Ok(()) +} diff --git a/src/routes/v2/version_file.rs b/src/routes/v2/version_file.rs index be74701e..3e948166 100644 --- a/src/routes/v2/version_file.rs +++ b/src/routes/v2/version_file.rs @@ -378,7 +378,7 @@ pub async fn get_versions_from_hashes( let version_ids = files.iter().map(|x| x.version_id).collect::>(); let versions_data = filter_authorized_versions( database::models::Version::get_many(&version_ids, &**pool, &redis).await?, - &user_option, + user_option.as_ref(), &pool, ) .await?; diff --git a/src/routes/v2/versions.rs b/src/routes/v2/versions.rs index 44517a84..8eca3ed9 100644 --- a/src/routes/v2/versions.rs +++ b/src/routes/v2/versions.rs @@ -158,7 +158,7 @@ pub async fn version_list( response.sort(); response.dedup_by(|a, b| a.inner.id == b.inner.id); - let response = filter_authorized_versions(response, &user_option, &pool).await?; + let response = filter_authorized_versions(response, user_option.as_ref(), &pool).await?; Ok(HttpResponse::Ok().json(response)) } else { @@ -243,7 +243,7 @@ pub async fn versions_get( .map(|x| x.1) .ok(); - let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?; + let versions = filter_authorized_versions(versions_data, user_option.as_ref(), &pool).await?; Ok(HttpResponse::Ok().json(versions)) } diff --git a/src/routes/v3/organizations.rs b/src/routes/v3/organizations.rs index a990f8d3..e2093f57 100644 --- a/src/routes/v3/organizations.rs +++ b/src/routes/v3/organizations.rs @@ -88,7 +88,7 @@ pub async fn organization_unfollow( ApiError::InvalidInput("The specified organization does not exist!".to_string()) })?; - DBOrganizationFollow::unfollow(current_user.id.into(), target.id.into(), &**pool).await?; + DBOrganizationFollow::unfollow(current_user.id.into(), target.id, &**pool).await?; Ok(HttpResponse::NoContent().body("")) } diff --git a/src/routes/v3/users.rs b/src/routes/v3/users.rs index 02e5263f..1e459dea 100644 --- a/src/routes/v3/users.rs +++ b/src/routes/v3/users.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, iter::FromIterator}; use crate::{ - auth::{filter_authorized_projects, get_user_from_headers}, + auth::{filter_authorized_projects, filter_authorized_versions, get_user_from_headers}, database::{ self, models::{ @@ -12,9 +12,9 @@ use crate::{ }, models::{ feeds::{FeedItem, FeedItemBody}, - ids::ProjectId, + ids::{ProjectId, VersionId}, pats::Scopes, - projects::Project, + projects::{Project, Version}, users::User, }, queue::session::AuthQueue, @@ -105,7 +105,7 @@ pub async fn user_unfollow( .await? .ok_or_else(|| ApiError::InvalidInput("The specified user does not exist!".to_string()))?; - DBUserFollow::unfollow(current_user.id.into(), target.id.into(), &**pool).await?; + DBUserFollow::unfollow(current_user.id.into(), target.id, &**pool).await?; Ok(HttpResponse::NoContent().body("")) } @@ -138,20 +138,32 @@ pub async fn current_user_feed( let followed_organizations = DBOrganizationFollow::get_follows_by_follower(current_user.id.into(), &**pool).await?; + // Feed by default shows the following: + // - Projects created by users you follow + // - Projects created by organizations you follow + // - Versions created by users you follow + // - Versions created by organizations you follow + // - Projects updated by users you follow + // - Projects updated by organizations you follow + let event_types = [ + EventType::ProjectCreated, + EventType::VersionCreated, + EventType::ProjectUpdated, + ]; let selectors = followed_users .into_iter() - .map(|follow| EventSelector { - id: follow.target_id.into(), - event_type: EventType::ProjectCreated, + .flat_map(|follow| { + event_types.iter().map(move |event_type| EventSelector { + id: follow.target_id.into(), + event_type: *event_type, + }) }) - .chain( - followed_organizations - .into_iter() - .map(|follow| EventSelector { - id: follow.target_id.into(), - event_type: EventType::ProjectCreated, - }), - ) + .chain(followed_organizations.into_iter().flat_map(|follow| { + event_types.iter().map(move |event_type| EventSelector { + id: follow.target_id.into(), + event_type: *event_type, + }) + })) .collect_vec(); let events = DBEvent::get_events(&[], &selectors, &**pool) .await? @@ -160,9 +172,32 @@ pub async fn current_user_feed( .take(params.offset.unwrap_or(usize::MAX)) .collect_vec(); + println!("ALL EVENTS: {:#?}", events); + let mut feed_items: Vec = Vec::new(); - let authorized_projects = - prefetch_authorized_event_projects(&events, &pool, &redis, ¤t_user).await?; + let authorized_versions = + prefetch_authorized_event_versions(&events, &pool, &redis, ¤t_user).await?; + let authorized_version_project_ids = authorized_versions + .values() + .map(|versions| versions.project_id) + .collect_vec(); + let authorized_projects = prefetch_authorized_event_projects( + &events, + Some(&authorized_version_project_ids), + &pool, + &redis, + ¤t_user, + ) + .await?; + + println!( + "All authorized versoins: {:#?}", + serde_json::to_string(&authorized_versions).unwrap() + ); + println!( + "All authorized projects: {:#?}", + serde_json::to_string(&authorized_projects).unwrap() + ); for event in events { let body = match event.event_data { @@ -176,6 +211,45 @@ pub async fn current_user_feed( project_title: p.title.clone(), } }), + EventData::ProjectUpdated { + project_id, + updater_id, + } => authorized_projects.get(&project_id.into()).map(|p| { + FeedItemBody::ProjectUpdated { + project_id: project_id.into(), + updater_id: updater_id.into(), + project_title: p.title.clone(), + } + }), + EventData::VersionCreated { + version_id, + creator_id, + } => { + println!("Making version ev"); + let authorized_version = authorized_versions.get(&version_id.into()); + let authorized_project = + authorized_version.and_then(|v| authorized_projects.get(&v.project_id)); + println!( + "av: {:#?}", + serde_json::to_string(&authorized_version).unwrap() + ); + println!( + "ap: {:#?}", + serde_json::to_string(&authorized_project).unwrap() + ); + if let (Some(authorized_version), Some(authorized_project)) = + (authorized_version, authorized_project) + { + Some(FeedItemBody::VersionCreated { + project_id: authorized_project.id, + version_id: authorized_version.id, + creator_id: creator_id.into(), + project_title: authorized_project.title.clone(), + }) + } else { + None + } + } }; if let Some(body) = body { @@ -194,19 +268,33 @@ pub async fn current_user_feed( async fn prefetch_authorized_event_projects( events: &[db_models::Event], + additional_ids: Option<&[ProjectId]>, pool: &web::Data, redis: &RedisPool, current_user: &User, ) -> Result, ApiError> { - let project_ids = events + let mut project_ids = events .iter() - .map(|e| match &e.event_data { + .filter_map(|e| match &e.event_data { EventData::ProjectCreated { project_id, creator_id: _, - } => *project_id, + } => Some(*project_id), + EventData::ProjectUpdated { + project_id, + updater_id: _, + } => Some(*project_id), + EventData::VersionCreated { .. } => None, }) .collect_vec(); + if let Some(additional_ids) = additional_ids { + project_ids.extend( + additional_ids + .iter() + .copied() + .map(db_models::ProjectId::from), + ); + } let projects = db_models::Project::get_many_ids(&project_ids, &***pool, redis).await?; let authorized_projects = filter_authorized_projects(projects, Some(current_user), pool).await?; @@ -214,3 +302,28 @@ async fn prefetch_authorized_event_projects( authorized_projects.into_iter().map(|p| (p.id, p)), )) } + +async fn prefetch_authorized_event_versions( + events: &[db_models::Event], + pool: &web::Data, + redis: &RedisPool, + current_user: &User, +) -> Result, ApiError> { + let version_ids = events + .iter() + .filter_map(|e| match &e.event_data { + EventData::VersionCreated { + version_id, + creator_id: _, + } => Some(*version_id), + EventData::ProjectCreated { .. } => None, + EventData::ProjectUpdated { .. } => None, + }) + .collect_vec(); + let versions = db_models::Version::get_many(&version_ids, &***pool, redis).await?; + let authorized_versions = + filter_authorized_versions(versions, Some(current_user), pool).await?; + Ok(HashMap::::from_iter( + authorized_versions.into_iter().map(|v| (v.id, v)), + )) +} diff --git a/tests/feed.rs b/tests/feed.rs index 9c44cf07..134eed3a 100644 --- a/tests/feed.rs +++ b/tests/feed.rs @@ -38,6 +38,65 @@ async fn get_feed_after_following_user_shows_previously_created_public_projects( .await } +#[actix_rt::test] +async fn get_feed_after_following_user_shows_previously_created_public_versions() { + with_test_environment(|env| async move { + let DummyProjectAlpha { + project_id: alpha_project_id, + .. + } = env.dummy.as_ref().unwrap().project_alpha.clone(); + + // Add version + let v = env.v2.create_default_version(&alpha_project_id, None, USER_USER_PAT) + .await; + + env.v3.follow_user(USER_USER_ID, FRIEND_USER_PAT).await; + + let feed = env.v3.get_feed(FRIEND_USER_PAT).await; + + assert_eq!(feed.len(), 2); + assert_matches!( + feed[1].body, + FeedItemBody::ProjectCreated { project_id, .. } if project_id.to_string() == alpha_project_id + ); + assert_matches!( + feed[0].body, + FeedItemBody::VersionCreated { version_id, .. } if version_id == v.id + ); + }) + .await +} + +#[actix_rt::test] + +async fn get_feed_after_following_user_shows_previously_edited_public_versions() { + with_test_environment(|env| async move { + let DummyProjectAlpha { + project_id: alpha_project_id, + .. + } = env.dummy.as_ref().unwrap().project_alpha.clone(); + + // Empty patch + env.v2.edit_project(&alpha_project_id, serde_json::json!({}), USER_USER_PAT) + .await; + + env.v3.follow_user(USER_USER_ID, FRIEND_USER_PAT).await; + + let feed = env.v3.get_feed(FRIEND_USER_PAT).await; + + assert_eq!(feed.len(), 2); + assert_matches!( + feed[1].body, + FeedItemBody::ProjectCreated { project_id, .. } if project_id.to_string() == alpha_project_id + ); + assert_matches!( + feed[0].body, + FeedItemBody::ProjectUpdated { project_id, .. } if project_id.to_string() == alpha_project_id + ); + }) + .await +} + #[actix_rt::test] async fn get_feed_when_following_user_that_creates_project_as_org_only_shows_event_when_following_org( ) { diff --git a/tests/version.rs b/tests/version.rs index a1a6d84d..04198c5b 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -81,7 +81,7 @@ async fn version_ordering_when_unspecified_orders_oldest_first() { let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( env.v2 - .create_default_version(&alpha_project_id, None, USER_USER_PAT) + .create_default_version(alpha_project_id, None, USER_USER_PAT) .await .id, ); @@ -105,7 +105,7 @@ async fn version_ordering_when_specified_orders_specified_before_unspecified() { let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( env.v2 - .create_default_version(&alpha_project_id, Some(10000), USER_USER_PAT) + .create_default_version(alpha_project_id, Some(10000), USER_USER_PAT) .await .id, );