summaryrefslogtreecommitdiff
path: root/src/message
diff options
context:
space:
mode:
Diffstat (limited to 'src/message')
-rw-r--r--src/message/app.rs14
-rw-r--r--src/message/history.rs12
-rw-r--r--src/message/repo.rs33
-rw-r--r--src/message/routes/message/mod.rs9
-rw-r--r--src/message/routes/message/test.rs37
5 files changed, 77 insertions, 28 deletions
diff --git a/src/message/app.rs b/src/message/app.rs
index eed6ba4..137a27d 100644
--- a/src/message/app.rs
+++ b/src/message/app.rs
@@ -45,16 +45,24 @@ impl<'a> Messages<'a> {
Ok(message.as_sent())
}
- pub async fn delete(&self, message: &Id, deleted_at: &DateTime) -> Result<(), DeleteError> {
+ pub async fn delete(
+ &self,
+ deleted_by: &Login,
+ message: &Id,
+ deleted_at: &DateTime,
+ ) -> Result<(), DeleteError> {
let mut tx = self.db.begin().await?;
let message = tx
.messages()
.by_id(message)
.await
.not_found(|| DeleteError::NotFound(message.clone()))?;
- message
+ let snapshot = message
.as_snapshot()
.ok_or_else(|| DeleteError::Deleted(message.id().clone()))?;
+ if snapshot.sender != deleted_by.id {
+ return Err(DeleteError::NotSender(deleted_by.clone()));
+ }
let deleted = tx.sequence().next(deleted_at).await?;
let message = tx.messages().delete(&message, &deleted).await?;
@@ -138,6 +146,8 @@ impl From<channel::repo::LoadError> for SendError {
pub enum DeleteError {
#[error("message {0} not found")]
NotFound(Id),
+ #[error("login {} not the message's sender", .0.id)]
+ NotSender(Login),
#[error("message {0} deleted")]
Deleted(Id),
#[error(transparent)]
diff --git a/src/message/history.rs b/src/message/history.rs
index 0424d0d..ed8f5df 100644
--- a/src/message/history.rs
+++ b/src/message/history.rs
@@ -1,8 +1,10 @@
+use itertools::Itertools as _;
+
use super::{
event::{Deleted, Event, Sent},
Id, Message,
};
-use crate::event::{Instant, ResumePoint, Sequence};
+use crate::event::{Instant, Sequence};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct History {
@@ -25,9 +27,9 @@ impl History {
self.message.clone()
}
- pub fn as_of(&self, resume_point: impl Into<ResumePoint>) -> Option<Message> {
+ pub fn as_of(&self, resume_point: Sequence) -> Option<Message> {
self.events()
- .filter(Sequence::up_to(resume_point.into()))
+ .filter(Sequence::up_to(resume_point))
.collect()
}
@@ -57,6 +59,8 @@ impl History {
}
pub fn events(&self) -> impl Iterator<Item = Event> {
- [self.sent()].into_iter().chain(self.deleted())
+ [self.sent()]
+ .into_iter()
+ .merge_by(self.deleted(), Sequence::merge)
}
}
diff --git a/src/message/repo.rs b/src/message/repo.rs
index c8ceceb..14f8eaf 100644
--- a/src/message/repo.rs
+++ b/src/message/repo.rs
@@ -4,7 +4,7 @@ use super::{snapshot::Message, Body, History, Id};
use crate::{
channel,
clock::DateTime,
- event::{Instant, ResumePoint, Sequence},
+ event::{Instant, Sequence},
login::{self, Login},
};
@@ -34,8 +34,8 @@ impl<'c> Messages<'c> {
let message = sqlx::query!(
r#"
insert into message
- (id, channel, sender, sent_at, sent_sequence, body)
- values ($1, $2, $3, $4, $5, $6)
+ (id, channel, sender, sent_at, sent_sequence, body, last_sequence)
+ values ($1, $2, $3, $4, $5, $6, $7)
returning
id as "id: Id",
channel as "channel: channel::Id",
@@ -50,6 +50,7 @@ impl<'c> Messages<'c> {
sent.at,
sent.sequence,
body,
+ sent.sequence,
)
.map(|row| History {
message: Message {
@@ -106,22 +107,22 @@ impl<'c> Messages<'c> {
Ok(messages)
}
- pub async fn all(&mut self, resume_at: ResumePoint) -> Result<Vec<History>, sqlx::Error> {
+ pub async fn all(&mut self, resume_at: Sequence) -> Result<Vec<History>, sqlx::Error> {
let messages = sqlx::query!(
r#"
select
message.channel as "channel: channel::Id",
message.sender as "sender: login::Id",
- id as "id: Id",
+ message.id as "id: Id",
message.body as "body: Body",
message.sent_at as "sent_at: DateTime",
message.sent_sequence as "sent_sequence: Sequence",
- deleted.deleted_at as "deleted_at: DateTime",
- deleted.deleted_sequence as "deleted_sequence: Sequence"
+ deleted.deleted_at as "deleted_at?: DateTime",
+ deleted.deleted_sequence as "deleted_sequence?: Sequence"
from message
left join message_deleted as deleted
using (id)
- where coalesce(message.sent_sequence <= $2, true)
+ where message.sent_sequence <= $1
order by message.sent_sequence
"#,
resume_at,
@@ -205,12 +206,14 @@ impl<'c> Messages<'c> {
sqlx::query!(
r#"
update message
- set body = ''
- where id = $1
+ set body = '', last_sequence = max(last_sequence, $1)
+ where id = $2
+ returning id as "id: Id"
"#,
+ deleted.sequence,
id,
)
- .execute(&mut *self.0)
+ .fetch_one(&mut *self.0)
.await?;
let message = self.by_id(id).await?;
@@ -282,7 +285,7 @@ impl<'c> Messages<'c> {
Ok(messages)
}
- pub async fn replay(&mut self, resume_at: ResumePoint) -> Result<Vec<History>, sqlx::Error> {
+ pub async fn replay(&mut self, resume_at: Sequence) -> Result<Vec<History>, sqlx::Error> {
let messages = sqlx::query!(
r#"
select
@@ -292,12 +295,12 @@ impl<'c> Messages<'c> {
message.sent_at as "sent_at: DateTime",
message.sent_sequence as "sent_sequence: Sequence",
message.body as "body: Body",
- deleted.deleted_at as "deleted_at: DateTime",
- deleted.deleted_sequence as "deleted_sequence: Sequence"
+ deleted.deleted_at as "deleted_at?: DateTime",
+ deleted.deleted_sequence as "deleted_sequence?: Sequence"
from message
left join message_deleted as deleted
using (id)
- where coalesce(message.sent_sequence > $1, true)
+ where message.last_sequence > $1
"#,
resume_at,
)
diff --git a/src/message/routes/message/mod.rs b/src/message/routes/message/mod.rs
index 45a7e9d..e92f556 100644
--- a/src/message/routes/message/mod.rs
+++ b/src/message/routes/message/mod.rs
@@ -20,9 +20,11 @@ pub mod delete {
State(app): State<App>,
Path(message): Path<message::Id>,
RequestedAt(deleted_at): RequestedAt,
- _: Identity,
+ identity: Identity,
) -> Result<Response, Error> {
- app.messages().delete(&message, &deleted_at).await?;
+ app.messages()
+ .delete(&identity.login, &message, &deleted_at)
+ .await?;
Ok(Response { id: message })
}
@@ -47,6 +49,9 @@ pub mod delete {
let Self(error) = self;
#[allow(clippy::match_wildcard_for_single_variants)]
match error {
+ DeleteError::NotSender(_) => {
+ (StatusCode::FORBIDDEN, error.to_string()).into_response()
+ }
DeleteError::NotFound(_) | DeleteError::Deleted(_) => {
NotFound(error).into_response()
}
diff --git a/src/message/routes/message/test.rs b/src/message/routes/message/test.rs
index ae89506..5178ab5 100644
--- a/src/message/routes/message/test.rs
+++ b/src/message/routes/message/test.rs
@@ -8,18 +8,17 @@ pub async fn delete_message() {
// Set up the environment
let app = fixtures::scratch_app().await;
- let sender = fixtures::login::create(&app, &fixtures::now()).await;
+ let sender = fixtures::identity::create(&app, &fixtures::now()).await;
let channel = fixtures::channel::create(&app, &fixtures::now()).await;
- let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
+ let message = fixtures::message::send(&app, &channel, &sender.login, &fixtures::now()).await;
// Send the request
- let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let response = delete::handler(
State(app.clone()),
Path(message.id.clone()),
fixtures::now(),
- deleter,
+ sender,
)
.await
.expect("deleting a valid message succeeds");
@@ -68,7 +67,7 @@ pub async fn delete_deleted() {
let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
app.messages()
- .delete(&message.id, &fixtures::now())
+ .delete(&sender, &message.id, &fixtures::now())
.await
.expect("deleting a recently-sent message succeeds");
@@ -155,3 +154,31 @@ pub async fn delete_purged() {
assert!(matches!(error, app::DeleteError::NotFound(id) if id == message.id));
}
+
+#[tokio::test]
+pub async fn delete_not_sender() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let sender = fixtures::login::create(&app, &fixtures::now()).await;
+ let channel = fixtures::channel::create(&app, &fixtures::now()).await;
+ let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
+
+ // Send the request
+
+ let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
+ let delete::Error(error) = delete::handler(
+ State(app.clone()),
+ Path(message.id.clone()),
+ fixtures::now(),
+ deleter.clone(),
+ )
+ .await
+ .expect_err("deleting a message someone else sent fails");
+
+ // Verify the response
+
+ assert!(
+ matches!(error, app::DeleteError::NotSender(error_sender) if deleter.login == error_sender)
+ );
+}