lib.rs 36 KB

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