use etwin_core::auth::AuthContext;
use etwin_core::clock::Clock;
use etwin_core::core::{Instant, Listing};
use etwin_core::forum::{
AddModeratorOptions, CreatePostOptions, CreateThreadOptions, DeleteModeratorOptions, DeletePostError,
DeletePostOptions, ForumActor, ForumPost, ForumPostIdRef, ForumPostListing, ForumPostRevision,
ForumPostRevisionContent, ForumPostRevisionListing, ForumRole, ForumRoleGrant, ForumSection, ForumSectionListing,
ForumSectionMeta, ForumSectionSelf, ForumStore, ForumThread, ForumThreadMeta, ForumThreadMetaWithSection,
GetForumSectionMetaOptions, GetForumSectionOptions, GetSectionMetaError, GetThreadOptions,
LatestForumPostRevisionListing, MarktwinText, RawAddModeratorOptions, RawCreatePostOptions,
RawCreatePostRevisionOptions, RawCreateThreadsOptions, RawDeleteModeratorOptions, RawForumActor, RawForumPost,
RawForumSectionMeta, RawForumThreadMeta, RawGetForumPostOptions, RawGetForumThreadMetaOptions, RawGetPostsOptions,
RawGetRoleGrantsOptions, RawGetSectionsOptions, RawGetThreadsOptions, ShortForumPost, UpdatePostOptions,
UpsertSystemSectionError, UpsertSystemSectionOptions, UserForumActor,
};
pub use etwin_core::forum::{CreatePostError, UpdatePostError};
use etwin_core::types::AnyError;
use etwin_core::user::{GetShortUserOptions, ShortUser, UserId, UserIdRef, UserStore};
use marktwin::emitter::emit_html;
use marktwin::grammar::Grammar;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::future::Future;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AddModeratorError {
#[error("section not found")]
SectionNotFound,
#[error("target grantee user not found")]
TargetUserNotFound,
#[error("current actor does not have the permission to add moderators to this section")]
Forbidden,
#[error(transparent)]
Other(AnyError),
}
#[derive(Error, Debug)]
pub enum DeleteModeratorError {
#[error("section not found")]
SectionNotFound,
#[error("target grantee user not found")]
TargetUserNotFound,
#[error("current actor does not have the permission to remove moderator")]
Forbidden,
#[error(transparent)]
Other(AnyError),
}
#[derive(Error, Debug)]
pub enum CreateThreadError {
#[error("section not found")]
SectionNotFound,
#[error("current actor does not have the permission to create a thread in this section")]
Forbidden,
#[error("failed to parse provided body")]
FailedToParseBody,
#[error("failed to render provided body")]
FailedToRenderBody,
#[error(transparent)]
Other(AnyError),
}
#[derive(Error, Debug)]
pub enum GetSectionError {
#[error("section not found")]
SectionNotFound,
#[error("internal error: {0}")]
Internal(&'static str, AnyError),
}
#[derive(Error, Debug)]
pub enum GetSectionsError {
#[error(transparent)]
Other(AnyError),
}
#[derive(Error, Debug)]
pub enum GetThreadError {
#[error("thread not found")]
ThreadNotFound,
#[error(transparent)]
Other(AnyError),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct GetForumPostOptions {
pub post: ForumPostIdRef,
pub revision_offset: u32,
pub revision_limit: u32,
pub time: Option<Instant>,
}
#[derive(Error, Debug)]
pub enum GetPostError {
#[error("post not found")]
PostNotFound,
#[error(transparent)]
Other(AnyError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ForumConfig {
pub threads_per_page: u32,
pub posts_per_page: u32,
}
pub struct ForumService<TyClock, TyForumStore, TyUserStore>
where
TyClock: Clock,
TyForumStore: ForumStore,
TyUserStore: UserStore,
{
#[allow(unused)]
clock: TyClock,
forum_store: TyForumStore,
user_store: TyUserStore,
}
pub type DynForumService = ForumService<Arc<dyn Clock>, Arc<dyn ForumStore>, Arc<dyn UserStore>>;
impl<TyClock, TyForumStore, TyUserStore> ForumService<TyClock, TyForumStore, TyUserStore>
where
TyClock: Clock,
TyForumStore: ForumStore,
TyUserStore: UserStore,
{
#[allow(clippy::too_many_arguments)]
pub fn new(clock: TyClock, forum_store: TyForumStore, user_store: TyUserStore) -> Self {
Self {
clock,
forum_store,
user_store,
}
}
pub fn config(&self) -> ForumConfig {
ForumConfig {
threads_per_page: 20,
posts_per_page: 10,
}
}
pub async fn add_moderator(
&self,
acx: &AuthContext,
options: &AddModeratorOptions,
) -> Result<ForumSection, AddModeratorError> {
let granter = match acx {
AuthContext::User(acx) if acx.is_administrator => &acx.user,
_ => return Err(AddModeratorError::Forbidden),
};
let grantee = self
.user_store
.get_short_user(&options.user.clone().into())
.await
.map_err(AddModeratorError::Other)?;
let grantee: ShortUser = grantee.ok_or(AddModeratorError::TargetUserNotFound)?;
self
.forum_store
.add_moderator(&RawAddModeratorOptions {
section: options.section.clone(),
target: grantee.as_ref(),
granter: granter.as_ref(),
})
.await
.map_err(AddModeratorError::Other)?;
let section = self
.get_section(
acx,
&GetForumSectionOptions {
section: options.section.clone(),
thread_offset: 0,
thread_limit: 10,
},
)
.await
.map_err(|e| AddModeratorError::Other(Box::new(e)))?;
Ok(section)
}
pub async fn delete_moderator(
&self,
acx: &AuthContext,
options: &DeleteModeratorOptions,
) -> Result<ForumSection, DeleteModeratorError> {
let (actor, is_admin) = match acx {
AuthContext::User(acx) => (&acx.user, acx.is_administrator),
_ => return Err(DeleteModeratorError::Forbidden),
};
let target = self
.user_store
.get_short_user(&options.user.clone().into())
.await
.map_err(DeleteModeratorError::Other)?;
let target: ShortUser = match target {
Some(target) => {
if is_admin || (actor.id == target.id) {
target
} else {
return Err(DeleteModeratorError::Forbidden);
}
}
None => {
return Err(if is_admin {
DeleteModeratorError::TargetUserNotFound
} else {
DeleteModeratorError::Forbidden
});
}
};
self
.forum_store
.delete_moderator(&RawDeleteModeratorOptions {
section: options.section.clone(),
target: target.as_ref(),
revoker: actor.as_ref(),
})
.await
.map_err(DeleteModeratorError::Other)?;
let section = self
.get_section(
acx,
&GetForumSectionOptions {
section: options.section.clone(),
thread_offset: 0,
thread_limit: 10,
},
)
.await
.map_err(|e| DeleteModeratorError::Other(Box::new(e)))?;
Ok(section)
}
#[allow(clippy::manual_async_fn)]
pub fn create_thread<'a>(
&'a self,
acx: &'a AuthContext,
options: &'a CreateThreadOptions,
) -> impl Future<Output = Result<ForumThread, CreateThreadError>> + Send + 'a {
async move {
let actor: ForumActor = match acx {
AuthContext::User(user) => ForumActor::UserForumActor(UserForumActor {
role: None,
user: user.user.clone(),
}),
AuthContext::Guest(_) => return Err(CreateThreadError::Forbidden),
_ => {
return Err(CreateThreadError::Other(
String::from("not implemented: non-user AuthContext").into(),
))
}
};
let grammar = Grammar {
admin: false,
depth: Some(4),
emphasis: true,
icons: HashSet::new(),
links: {
let mut links = HashSet::new();
links.insert(String::from("http"));
links.insert(String::from("https"));
links
},
r#mod: false,
quote: false,
spoiler: false,
strong: true,
strikethrough: true,
};
let body = {
let body = marktwin::parser::parse(&grammar, options.body.as_str());
let body =
marktwin::ast::concrete::Root::try_from(body.syntax()).map_err(|()| CreateThreadError::FailedToParseBody)?;
let mut bytes: Vec<u8> = Vec::new();
emit_html(&mut bytes, &body).map_err(|_| CreateThreadError::FailedToRenderBody)?;
String::from_utf8(bytes).map_err(|_| CreateThreadError::FailedToRenderBody)?
};
let thread = self
.forum_store
.create_thread(&RawCreateThreadsOptions {
actor: actor.to_raw(),
section: options.section.clone(),
title: options.title.clone(),
body_mkt: options.body.clone(),
body_html: body,
})
.await
.map_err(|e| CreateThreadError::Other(format!("internal create_thread errpr: {}", e).into()))?;
let section = self
.forum_store
.get_section_meta(&GetForumSectionMetaOptions {
section: thread.section.into(),
})
.await
.map_err(|e| match e {
GetSectionMetaError::NotFound => CreateThreadError::SectionNotFound,
GetSectionMetaError::Other(e) => CreateThreadError::Other(e),
})?;
let post_revision = ForumPostRevision {
id: thread.post_revision.id,
time: thread.post_revision.time,
author: actor.clone(),
content: thread.post_revision.content,
moderation: thread.post_revision.moderation,
comment: thread.post_revision.comment,
};
let post = ShortForumPost {
id: thread.post_id,
ctime: thread.ctime,
author: actor.clone(),
revisions: LatestForumPostRevisionListing {
count: 1,
last: post_revision,
},
};
Ok(ForumThread {
id: thread.id,
key: thread.key,
title: thread.title,
ctime: thread.ctime,
section: ForumSectionMeta {
id: section.id,
key: section.key,
display_name: section.display_name,
ctime: section.ctime,
locale: section.locale,
threads: section.threads,
this: match acx {
AuthContext::User(acx) => {
let mut roles = Vec::new();
if acx.is_administrator {
roles.push(ForumRole::Administrator);
}
if section
.role_grants
.iter()
.any(|grant| grant.role == ForumRole::Moderator && grant.user.id == acx.user.id)
{
roles.push(ForumRole::Moderator);
}
ForumSectionSelf { roles }
}
_ => ForumSectionSelf { roles: vec![] },
},
},
posts: Listing {
offset: 0,
limit: 10,
count: 1,
items: vec![post],
},
is_pinned: false,
is_locked: false,
})
}
}
#[allow(clippy::manual_async_fn)]
pub fn create_post<'a>(
&'a self,
acx: &'a AuthContext,
options: &'a CreatePostOptions,
) -> impl Future<Output = Result<ForumPost, CreatePostError>> + Send + 'a {
async move {
let actor: ForumActor = match acx {
AuthContext::User(user) => ForumActor::UserForumActor(UserForumActor {
role: None,
user: user.user.clone(),
}),
AuthContext::Guest(_) => return Err(CreatePostError::Forbidden),
_ => todo!(),
};
let grammar = Grammar {
admin: false,
depth: Some(4),
emphasis: true,
icons: HashSet::new(),
links: {
let mut links = HashSet::new();
links.insert(String::from("http"));
links.insert(String::from("https"));
links
},
r#mod: false,
quote: false,
spoiler: false,
strong: true,
strikethrough: true,
};
let body = {
let body = marktwin::parser::parse(&grammar, options.body.as_str());
let body =
marktwin::ast::concrete::Root::try_from(body.syntax()).map_err(|()| CreatePostError::FailedToParseBody)?;
let mut bytes: Vec<u8> = Vec::new();
emit_html(&mut bytes, &body).map_err(|_| CreatePostError::FailedToRenderBody)?;
String::from_utf8(bytes).map_err(|_| CreatePostError::FailedToRenderBody)?
};
let post = self
.forum_store
.create_post(&RawCreatePostOptions {
actor: actor.to_raw(),
thread: options.thread.clone(),
body: ForumPostRevisionContent {
marktwin: options.body.clone(),
html: body,
},
})
.await
.map_err(CreatePostError::Other)?;
let thread: RawForumThreadMeta = self
.forum_store
.get_thread_meta(&RawGetForumThreadMetaOptions {
thread: post.thread.into(),
})
.await
.map_err(|e| CreatePostError::Other(Box::new(e)))?;
let section: RawForumSectionMeta = self
.forum_store
.get_section_meta(&GetForumSectionMetaOptions {
section: post.section.into(),
})
.await
.map_err(|e| CreatePostError::Other(Box::new(e)))?;
let section = ForumSectionMeta {
id: section.id,
key: section.key,
display_name: section.display_name,
ctime: section.ctime,
locale: section.locale,
threads: section.threads,
this: match acx {
AuthContext::User(acx) => {
let mut roles = Vec::new();
if acx.is_administrator {
roles.push(ForumRole::Administrator);
}
if section
.role_grants
.iter()
.any(|grant| grant.role == ForumRole::Moderator && grant.user.id == acx.user.id)
{
roles.push(ForumRole::Moderator);
}
ForumSectionSelf { roles }
}
_ => ForumSectionSelf { roles: vec![] },
},
};
let post_revision = ForumPostRevision {
id: post.revision.id,
time: post.revision.time,
author: actor.clone(),
content: post.revision.content,
moderation: post.revision.moderation,
comment: post.revision.comment,
};
Ok(ForumPost {
id: post.id,
ctime: post.revision.time,
author: actor.clone(),
revisions: ForumPostRevisionListing {
offset: 0,
limit: 100,
count: 1,
items: vec![post_revision],
},
thread: ForumThreadMetaWithSection {
id: thread.id,
key: thread.key,
title: thread.title,
ctime: thread.ctime,
is_pinned: thread.is_pinned,
is_locked: thread.is_locked,
posts: thread.posts,
section,
},
})
}
}
pub async fn update_post(
&self,
acx: &AuthContext,
options: &UpdatePostOptions,
) -> Result<ForumPost, UpdatePostError> {
let user: UserIdRef;
let is_admin: bool;
let actor: ForumActor = match acx {
AuthContext::User(u) => {
is_admin = u.is_administrator;
user = u.user.id.into();
ForumActor::UserForumActor(UserForumActor {
role: None,
user: u.user.clone(),
})
}
AuthContext::Guest(_) => return Err(UpdatePostError::Forbidden),
_ => todo!(),
};
let post = self
.forum_store
.get_post(&RawGetForumPostOptions {
post: options.post.into(),
offset: 0,
limit: 100,
})
.await
.map_err(UpdatePostError::Other)?;
let latest_revision = post.revisions.items.last().unwrap();
let section = post.thread.section;
let grants = self
.forum_store
.get_role_grants(&RawGetRoleGrantsOptions {
section,
user: Some(user),
})
.await
.map_err(UpdatePostError::Other)?;
let is_moderator = is_admin || grants.iter().any(|rg| rg.role == ForumRole::Moderator);
let grammar = Grammar {
admin: false,
depth: Some(4),
emphasis: true,
icons: HashSet::new(),
links: {
let mut links = HashSet::new();
links.insert(String::from("http"));
links.insert(String::from("https"));
links
},
r#mod: false,
quote: false,
spoiler: false,
strong: true,
strikethrough: true,
};
let old_body = latest_revision.content.as_ref().map(|c| &c.marktwin);
let new_body: Option<MarktwinText> = match options.content.as_ref() {
Some(new_body) if new_body.as_ref() != old_body => {
match new_body.as_ref() {
None => {
if !is_moderator {
return Err(UpdatePostError::Forbidden);
}
}
Some(_) => {
return Err(UpdatePostError::Forbidden);
}
}
new_body.clone()
}
_ => {
old_body.cloned()
}
};
let old_mod_body = latest_revision.moderation.as_ref().map(|c| &c.marktwin);
let new_mod_body: Option<MarktwinText> = match options.moderation.as_ref() {
Some(new_mod_body) if new_mod_body.as_ref() != old_mod_body => {
if !is_moderator {
return Err(UpdatePostError::Forbidden);
}
new_mod_body.clone()
}
_ => {
old_mod_body.cloned()
}
};
let new_revision_res = self
.forum_store
.create_post_revision(&RawCreatePostRevisionOptions {
actor: actor.to_raw(),
post: post.id.into(),
body: match new_body {
None => None,
Some(marktwin) => {
let body = marktwin::parser::parse(&grammar, marktwin.as_str());
let body = marktwin::ast::concrete::Root::try_from(body.syntax())
.map_err(|()| UpdatePostError::FailedToParseBody)?;
let html = {
let mut bytes: Vec<u8> = Vec::new();
emit_html(&mut bytes, &body).map_err(|_| UpdatePostError::FailedToRenderBody)?;
String::from_utf8(bytes).map_err(|_| UpdatePostError::FailedToRenderBody)?
};
Some(ForumPostRevisionContent { marktwin, html })
}
},
mod_body: match new_mod_body {
None => None,
Some(marktwin) => {
let body = marktwin::parser::parse(&grammar, marktwin.as_str());
let body = marktwin::ast::concrete::Root::try_from(body.syntax())
.map_err(|()| UpdatePostError::FailedToParseBody)?;
let html = {
let mut bytes: Vec<u8> = Vec::new();
emit_html(&mut bytes, &body).map_err(|_| UpdatePostError::FailedToRenderBody)?;
String::from_utf8(bytes).map_err(|_| UpdatePostError::FailedToRenderBody)?
};
Some(ForumPostRevisionContent { marktwin, html })
}
},
comment: options.comment.clone(),
})
.await
.map_err(UpdatePostError::Other)?;
let section: RawForumSectionMeta = self
.forum_store
.get_section_meta(&GetForumSectionMetaOptions {
section: new_revision_res.section.into(),
})
.await
.map_err(|e| UpdatePostError::Other(Box::new(e)))?;
let section = ForumSectionMeta {
id: section.id,
key: section.key,
display_name: section.display_name,
ctime: section.ctime,
locale: section.locale,
threads: section.threads,
this: match acx {
AuthContext::User(acx) => {
let mut roles = Vec::new();
if acx.is_administrator {
roles.push(ForumRole::Administrator);
}
if section
.role_grants
.iter()
.any(|grant| grant.role == ForumRole::Moderator && grant.user.id == acx.user.id)
{
roles.push(ForumRole::Moderator);
}
ForumSectionSelf { roles }
}
_ => ForumSectionSelf { roles: vec![] },
},
};
let time = new_revision_res.revision.time;
let mut revisions = post.revisions;
revisions.items.push(new_revision_res.revision);
revisions.count += 1;
assert!(u32::try_from(revisions.items.len()).unwrap() <= revisions.limit);
let mut actor_cache: HashMap<UserId, ForumActor> = HashMap::new();
let revisions = ForumPostRevisionListing {
offset: revisions.offset,
limit: revisions.limit,
count: revisions.count,
items: {
let mut items = Vec::new();
for item in revisions.items.into_iter() {
items.push(ForumPostRevision {
id: item.id,
time: item.time,
author: {
match item.author {
RawForumActor::UserForumActor(a) => {
let e = actor_cache.entry(a.user.id);
let actor = match e {
Entry::Vacant(e) => {
let actor = ForumActor::UserForumActor(UserForumActor {
role: None,
user: self
.user_store
.get_short_user(&GetShortUserOptions {
r#ref: a.user.id.into(),
time: Some(time),
})
.await
.map_err(UpdatePostError::Other)?
.unwrap(),
});
e.insert(actor)
}
Entry::Occupied(e) => e.into_mut(),
};
actor.clone()
}
_ => todo!(),
}
},
content: item.content,
moderation: item.moderation,
comment: item.comment,
})
}
items
},
};
Ok(ForumPost {
id: post.id,
ctime: post.ctime,
author: {
assert_eq!(revisions.offset, 0);
revisions.items[0].author.clone()
},
revisions,
thread: ForumThreadMetaWithSection {
id: post.thread.id,
key: post.thread.key,
title: post.thread.title,
ctime: post.thread.ctime,
is_pinned: post.thread.is_pinned,
is_locked: post.thread.is_locked,
posts: post.thread.posts,
section,
},
})
}
pub async fn delete_post(
&self,
acx: &AuthContext,
options: &DeletePostOptions,
) -> Result<ForumPost, DeletePostError> {
let update_res = self
.update_post(
acx,
&UpdatePostOptions {
post: options.post,
revision: options.revision,
content: Some(None),
moderation: Some(None),
comment: options.comment.clone(),
},
)
.await;
match update_res {
Ok(post) => Ok(post),
Err(e) => Err(DeletePostError::Other(e.into())),
}
}
pub async fn get_sections(&self, acx: &AuthContext) -> Result<ForumSectionListing, GetSectionsError> {
let sections: Listing<RawForumSectionMeta> = self
.forum_store
.get_sections(&RawGetSectionsOptions { offset: 0, limit: 20 })
.await
.map_err(GetSectionsError::Other)?;
let mut items: Vec<ForumSectionMeta> = Vec::new();
for section in sections.items.into_iter() {
let forum_self = match acx {
AuthContext::User(acx) => {
let mut roles = Vec::new();
if acx.is_administrator {
roles.push(ForumRole::Administrator);
}
if section
.role_grants
.iter()
.any(|grant| grant.role == ForumRole::Moderator && grant.user.id == acx.user.id)
{
roles.push(ForumRole::Moderator);
}
ForumSectionSelf { roles }
}
_ => ForumSectionSelf { roles: vec![] },
};
let section = ForumSectionMeta {
id: section.id,
key: section.key,
display_name: section.display_name,
ctime: section.ctime,
locale: section.locale,
threads: section.threads,
this: forum_self,
};
items.push(section);
}
Ok(Listing {
offset: sections.offset,
limit: sections.limit,
count: sections.count,
items,
})
}
pub async fn upsert_system_section(
&self,
options: &UpsertSystemSectionOptions,
) -> Result<ForumSection, UpsertSystemSectionError> {
self.forum_store.upsert_system_section(options).await
}
pub async fn get_section(
&self,
acx: &AuthContext,
options: &GetForumSectionOptions,
) -> Result<ForumSection, GetSectionError> {
let section_meta: RawForumSectionMeta = self
.forum_store
.get_section_meta(&GetForumSectionMetaOptions {
section: options.section.clone(),
})
.await
.map_err(|e| match e {
GetSectionMetaError::NotFound => GetSectionError::SectionNotFound,
GetSectionMetaError::Other(e) => GetSectionError::Internal("get_section_meta", e),
})?;
let threads = self
.forum_store
.get_threads(&RawGetThreadsOptions {
section: section_meta.as_ref().into(),
offset: options.thread_offset,
limit: options.thread_limit,
})
.await
.map_err(|e| GetSectionError::Internal("get_threads", e))?;
let threads = Listing {
offset: threads.offset,
limit: threads.limit,
count: threads.count,
items: threads
.items
.into_iter()
.map(|thread| ForumThreadMeta {
id: thread.id,
key: thread.key,
title: thread.title,
ctime: thread.ctime,
is_pinned: thread.is_pinned,
is_locked: thread.is_locked,
posts: thread.posts,
})
.collect(),
};
let forum_self = match acx {
AuthContext::User(acx) => {
let mut roles = Vec::new();
if acx.is_administrator {
roles.push(ForumRole::Administrator);
}
if section_meta
.role_grants
.iter()
.any(|grant| grant.role == ForumRole::Moderator && grant.user.id == acx.user.id)
{
roles.push(ForumRole::Moderator);
}
ForumSectionSelf { roles }
}
_ => ForumSectionSelf { roles: vec![] },
};
let mut role_grants: Vec<ForumRoleGrant> = Vec::new();
for grant in section_meta.role_grants.into_iter() {
let grantee = self
.user_store
.get_short_user(&grant.user.into())
.await
.map_err(|e| GetSectionError::Internal("get_short_user (grantee)", e))?
.expect("failed to resolve grantee");
let granter = self
.user_store
.get_short_user(&grant.granted_by.into())
.await
.map_err(|e| GetSectionError::Internal("get_short_user (granter)", e))?
.expect("failed to resolve grantee");
role_grants.push(ForumRoleGrant {
role: grant.role,
user: grantee,
start_time: grant.start_time,
granted_by: granter,
});
}
Ok(ForumSection {
id: section_meta.id,
key: section_meta.key,
display_name: section_meta.display_name,
ctime: section_meta.ctime,
locale: section_meta.locale,
threads,
role_grants,
this: forum_self,
})
}
pub async fn get_thread(&self, acx: &AuthContext, options: &GetThreadOptions) -> Result<ForumThread, GetThreadError> {
let time = self.clock.now();
let thread_meta: RawForumThreadMeta = self
.forum_store
.get_thread_meta(&RawGetForumThreadMetaOptions {
thread: options.thread.clone(),
})
.await
.map_err(|e| GetThreadError::Other(Box::new(e)))?;
let posts = self
.forum_store
.get_posts(&RawGetPostsOptions {
thread: thread_meta.as_ref().into(),
offset: options.post_offset,
limit: options.post_limit,
})
.await
.map_err(GetThreadError::Other)?;
let section_meta: RawForumSectionMeta = self
.forum_store
.get_section_meta(&GetForumSectionMetaOptions {
section: thread_meta.section.into(),
})
.await
.map_err(|e| GetThreadError::Other(Box::new(e)))?;
let forum_self = match acx {
AuthContext::User(acx) => {
let mut roles = Vec::new();
if acx.is_administrator {
roles.push(ForumRole::Administrator);
}
if section_meta
.role_grants
.iter()
.any(|grant| grant.role == ForumRole::Moderator && grant.user.id == acx.user.id)
{
roles.push(ForumRole::Moderator);
}
ForumSectionSelf { roles }
}
_ => ForumSectionSelf { roles: vec![] },
};
let mut role_grants: Vec<ForumRoleGrant> = Vec::new();
for grant in section_meta.role_grants.into_iter() {
let grantee = self
.user_store
.get_short_user(&grant.user.into())
.await
.map_err(GetThreadError::Other)?
.expect("failed to resolve grantee");
let granter = self
.user_store
.get_short_user(&grant.granted_by.into())
.await
.map_err(GetThreadError::Other)?
.expect("failed to resolve grantee");
role_grants.push(ForumRoleGrant {
role: grant.role,
user: grantee,
start_time: grant.start_time,
granted_by: granter,
});
}
let posts = ForumPostListing {
offset: posts.offset,
limit: posts.limit,
count: posts.count,
items: {
let mut items: Vec<ShortForumPost> = Vec::new();
for item in posts.items {
let last_revision = item.revisions.last;
let first_author = self
.user_store
.get_short_user(
&(match item.author {
RawForumActor::UserForumActor(a) => GetShortUserOptions {
r#ref: a.user.id.into(),
time: Some(time),
},
_ => todo!(),
}),
)
.await
.map_err(GetThreadError::Other)?
.unwrap();
let last_author = self
.user_store
.get_short_user(
&(match last_revision.author {
RawForumActor::UserForumActor(a) => GetShortUserOptions {
r#ref: a.user.id.into(),
time: Some(time),
},
_ => todo!(),
}),
)
.await
.map_err(GetThreadError::Other)?
.unwrap();
items.push(ShortForumPost {
id: item.id,
ctime: item.ctime,
author: ForumActor::UserForumActor(UserForumActor {
role: None,
user: first_author,
}),
revisions: LatestForumPostRevisionListing {
count: item.revisions.count,
last: ForumPostRevision {
id: last_revision.id,
time: last_revision.time,
author: ForumActor::UserForumActor(UserForumActor {
role: None,
user: last_author,
}),
content: last_revision.content,
moderation: last_revision.moderation,
comment: last_revision.comment,
},
},
});
}
items
},
};
Ok(ForumThread {
id: thread_meta.id,
key: thread_meta.key,
title: thread_meta.title,
ctime: thread_meta.ctime,
posts,
is_pinned: thread_meta.is_pinned,
is_locked: thread_meta.is_locked,
section: ForumSectionMeta {
id: section_meta.id,
key: section_meta.key,
display_name: section_meta.display_name,
ctime: section_meta.ctime,
locale: section_meta.locale,
threads: section_meta.threads,
this: forum_self,
},
})
}
pub async fn get_post(&self, acx: &AuthContext, options: &GetForumPostOptions) -> Result<ForumPost, GetPostError> {
let time = options.time.unwrap_or_else(|| self.clock.now());
let post: RawForumPost = self
.forum_store
.get_post(&RawGetForumPostOptions {
post: options.post,
offset: options.revision_offset,
limit: options.revision_limit,
})
.await
.map_err(|e| GetPostError::Other(e))?;
let author = match post.author {
RawForumActor::UserForumActor(a) => ForumActor::UserForumActor(UserForumActor {
role: a.role,
user: self
.user_store
.get_short_user(&GetShortUserOptions {
r#ref: a.user.id.into(),
time: Some(time),
})
.await
.map_err(GetPostError::Other)?
.ok_or_else(|| GetPostError::Other(String::from("AssertionError: Expected post author to exist").into()))?,
}),
_ => todo!(),
};
let thread = post.thread;
let section = self
.forum_store
.get_section_meta(&GetForumSectionMetaOptions {
section: thread.section.into(),
})
.await
.map_err(|e| GetPostError::Other(Box::new(e)))?;
let forum_self = match acx {
AuthContext::User(acx) => {
let mut roles = Vec::new();
if acx.is_administrator {
roles.push(ForumRole::Administrator);
}
if section
.role_grants
.iter()
.any(|grant| grant.role == ForumRole::Moderator && grant.user.id == acx.user.id)
{
roles.push(ForumRole::Moderator);
}
ForumSectionSelf { roles }
}
_ => ForumSectionSelf { roles: vec![] },
};
let revisions = post.revisions;
let revisions = ForumPostRevisionListing {
offset: revisions.offset,
limit: revisions.limit,
count: revisions.count,
items: {
let mut items: Vec<ForumPostRevision> = Vec::new();
for item in revisions.items {
let author = match item.author {
RawForumActor::UserForumActor(a) => ForumActor::UserForumActor(UserForumActor {
role: a.role,
user: self
.user_store
.get_short_user(&GetShortUserOptions {
r#ref: a.user.id.into(),
time: Some(time),
})
.await
.map_err(GetPostError::Other)?
.ok_or_else(|| {
GetPostError::Other(String::from("AssertionError: Expected revision author to exist").into())
})?,
}),
_ => todo!(),
};
items.push(ForumPostRevision {
id: item.id,
time: item.time,
author,
content: item.content,
moderation: item.moderation,
comment: item.comment,
});
}
items
},
};
Ok(ForumPost {
id: post.id,
ctime: post.ctime,
author,
revisions,
thread: ForumThreadMetaWithSection {
id: thread.id,
key: thread.key,
title: thread.title,
ctime: thread.ctime,
is_pinned: thread.is_pinned,
is_locked: thread.is_locked,
posts: thread.posts,
section: ForumSectionMeta {
id: section.id,
key: section.key,
display_name: section.display_name,
ctime: section.ctime,
locale: section.locale,
threads: section.threads,
this: forum_self,
},
},
})
}
}
#[cfg(feature = "neon")]
impl<TyClock, TyForumStore, TyUserStore> neon::prelude::Finalize for ForumService<TyClock, TyForumStore, TyUserStore>
where
TyClock: Clock,
TyForumStore: ForumStore,
TyUserStore: UserStore,
{
}