Merge branch 'main' into guest-exp
This commit is contained in:
commit
ea4e67fb76
141 changed files with 6720 additions and 2077 deletions
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use sea_orm::sea_query::Query;
|
||||
|
||||
impl Database {
|
||||
pub async fn create_access_token(
|
||||
|
|
|
@ -349,11 +349,11 @@ impl Database {
|
|||
&self,
|
||||
channel_id: ChannelId,
|
||||
invitee_id: UserId,
|
||||
admin_id: UserId,
|
||||
inviter_id: UserId,
|
||||
role: ChannelRole,
|
||||
) -> Result<Channel> {
|
||||
) -> Result<InviteMemberResult> {
|
||||
self.transaction(move |tx| async move {
|
||||
self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
|
||||
self.check_user_is_channel_admin(channel_id, inviter_id, &*tx)
|
||||
.await?;
|
||||
|
||||
channel_member::ActiveModel {
|
||||
|
@ -371,11 +371,31 @@ impl Database {
|
|||
.await?
|
||||
.unwrap();
|
||||
|
||||
Ok(Channel {
|
||||
let channel = Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility,
|
||||
name: channel.name,
|
||||
role,
|
||||
};
|
||||
|
||||
let notifications = self
|
||||
.create_notification(
|
||||
invitee_id,
|
||||
rpc::Notification::ChannelInvitation {
|
||||
channel_id: channel_id.to_proto(),
|
||||
channel_name: channel.name.clone(),
|
||||
inviter_id: inviter_id.to_proto(),
|
||||
},
|
||||
true,
|
||||
&*tx,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
Ok(InviteMemberResult {
|
||||
channel,
|
||||
notifications,
|
||||
})
|
||||
})
|
||||
.await
|
||||
|
@ -445,9 +465,9 @@ impl Database {
|
|||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
accept: bool,
|
||||
) -> Result<Option<MembershipUpdated>> {
|
||||
) -> Result<RespondToChannelInvite> {
|
||||
self.transaction(move |tx| async move {
|
||||
if accept {
|
||||
let membership_update = if accept {
|
||||
let rows_affected = channel_member::Entity::update_many()
|
||||
.set(channel_member::ActiveModel {
|
||||
accepted: ActiveValue::Set(accept),
|
||||
|
@ -467,26 +487,45 @@ impl Database {
|
|||
Err(anyhow!("no such invitation"))?;
|
||||
}
|
||||
|
||||
return Ok(Some(
|
||||
Some(
|
||||
self.calculate_membership_updated(channel_id, user_id, &*tx)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
)
|
||||
} else {
|
||||
let rows_affected = channel_member::Entity::delete_many()
|
||||
.filter(
|
||||
channel_member::Column::ChannelId
|
||||
.eq(channel_id)
|
||||
.and(channel_member::Column::UserId.eq(user_id))
|
||||
.and(channel_member::Column::Accepted.eq(false)),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?
|
||||
.rows_affected;
|
||||
if rows_affected == 0 {
|
||||
Err(anyhow!("no such invitation"))?;
|
||||
}
|
||||
|
||||
let rows_affected = channel_member::ActiveModel {
|
||||
channel_id: ActiveValue::Unchanged(channel_id),
|
||||
user_id: ActiveValue::Unchanged(user_id),
|
||||
..Default::default()
|
||||
}
|
||||
.delete(&*tx)
|
||||
.await?
|
||||
.rows_affected;
|
||||
None
|
||||
};
|
||||
|
||||
if rows_affected == 0 {
|
||||
Err(anyhow!("no such invitation"))?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(RespondToChannelInvite {
|
||||
membership_update,
|
||||
notifications: self
|
||||
.mark_notification_as_read_with_response(
|
||||
user_id,
|
||||
&rpc::Notification::ChannelInvitation {
|
||||
channel_id: channel_id.to_proto(),
|
||||
channel_name: Default::default(),
|
||||
inviter_id: Default::default(),
|
||||
},
|
||||
accept,
|
||||
&*tx,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -550,7 +589,7 @@ impl Database {
|
|||
channel_id: ChannelId,
|
||||
member_id: UserId,
|
||||
admin_id: UserId,
|
||||
) -> Result<MembershipUpdated> {
|
||||
) -> Result<RemoveChannelMemberResult> {
|
||||
self.transaction(|tx| async move {
|
||||
self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
|
||||
.await?;
|
||||
|
@ -568,9 +607,22 @@ impl Database {
|
|||
Err(anyhow!("no such member"))?;
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.calculate_membership_updated(channel_id, member_id, &*tx)
|
||||
.await?)
|
||||
Ok(RemoveChannelMemberResult {
|
||||
membership_update: self
|
||||
.calculate_membership_updated(channel_id, member_id, &*tx)
|
||||
.await?,
|
||||
notification_id: self
|
||||
.remove_notification(
|
||||
member_id,
|
||||
rpc::Notification::ChannelInvitation {
|
||||
channel_id: channel_id.to_proto(),
|
||||
channel_name: Default::default(),
|
||||
inviter_id: Default::default(),
|
||||
},
|
||||
&*tx,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -911,6 +963,47 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn get_channel_participant_details(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
) -> Result<Vec<proto::ChannelMember>> {
|
||||
let (role, members) = self
|
||||
.transaction(move |tx| async move {
|
||||
let role = self
|
||||
.check_user_is_channel_participant(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
Ok((
|
||||
role,
|
||||
self.get_channel_participant_details_internal(channel_id, &*tx)
|
||||
.await?,
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
|
||||
if role == ChannelRole::Admin {
|
||||
Ok(members
|
||||
.into_iter()
|
||||
.map(|channel_member| channel_member.to_proto())
|
||||
.collect())
|
||||
} else {
|
||||
return Ok(members
|
||||
.into_iter()
|
||||
.filter_map(|member| {
|
||||
if member.kind == proto::channel_member::Kind::Invitee {
|
||||
return None;
|
||||
}
|
||||
Some(ChannelMember {
|
||||
role: member.role,
|
||||
user_id: member.user_id,
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
})
|
||||
})
|
||||
.map(|channel_member| channel_member.to_proto())
|
||||
.collect());
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_channel_participant_details_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -1003,28 +1096,6 @@ impl Database {
|
|||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_channel_participant_details(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
admin_id: UserId,
|
||||
) -> Result<Vec<proto::ChannelMember>> {
|
||||
let members = self
|
||||
.transaction(move |tx| async move {
|
||||
self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
Ok(self
|
||||
.get_channel_participant_details_internal(channel_id, &*tx)
|
||||
.await?)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(members
|
||||
.into_iter()
|
||||
.map(|channel_member| channel_member.to_proto())
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_channel_participants(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -1062,9 +1133,10 @@ impl Database {
|
|||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
match self.channel_role_for_user(channel_id, user_id, tx).await? {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(()),
|
||||
) -> Result<ChannelRole> {
|
||||
let channel_role = self.channel_role_for_user(channel_id, user_id, tx).await?;
|
||||
match channel_role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
|
||||
Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
|
||||
"user is not a channel member or channel does not exist"
|
||||
))?,
|
||||
|
|
|
@ -8,7 +8,6 @@ impl Database {
|
|||
user_id_b: UserId,
|
||||
a_to_b: bool,
|
||||
accepted: bool,
|
||||
should_notify: bool,
|
||||
user_a_busy: bool,
|
||||
user_b_busy: bool,
|
||||
}
|
||||
|
@ -53,7 +52,6 @@ impl Database {
|
|||
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 {
|
||||
|
@ -63,19 +61,16 @@ impl Database {
|
|||
} 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 {
|
||||
|
@ -124,7 +119,11 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
|
||||
pub async fn send_contact_request(
|
||||
&self,
|
||||
sender_id: UserId,
|
||||
receiver_id: UserId,
|
||||
) -> Result<NotificationBatch> {
|
||||
self.transaction(|tx| async move {
|
||||
let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
|
||||
(sender_id, receiver_id, true)
|
||||
|
@ -161,11 +160,22 @@ impl Database {
|
|||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
if rows_affected == 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("contact already requested"))?
|
||||
if rows_affected == 0 {
|
||||
Err(anyhow!("contact already requested"))?;
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.create_notification(
|
||||
receiver_id,
|
||||
rpc::Notification::ContactRequest {
|
||||
sender_id: sender_id.to_proto(),
|
||||
},
|
||||
true,
|
||||
&*tx,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -179,7 +189,11 @@ impl Database {
|
|||
///
|
||||
/// * `requester_id` - The user that initiates this request
|
||||
/// * `responder_id` - The user that will be removed
|
||||
pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<bool> {
|
||||
pub async fn remove_contact(
|
||||
&self,
|
||||
requester_id: UserId,
|
||||
responder_id: UserId,
|
||||
) -> Result<(bool, Option<NotificationId>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let (id_a, id_b) = if responder_id < requester_id {
|
||||
(responder_id, requester_id)
|
||||
|
@ -198,7 +212,21 @@ impl Database {
|
|||
.ok_or_else(|| anyhow!("no such contact"))?;
|
||||
|
||||
contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
|
||||
Ok(contact.accepted)
|
||||
|
||||
let mut deleted_notification_id = None;
|
||||
if !contact.accepted {
|
||||
deleted_notification_id = self
|
||||
.remove_notification(
|
||||
responder_id,
|
||||
rpc::Notification::ContactRequest {
|
||||
sender_id: requester_id.to_proto(),
|
||||
},
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((contact.accepted, deleted_notification_id))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -249,7 +277,7 @@ impl Database {
|
|||
responder_id: UserId,
|
||||
requester_id: UserId,
|
||||
accept: bool,
|
||||
) -> Result<()> {
|
||||
) -> Result<NotificationBatch> {
|
||||
self.transaction(|tx| async move {
|
||||
let (id_a, id_b, a_to_b) = if responder_id < requester_id {
|
||||
(responder_id, requester_id, false)
|
||||
|
@ -287,11 +315,38 @@ impl Database {
|
|||
result.rows_affected
|
||||
};
|
||||
|
||||
if rows_affected == 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
if rows_affected == 0 {
|
||||
Err(anyhow!("no such contact request"))?
|
||||
}
|
||||
|
||||
let mut notifications = Vec::new();
|
||||
notifications.extend(
|
||||
self.mark_notification_as_read_with_response(
|
||||
responder_id,
|
||||
&rpc::Notification::ContactRequest {
|
||||
sender_id: requester_id.to_proto(),
|
||||
},
|
||||
accept,
|
||||
&*tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
if accept {
|
||||
notifications.extend(
|
||||
self.create_notification(
|
||||
requester_id,
|
||||
rpc::Notification::ContactRequestAccepted {
|
||||
responder_id: responder_id.to_proto(),
|
||||
},
|
||||
true,
|
||||
&*tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(notifications)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use super::*;
|
||||
use futures::Stream;
|
||||
use rpc::Notification;
|
||||
use sea_orm::TryInsertResult;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
impl Database {
|
||||
|
@ -87,43 +90,118 @@ impl Database {
|
|||
condition = condition.add(channel_message::Column::Id.lt(before_message_id));
|
||||
}
|
||||
|
||||
let mut rows = channel_message::Entity::find()
|
||||
let rows = channel_message::Entity::find()
|
||||
.filter(condition)
|
||||
.order_by_desc(channel_message::Column::Id)
|
||||
.limit(count as u64)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
let nonce = row.nonce.as_u64_pair();
|
||||
messages.push(proto::ChannelMessage {
|
||||
id: row.id.to_proto(),
|
||||
sender_id: row.sender_id.to_proto(),
|
||||
body: row.body,
|
||||
timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
|
||||
nonce: Some(proto::Nonce {
|
||||
upper_half: nonce.0,
|
||||
lower_half: nonce.1,
|
||||
self.load_channel_messages(rows, &*tx).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_channel_messages_by_id(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
message_ids: &[MessageId],
|
||||
) -> Result<Vec<proto::ChannelMessage>> {
|
||||
self.transaction(|tx| async move {
|
||||
let rows = channel_message::Entity::find()
|
||||
.filter(channel_message::Column::Id.is_in(message_ids.iter().copied()))
|
||||
.order_by_desc(channel_message::Column::Id)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut channel_ids = HashSet::<ChannelId>::default();
|
||||
let messages = self
|
||||
.load_channel_messages(
|
||||
rows.map(|row| {
|
||||
row.map(|row| {
|
||||
channel_ids.insert(row.channel_id);
|
||||
row
|
||||
})
|
||||
}),
|
||||
});
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for channel_id in channel_ids {
|
||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
}
|
||||
drop(rows);
|
||||
messages.reverse();
|
||||
|
||||
Ok(messages)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_channel_messages(
|
||||
&self,
|
||||
mut rows: impl Send + Unpin + Stream<Item = Result<channel_message::Model, sea_orm::DbErr>>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::ChannelMessage>> {
|
||||
let mut messages = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
let nonce = row.nonce.as_u64_pair();
|
||||
messages.push(proto::ChannelMessage {
|
||||
id: row.id.to_proto(),
|
||||
sender_id: row.sender_id.to_proto(),
|
||||
body: row.body,
|
||||
timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
|
||||
mentions: vec![],
|
||||
nonce: Some(proto::Nonce {
|
||||
upper_half: nonce.0,
|
||||
lower_half: nonce.1,
|
||||
}),
|
||||
});
|
||||
}
|
||||
drop(rows);
|
||||
messages.reverse();
|
||||
|
||||
let mut mentions = channel_message_mention::Entity::find()
|
||||
.filter(channel_message_mention::Column::MessageId.is_in(messages.iter().map(|m| m.id)))
|
||||
.order_by_asc(channel_message_mention::Column::MessageId)
|
||||
.order_by_asc(channel_message_mention::Column::StartOffset)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut message_ix = 0;
|
||||
while let Some(mention) = mentions.next().await {
|
||||
let mention = mention?;
|
||||
let message_id = mention.message_id.to_proto();
|
||||
while let Some(message) = messages.get_mut(message_ix) {
|
||||
if message.id < message_id {
|
||||
message_ix += 1;
|
||||
} else {
|
||||
if message.id == message_id {
|
||||
message.mentions.push(proto::ChatMention {
|
||||
range: Some(proto::Range {
|
||||
start: mention.start_offset as u64,
|
||||
end: mention.end_offset as u64,
|
||||
}),
|
||||
user_id: mention.user_id.to_proto(),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
pub async fn create_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
body: &str,
|
||||
mentions: &[proto::ChatMention],
|
||||
timestamp: OffsetDateTime,
|
||||
nonce: u128,
|
||||
) -> Result<(MessageId, Vec<ConnectionId>, Vec<UserId>)> {
|
||||
) -> Result<CreatedChannelMessage> {
|
||||
self.transaction(|tx| async move {
|
||||
self.check_user_is_channel_participant(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
|
@ -153,7 +231,7 @@ impl Database {
|
|||
let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
|
||||
let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
|
||||
|
||||
let message = channel_message::Entity::insert(channel_message::ActiveModel {
|
||||
let result = channel_message::Entity::insert(channel_message::ActiveModel {
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
sender_id: ActiveValue::Set(user_id),
|
||||
body: ActiveValue::Set(body.to_string()),
|
||||
|
@ -162,35 +240,85 @@ impl Database {
|
|||
id: ActiveValue::NotSet,
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(channel_message::Column::Nonce)
|
||||
.update_column(channel_message::Column::Nonce)
|
||||
.to_owned(),
|
||||
OnConflict::columns([
|
||||
channel_message::Column::SenderId,
|
||||
channel_message::Column::Nonce,
|
||||
])
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.do_nothing()
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryConnectionId {
|
||||
ConnectionId,
|
||||
}
|
||||
let message_id;
|
||||
let mut notifications = Vec::new();
|
||||
match result {
|
||||
TryInsertResult::Inserted(result) => {
|
||||
message_id = result.last_insert_id;
|
||||
let mentioned_user_ids =
|
||||
mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
|
||||
let mentions = mentions
|
||||
.iter()
|
||||
.filter_map(|mention| {
|
||||
let range = mention.range.as_ref()?;
|
||||
if !body.is_char_boundary(range.start as usize)
|
||||
|| !body.is_char_boundary(range.end as usize)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(channel_message_mention::ActiveModel {
|
||||
message_id: ActiveValue::Set(message_id),
|
||||
start_offset: ActiveValue::Set(range.start as i32),
|
||||
end_offset: ActiveValue::Set(range.end as i32),
|
||||
user_id: ActiveValue::Set(UserId::from_proto(mention.user_id)),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !mentions.is_empty() {
|
||||
channel_message_mention::Entity::insert_many(mentions)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Observe this message for the sender
|
||||
self.observe_channel_message_internal(
|
||||
channel_id,
|
||||
user_id,
|
||||
message.last_insert_id,
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
for mentioned_user in mentioned_user_ids {
|
||||
notifications.extend(
|
||||
self.create_notification(
|
||||
UserId::from_proto(mentioned_user),
|
||||
rpc::Notification::ChannelMessageMention {
|
||||
message_id: message_id.to_proto(),
|
||||
sender_id: user_id.to_proto(),
|
||||
channel_id: channel_id.to_proto(),
|
||||
},
|
||||
false,
|
||||
&*tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
message_id = channel_message::Entity::find()
|
||||
.filter(channel_message::Column::Nonce.eq(Uuid::from_u128(nonce)))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("failed to insert message"))?
|
||||
.id;
|
||||
}
|
||||
}
|
||||
|
||||
let mut channel_members = self.get_channel_participants(channel_id, &*tx).await?;
|
||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||
|
||||
Ok((
|
||||
message.last_insert_id,
|
||||
Ok(CreatedChannelMessage {
|
||||
message_id,
|
||||
participant_connection_ids,
|
||||
channel_members,
|
||||
))
|
||||
notifications,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -200,11 +328,24 @@ impl Database {
|
|||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
message_id: MessageId,
|
||||
) -> Result<()> {
|
||||
) -> Result<NotificationBatch> {
|
||||
self.transaction(|tx| async move {
|
||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
let mut batch = NotificationBatch::default();
|
||||
batch.extend(
|
||||
self.mark_notification_as_read(
|
||||
user_id,
|
||||
&Notification::ChannelMessageMention {
|
||||
message_id: message_id.to_proto(),
|
||||
sender_id: Default::default(),
|
||||
channel_id: Default::default(),
|
||||
},
|
||||
&*tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
Ok(batch)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
262
crates/collab/src/db/queries/notifications.rs
Normal file
262
crates/collab/src/db/queries/notifications.rs
Normal file
|
@ -0,0 +1,262 @@
|
|||
use super::*;
|
||||
use rpc::Notification;
|
||||
|
||||
impl Database {
|
||||
pub async fn initialize_notification_kinds(&mut self) -> Result<()> {
|
||||
notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map(
|
||||
|kind| notification_kind::ActiveModel {
|
||||
name: ActiveValue::Set(kind.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.on_conflict(OnConflict::new().do_nothing().to_owned())
|
||||
.exec_without_returning(&self.pool)
|
||||
.await?;
|
||||
|
||||
let mut rows = notification_kind::Entity::find().stream(&self.pool).await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
self.notification_kinds_by_name.insert(row.name, row.id);
|
||||
}
|
||||
|
||||
for name in Notification::all_variant_names() {
|
||||
if let Some(id) = self.notification_kinds_by_name.get(*name).copied() {
|
||||
self.notification_kinds_by_id.insert(id, name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
limit: usize,
|
||||
before_id: Option<NotificationId>,
|
||||
) -> Result<Vec<proto::Notification>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut result = Vec::new();
|
||||
let mut condition =
|
||||
Condition::all().add(notification::Column::RecipientId.eq(recipient_id));
|
||||
|
||||
if let Some(before_id) = before_id {
|
||||
condition = condition.add(notification::Column::Id.lt(before_id));
|
||||
}
|
||||
|
||||
let mut rows = notification::Entity::find()
|
||||
.filter(condition)
|
||||
.order_by_desc(notification::Column::Id)
|
||||
.limit(limit as u64)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
let kind = row.kind;
|
||||
if let Some(proto) = model_to_proto(self, row) {
|
||||
result.push(proto);
|
||||
} else {
|
||||
log::warn!("unknown notification kind {:?}", kind);
|
||||
}
|
||||
}
|
||||
result.reverse();
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a notification. If `avoid_duplicates` is set to true, then avoid
|
||||
/// creating a new notification if the given recipient already has an
|
||||
/// unread notification with the given kind and entity id.
|
||||
pub async fn create_notification(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
notification: Notification,
|
||||
avoid_duplicates: bool,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<(UserId, proto::Notification)>> {
|
||||
if avoid_duplicates {
|
||||
if self
|
||||
.find_notification(recipient_id, ¬ification, tx)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
let proto = notification.to_proto();
|
||||
let kind = notification_kind_from_proto(self, &proto)?;
|
||||
let model = notification::ActiveModel {
|
||||
recipient_id: ActiveValue::Set(recipient_id),
|
||||
kind: ActiveValue::Set(kind),
|
||||
entity_id: ActiveValue::Set(proto.entity_id.map(|id| id as i32)),
|
||||
content: ActiveValue::Set(proto.content.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(Some((
|
||||
recipient_id,
|
||||
proto::Notification {
|
||||
id: model.id.as_ref().to_proto(),
|
||||
kind: proto.kind,
|
||||
timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64,
|
||||
is_read: false,
|
||||
response: None,
|
||||
content: proto.content,
|
||||
entity_id: proto.entity_id,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
/// Remove an unread notification with the given recipient, kind and
|
||||
/// entity id.
|
||||
pub async fn remove_notification(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
notification: Notification,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<NotificationId>> {
|
||||
let id = self
|
||||
.find_notification(recipient_id, ¬ification, tx)
|
||||
.await?;
|
||||
if let Some(id) = id {
|
||||
notification::Entity::delete_by_id(id).exec(tx).await?;
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Populate the response for the notification with the given kind and
|
||||
/// entity id.
|
||||
pub async fn mark_notification_as_read_with_response(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
notification: &Notification,
|
||||
response: bool,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<(UserId, proto::Notification)>> {
|
||||
self.mark_notification_as_read_internal(recipient_id, notification, Some(response), tx)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn mark_notification_as_read(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
notification: &Notification,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<(UserId, proto::Notification)>> {
|
||||
self.mark_notification_as_read_internal(recipient_id, notification, None, tx)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn mark_notification_as_read_by_id(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
notification_id: NotificationId,
|
||||
) -> Result<NotificationBatch> {
|
||||
self.transaction(|tx| async move {
|
||||
let row = notification::Entity::update(notification::ActiveModel {
|
||||
id: ActiveValue::Unchanged(notification_id),
|
||||
recipient_id: ActiveValue::Unchanged(recipient_id),
|
||||
is_read: ActiveValue::Set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
Ok(model_to_proto(self, row)
|
||||
.map(|notification| (recipient_id, notification))
|
||||
.into_iter()
|
||||
.collect())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn mark_notification_as_read_internal(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
notification: &Notification,
|
||||
response: Option<bool>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<(UserId, proto::Notification)>> {
|
||||
if let Some(id) = self
|
||||
.find_notification(recipient_id, notification, &*tx)
|
||||
.await?
|
||||
{
|
||||
let row = notification::Entity::update(notification::ActiveModel {
|
||||
id: ActiveValue::Unchanged(id),
|
||||
recipient_id: ActiveValue::Unchanged(recipient_id),
|
||||
is_read: ActiveValue::Set(true),
|
||||
response: if let Some(response) = response {
|
||||
ActiveValue::Set(Some(response))
|
||||
} else {
|
||||
ActiveValue::NotSet
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.exec(tx)
|
||||
.await?;
|
||||
Ok(model_to_proto(self, row).map(|notification| (recipient_id, notification)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an unread notification by its recipient, kind and entity id.
|
||||
async fn find_notification(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
notification: &Notification,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<NotificationId>> {
|
||||
let proto = notification.to_proto();
|
||||
let kind = notification_kind_from_proto(self, &proto)?;
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryIds {
|
||||
Id,
|
||||
}
|
||||
|
||||
Ok(notification::Entity::find()
|
||||
.select_only()
|
||||
.column(notification::Column::Id)
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(notification::Column::RecipientId.eq(recipient_id))
|
||||
.add(notification::Column::IsRead.eq(false))
|
||||
.add(notification::Column::Kind.eq(kind))
|
||||
.add(if proto.entity_id.is_some() {
|
||||
notification::Column::EntityId.eq(proto.entity_id)
|
||||
} else {
|
||||
notification::Column::EntityId.is_null()
|
||||
}),
|
||||
)
|
||||
.into_values::<_, QueryIds>()
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
fn model_to_proto(this: &Database, row: notification::Model) -> Option<proto::Notification> {
|
||||
let kind = this.notification_kinds_by_id.get(&row.kind)?;
|
||||
Some(proto::Notification {
|
||||
id: row.id.to_proto(),
|
||||
kind: kind.to_string(),
|
||||
timestamp: row.created_at.assume_utc().unix_timestamp() as u64,
|
||||
is_read: row.is_read,
|
||||
response: row.response,
|
||||
content: row.content,
|
||||
entity_id: row.entity_id.map(|id| id as u64),
|
||||
})
|
||||
}
|
||||
|
||||
fn notification_kind_from_proto(
|
||||
this: &Database,
|
||||
proto: &proto::Notification,
|
||||
) -> Result<NotificationKindId> {
|
||||
Ok(this
|
||||
.notification_kinds_by_name
|
||||
.get(&proto.kind)
|
||||
.copied()
|
||||
.ok_or_else(|| anyhow!("invalid notification kind {:?}", proto.kind))?)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue