Detailed changes
@@ -1,4 +1,5 @@
mod access_token;
+mod contact;
mod project;
mod project_collaborator;
mod room;
@@ -18,8 +19,11 @@ use sea_orm::{
entity::prelude::*, ConnectOptions, DatabaseConnection, DatabaseTransaction, DbErr,
TransactionTrait,
};
-use sea_orm::{ActiveValue, ConnectionTrait, IntoActiveModel, QueryOrder, QuerySelect};
-use sea_query::{OnConflict, Query};
+use sea_orm::{
+ ActiveValue, ConnectionTrait, FromQueryResult, IntoActiveModel, JoinType, QueryOrder,
+ QuerySelect,
+};
+use sea_query::{Alias, Expr, OnConflict, Query};
use serde::{Deserialize, Serialize};
use sqlx::migrate::{Migrate, Migration, MigrationSource};
use sqlx::Connection;
@@ -29,6 +33,7 @@ use std::time::Duration;
use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc};
use tokio::sync::{Mutex, OwnedMutexGuard};
+pub use contact::Contact;
pub use user::Model as User;
pub struct Database {
@@ -95,6 +100,8 @@ impl Database {
Ok(new_migrations)
}
+ // users
+
pub async fn create_user(
&self,
email_address: &str,
@@ -197,6 +204,292 @@ impl Database {
.await
}
+ // contacts
+
+ pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
+ #[derive(Debug, FromQueryResult)]
+ struct ContactWithUserBusyStatuses {
+ user_id_a: UserId,
+ user_id_b: UserId,
+ a_to_b: bool,
+ accepted: bool,
+ should_notify: bool,
+ user_a_busy: bool,
+ user_b_busy: bool,
+ }
+
+ self.transact(|tx| async move {
+ let user_a_participant = Alias::new("user_a_participant");
+ let user_b_participant = Alias::new("user_b_participant");
+ let mut db_contacts = contact::Entity::find()
+ .column_as(
+ Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
+ .is_not_null(),
+ "user_a_busy",
+ )
+ .column_as(
+ Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
+ .is_not_null(),
+ "user_b_busy",
+ )
+ .filter(
+ contact::Column::UserIdA
+ .eq(user_id)
+ .or(contact::Column::UserIdB.eq(user_id)),
+ )
+ .join_as(
+ JoinType::LeftJoin,
+ contact::Relation::UserARoomParticipant.def(),
+ user_a_participant,
+ )
+ .join_as(
+ JoinType::LeftJoin,
+ contact::Relation::UserBRoomParticipant.def(),
+ user_b_participant,
+ )
+ .into_model::<ContactWithUserBusyStatuses>()
+ .stream(&tx)
+ .await?;
+
+ let mut contacts = Vec::new();
+ while let Some(db_contact) = db_contacts.next().await {
+ let db_contact = db_contact?;
+ if db_contact.user_id_a == user_id {
+ if db_contact.accepted {
+ contacts.push(Contact::Accepted {
+ user_id: db_contact.user_id_b,
+ should_notify: db_contact.should_notify && db_contact.a_to_b,
+ busy: db_contact.user_b_busy,
+ });
+ } else if db_contact.a_to_b {
+ contacts.push(Contact::Outgoing {
+ user_id: db_contact.user_id_b,
+ })
+ } else {
+ contacts.push(Contact::Incoming {
+ user_id: db_contact.user_id_b,
+ should_notify: db_contact.should_notify,
+ });
+ }
+ } else if db_contact.accepted {
+ contacts.push(Contact::Accepted {
+ user_id: db_contact.user_id_a,
+ should_notify: db_contact.should_notify && !db_contact.a_to_b,
+ busy: db_contact.user_a_busy,
+ });
+ } else if db_contact.a_to_b {
+ contacts.push(Contact::Incoming {
+ user_id: db_contact.user_id_a,
+ should_notify: db_contact.should_notify,
+ });
+ } else {
+ contacts.push(Contact::Outgoing {
+ user_id: db_contact.user_id_a,
+ });
+ }
+ }
+
+ contacts.sort_unstable_by_key(|contact| contact.user_id());
+
+ Ok(contacts)
+ })
+ .await
+ }
+
+ pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
+ self.transact(|tx| async move {
+ let (id_a, id_b) = if user_id_1 < user_id_2 {
+ (user_id_1, user_id_2)
+ } else {
+ (user_id_2, user_id_1)
+ };
+
+ Ok(contact::Entity::find()
+ .filter(
+ contact::Column::UserIdA
+ .eq(id_a)
+ .and(contact::Column::UserIdB.eq(id_b))
+ .and(contact::Column::Accepted.eq(true)),
+ )
+ .one(&tx)
+ .await?
+ .is_some())
+ })
+ .await
+ }
+
+ pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
+ self.transact(|mut tx| async move {
+ let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
+ (sender_id, receiver_id, true)
+ } else {
+ (receiver_id, sender_id, false)
+ };
+
+ let rows_affected = contact::Entity::insert(contact::ActiveModel {
+ user_id_a: ActiveValue::set(id_a),
+ user_id_b: ActiveValue::set(id_b),
+ a_to_b: ActiveValue::set(a_to_b),
+ accepted: ActiveValue::set(false),
+ should_notify: ActiveValue::set(true),
+ ..Default::default()
+ })
+ .on_conflict(
+ OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB])
+ .values([
+ (contact::Column::Accepted, true.into()),
+ (contact::Column::ShouldNotify, false.into()),
+ ])
+ .action_and_where(
+ contact::Column::Accepted.eq(false).and(
+ contact::Column::AToB
+ .eq(a_to_b)
+ .and(contact::Column::UserIdA.eq(id_b))
+ .or(contact::Column::AToB
+ .ne(a_to_b)
+ .and(contact::Column::UserIdA.eq(id_a))),
+ ),
+ )
+ .to_owned(),
+ )
+ .exec_without_returning(&tx)
+ .await?;
+
+ if rows_affected == 1 {
+ tx.commit().await?;
+ Ok(())
+ } else {
+ Err(anyhow!("contact already requested"))?
+ }
+ })
+ .await
+ }
+
+ pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
+ self.transact(|mut tx| async move {
+ // let (id_a, id_b) = if responder_id < requester_id {
+ // (responder_id, requester_id)
+ // } else {
+ // (requester_id, responder_id)
+ // };
+ // let query = "
+ // DELETE FROM contacts
+ // WHERE user_id_a = $1 AND user_id_b = $2;
+ // ";
+ // let result = sqlx::query(query)
+ // .bind(id_a.0)
+ // .bind(id_b.0)
+ // .execute(&mut tx)
+ // .await?;
+
+ // if result.rows_affected() == 1 {
+ // tx.commit().await?;
+ // Ok(())
+ // } else {
+ // Err(anyhow!("no such contact"))?
+ // }
+ todo!()
+ })
+ .await
+ }
+
+ pub async fn dismiss_contact_notification(
+ &self,
+ user_id: UserId,
+ contact_user_id: UserId,
+ ) -> Result<()> {
+ self.transact(|tx| async move {
+ let (id_a, id_b, a_to_b) = if user_id < contact_user_id {
+ (user_id, contact_user_id, true)
+ } else {
+ (contact_user_id, user_id, false)
+ };
+
+ let result = contact::Entity::update_many()
+ .set(contact::ActiveModel {
+ should_notify: ActiveValue::set(false),
+ ..Default::default()
+ })
+ .filter(
+ contact::Column::UserIdA
+ .eq(id_a)
+ .and(contact::Column::UserIdB.eq(id_b))
+ .and(
+ contact::Column::AToB
+ .eq(a_to_b)
+ .and(contact::Column::Accepted.eq(true))
+ .or(contact::Column::AToB
+ .ne(a_to_b)
+ .and(contact::Column::Accepted.eq(false))),
+ ),
+ )
+ .exec(&tx)
+ .await?;
+ if result.rows_affected == 0 {
+ Err(anyhow!("no such contact request"))?
+ } else {
+ tx.commit().await?;
+ Ok(())
+ }
+ })
+ .await
+ }
+
+ pub async fn respond_to_contact_request(
+ &self,
+ responder_id: UserId,
+ requester_id: UserId,
+ accept: bool,
+ ) -> Result<()> {
+ self.transact(|tx| async move {
+ let (id_a, id_b, a_to_b) = if responder_id < requester_id {
+ (responder_id, requester_id, false)
+ } else {
+ (requester_id, responder_id, true)
+ };
+ let rows_affected = if accept {
+ let result = contact::Entity::update_many()
+ .set(contact::ActiveModel {
+ accepted: ActiveValue::set(true),
+ should_notify: ActiveValue::set(true),
+ ..Default::default()
+ })
+ .filter(
+ contact::Column::UserIdA
+ .eq(id_a)
+ .and(contact::Column::UserIdB.eq(id_b))
+ .and(contact::Column::AToB.eq(a_to_b)),
+ )
+ .exec(&tx)
+ .await?;
+ result.rows_affected
+ } else {
+ let result = contact::Entity::delete_many()
+ .filter(
+ contact::Column::UserIdA
+ .eq(id_a)
+ .and(contact::Column::UserIdB.eq(id_b))
+ .and(contact::Column::AToB.eq(a_to_b))
+ .and(contact::Column::Accepted.eq(false)),
+ )
+ .exec(&tx)
+ .await?;
+
+ result.rows_affected
+ };
+
+ if rows_affected == 1 {
+ tx.commit().await?;
+ Ok(())
+ } else {
+ Err(anyhow!("no such contact request"))?
+ }
+ })
+ .await
+ }
+
+ // projects
+
pub async fn share_project(
&self,
room_id: RoomId,
@@ -632,6 +925,7 @@ macro_rules! id_type {
}
id_type!(AccessTokenId);
+id_type!(ContactId);
id_type!(UserId);
id_type!(RoomId);
id_type!(RoomParticipantId);
@@ -0,0 +1,58 @@
+use super::{ContactId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "contacts")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: ContactId,
+ pub user_id_a: UserId,
+ pub user_id_b: UserId,
+ pub a_to_b: bool,
+ pub should_notify: bool,
+ pub accepted: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::room_participant::Entity",
+ from = "Column::UserIdA",
+ to = "super::room_participant::Column::UserId"
+ )]
+ UserARoomParticipant,
+ #[sea_orm(
+ belongs_to = "super::room_participant::Entity",
+ from = "Column::UserIdB",
+ to = "super::room_participant::Column::UserId"
+ )]
+ UserBRoomParticipant,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Contact {
+ Accepted {
+ user_id: UserId,
+ should_notify: bool,
+ busy: bool,
+ },
+ Outgoing {
+ user_id: UserId,
+ },
+ Incoming {
+ user_id: UserId,
+ should_notify: bool,
+ },
+}
+
+impl Contact {
+ pub fn user_id(&self) -> UserId {
+ match self {
+ Contact::Accepted { user_id, .. } => *user_id,
+ Contact::Outgoing { user_id } => *user_id,
+ Contact::Incoming { user_id, .. } => *user_id,
+ }
+ }
+}
@@ -18,6 +18,12 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::user::Entity",
+ from = "Column::UserId",
+ to = "super::user::Column::Id"
+ )]
+ User,
#[sea_orm(
belongs_to = "super::room::Entity",
from = "Column::RoomId",
@@ -26,6 +32,12 @@ pub enum Relation {
Room,
}
+impl Related<super::user::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::User.def()
+ }
+}
+
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
@@ -192,174 +192,174 @@ test_both_dbs!(
}
);
-// test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
-// let mut user_ids = Vec::new();
-// for i in 0..3 {
-// user_ids.push(
-// db.create_user(
-// &format!("user{i}@example.com"),
-// false,
-// NewUserParams {
-// github_login: format!("user{i}"),
-// github_user_id: i,
-// invite_count: 0,
-// },
-// )
-// .await
-// .unwrap()
-// .user_id,
-// );
-// }
-
-// let user_1 = user_ids[0];
-// let user_2 = user_ids[1];
-// let user_3 = user_ids[2];
-
-// // User starts with no contacts
-// assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
-
-// // User requests a contact. Both users see the pending request.
-// db.send_contact_request(user_1, user_2).await.unwrap();
-// assert!(!db.has_contact(user_1, user_2).await.unwrap());
-// assert!(!db.has_contact(user_2, user_1).await.unwrap());
-// assert_eq!(
-// db.get_contacts(user_1).await.unwrap(),
-// &[Contact::Outgoing { user_id: user_2 }],
-// );
-// assert_eq!(
-// db.get_contacts(user_2).await.unwrap(),
-// &[Contact::Incoming {
-// user_id: user_1,
-// should_notify: true
-// }]
-// );
-
-// // User 2 dismisses the contact request notification without accepting or rejecting.
-// // We shouldn't notify them again.
-// db.dismiss_contact_notification(user_1, user_2)
-// .await
-// .unwrap_err();
-// db.dismiss_contact_notification(user_2, user_1)
-// .await
-// .unwrap();
-// assert_eq!(
-// db.get_contacts(user_2).await.unwrap(),
-// &[Contact::Incoming {
-// user_id: user_1,
-// should_notify: false
-// }]
-// );
-
-// // User can't accept their own contact request
-// db.respond_to_contact_request(user_1, user_2, true)
-// .await
-// .unwrap_err();
-
-// // User accepts a contact request. Both users see the contact.
-// db.respond_to_contact_request(user_2, user_1, true)
-// .await
-// .unwrap();
-// assert_eq!(
-// db.get_contacts(user_1).await.unwrap(),
-// &[Contact::Accepted {
-// user_id: user_2,
-// should_notify: true,
-// busy: false,
-// }],
-// );
-// assert!(db.has_contact(user_1, user_2).await.unwrap());
-// assert!(db.has_contact(user_2, user_1).await.unwrap());
-// assert_eq!(
-// db.get_contacts(user_2).await.unwrap(),
-// &[Contact::Accepted {
-// user_id: user_1,
-// should_notify: false,
-// busy: false,
-// }]
-// );
-
-// // Users cannot re-request existing contacts.
-// db.send_contact_request(user_1, user_2).await.unwrap_err();
-// db.send_contact_request(user_2, user_1).await.unwrap_err();
-
-// // Users can't dismiss notifications of them accepting other users' requests.
-// db.dismiss_contact_notification(user_2, user_1)
-// .await
-// .unwrap_err();
-// assert_eq!(
-// db.get_contacts(user_1).await.unwrap(),
-// &[Contact::Accepted {
-// user_id: user_2,
-// should_notify: true,
-// busy: false,
-// }]
-// );
-
-// // Users can dismiss notifications of other users accepting their requests.
-// db.dismiss_contact_notification(user_1, user_2)
-// .await
-// .unwrap();
-// assert_eq!(
-// db.get_contacts(user_1).await.unwrap(),
-// &[Contact::Accepted {
-// user_id: user_2,
-// should_notify: false,
-// busy: false,
-// }]
-// );
+test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
+ let mut user_ids = Vec::new();
+ for i in 0..3 {
+ user_ids.push(
+ db.create_user(
+ &format!("user{i}@example.com"),
+ false,
+ NewUserParams {
+ github_login: format!("user{i}"),
+ github_user_id: i,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id,
+ );
+ }
-// // Users send each other concurrent contact requests and
-// // see that they are immediately accepted.
-// db.send_contact_request(user_1, user_3).await.unwrap();
-// db.send_contact_request(user_3, user_1).await.unwrap();
-// assert_eq!(
-// db.get_contacts(user_1).await.unwrap(),
-// &[
-// Contact::Accepted {
-// user_id: user_2,
-// should_notify: false,
-// busy: false,
-// },
-// Contact::Accepted {
-// user_id: user_3,
-// should_notify: false,
-// busy: false,
-// }
-// ]
-// );
-// assert_eq!(
-// db.get_contacts(user_3).await.unwrap(),
-// &[Contact::Accepted {
-// user_id: user_1,
-// should_notify: false,
-// busy: false,
-// }],
-// );
+ let user_1 = user_ids[0];
+ let user_2 = user_ids[1];
+ let user_3 = user_ids[2];
+
+ // User starts with no contacts
+ assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
+
+ // User requests a contact. Both users see the pending request.
+ db.send_contact_request(user_1, user_2).await.unwrap();
+ assert!(!db.has_contact(user_1, user_2).await.unwrap());
+ assert!(!db.has_contact(user_2, user_1).await.unwrap());
+ assert_eq!(
+ db.get_contacts(user_1).await.unwrap(),
+ &[Contact::Outgoing { user_id: user_2 }],
+ );
+ assert_eq!(
+ db.get_contacts(user_2).await.unwrap(),
+ &[Contact::Incoming {
+ user_id: user_1,
+ should_notify: true
+ }]
+ );
+
+ // User 2 dismisses the contact request notification without accepting or rejecting.
+ // We shouldn't notify them again.
+ db.dismiss_contact_notification(user_1, user_2)
+ .await
+ .unwrap_err();
+ db.dismiss_contact_notification(user_2, user_1)
+ .await
+ .unwrap();
+ assert_eq!(
+ db.get_contacts(user_2).await.unwrap(),
+ &[Contact::Incoming {
+ user_id: user_1,
+ should_notify: false
+ }]
+ );
+
+ // User can't accept their own contact request
+ db.respond_to_contact_request(user_1, user_2, true)
+ .await
+ .unwrap_err();
-// // User declines a contact request. Both users see that it is gone.
-// db.send_contact_request(user_2, user_3).await.unwrap();
-// db.respond_to_contact_request(user_3, user_2, false)
-// .await
-// .unwrap();
-// assert!(!db.has_contact(user_2, user_3).await.unwrap());
-// assert!(!db.has_contact(user_3, user_2).await.unwrap());
-// assert_eq!(
-// db.get_contacts(user_2).await.unwrap(),
-// &[Contact::Accepted {
-// user_id: user_1,
-// should_notify: false,
-// busy: false,
-// }]
-// );
-// assert_eq!(
-// db.get_contacts(user_3).await.unwrap(),
-// &[Contact::Accepted {
-// user_id: user_1,
-// should_notify: false,
-// busy: false,
-// }],
-// );
-// });
+ // User accepts a contact request. Both users see the contact.
+ db.respond_to_contact_request(user_2, user_1, true)
+ .await
+ .unwrap();
+ assert_eq!(
+ db.get_contacts(user_1).await.unwrap(),
+ &[Contact::Accepted {
+ user_id: user_2,
+ should_notify: true,
+ busy: false,
+ }],
+ );
+ assert!(db.has_contact(user_1, user_2).await.unwrap());
+ assert!(db.has_contact(user_2, user_1).await.unwrap());
+ assert_eq!(
+ db.get_contacts(user_2).await.unwrap(),
+ &[Contact::Accepted {
+ user_id: user_1,
+ should_notify: false,
+ busy: false,
+ }]
+ );
+
+ // Users cannot re-request existing contacts.
+ db.send_contact_request(user_1, user_2).await.unwrap_err();
+ db.send_contact_request(user_2, user_1).await.unwrap_err();
+
+ // Users can't dismiss notifications of them accepting other users' requests.
+ db.dismiss_contact_notification(user_2, user_1)
+ .await
+ .unwrap_err();
+ assert_eq!(
+ db.get_contacts(user_1).await.unwrap(),
+ &[Contact::Accepted {
+ user_id: user_2,
+ should_notify: true,
+ busy: false,
+ }]
+ );
+
+ // Users can dismiss notifications of other users accepting their requests.
+ db.dismiss_contact_notification(user_1, user_2)
+ .await
+ .unwrap();
+ assert_eq!(
+ db.get_contacts(user_1).await.unwrap(),
+ &[Contact::Accepted {
+ user_id: user_2,
+ should_notify: false,
+ busy: false,
+ }]
+ );
+
+ // Users send each other concurrent contact requests and
+ // see that they are immediately accepted.
+ db.send_contact_request(user_1, user_3).await.unwrap();
+ db.send_contact_request(user_3, user_1).await.unwrap();
+ assert_eq!(
+ db.get_contacts(user_1).await.unwrap(),
+ &[
+ Contact::Accepted {
+ user_id: user_2,
+ should_notify: false,
+ busy: false,
+ },
+ Contact::Accepted {
+ user_id: user_3,
+ should_notify: false,
+ busy: false,
+ }
+ ]
+ );
+ assert_eq!(
+ db.get_contacts(user_3).await.unwrap(),
+ &[Contact::Accepted {
+ user_id: user_1,
+ should_notify: false,
+ busy: false,
+ }],
+ );
+
+ // User declines a contact request. Both users see that it is gone.
+ db.send_contact_request(user_2, user_3).await.unwrap();
+ db.respond_to_contact_request(user_3, user_2, false)
+ .await
+ .unwrap();
+ assert!(!db.has_contact(user_2, user_3).await.unwrap());
+ assert!(!db.has_contact(user_3, user_2).await.unwrap());
+ assert_eq!(
+ db.get_contacts(user_2).await.unwrap(),
+ &[Contact::Accepted {
+ user_id: user_1,
+ should_notify: false,
+ busy: false,
+ }]
+ );
+ assert_eq!(
+ db.get_contacts(user_3).await.unwrap(),
+ &[Contact::Accepted {
+ user_id: user_1,
+ should_notify: false,
+ busy: false,
+ }],
+ );
+});
test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
let NewUserResult {
@@ -20,6 +20,8 @@ pub struct Model {
pub enum Relation {
#[sea_orm(has_many = "super::access_token::Entity")]
AccessToken,
+ #[sea_orm(has_one = "super::room_participant::Entity")]
+ RoomParticipant,
}
impl Related<super::access_token::Entity> for Entity {
@@ -28,4 +30,10 @@ impl Related<super::access_token::Entity> for Entity {
}
}
+impl Related<super::room_participant::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::RoomParticipant.def()
+ }
+}
+
impl ActiveModelBehavior for ActiveModel {}