lib.rs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. // Clippy linter warning
  2. #![allow(clippy::type_complexity)]
  3. // disable it because of possible frontend API break
  4. // TODO: remove post-Constaninople
  5. // Ensure we're `no_std` when compiling for Wasm.
  6. #![cfg_attr(not(feature = "std"), no_std)]
  7. #[cfg(feature = "std")]
  8. use serde::{Deserialize, Serialize};
  9. //TODO: Convert errors to the Substrate decl_error! macro.
  10. /// Result with string error message. This exists for backward compatibility purpose.
  11. pub type DispatchResult = Result<(), &'static str>;
  12. use codec::{Codec, Decode, Encode};
  13. use frame_support::{decl_event, decl_module, decl_storage, ensure, Parameter};
  14. use sp_arithmetic::traits::{BaseArithmetic, One};
  15. use sp_runtime::traits::{MaybeSerialize, Member};
  16. use sp_std::borrow::ToOwned;
  17. use sp_std::vec;
  18. use sp_std::vec::Vec;
  19. mod mock;
  20. mod tests;
  21. use common::constraints::InputValidationLengthConstraint;
  22. use common::BlockAndTime;
  23. /// Constants
  24. /////////////////////////////////////////////////////////////////
  25. /// The greatest valid depth of a category.
  26. /// The depth of a root category is 0.
  27. const MAX_CATEGORY_DEPTH: u16 = 3;
  28. /// Error messages for dispatchables
  29. const ERROR_FORUM_SUDO_NOT_SET: &str = "Forum sudo not set.";
  30. const ERROR_ORIGIN_NOT_FORUM_SUDO: &str = "Origin not forum sudo.";
  31. const ERROR_CATEGORY_TITLE_TOO_SHORT: &str = "Category title too short.";
  32. const ERROR_CATEGORY_TITLE_TOO_LONG: &str = "Category title too long.";
  33. const ERROR_CATEGORY_DESCRIPTION_TOO_SHORT: &str = "Category description too long.";
  34. const ERROR_CATEGORY_DESCRIPTION_TOO_LONG: &str = "Category description too long.";
  35. const ERROR_ANCESTOR_CATEGORY_IMMUTABLE: &str =
  36. "Ancestor category immutable, i.e. deleted or archived";
  37. const ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED: &str = "Maximum valid category depth exceeded.";
  38. const ERROR_CATEGORY_DOES_NOT_EXIST: &str = "Category does not exist.";
  39. const ERROR_NOT_FORUM_USER: &str = "Not forum user.";
  40. const ERROR_THREAD_TITLE_TOO_SHORT: &str = "Thread title too short.";
  41. const ERROR_THREAD_TITLE_TOO_LONG: &str = "Thread title too long.";
  42. const ERROR_POST_TEXT_TOO_SHORT: &str = "Post text too short.";
  43. const ERROR_POST_TEXT_TOO_LONG: &str = "Post too long.";
  44. const ERROR_THREAD_DOES_NOT_EXIST: &str = "Thread does not exist";
  45. const ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT: &str = "Thread moderation rationale too short.";
  46. const ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG: &str = "Thread moderation rationale too long.";
  47. const ERROR_THREAD_ALREADY_MODERATED: &str = "Thread already moderated.";
  48. const ERROR_THREAD_MODERATED: &str = "Thread is moderated.";
  49. const ERROR_POST_DOES_NOT_EXIST: &str = "Post does not exist.";
  50. const ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR: &str = "Account does not match post author.";
  51. const ERROR_POST_MODERATED: &str = "Post is moderated.";
  52. const ERROR_POST_MODERATION_RATIONALE_TOO_SHORT: &str = "Post moderation rationale too short.";
  53. const ERROR_POST_MODERATION_RATIONALE_TOO_LONG: &str = "Post moderation rationale too long.";
  54. const ERROR_CATEGORY_NOT_BEING_UPDATED: &str = "Category not being updated.";
  55. const ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED: &str =
  56. "Category cannot be unarchived when deleted.";
  57. use frame_system::{ensure_root, ensure_signed};
  58. /// Represents a user in this forum.
  59. #[derive(Debug, Copy, Clone)]
  60. pub struct ForumUser<AccountId> {
  61. /// Identifier of user
  62. pub id: AccountId, // In the future one could add things like
  63. // - updating post count of a user
  64. // - updating status (e.g. hero, new, etc.)
  65. //
  66. }
  67. /// Represents a regsitry of `ForumUser` instances.
  68. pub trait ForumUserRegistry<AccountId> {
  69. fn get_forum_user(id: &AccountId) -> Option<ForumUser<AccountId>>;
  70. }
  71. /// Represents a moderation outcome applied to a post or a thread.
  72. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  73. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  74. pub struct ModerationAction<BlockNumber, Moment, AccountId> {
  75. /// When action occured.
  76. moderated_at: BlockAndTime<BlockNumber, Moment>,
  77. /// Account forum sudo which acted.
  78. moderator_id: AccountId,
  79. /// Moderation rationale
  80. rationale: Vec<u8>,
  81. }
  82. /// Represents a revision of the text of a Post
  83. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  84. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  85. pub struct PostTextChange<BlockNumber, Moment> {
  86. /// When this expiration occured
  87. expired_at: BlockAndTime<BlockNumber, Moment>,
  88. /// Text that expired
  89. text: Vec<u8>,
  90. }
  91. /// Represents a thread post
  92. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  93. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  94. pub struct Post<BlockNumber, Moment, AccountId, ThreadId, PostId> {
  95. /// Post identifier
  96. pub id: PostId,
  97. /// Id of thread to which this post corresponds.
  98. thread_id: ThreadId,
  99. /// The post number of this post in its thread, i.e. total number of posts added (including this)
  100. /// to a thread when it was added.
  101. /// Is needed to give light clients assurance about getting all posts in a given range,
  102. // `created_at` is not sufficient.
  103. /// Starts at 1 for first post in thread.
  104. nr_in_thread: u32,
  105. /// Current text of post
  106. current_text: Vec<u8>,
  107. /// Possible moderation of this post
  108. moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
  109. /// Edits of post ordered chronologically by edit time.
  110. text_change_history: Vec<PostTextChange<BlockNumber, Moment>>,
  111. /// When post was submitted.
  112. created_at: BlockAndTime<BlockNumber, Moment>,
  113. /// Author of post.
  114. author_id: AccountId,
  115. }
  116. /// Represents a thread
  117. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  118. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  119. pub struct Thread<BlockNumber, Moment, AccountId, ThreadId> {
  120. /// Thread identifier
  121. pub id: ThreadId,
  122. /// Title
  123. title: Vec<u8>,
  124. /// Category in which this thread lives
  125. category_id: CategoryId,
  126. /// The thread number of this thread in its category, i.e. total number of thread added (including this)
  127. /// to a category when it was added.
  128. /// Is needed to give light clients assurance about getting all threads in a given range,
  129. /// `created_at` is not sufficient.
  130. /// Starts at 1 for first thread in category.
  131. nr_in_category: u32,
  132. /// Possible moderation of this thread
  133. moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
  134. /// Number of unmoderated and moderated posts in this thread.
  135. /// The sum of these two only increases, and former is incremented
  136. /// for each new post added to this thread. A new post is added
  137. /// with a `nr_in_thread` equal to this sum
  138. ///
  139. /// When there is a moderation
  140. /// of a post, the variables are incremented and decremented, respectively.
  141. ///
  142. /// These values are vital for light clients, in order to validate that they are
  143. /// not being censored from posts in a thread.
  144. num_unmoderated_posts: u32,
  145. num_moderated_posts: u32,
  146. /// When thread was established.
  147. created_at: BlockAndTime<BlockNumber, Moment>,
  148. /// Author of post.
  149. author_id: AccountId,
  150. }
  151. impl<BlockNumber, Moment, AccountId, ThreadId> Thread<BlockNumber, Moment, AccountId, ThreadId> {
  152. fn num_posts_ever_created(&self) -> u32 {
  153. self.num_unmoderated_posts + self.num_moderated_posts
  154. }
  155. }
  156. /// Represents a category identifier
  157. pub type CategoryId = u64;
  158. /// Represents
  159. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  160. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  161. pub struct ChildPositionInParentCategory {
  162. /// Id of parent category
  163. parent_id: CategoryId,
  164. /// Nr of the child in the parent
  165. /// Starts at 1
  166. child_nr_in_parent_category: u32,
  167. }
  168. /// Represents a category
  169. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  170. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  171. pub struct Category<BlockNumber, Moment, AccountId> {
  172. /// Category identifier
  173. pub id: CategoryId,
  174. /// Title
  175. title: Vec<u8>,
  176. /// Description
  177. description: Vec<u8>,
  178. /// When category was established.
  179. created_at: BlockAndTime<BlockNumber, Moment>,
  180. /// Whether category is deleted.
  181. deleted: bool,
  182. /// Whether category is archived.
  183. archived: bool,
  184. /// Number of subcategories (deleted, archived or neither),
  185. /// unmoderated threads and moderated threads, _directly_ in this category.
  186. ///
  187. /// As noted, the first is unaffected by any change in state of direct subcategory.
  188. ///
  189. /// The sum of the latter two only increases, and former is incremented
  190. /// for each new thread added to this category. A new thread is added
  191. /// with a `nr_in_category` equal to this sum.
  192. ///
  193. /// When there is a moderation
  194. /// of a thread, the variables are incremented and decremented, respectively.
  195. ///
  196. /// These values are vital for light clients, in order to validate that they are
  197. /// not being censored from subcategories or threads in a category.
  198. num_direct_subcategories: u32,
  199. num_direct_unmoderated_threads: u32,
  200. num_direct_moderated_threads: u32,
  201. /// Position as child in parent, if present, otherwise this category is a root category
  202. position_in_parent_category: Option<ChildPositionInParentCategory>,
  203. /// Account of the moderator which created category.
  204. moderator_id: AccountId,
  205. }
  206. impl<BlockNumber, Moment, AccountId> Category<BlockNumber, Moment, AccountId> {
  207. fn num_threads_created(&self) -> u32 {
  208. self.num_direct_unmoderated_threads + self.num_direct_moderated_threads
  209. }
  210. }
  211. /// Represents a sequence of categories which have child-parent relatioonship
  212. /// where last element is final ancestor, or root, in the context of the category tree.
  213. type CategoryTreePath<BlockNumber, Moment, AccountId> =
  214. Vec<Category<BlockNumber, Moment, AccountId>>;
  215. pub trait Trait: frame_system::Trait + pallet_timestamp::Trait + Sized {
  216. type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
  217. type MembershipRegistry: ForumUserRegistry<Self::AccountId>;
  218. /// Thread Id type
  219. type ThreadId: Parameter
  220. + Member
  221. + BaseArithmetic
  222. + Codec
  223. + Default
  224. + Copy
  225. + MaybeSerialize
  226. + PartialEq;
  227. /// Post Id type
  228. type PostId: Parameter
  229. + Member
  230. + BaseArithmetic
  231. + Codec
  232. + Default
  233. + Copy
  234. + MaybeSerialize
  235. + PartialEq;
  236. }
  237. decl_storage! {
  238. trait Store for Module<T: Trait> as Forum {
  239. /// Map category identifier to corresponding category.
  240. pub CategoryById get(fn category_by_id) config(): map hasher(blake2_128_concat)
  241. CategoryId => Category<T::BlockNumber, T::Moment, T::AccountId>;
  242. /// Category identifier value to be used for the next Category created.
  243. pub NextCategoryId get(fn next_category_id) config(): CategoryId;
  244. /// Map thread identifier to corresponding thread.
  245. pub ThreadById get(fn thread_by_id) config(): map hasher(blake2_128_concat)
  246. T::ThreadId => Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>;
  247. /// Thread identifier value to be used for next Thread in threadById.
  248. pub NextThreadId get(fn next_thread_id) config(): T::ThreadId;
  249. /// Map post identifier to corresponding post.
  250. pub PostById get(fn post_by_id) config(): map hasher(blake2_128_concat)
  251. T::PostId => Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>;
  252. /// Post identifier value to be used for for next post created.
  253. pub NextPostId get(fn next_post_id) config(): T::PostId;
  254. /// Account of forum sudo.
  255. pub ForumSudo get(fn forum_sudo) config(): Option<T::AccountId>;
  256. /// Input constraints
  257. /// These are all forward looking, that is they are enforced on all
  258. /// future calls.
  259. pub CategoryTitleConstraint get(fn category_title_constraint) config(): InputValidationLengthConstraint;
  260. pub CategoryDescriptionConstraint get(fn category_description_constraint) config(): InputValidationLengthConstraint;
  261. pub ThreadTitleConstraint get(fn thread_title_constraint) config(): InputValidationLengthConstraint;
  262. pub PostTextConstraint get(fn post_text_constraint) config(): InputValidationLengthConstraint;
  263. pub ThreadModerationRationaleConstraint get(fn thread_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
  264. pub PostModerationRationaleConstraint get(fn post_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
  265. }
  266. }
  267. decl_event!(
  268. pub enum Event<T>
  269. where
  270. <T as frame_system::Trait>::AccountId,
  271. <T as Trait>::ThreadId,
  272. <T as Trait>::PostId,
  273. {
  274. /// A category was introduced
  275. CategoryCreated(CategoryId),
  276. /// A category with given id was updated.
  277. /// The second argument reflects the new archival status of the category, if changed.
  278. /// The third argument reflects the new deletion status of the category, if changed.
  279. CategoryUpdated(CategoryId, Option<bool>, Option<bool>),
  280. /// A thread with given id was created.
  281. ThreadCreated(ThreadId),
  282. /// A thread with given id was moderated.
  283. ThreadModerated(ThreadId),
  284. /// Post with given id was created.
  285. PostAdded(PostId),
  286. /// Post with givne id was moderated.
  287. PostModerated(PostId),
  288. /// Post with given id had its text updated.
  289. /// The second argument reflects the number of total edits when the text update occurs.
  290. PostTextUpdated(PostId, u64),
  291. /// Given account was set as forum sudo.
  292. ForumSudoSet(Option<AccountId>, Option<AccountId>),
  293. }
  294. );
  295. decl_module! {
  296. pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  297. fn deposit_event() = default;
  298. /// Set forum sudo.
  299. #[weight = 10_000_000] // TODO: adjust weight
  300. fn set_forum_sudo(origin, new_forum_sudo: Option<T::AccountId>) -> DispatchResult {
  301. ensure_root(origin)?;
  302. /*
  303. * Question: when this routine is called by non sudo or with bad signature, what error is raised?
  304. * Update ERror set in spec
  305. */
  306. // Hold on to old value
  307. let old_forum_sudo = <ForumSudo<T>>::get();
  308. // Update forum sudo
  309. match new_forum_sudo.clone() {
  310. Some(account_id) => <ForumSudo<T>>::put(account_id),
  311. None => <ForumSudo<T>>::kill()
  312. };
  313. // Generate event
  314. Self::deposit_event(RawEvent::ForumSudoSet(old_forum_sudo, new_forum_sudo));
  315. // All good.
  316. Ok(())
  317. }
  318. /// Add a new category.
  319. #[weight = 10_000_000] // TODO: adjust weight
  320. fn create_category(origin, parent: Option<CategoryId>, title: Vec<u8>, description: Vec<u8>) -> DispatchResult {
  321. // Check that its a valid signature
  322. let who = ensure_signed(origin)?;
  323. // Not signed by forum SUDO
  324. Self::ensure_is_forum_sudo(&who)?;
  325. // Validate title
  326. Self::ensure_category_title_is_valid(&title)?;
  327. // Validate description
  328. Self::ensure_category_description_is_valid(&description)?;
  329. // Position in parent field value for new category
  330. let mut position_in_parent_category_field = None;
  331. // If not root, then check that we can create in parent category
  332. if let Some(parent_category_id) = parent {
  333. let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(parent_category_id)?;
  334. // Can we mutate in this category?
  335. Self::ensure_can_add_subcategory_path_leaf(&category_tree_path)?;
  336. /*
  337. * Here we are safe to mutate
  338. */
  339. // Increment number of subcategories to reflect this new category being
  340. // added as a child
  341. <CategoryById<T>>::mutate(parent_category_id, |c| {
  342. c.num_direct_subcategories += 1;
  343. });
  344. // Set `position_in_parent_category_field`
  345. let parent_category = category_tree_path.first().unwrap();
  346. position_in_parent_category_field = Some(ChildPositionInParentCategory{
  347. parent_id: parent_category_id,
  348. child_nr_in_parent_category: parent_category.num_direct_subcategories
  349. });
  350. }
  351. /*
  352. * Here we are safe to mutate
  353. */
  354. let next_category_id = NextCategoryId::get();
  355. // Create new category
  356. let new_category = Category {
  357. id : next_category_id,
  358. title,
  359. description,
  360. created_at : common::current_block_time::<T>(),
  361. deleted: false,
  362. archived: false,
  363. num_direct_subcategories: 0,
  364. num_direct_unmoderated_threads: 0,
  365. num_direct_moderated_threads: 0,
  366. position_in_parent_category: position_in_parent_category_field,
  367. moderator_id: who
  368. };
  369. // Insert category in map
  370. <CategoryById<T>>::insert(new_category.id, new_category);
  371. // Update other things
  372. NextCategoryId::put(next_category_id + 1);
  373. // Generate event
  374. Self::deposit_event(RawEvent::CategoryCreated(next_category_id));
  375. Ok(())
  376. }
  377. /// Update category
  378. #[weight = 10_000_000] // TODO: adjust weight
  379. fn update_category(origin, category_id: CategoryId, new_archival_status: Option<bool>, new_deletion_status: Option<bool>) -> DispatchResult {
  380. // Check that its a valid signature
  381. let who = ensure_signed(origin)?;
  382. // Not signed by forum SUDO
  383. Self::ensure_is_forum_sudo(&who)?;
  384. // Make sure something is actually being changed
  385. ensure!(
  386. new_archival_status.is_some() || new_deletion_status.is_some(),
  387. ERROR_CATEGORY_NOT_BEING_UPDATED
  388. );
  389. // Get path from parent to root of category tree.
  390. let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
  391. // When we are dealing with a non-root category, we
  392. // must ensure mutability of our category by traversing to
  393. // root.
  394. if category_tree_path.len() > 1 {
  395. // We must skip checking category itself.
  396. // NB: This is kind of hacky way to avoid last element,
  397. // something clearn can be done later.
  398. let mut path_to_check = category_tree_path;
  399. path_to_check.remove(0);
  400. Self::ensure_can_mutate_in_path_leaf(&path_to_check)?;
  401. }
  402. // If the category itself is already deleted, then this
  403. // update *must* simultaneously do an undelete, otherwise it is blocked,
  404. // as we do not permit unarchiving a deleted category. Doing
  405. // a simultanous undelete and unarchive is accepted.
  406. let category = <CategoryById<T>>::get(category_id);
  407. ensure!(
  408. !category.deleted || (new_deletion_status == Some(false)),
  409. ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED
  410. );
  411. // Mutate category, and set possible new change parameters
  412. <CategoryById<T>>::mutate(category_id, |c| {
  413. if let Some(archived) = new_archival_status {
  414. c.archived = archived;
  415. }
  416. if let Some(deleted) = new_deletion_status {
  417. c.deleted = deleted;
  418. }
  419. });
  420. // Generate event
  421. Self::deposit_event(RawEvent::CategoryUpdated(category_id, new_archival_status, new_deletion_status));
  422. Ok(())
  423. }
  424. /// Create new thread in category
  425. #[weight = 10_000_000] // TODO: adjust weight
  426. fn create_thread(origin, category_id: CategoryId, title: Vec<u8>, text: Vec<u8>) -> DispatchResult {
  427. /*
  428. * Update SPEC with new errors,
  429. * and mutation of Category class,
  430. * as well as side effect to update Category::num_threads_created.
  431. */
  432. // Check that its a valid signature
  433. let who = ensure_signed(origin)?;
  434. // Check that account is forum member
  435. Self::ensure_is_forum_member(&who)?;
  436. // Get path from parent to root of category tree.
  437. let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
  438. // No ancestor is blocking us doing mutation in this category
  439. Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
  440. // Validate title
  441. Self::ensure_thread_title_is_valid(&title)?;
  442. // Validate post text
  443. Self::ensure_post_text_is_valid(&text)?;
  444. /*
  445. * Here it is safe to mutate state.
  446. */
  447. // Add thread
  448. let thread = Self::add_new_thread(category_id, &title, &who);
  449. // Add inital post to thread
  450. Self::add_new_post(thread.id, &text, &who);
  451. // Generate event
  452. Self::deposit_event(RawEvent::ThreadCreated(thread.id));
  453. Ok(())
  454. }
  455. /// Moderate thread
  456. #[weight = 10_000_000] // TODO: adjust weight
  457. fn moderate_thread(origin, thread_id: T::ThreadId, rationale: Vec<u8>) -> DispatchResult {
  458. // Check that its a valid signature
  459. let who = ensure_signed(origin)?;
  460. // Signed by forum SUDO
  461. Self::ensure_is_forum_sudo(&who)?;
  462. // Get thread
  463. let mut thread = Self::ensure_thread_exists(thread_id)?;
  464. // Thread is not already moderated
  465. ensure!(thread.moderation.is_none(), ERROR_THREAD_ALREADY_MODERATED);
  466. // Rationale valid
  467. Self::ensure_thread_moderation_rationale_is_valid(&rationale)?;
  468. // Can mutate in corresponding category
  469. let path = Self::build_category_tree_path(thread.category_id);
  470. // Path must be non-empty, as category id is from thread in state
  471. assert!(!path.is_empty());
  472. Self::ensure_can_mutate_in_path_leaf(&path)?;
  473. /*
  474. * Here we are safe to mutate
  475. */
  476. // Add moderation to thread
  477. thread.moderation = Some(ModerationAction {
  478. moderated_at: common::current_block_time::<T>(),
  479. moderator_id: who,
  480. rationale
  481. });
  482. <ThreadById<T>>::insert(thread_id, thread.clone());
  483. // Update moderation/umoderation count of corresponding category
  484. <CategoryById<T>>::mutate(thread.category_id, |category| {
  485. category.num_direct_unmoderated_threads -= 1;
  486. category.num_direct_moderated_threads += 1;
  487. });
  488. // Generate event
  489. Self::deposit_event(RawEvent::ThreadModerated(thread_id));
  490. Ok(())
  491. }
  492. /// Edit post text
  493. #[weight = 10_000_000] // TODO: adjust weight
  494. fn add_post(origin, thread_id: T::ThreadId, text: Vec<u8>) -> DispatchResult {
  495. /*
  496. * Update SPEC with new errors,
  497. */
  498. // Check that its a valid signature
  499. let who = ensure_signed(origin)?;
  500. // Check that account is forum member
  501. Self::ensure_is_forum_member(&who)?;
  502. // Validate post text
  503. Self::ensure_post_text_is_valid(&text)?;
  504. // Make sure thread exists and is mutable
  505. let thread = Self::ensure_thread_is_mutable(thread_id)?;
  506. // Get path from parent to root of category tree.
  507. let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(thread.category_id)?;
  508. // No ancestor is blocking us doing mutation in this category
  509. Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
  510. /*
  511. * Here we are safe to mutate
  512. */
  513. let post = Self::add_new_post(thread_id, &text, &who);
  514. // Generate event
  515. Self::deposit_event(RawEvent::PostAdded(post.id));
  516. Ok(())
  517. }
  518. /// Edit post text
  519. #[weight = 10_000_000] // TODO: adjust weight
  520. fn edit_post_text(origin, post_id: T::PostId, new_text: Vec<u8>) -> DispatchResult {
  521. /* Edit spec.
  522. - forum member guard missing
  523. - check that both post and thread and category are mutable
  524. */
  525. // Check that its a valid signature
  526. let who = ensure_signed(origin)?;
  527. // Check that account is forum member
  528. Self::ensure_is_forum_member(&who)?;
  529. // Validate post text
  530. Self::ensure_post_text_is_valid(&new_text)?;
  531. // Make sure there exists a mutable post with post id `post_id`
  532. let post = Self::ensure_post_is_mutable(post_id)?;
  533. // Signer does not match creator of post with identifier postId
  534. ensure!(post.author_id == who, ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR);
  535. /*
  536. * Here we are safe to mutate
  537. */
  538. <PostById<T>>::mutate(post_id, |p| {
  539. let expired_post_text = PostTextChange {
  540. expired_at: common::current_block_time::<T>(),
  541. text: post.current_text.clone()
  542. };
  543. // Set current text to new text
  544. p.current_text = new_text;
  545. // Copy current text to history of expired texts
  546. p.text_change_history.push(expired_post_text);
  547. });
  548. // Generate event
  549. Self::deposit_event(RawEvent::PostTextUpdated(post.id, post.text_change_history.len() as u64));
  550. Ok(())
  551. }
  552. /// Moderate post
  553. #[weight = 10_000_000] // TODO: adjust weight
  554. fn moderate_post(origin, post_id: T::PostId, rationale: Vec<u8>) -> DispatchResult {
  555. // Check that its a valid signature
  556. let who = ensure_signed(origin)?;
  557. // Signed by forum SUDO
  558. Self::ensure_is_forum_sudo(&who)?;
  559. // Make sure post exists and is mutable
  560. let post = Self::ensure_post_is_mutable(post_id)?;
  561. Self::ensure_post_moderation_rationale_is_valid(&rationale)?;
  562. /*
  563. * Here we are safe to mutate
  564. */
  565. // Update moderation action on post
  566. let moderation_action = ModerationAction{
  567. moderated_at: common::current_block_time::<T>(),
  568. moderator_id: who,
  569. rationale
  570. };
  571. <PostById<T>>::mutate(post_id, |p| {
  572. p.moderation = Some(moderation_action);
  573. });
  574. // Update moderated and unmoderated post count of corresponding thread
  575. <ThreadById<T>>::mutate(post.thread_id, |t| {
  576. t.num_unmoderated_posts -= 1;
  577. t.num_moderated_posts += 1;
  578. });
  579. // Generate event
  580. Self::deposit_event(RawEvent::PostModerated(post.id));
  581. Ok(())
  582. }
  583. }
  584. }
  585. impl<T: Trait> Module<T> {
  586. fn ensure_category_title_is_valid(title: &[u8]) -> DispatchResult {
  587. CategoryTitleConstraint::get().ensure_valid(
  588. title.len(),
  589. ERROR_CATEGORY_TITLE_TOO_SHORT,
  590. ERROR_CATEGORY_TITLE_TOO_LONG,
  591. )
  592. }
  593. fn ensure_category_description_is_valid(description: &[u8]) -> DispatchResult {
  594. CategoryDescriptionConstraint::get().ensure_valid(
  595. description.len(),
  596. ERROR_CATEGORY_DESCRIPTION_TOO_SHORT,
  597. ERROR_CATEGORY_DESCRIPTION_TOO_LONG,
  598. )
  599. }
  600. fn ensure_thread_moderation_rationale_is_valid(rationale: &[u8]) -> DispatchResult {
  601. ThreadModerationRationaleConstraint::get().ensure_valid(
  602. rationale.len(),
  603. ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT,
  604. ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG,
  605. )
  606. }
  607. fn ensure_thread_title_is_valid(title: &[u8]) -> DispatchResult {
  608. ThreadTitleConstraint::get().ensure_valid(
  609. title.len(),
  610. ERROR_THREAD_TITLE_TOO_SHORT,
  611. ERROR_THREAD_TITLE_TOO_LONG,
  612. )
  613. }
  614. fn ensure_post_text_is_valid(text: &[u8]) -> DispatchResult {
  615. PostTextConstraint::get().ensure_valid(
  616. text.len(),
  617. ERROR_POST_TEXT_TOO_SHORT,
  618. ERROR_POST_TEXT_TOO_LONG,
  619. )
  620. }
  621. fn ensure_post_moderation_rationale_is_valid(rationale: &[u8]) -> DispatchResult {
  622. PostModerationRationaleConstraint::get().ensure_valid(
  623. rationale.len(),
  624. ERROR_POST_MODERATION_RATIONALE_TOO_SHORT,
  625. ERROR_POST_MODERATION_RATIONALE_TOO_LONG,
  626. )
  627. }
  628. fn ensure_post_is_mutable(
  629. post_id: T::PostId,
  630. ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>, &'static str>
  631. {
  632. // Make sure post exists
  633. let post = Self::ensure_post_exists(post_id)?;
  634. // and is unmoderated
  635. ensure!(post.moderation.is_none(), ERROR_POST_MODERATED);
  636. // and make sure thread is mutable
  637. Self::ensure_thread_is_mutable(post.thread_id)?;
  638. Ok(post)
  639. }
  640. fn ensure_post_exists(
  641. post_id: T::PostId,
  642. ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>, &'static str>
  643. {
  644. if <PostById<T>>::contains_key(post_id) {
  645. Ok(<PostById<T>>::get(post_id))
  646. } else {
  647. Err(ERROR_POST_DOES_NOT_EXIST)
  648. }
  649. }
  650. fn ensure_thread_is_mutable(
  651. thread_id: T::ThreadId,
  652. ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>, &'static str> {
  653. // Make sure thread exists
  654. let thread = Self::ensure_thread_exists(thread_id)?;
  655. // and is unmoderated
  656. ensure!(thread.moderation.is_none(), ERROR_THREAD_MODERATED);
  657. // and corresponding category is mutable
  658. Self::ensure_catgory_is_mutable(thread.category_id)?;
  659. Ok(thread)
  660. }
  661. fn ensure_thread_exists(
  662. thread_id: T::ThreadId,
  663. ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>, &'static str> {
  664. if <ThreadById<T>>::contains_key(thread_id) {
  665. Ok(<ThreadById<T>>::get(thread_id))
  666. } else {
  667. Err(ERROR_THREAD_DOES_NOT_EXIST)
  668. }
  669. }
  670. fn ensure_forum_sudo_set() -> Result<T::AccountId, &'static str> {
  671. match <ForumSudo<T>>::get() {
  672. Some(account_id) => Ok(account_id),
  673. None => Err(ERROR_FORUM_SUDO_NOT_SET),
  674. }
  675. }
  676. fn ensure_is_forum_sudo(account_id: &T::AccountId) -> DispatchResult {
  677. let forum_sudo_account = Self::ensure_forum_sudo_set()?;
  678. ensure!(
  679. *account_id == forum_sudo_account,
  680. ERROR_ORIGIN_NOT_FORUM_SUDO
  681. );
  682. Ok(())
  683. }
  684. fn ensure_is_forum_member(
  685. account_id: &T::AccountId,
  686. ) -> Result<ForumUser<T::AccountId>, &'static str> {
  687. let forum_user_query = T::MembershipRegistry::get_forum_user(account_id);
  688. if let Some(forum_user) = forum_user_query {
  689. Ok(forum_user)
  690. } else {
  691. Err(ERROR_NOT_FORUM_USER)
  692. }
  693. }
  694. fn ensure_catgory_is_mutable(category_id: CategoryId) -> DispatchResult {
  695. let category_tree_path = Self::build_category_tree_path(category_id);
  696. Self::ensure_can_mutate_in_path_leaf(&category_tree_path)
  697. }
  698. // TODO: remove post-Constaninople
  699. // Clippy linter warning.
  700. // Disable it because of possible frontend API break.
  701. #[allow(clippy::ptr_arg)]
  702. fn ensure_can_mutate_in_path_leaf(
  703. category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
  704. ) -> DispatchResult {
  705. // Is parent category directly or indirectly deleted or archived category
  706. ensure!(
  707. !category_tree_path.iter().any(
  708. |c: &Category<T::BlockNumber, T::Moment, T::AccountId>| c.deleted || c.archived
  709. ),
  710. ERROR_ANCESTOR_CATEGORY_IMMUTABLE
  711. );
  712. Ok(())
  713. }
  714. // TODO: remove post-Constaninople
  715. // Clippy linter warning
  716. #[allow(clippy::ptr_arg)] // disable it because of possible frontend API break
  717. fn ensure_can_add_subcategory_path_leaf(
  718. category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
  719. ) -> DispatchResult {
  720. Self::ensure_can_mutate_in_path_leaf(category_tree_path)?;
  721. // Does adding a new category exceed maximum depth
  722. let depth_of_new_category = 1 + 1 + category_tree_path.len();
  723. ensure!(
  724. depth_of_new_category <= MAX_CATEGORY_DEPTH as usize,
  725. ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED
  726. );
  727. Ok(())
  728. }
  729. fn ensure_valid_category_and_build_category_tree_path(
  730. category_id: CategoryId,
  731. ) -> Result<CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
  732. ensure!(
  733. <CategoryById<T>>::contains_key(&category_id),
  734. ERROR_CATEGORY_DOES_NOT_EXIST
  735. );
  736. // Get path from parent to root of category tree.
  737. let category_tree_path = Self::build_category_tree_path(category_id);
  738. assert!(!category_tree_path.is_empty());
  739. Ok(category_tree_path)
  740. }
  741. /// Builds path and populates in `path`.
  742. /// Requires that `category_id` is valid
  743. fn build_category_tree_path(
  744. category_id: CategoryId,
  745. ) -> CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId> {
  746. // Get path from parent to root of category tree.
  747. let mut category_tree_path = vec![];
  748. Self::_build_category_tree_path(category_id, &mut category_tree_path);
  749. category_tree_path
  750. }
  751. /// Builds path and populates in `path`.
  752. /// Requires that `category_id` is valid
  753. fn _build_category_tree_path(
  754. category_id: CategoryId,
  755. path: &mut CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
  756. ) {
  757. // Grab category
  758. let category = <CategoryById<T>>::get(category_id);
  759. // Copy out position_in_parent_category
  760. let position_in_parent_category_field = category.position_in_parent_category.clone();
  761. // Add category to path container
  762. path.push(category);
  763. // Make recursive call on parent if we are not at root
  764. if let Some(child_position_in_parent) = position_in_parent_category_field {
  765. assert!(<CategoryById<T>>::contains_key(
  766. &child_position_in_parent.parent_id
  767. ));
  768. Self::_build_category_tree_path(child_position_in_parent.parent_id, path);
  769. }
  770. }
  771. fn add_new_thread(
  772. category_id: CategoryId,
  773. title: &[u8],
  774. author_id: &T::AccountId,
  775. ) -> Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId> {
  776. // Get category
  777. let category = <CategoryById<T>>::get(category_id);
  778. // Create and add new thread
  779. let new_thread_id = NextThreadId::<T>::get();
  780. let new_thread = Thread {
  781. id: new_thread_id,
  782. title: title.to_owned(),
  783. category_id,
  784. nr_in_category: category.num_threads_created() + 1,
  785. moderation: None,
  786. num_unmoderated_posts: 0,
  787. num_moderated_posts: 0,
  788. created_at: common::current_block_time::<T>(),
  789. author_id: author_id.clone(),
  790. };
  791. // Store thread
  792. <ThreadById<T>>::insert(new_thread_id, new_thread.clone());
  793. // Update next thread id
  794. NextThreadId::<T>::mutate(|n| {
  795. *n += One::one();
  796. });
  797. // Update unmoderated thread count in corresponding category
  798. <CategoryById<T>>::mutate(category_id, |c| {
  799. c.num_direct_unmoderated_threads += 1;
  800. });
  801. new_thread
  802. }
  803. /// Creates and ads a new post ot the given thread, and makes all required state updates
  804. /// `thread_id` must be valid
  805. fn add_new_post(
  806. thread_id: T::ThreadId,
  807. text: &[u8],
  808. author_id: &T::AccountId,
  809. ) -> Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId> {
  810. // Get thread
  811. let thread = <ThreadById<T>>::get(thread_id);
  812. // Make and add initial post
  813. let new_post_id = NextPostId::<T>::get();
  814. let new_post = Post {
  815. id: new_post_id,
  816. thread_id,
  817. nr_in_thread: thread.num_posts_ever_created() + 1,
  818. current_text: text.to_owned(),
  819. moderation: None,
  820. text_change_history: vec![],
  821. created_at: common::current_block_time::<T>(),
  822. author_id: author_id.clone(),
  823. };
  824. // Store post
  825. <PostById<T>>::insert(new_post_id, new_post.clone());
  826. // Update next post id
  827. NextPostId::<T>::mutate(|n| {
  828. *n += One::one();
  829. });
  830. // Update unmoderated post count of thread
  831. <ThreadById<T>>::mutate(thread_id, |t| {
  832. t.num_unmoderated_posts += 1;
  833. });
  834. new_post
  835. }
  836. }