lib.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. //! # Proposals discussion module
  2. //! Proposals `discussion` module for the Joystream platform. Version 2.
  3. //! It contains discussion system of the proposals.
  4. //!
  5. //! ## Overview
  6. //!
  7. //! The proposals discussion module is used by the codex module to provide a platform for discussions
  8. //! about different proposals. It allows to create discussion threads and then add and update related
  9. //! posts.
  10. //!
  11. //! ## Supported extrinsics
  12. //! - [add_post](./struct.Module.html#method.add_post) - adds a post to an existing discussion thread
  13. //! - [update_post](./struct.Module.html#method.update_post) - updates existing post
  14. //!
  15. //! ## Public API methods
  16. //! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread
  17. //! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures safe thread creation
  18. //!
  19. //! ## Usage
  20. //!
  21. //! ```
  22. //! use frame_support::decl_module;
  23. //! use frame_system::ensure_root;
  24. //! use pallet_proposals_discussion::{self as discussions};
  25. //!
  26. //! pub trait Trait: discussions::Trait + membership::Trait {}
  27. //!
  28. //! decl_module! {
  29. //! pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  30. //! #[weight = 10_000_000]
  31. //! pub fn create_discussion(origin, title: Vec<u8>, author_id : T::MemberId) {
  32. //! ensure_root(origin)?;
  33. //! <discussions::Module<T>>::ensure_can_create_thread(author_id, &title)?;
  34. //! <discussions::Module<T>>::create_thread(author_id, title)?;
  35. //! }
  36. //! }
  37. //! }
  38. //! # fn main() {}
  39. //! ```
  40. // Ensure we're `no_std` when compiling for Wasm.
  41. #![cfg_attr(not(feature = "std"), no_std)]
  42. // Internal Substrate warning (decl_event).
  43. #![allow(clippy::unused_unit)]
  44. // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
  45. //#![warn(missing_docs)]
  46. #[cfg(test)]
  47. mod tests;
  48. mod types;
  49. use frame_support::dispatch::{DispatchError, DispatchResult};
  50. use frame_support::traits::Get;
  51. use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
  52. use sp_std::clone::Clone;
  53. use sp_std::vec::Vec;
  54. use common::origin::ActorOriginValidator;
  55. use types::{DiscussionPost, DiscussionThread, ThreadCounter};
  56. type MemberId<T> = <T as common::MembershipTypes>::MemberId;
  57. decl_event!(
  58. /// Proposals engine events
  59. pub enum Event<T>
  60. where
  61. <T as Trait>::ThreadId,
  62. MemberId = MemberId<T>,
  63. <T as Trait>::PostId,
  64. {
  65. /// Emits on thread creation.
  66. ThreadCreated(ThreadId, MemberId),
  67. /// Emits on post creation.
  68. PostCreated(PostId, MemberId),
  69. /// Emits on post update.
  70. PostUpdated(PostId, MemberId),
  71. }
  72. );
  73. /// 'Proposal discussion' substrate module Trait
  74. pub trait Trait: frame_system::Trait + membership::Trait {
  75. /// Discussion event type.
  76. type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
  77. /// Validates post author id and origin combination
  78. type PostAuthorOriginValidator: ActorOriginValidator<
  79. Self::Origin,
  80. MemberId<Self>,
  81. Self::AccountId,
  82. >;
  83. /// Discussion thread Id type
  84. type ThreadId: From<u64> + Into<u64> + Parameter + Default + Copy;
  85. /// Post Id type
  86. type PostId: From<u64> + Parameter + Default + Copy;
  87. /// Defines post edition number limit.
  88. type MaxPostEditionNumber: Get<u32>;
  89. /// Defines thread title length limit.
  90. type ThreadTitleLengthLimit: Get<u32>;
  91. /// Defines post length limit.
  92. type PostLengthLimit: Get<u32>;
  93. /// Defines max thread by same author in a row number limit.
  94. type MaxThreadInARowNumber: Get<u32>;
  95. }
  96. decl_error! {
  97. /// Discussion module predefined errors
  98. pub enum Error for Module<T: Trait> {
  99. /// Author should match the post creator
  100. NotAuthor,
  101. /// Post edition limit reached
  102. PostEditionNumberExceeded,
  103. /// Discussion cannot have an empty title
  104. EmptyTitleProvided,
  105. /// Title is too long
  106. TitleIsTooLong,
  107. /// Thread doesn't exist
  108. ThreadDoesntExist,
  109. /// Post doesn't exist
  110. PostDoesntExist,
  111. /// Post cannot be empty
  112. EmptyPostProvided,
  113. /// Post is too long
  114. PostIsTooLong,
  115. /// Max number of threads by same author in a row limit exceeded
  116. MaxThreadInARowLimitExceeded,
  117. /// Require root origin in extrinsics
  118. RequireRootOrigin,
  119. }
  120. }
  121. // Storage for the proposals discussion module
  122. decl_storage! {
  123. pub trait Store for Module<T: Trait> as ProposalDiscussion {
  124. /// Map thread identifier to corresponding thread.
  125. pub ThreadById get(fn thread_by_id): map hasher(blake2_128_concat)
  126. T::ThreadId => DiscussionThread<MemberId<T>, T::BlockNumber>;
  127. /// Count of all threads that have been created.
  128. pub ThreadCount get(fn thread_count): u64;
  129. /// Map thread id and post id to corresponding post.
  130. pub PostThreadIdByPostId:
  131. double_map hasher(blake2_128_concat) T::ThreadId, hasher(blake2_128_concat) T::PostId =>
  132. DiscussionPost<MemberId<T>, T::BlockNumber, T::ThreadId>;
  133. /// Count of all posts that have been created.
  134. pub PostCount get(fn post_count): u64;
  135. /// Last author thread counter (part of the antispam mechanism)
  136. pub LastThreadAuthorCounter get(fn last_thread_author_counter):
  137. Option<ThreadCounter<MemberId<T>>>;
  138. }
  139. }
  140. decl_module! {
  141. /// 'Proposal discussion' substrate module
  142. pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  143. /// Predefined errors
  144. type Error = Error<T>;
  145. /// Emits an event. Default substrate implementation.
  146. fn deposit_event() = default;
  147. /// Exports post edition number limit const.
  148. const MaxPostEditionNumber: u32 = T::MaxPostEditionNumber::get();
  149. /// Exports thread title length limit const.
  150. const ThreadTitleLengthLimit: u32 = T::ThreadTitleLengthLimit::get();
  151. /// Exports post length limit const.
  152. const PostLengthLimit: u32 = T::PostLengthLimit::get();
  153. /// Exports max thread by same author in a row number limit const.
  154. const MaxThreadInARowNumber: u32 = T::MaxThreadInARowNumber::get();
  155. /// Adds a post with author origin check.
  156. #[weight = 10_000_000] // TODO: adjust weight
  157. pub fn add_post(
  158. origin,
  159. post_author_id: MemberId<T>,
  160. thread_id : T::ThreadId,
  161. text : Vec<u8>
  162. ) {
  163. T::PostAuthorOriginValidator::ensure_actor_origin(
  164. origin,
  165. post_author_id,
  166. )?;
  167. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  168. ensure!(!text.is_empty(),Error::<T>::EmptyPostProvided);
  169. ensure!(
  170. text.len() as u32 <= T::PostLengthLimit::get(),
  171. Error::<T>::PostIsTooLong
  172. );
  173. // mutation
  174. let next_post_count_value = Self::post_count() + 1;
  175. let new_post_id = next_post_count_value;
  176. let new_post = DiscussionPost {
  177. text,
  178. created_at: Self::current_block(),
  179. updated_at: Self::current_block(),
  180. author_id: post_author_id,
  181. edition_number : 0,
  182. thread_id,
  183. };
  184. let post_id = T::PostId::from(new_post_id);
  185. <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
  186. PostCount::put(next_post_count_value);
  187. Self::deposit_event(RawEvent::PostCreated(post_id, post_author_id));
  188. }
  189. /// Updates a post with author origin check. Update attempts number is limited.
  190. #[weight = 10_000_000] // TODO: adjust weight
  191. pub fn update_post(
  192. origin,
  193. post_author_id: MemberId<T>,
  194. thread_id: T::ThreadId,
  195. post_id : T::PostId,
  196. text : Vec<u8>
  197. ){
  198. T::PostAuthorOriginValidator::ensure_actor_origin(
  199. origin,
  200. post_author_id,
  201. )?;
  202. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  203. ensure!(<PostThreadIdByPostId<T>>::contains_key(thread_id, post_id), Error::<T>::PostDoesntExist);
  204. ensure!(!text.is_empty(), Error::<T>::EmptyPostProvided);
  205. ensure!(
  206. text.len() as u32 <= T::PostLengthLimit::get(),
  207. Error::<T>::PostIsTooLong
  208. );
  209. let post = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id);
  210. ensure!(post.author_id == post_author_id, Error::<T>::NotAuthor);
  211. ensure!(post.edition_number < T::MaxPostEditionNumber::get(),
  212. Error::<T>::PostEditionNumberExceeded);
  213. let new_post = DiscussionPost {
  214. text,
  215. updated_at: Self::current_block(),
  216. edition_number: post.edition_number + 1,
  217. ..post
  218. };
  219. // mutation
  220. <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
  221. Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));
  222. }
  223. }
  224. }
  225. impl<T: Trait> Module<T> {
  226. /// Create the discussion thread. Cannot add more threads than 'predefined limit = MaxThreadInARowNumber'
  227. /// times in a row by the same author.
  228. pub fn create_thread(
  229. thread_author_id: MemberId<T>,
  230. title: Vec<u8>,
  231. ) -> Result<T::ThreadId, DispatchError> {
  232. Self::ensure_can_create_thread(thread_author_id, &title)?;
  233. let next_thread_count_value = Self::thread_count() + 1;
  234. let new_thread_id = next_thread_count_value;
  235. let new_thread = DiscussionThread {
  236. title,
  237. created_at: Self::current_block(),
  238. author_id: thread_author_id,
  239. };
  240. // get new 'threads in a row' counter for the author
  241. let current_thread_counter = Self::get_updated_thread_counter(thread_author_id);
  242. // mutation
  243. let thread_id = T::ThreadId::from(new_thread_id);
  244. <ThreadById<T>>::insert(thread_id, new_thread);
  245. ThreadCount::put(next_thread_count_value);
  246. <LastThreadAuthorCounter<T>>::put(current_thread_counter);
  247. Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id));
  248. Ok(thread_id)
  249. }
  250. /// Ensures thread can be created.
  251. /// Checks:
  252. /// - title is valid
  253. /// - max thread in a row by the same author
  254. pub fn ensure_can_create_thread(thread_author_id: MemberId<T>, title: &[u8]) -> DispatchResult {
  255. ensure!(!title.is_empty(), Error::<T>::EmptyTitleProvided);
  256. ensure!(
  257. title.len() as u32 <= T::ThreadTitleLengthLimit::get(),
  258. Error::<T>::TitleIsTooLong
  259. );
  260. // get new 'threads in a row' counter for the author
  261. let current_thread_counter = Self::get_updated_thread_counter(thread_author_id);
  262. ensure!(
  263. current_thread_counter.counter as u32 <= T::MaxThreadInARowNumber::get(),
  264. Error::<T>::MaxThreadInARowLimitExceeded
  265. );
  266. Ok(())
  267. }
  268. }
  269. impl<T: Trait> Module<T> {
  270. // Wrapper-function over frame_system::block_number()
  271. fn current_block() -> T::BlockNumber {
  272. <frame_system::Module<T>>::block_number()
  273. }
  274. // returns incremented thread counter if last thread author equals with provided parameter
  275. fn get_updated_thread_counter(author_id: MemberId<T>) -> ThreadCounter<MemberId<T>> {
  276. // if thread counter exists
  277. if let Some(last_thread_author_counter) = Self::last_thread_author_counter() {
  278. // if last(previous) author is the same as current author
  279. if last_thread_author_counter.author_id == author_id {
  280. return last_thread_author_counter.increment();
  281. }
  282. }
  283. // else return new counter (set with 1 thread number)
  284. ThreadCounter::new(author_id)
  285. }
  286. }