There's an issue with the presence of returns with serde_json.
They can be serialized from borrowed strings, but they cannot
be deserialized into borrowed strings. I haven't looked into
why, but it's easy to just Own Things ™️
Also:
- Fix self-federating for comment create & update (this will
likely need to be fixed for reacts when I get around to
that)
- Expose API to retrieve replies for a given comment
- Expose Comment Create & Update actions
387 lines
12 KiB
Rust
387 lines
12 KiB
Rust
use super::{Comment, CommentChanges, ReplyComment, StoreError, SubmissionComment, Undo};
|
|
use chrono::{DateTime, Utc};
|
|
use sled::{Db, Transactional, Tree};
|
|
use std::io::Cursor;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Store {
|
|
comment_tree: Tree,
|
|
submission_tree: Tree,
|
|
submission_created_tree: Tree,
|
|
reply_created_tree: Tree,
|
|
created_tree: Tree,
|
|
count_tree: Tree,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
|
struct StoredComment {
|
|
id: Uuid,
|
|
submission_id: Uuid,
|
|
profile_id: Uuid,
|
|
comment_id: Option<Uuid>,
|
|
body: String,
|
|
published: DateTime<Utc>,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl Store {
|
|
pub fn build(db: &Db) -> Result<Self, sled::Error> {
|
|
Ok(Store {
|
|
comment_tree: db.open_tree("profiles/comments")?,
|
|
submission_tree: db.open_tree("profiles/comments/submissions")?,
|
|
submission_created_tree: db.open_tree("profiles/comments/submissions/created")?,
|
|
reply_created_tree: db.open_tree("/profiles/comments/reply/created")?,
|
|
created_tree: db.open_tree("profiles/comments/created")?,
|
|
count_tree: db.open_tree("profiles/comments/count")?,
|
|
})
|
|
}
|
|
|
|
pub fn create(
|
|
&self,
|
|
submission_id: Uuid,
|
|
profile_id: Uuid,
|
|
comment_id: Option<Uuid>,
|
|
body: &str,
|
|
published: DateTime<Utc>,
|
|
) -> Result<Comment, StoreError> {
|
|
let mut id;
|
|
let mut stored_comment;
|
|
|
|
let now = Utc::now().into();
|
|
let mut stored_comment_vec = vec![];
|
|
|
|
while {
|
|
stored_comment_vec.clear();
|
|
let cursor = Cursor::new(&mut stored_comment_vec);
|
|
id = Uuid::new_v4();
|
|
stored_comment = StoredComment {
|
|
id,
|
|
submission_id,
|
|
profile_id,
|
|
comment_id,
|
|
body: body.to_owned(),
|
|
published,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
serde_json::to_writer(cursor, &stored_comment)?;
|
|
self.comment_tree
|
|
.compare_and_swap(
|
|
id_comment_key(id),
|
|
None as Option<&[u8]>,
|
|
Some(stored_comment_vec.as_slice()),
|
|
)?
|
|
.is_err()
|
|
} {}
|
|
|
|
let res = [
|
|
&self.submission_tree,
|
|
&self.submission_created_tree,
|
|
&self.reply_created_tree,
|
|
&self.created_tree,
|
|
&self.count_tree,
|
|
]
|
|
.transaction(move |trees| {
|
|
let submission_tree = &trees[0];
|
|
let submission_created_tree = &trees[1];
|
|
let reply_created_tree = &trees[2];
|
|
let created_tree = &trees[3];
|
|
let count_tree = &trees[4];
|
|
|
|
submission_tree.insert(
|
|
submission_id_key(submission_id, id).as_bytes(),
|
|
id.as_bytes(),
|
|
)?;
|
|
submission_created_tree.insert(
|
|
submission_id_created_comment_key(submission_id, now, id).as_bytes(),
|
|
id.as_bytes(),
|
|
)?;
|
|
created_tree.insert(created_comment_key(now, id).as_bytes(), id.as_bytes())?;
|
|
if let Some(comment_id) = comment_id {
|
|
reply_created_tree.insert(
|
|
reply_created_key(comment_id, now, id).as_bytes(),
|
|
id.as_bytes(),
|
|
)?;
|
|
}
|
|
|
|
super::count(
|
|
count_tree,
|
|
&submission_id_comment_count_key(submission_id),
|
|
|c| c.saturating_add(1),
|
|
)?;
|
|
|
|
Ok(())
|
|
});
|
|
|
|
if let Err(e) = res {
|
|
self.comment_tree.remove(id_comment_key(id))?;
|
|
return Err(e.into());
|
|
}
|
|
|
|
Ok(stored_comment.into())
|
|
}
|
|
|
|
pub fn count(&self, submission_id: Uuid) -> Result<u64, StoreError> {
|
|
match self
|
|
.count_tree
|
|
.get(submission_id_comment_count_key(submission_id))?
|
|
{
|
|
Some(ivec) => Ok(String::from_utf8_lossy(&ivec).parse::<u64>().unwrap_or(0)),
|
|
None => Ok(0),
|
|
}
|
|
}
|
|
|
|
pub fn update(&self, changes: &CommentChanges) -> Result<Comment, StoreError> {
|
|
let stored_comment_ivec = match self.comment_tree.get(id_comment_key(changes.id))? {
|
|
Some(ivec) => ivec,
|
|
None => return Err(StoreError::Missing),
|
|
};
|
|
|
|
let mut stored_comment: StoredComment = serde_json::from_slice(&stored_comment_ivec)?;
|
|
|
|
if let Some(body) = changes.body.as_ref() {
|
|
stored_comment.body = body.clone();
|
|
}
|
|
stored_comment.updated_at = Utc::now().into();
|
|
|
|
let stored_comment_vec = serde_json::to_vec(&stored_comment)?;
|
|
|
|
if self
|
|
.comment_tree
|
|
.compare_and_swap(
|
|
id_comment_key(changes.id),
|
|
Some(&stored_comment_ivec),
|
|
Some(stored_comment_vec.as_slice()),
|
|
)?
|
|
.is_err()
|
|
{
|
|
return Err(StoreError::DoubleStore);
|
|
}
|
|
|
|
Ok(stored_comment.into())
|
|
}
|
|
|
|
pub fn replies_for(&self, comment_id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> {
|
|
self.reply_created_tree
|
|
.scan_prefix(reply_created_prefix(comment_id))
|
|
.values()
|
|
.filter_map(|res| res.ok())
|
|
.filter_map(uuid_from_ivec)
|
|
}
|
|
|
|
pub fn for_submission(&self, submission_id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> {
|
|
self.submission_created_tree
|
|
.scan_prefix(submission_id_created_prefix(submission_id))
|
|
.values()
|
|
.filter_map(|res| res.ok())
|
|
.filter_map(uuid_from_ivec)
|
|
.rev()
|
|
}
|
|
|
|
fn date_range<K>(
|
|
&self,
|
|
range: impl std::ops::RangeBounds<K>,
|
|
) -> impl DoubleEndedIterator<Item = Uuid>
|
|
where
|
|
K: AsRef<[u8]>,
|
|
{
|
|
self.submission_created_tree
|
|
.range(range)
|
|
.values()
|
|
.filter_map(|res| res.ok())
|
|
.filter_map(uuid_from_ivec)
|
|
}
|
|
|
|
pub fn newer_than(&self, id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> {
|
|
let this = self.clone();
|
|
|
|
self.comment_tree
|
|
.get(id_comment_key(id))
|
|
.ok()
|
|
.and_then(|opt| opt)
|
|
.and_then(|stored_comment_ivec| {
|
|
let stored_comment: StoredComment =
|
|
serde_json::from_slice(&stored_comment_ivec).ok()?;
|
|
|
|
Some((stored_comment.submission_id, stored_comment.created_at))
|
|
})
|
|
.into_iter()
|
|
.flat_map(move |(submission_id, created_at)| {
|
|
let range_start =
|
|
submission_id_created_comment_range_start(submission_id, created_at);
|
|
let range_start = range_start.as_bytes().to_vec();
|
|
this.date_range(range_start..)
|
|
})
|
|
}
|
|
|
|
pub fn older_than(&self, id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> {
|
|
let this = self.clone();
|
|
|
|
self.comment_tree
|
|
.get(id_comment_key(id))
|
|
.ok()
|
|
.and_then(|opt| opt)
|
|
.and_then(|stored_comment_ivec| {
|
|
let stored_comment: StoredComment =
|
|
serde_json::from_slice(&stored_comment_ivec).ok()?;
|
|
|
|
Some((stored_comment.submission_id, stored_comment.created_at))
|
|
})
|
|
.into_iter()
|
|
.flat_map(move |(submission_id, created_at)| {
|
|
let range_end =
|
|
submission_id_created_comment_range_start(submission_id, created_at);
|
|
let range_end = range_end.as_bytes().to_vec();
|
|
this.date_range(..range_end)
|
|
})
|
|
.rev()
|
|
}
|
|
|
|
pub fn by_id(&self, id: Uuid) -> Result<Option<Comment>, StoreError> {
|
|
let stored_comment_ivec = match self.comment_tree.get(id_comment_key(id))? {
|
|
Some(ivec) => ivec,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
let stored_comment: StoredComment = serde_json::from_slice(&stored_comment_ivec)?;
|
|
|
|
Ok(Some(stored_comment.into()))
|
|
}
|
|
|
|
pub fn delete(&self, comment_id: Uuid) -> Result<Option<Undo<Comment>>, StoreError> {
|
|
let stored_comment_ivec = match self.comment_tree.get(id_comment_key(comment_id))? {
|
|
Some(ivec) => ivec,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
let stored_comment: StoredComment = serde_json::from_slice(&stored_comment_ivec)?;
|
|
let created_at = stored_comment.created_at;
|
|
let submission_id = stored_comment.submission_id;
|
|
|
|
let comment = stored_comment.into();
|
|
|
|
[
|
|
&self.comment_tree,
|
|
&self.submission_tree,
|
|
&self.submission_created_tree,
|
|
&self.created_tree,
|
|
&self.count_tree,
|
|
]
|
|
.transaction(move |trees| {
|
|
let comment_tree = &trees[0];
|
|
let submission_tree = &trees[1];
|
|
let submission_created_tree = &trees[2];
|
|
let created_tree = &trees[3];
|
|
let count_tree = &trees[4];
|
|
|
|
created_tree.remove(created_comment_key(created_at, comment_id).as_bytes())?;
|
|
submission_created_tree.remove(
|
|
submission_id_created_comment_key(submission_id, created_at, comment_id).as_bytes(),
|
|
)?;
|
|
submission_tree.remove(submission_id_key(submission_id, comment_id).as_bytes())?;
|
|
comment_tree.remove(id_comment_key(comment_id).as_bytes())?;
|
|
|
|
super::count(
|
|
count_tree,
|
|
&submission_id_comment_count_key(submission_id),
|
|
|c| c.saturating_sub(1),
|
|
)?;
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(Some(Undo(comment)))
|
|
}
|
|
}
|
|
|
|
// Maps id -> Comment
|
|
fn id_comment_key(id: Uuid) -> String {
|
|
format!("/comment/{}/data", id)
|
|
}
|
|
|
|
// Maps submission_id -> id
|
|
fn submission_id_key(submission_id: Uuid, id: Uuid) -> String {
|
|
format!("/submission/{}/comment/{}", submission_id, id)
|
|
}
|
|
|
|
fn submission_id_created_prefix(submission_id: Uuid) -> String {
|
|
format!("/submission/{}/created", submission_id)
|
|
}
|
|
|
|
// Maps submission_id + created_at -> id
|
|
fn submission_id_created_comment_key(
|
|
submission_id: Uuid,
|
|
created_at: DateTime<Utc>,
|
|
id: Uuid,
|
|
) -> String {
|
|
format!(
|
|
"/submission/{}/created/{}/comment/{}",
|
|
submission_id,
|
|
created_at.to_rfc3339(),
|
|
id,
|
|
)
|
|
}
|
|
|
|
fn reply_created_key(reply_to_id: Uuid, created_at: DateTime<Utc>, id: Uuid) -> String {
|
|
format!(
|
|
"/reply-to/{}/created/{}/comment/{}",
|
|
reply_to_id,
|
|
created_at.to_rfc3339(),
|
|
id
|
|
)
|
|
}
|
|
|
|
fn reply_created_prefix(reply_to_id: Uuid) -> String {
|
|
format!("/reply-to/{}/created", reply_to_id)
|
|
}
|
|
|
|
fn submission_id_created_comment_range_start(
|
|
submission_id: Uuid,
|
|
created_at: DateTime<Utc>,
|
|
) -> String {
|
|
format!(
|
|
"/submission/{}/created/{}",
|
|
submission_id,
|
|
created_at.to_rfc3339()
|
|
)
|
|
}
|
|
|
|
// Maps created_at -> id
|
|
fn created_comment_key(created_at: DateTime<Utc>, id: Uuid) -> String {
|
|
format!("/created/{}/comment/{}", created_at.to_rfc3339(), id)
|
|
}
|
|
|
|
// Maps submission -> comment count
|
|
fn submission_id_comment_count_key(submission_id: Uuid) -> String {
|
|
format!("/submission/{}/count", submission_id)
|
|
}
|
|
|
|
fn uuid_from_ivec(ivec: sled::IVec) -> Option<Uuid> {
|
|
Uuid::from_slice(&ivec).ok()
|
|
}
|
|
|
|
impl From<StoredComment> for Comment {
|
|
fn from(sc: StoredComment) -> Self {
|
|
if let Some(comment_id) = sc.comment_id {
|
|
Comment::Reply(ReplyComment {
|
|
id: sc.id,
|
|
submission_id: sc.submission_id,
|
|
profile_id: sc.profile_id,
|
|
comment_id,
|
|
body: sc.body,
|
|
published: sc.published,
|
|
})
|
|
} else {
|
|
Comment::Submission(SubmissionComment {
|
|
id: sc.id,
|
|
submission_id: sc.submission_id,
|
|
profile_id: sc.profile_id,
|
|
body: sc.body,
|
|
published: sc.published,
|
|
})
|
|
}
|
|
}
|
|
}
|