diff --git a/Cargo.lock b/Cargo.lock index 600c36d..73c149d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,7 +140,7 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" [[package]] name = "apb" -version = "0.2.0" +version = "0.2.1" dependencies = [ "async-trait", "chrono", diff --git a/upub/core/src/model/actor.rs b/upub/core/src/model/actor.rs index 36df4da..e4b4765 100644 --- a/upub/core/src/model/actor.rs +++ b/upub/core/src/model/actor.rs @@ -50,6 +50,8 @@ pub enum Relation { on_delete = "NoAction" )] Instances, + #[sea_orm(has_many = "super::dislike::Entity")] + Dislikes, #[sea_orm(has_many = "super::like::Entity")] Likes, #[sea_orm(has_many = "super::mention::Entity")] @@ -98,6 +100,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Dislikes.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::Likes.def() diff --git a/upub/core/src/model/dislike.rs b/upub/core/src/model/dislike.rs new file mode 100644 index 0000000..15e77e1 --- /dev/null +++ b/upub/core/src/model/dislike.rs @@ -0,0 +1,51 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "dislikes")] +pub struct Model { + #[sea_orm(primary_key)] + pub internal: i64, + pub actor: i64, + pub object: i64, + pub published: ChronoDateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::actor::Entity", + from = "Column::Actor", + to = "super::actor::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Actors, + #[sea_orm( + belongs_to = "super::object::Entity", + from = "Column::Object", + to = "super::object::Column::Internal", + on_update = "Cascade", + on_delete = "Cascade" + )] + Objects, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Actors.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Objects.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_uid_oid(uid: i64, oid: i64) -> Select { + Entity::find().filter(Column::Actor.eq(uid)).filter(Column::Object.eq(oid)) + } +} diff --git a/upub/core/src/model/mod.rs b/upub/core/src/model/mod.rs index 1f97087..cdb408f 100644 --- a/upub/core/src/model/mod.rs +++ b/upub/core/src/model/mod.rs @@ -13,6 +13,7 @@ pub mod job; pub mod relation; pub mod announce; pub mod like; +pub mod dislike; pub mod hashtag; pub mod mention; diff --git a/upub/core/src/model/object.rs b/upub/core/src/model/object.rs index e26b9ee..f3348e8 100644 --- a/upub/core/src/model/object.rs +++ b/upub/core/src/model/object.rs @@ -50,6 +50,8 @@ pub enum Relation { Announces, #[sea_orm(has_many = "super::attachment::Entity")] Attachments, + #[sea_orm(has_many = "super::dislike::Entity")] + Dislikes, #[sea_orm(has_many = "super::hashtag::Entity")] Hashtags, #[sea_orm(has_many = "super::like::Entity")] @@ -96,6 +98,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Dislikes.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::Hashtags.def() diff --git a/upub/core/src/traits/process.rs b/upub/core/src/traits/process.rs index 034548e..d62eb75 100644 --- a/upub/core/src/traits/process.rs +++ b/upub/core/src/traits/process.rs @@ -39,8 +39,8 @@ impl Processor for crate::Context { async fn process(&self, activity: impl apb::Activity, tx: &DatabaseTransaction) -> Result<(), ProcessorError> { // TODO we could process Links and bare Objects maybe, but probably out of AP spec? match activity.activity_type()? { - // TODO emojireacts are NOT likes, but let's process them like ones for now maybe? - apb::ActivityType::Like | apb::ActivityType::EmojiReact => Ok(like(self, activity, tx).await?), + apb::ActivityType::Like => Ok(like(self, activity, tx).await?), + apb::ActivityType::Dislike => Ok(dislike(self, activity, tx).await?), apb::ActivityType::Create => Ok(create(self, activity, tx).await?), apb::ActivityType::Follow => Ok(follow(self, activity, tx).await?), apb::ActivityType::Announce => Ok(announce(self, activity, tx).await?), @@ -115,6 +115,36 @@ pub async fn like(ctx: &crate::Context, activity: impl apb::Activity, tx: &Datab Ok(()) } +// TODO basically same as like, can we make one function, maybe with const generic??? +pub async fn dislike(ctx: &crate::Context, activity: impl apb::Activity, tx: &DatabaseTransaction) -> Result<(), ProcessorError> { + let actor = ctx.fetch_user(activity.actor().id()?, tx).await?; + let obj = ctx.fetch_object(activity.object().id()?, tx).await?; + if crate::model::like::Entity::find_by_uid_oid(actor.internal, obj.internal) + .any(tx) + .await? + { + return Err(ProcessorError::AlreadyProcessed); + } + + let dislike = crate::model::dislike::ActiveModel { + internal: NotSet, + actor: Set(actor.internal), + object: Set(obj.internal), + published: Set(activity.published().unwrap_or_else(|_|chrono::Utc::now())), + }; + + crate::model::dislike::Entity::insert(dislike).exec(tx).await?; + + // only dislikes mentioning local users are stored to generate notifications, everything else + // produces side effects but no activity, and thus no notification + if ctx.is_local(&actor.id) || activity.mentioning().iter().any(|x| ctx.is_local(x)) { + ctx.insert_activity(activity, tx).await?; + } + + tracing::debug!("{} disliked {}", actor.id, obj.id); + Ok(()) +} + pub async fn follow(ctx: &crate::Context, activity: impl apb::Activity, tx: &DatabaseTransaction) -> Result<(), ProcessorError> { let source_actor = crate::model::actor::Entity::find_by_ap_id(activity.actor().id()?) .one(tx) diff --git a/upub/migrations/src/lib.rs b/upub/migrations/src/lib.rs index bd437c2..96f8f02 100644 --- a/upub/migrations/src/lib.rs +++ b/upub/migrations/src/lib.rs @@ -10,6 +10,7 @@ mod m20240605_000001_add_jobs_table; mod m20240606_000001_add_audience_to_objects; mod m20240607_000001_activity_ref_is_optional; mod m20240609_000001_add_instance_field_to_relations; +mod m20240623_000001_add_dislikes_table; pub struct Migrator; @@ -27,6 +28,7 @@ impl MigratorTrait for Migrator { Box::new(m20240606_000001_add_audience_to_objects::Migration), Box::new(m20240607_000001_activity_ref_is_optional::Migration), Box::new(m20240609_000001_add_instance_field_to_relations::Migration), + Box::new(m20240623_000001_add_dislikes_table::Migration), ] } } diff --git a/upub/migrations/src/m20240623_000001_add_dislikes_table.rs b/upub/migrations/src/m20240623_000001_add_dislikes_table.rs new file mode 100644 index 0000000..e1d8d7f --- /dev/null +++ b/upub/migrations/src/m20240623_000001_add_dislikes_table.rs @@ -0,0 +1,85 @@ +use sea_orm_migration::prelude::*; + +use super::m20240524_000001_create_actor_activity_object_tables::{Actors, Objects}; + +#[derive(DeriveIden)] +#[allow(clippy::enum_variant_names)] +pub enum Dislikes { + Table, + Internal, + Actor, + Object, + Published, +} + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Dislikes::Table) + .comment("all dislike events, joining actor to object") + .col( + ColumnDef::new(Dislikes::Internal) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + ) + .col(ColumnDef::new(Dislikes::Actor).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-dislikes-actor") + .from(Dislikes::Table, Dislikes::Actor) + .to(Actors::Table, Actors::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Dislikes::Object).big_integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fkey-dislikes-object") + .from(Dislikes::Table, Dislikes::Object) + .to(Objects::Table, Objects::Internal) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade) + ) + .col(ColumnDef::new(Dislikes::Published).timestamp_with_time_zone().not_null().default(Expr::current_timestamp())) + .to_owned() + ) + .await?; + + manager + .create_index(Index::create().name("index-dislikes-actor").table(Dislikes::Table).col(Dislikes::Actor).to_owned()) + .await?; + + manager + .create_index(Index::create().name("index-dislikes-object").table(Dislikes::Table).col(Dislikes::Object).to_owned()) + .await?; + + manager + .create_index( + Index::create() + .unique() + .name("index-dislikes-actor-object") + .table(Dislikes::Table) + .col(Dislikes::Actor) + .col(Dislikes::Object) + .to_owned() + ).await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Dislikes::Table).to_owned()) + .await?; + + Ok(()) + } +}