lib.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. //! # Proposals discussion module
  2. //! Proposals `discussion` module for the Joystream platform. Version 3.
  3. //! It contains discussion subsystem 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. //! - [change_thread_mode](./struct.Module.html#method.change_thread_mode) - changes thread permission mode
  15. //!
  16. //! ## Public API methods
  17. //! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread
  18. //! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures safe thread creation
  19. //!
  20. //! ## Usage
  21. //!
  22. //! ```
  23. //! use frame_support::decl_module;
  24. //! use frame_system::ensure_root;
  25. //! use pallet_proposals_discussion::{self as discussions, ThreadMode};
  26. //!
  27. //! pub trait Trait: discussions::Trait + membership::Trait {}
  28. //!
  29. //! decl_module! {
  30. //! pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  31. //! #[weight = 10_000_000]
  32. //! pub fn create_discussion(origin, title: Vec<u8>, author_id : T::MemberId) {
  33. //! ensure_root(origin)?;
  34. //! let thread_mode = ThreadMode::Open;
  35. //! <discussions::Module<T>>::ensure_can_create_thread(&thread_mode)?;
  36. //! <discussions::Module<T>>::create_thread(author_id, thread_mode)?;
  37. //! }
  38. //! }
  39. //! }
  40. //! # fn main() {}
  41. //! ```
  42. // Ensure we're `no_std` when compiling for Wasm.
  43. #![cfg_attr(not(feature = "std"), no_std)]
  44. // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
  45. //#![warn(missing_docs)]
  46. mod benchmarking;
  47. #[cfg(test)]
  48. mod tests;
  49. mod types;
  50. use frame_support::dispatch::{DispatchError, DispatchResult};
  51. use frame_support::sp_runtime::SaturatedConversion;
  52. use frame_support::traits::Get;
  53. use frame_support::{
  54. decl_error, decl_event, decl_module, decl_storage, ensure, weights::Weight, Parameter,
  55. };
  56. use sp_std::clone::Clone;
  57. use sp_std::vec::Vec;
  58. use common::origin::ActorOriginValidator;
  59. use types::{DiscussionPost, DiscussionThread};
  60. pub use types::ThreadMode;
  61. type MemberId<T> = <T as membership::Trait>::MemberId;
  62. /// Proposals discussion WeightInfo.
  63. /// Note: This was auto generated through the benchmark CLI using the `--weight-trait` flag
  64. pub trait WeightInfo {
  65. fn add_post(i: u32) -> Weight; // Note: since parameter doesn't affect weight it's discarded
  66. fn update_post() -> Weight; // Note: since parameter doesn't affect weight it's discarded
  67. fn change_thread_mode(i: u32) -> Weight;
  68. }
  69. type WeightInfoDiscussion<T> = <T as Trait>::WeightInfo;
  70. decl_event!(
  71. /// Proposals engine events
  72. pub enum Event<T>
  73. where
  74. <T as Trait>::ThreadId,
  75. MemberId = MemberId<T>,
  76. <T as Trait>::PostId,
  77. {
  78. /// Emits on thread creation.
  79. ThreadCreated(ThreadId, MemberId),
  80. /// Emits on post creation.
  81. PostCreated(PostId, MemberId),
  82. /// Emits on post update.
  83. PostUpdated(PostId, MemberId),
  84. /// Emits on thread mode change.
  85. ThreadModeChanged(ThreadId, ThreadMode<MemberId>),
  86. }
  87. );
  88. /// Defines whether the member is an active councilor.
  89. pub trait CouncilMembership<AccountId, MemberId> {
  90. /// Defines whether the member is an active councilor.
  91. fn is_council_member(account_id: &AccountId, member_id: &MemberId) -> bool;
  92. }
  93. /// 'Proposal discussion' substrate module Trait
  94. pub trait Trait: frame_system::Trait + membership::Trait {
  95. /// Discussion event type.
  96. type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
  97. /// Validates post author id and origin combination
  98. type AuthorOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
  99. /// Defines whether the member is an active councilor.
  100. type CouncilOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
  101. /// Discussion thread Id type
  102. type ThreadId: From<u64> + Into<u64> + Parameter + Default + Copy;
  103. /// Post Id type
  104. type PostId: From<u64> + Parameter + Default + Copy;
  105. /// Defines author list size limit for the Closed discussion.
  106. type MaxWhiteListSize: Get<u32>;
  107. /// Weight information for extrinsics in this pallet.
  108. type WeightInfo: WeightInfo;
  109. }
  110. decl_error! {
  111. /// Discussion module predefined errors
  112. pub enum Error for Module<T: Trait> {
  113. /// Author should match the post creator
  114. NotAuthor,
  115. /// Thread doesn't exist
  116. ThreadDoesntExist,
  117. /// Post doesn't exist
  118. PostDoesntExist,
  119. /// Require root origin in extrinsics
  120. RequireRootOrigin,
  121. /// The thread has Closed mode. And post author doesn't belong to council or allowed members.
  122. CannotPostOnClosedThread,
  123. /// Should be thread author or councilor.
  124. NotAuthorOrCouncilor,
  125. /// Max allowed authors list limit exceeded.
  126. MaxWhiteListSizeExceeded,
  127. }
  128. }
  129. // Storage for the proposals discussion module
  130. decl_storage! {
  131. pub trait Store for Module<T: Trait> as ProposalDiscussion {
  132. /// Map thread identifier to corresponding thread.
  133. pub ThreadById get(fn thread_by_id): map hasher(blake2_128_concat)
  134. T::ThreadId => DiscussionThread<MemberId<T>, T::BlockNumber, MemberId<T>>;
  135. /// Count of all threads that have been created.
  136. pub ThreadCount get(fn thread_count): u64;
  137. /// Map thread id and post id to corresponding post.
  138. pub PostThreadIdByPostId:
  139. double_map hasher(blake2_128_concat) T::ThreadId, hasher(blake2_128_concat) T::PostId =>
  140. DiscussionPost<MemberId<T>>;
  141. /// Count of all posts that have been created.
  142. pub PostCount get(fn post_count): u64;
  143. }
  144. }
  145. decl_module! {
  146. /// 'Proposal discussion' substrate module
  147. pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  148. /// Predefined errors
  149. type Error = Error<T>;
  150. /// Emits an event. Default substrate implementation.
  151. fn deposit_event() = default;
  152. /// Adds a post with author origin check.
  153. ///
  154. /// <weight>
  155. ///
  156. /// ## Weight
  157. /// `O (W)` where:
  158. /// - `W` is the number of whitelisted members for `thread_id`
  159. /// - DB:
  160. /// - O(1) doesn't depend on the state or parameters
  161. /// # </weight>
  162. #[weight = WeightInfoDiscussion::<T>::add_post(
  163. T::MaxWhiteListSize::get(),
  164. )]
  165. pub fn add_post(
  166. origin,
  167. post_author_id: MemberId<T>,
  168. thread_id : T::ThreadId,
  169. _text : Vec<u8>
  170. ) {
  171. T::AuthorOriginValidator::ensure_actor_origin(
  172. origin.clone(),
  173. post_author_id,
  174. )?;
  175. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  176. Self::ensure_thread_mode(origin, post_author_id, thread_id)?;
  177. // mutation
  178. let next_post_count_value = Self::post_count() + 1;
  179. let new_post_id = next_post_count_value;
  180. let new_post = DiscussionPost {
  181. author_id: post_author_id,
  182. };
  183. let post_id = T::PostId::from(new_post_id);
  184. <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
  185. PostCount::put(next_post_count_value);
  186. Self::deposit_event(RawEvent::PostCreated(post_id, post_author_id));
  187. }
  188. /// Updates a post with author origin check. Update attempts number is limited.
  189. ///
  190. /// <weight>
  191. ///
  192. /// ## Weight
  193. /// `O (1)` doesn't depend on the state or parameters
  194. /// - DB:
  195. /// - O(1) doesn't depend on the state or parameters
  196. /// # </weight>
  197. #[weight = WeightInfoDiscussion::<T>::update_post()]
  198. pub fn update_post(
  199. origin,
  200. post_author_id: MemberId<T>,
  201. thread_id: T::ThreadId,
  202. post_id : T::PostId,
  203. _text : Vec<u8>
  204. ){
  205. T::AuthorOriginValidator::ensure_actor_origin(
  206. origin,
  207. post_author_id,
  208. )?;
  209. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  210. ensure!(<PostThreadIdByPostId<T>>::contains_key(thread_id, post_id), Error::<T>::PostDoesntExist);
  211. let post = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id);
  212. ensure!(post.author_id == post_author_id, Error::<T>::NotAuthor);
  213. // mutation
  214. Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));
  215. }
  216. /// Changes thread permission mode.
  217. ///
  218. /// <weight>
  219. ///
  220. /// ## Weight
  221. /// `O (W)` if ThreadMode is close or O(1) otherwise where:
  222. /// - `W` is the number of whitelisted members in `mode`
  223. /// - DB:
  224. /// - O(1) doesn't depend on the state or parameters
  225. /// # </weight>
  226. #[weight = WeightInfoDiscussion::<T>::change_thread_mode(
  227. if let ThreadMode::Closed(ref list) = mode {
  228. list.len().saturated_into()
  229. } else {
  230. 0
  231. }
  232. )]
  233. pub fn change_thread_mode(
  234. origin,
  235. member_id: MemberId<T>,
  236. thread_id : T::ThreadId,
  237. mode : ThreadMode<MemberId<T>>
  238. ) {
  239. T::AuthorOriginValidator::ensure_actor_origin(origin.clone(), member_id)?;
  240. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  241. if let ThreadMode::Closed(ref list) = mode{
  242. ensure!(
  243. list.len() <= (T::MaxWhiteListSize::get()).saturated_into(),
  244. Error::<T>::MaxWhiteListSizeExceeded
  245. );
  246. }
  247. let thread = Self::thread_by_id(&thread_id);
  248. let is_councilor =
  249. T::CouncilOriginValidator::ensure_actor_origin(origin, member_id)
  250. .is_ok();
  251. let is_thread_author = thread.author_id == member_id;
  252. ensure!(is_thread_author || is_councilor, Error::<T>::NotAuthorOrCouncilor);
  253. // mutation
  254. <ThreadById<T>>::mutate(thread_id, |thread| {
  255. thread.mode = mode.clone();
  256. });
  257. Self::deposit_event(RawEvent::ThreadModeChanged(thread_id, mode));
  258. }
  259. }
  260. }
  261. impl<T: Trait> Module<T> {
  262. /// Create the discussion thread.
  263. /// times in a row by the same author.
  264. pub fn create_thread(
  265. thread_author_id: MemberId<T>,
  266. mode: ThreadMode<MemberId<T>>,
  267. ) -> Result<T::ThreadId, DispatchError> {
  268. Self::ensure_can_create_thread(&mode)?;
  269. let next_thread_count_value = Self::thread_count() + 1;
  270. let new_thread_id = next_thread_count_value;
  271. let new_thread = DiscussionThread {
  272. activated_at: Self::current_block(),
  273. author_id: thread_author_id,
  274. mode,
  275. };
  276. // mutation
  277. let thread_id = T::ThreadId::from(new_thread_id);
  278. <ThreadById<T>>::insert(thread_id, new_thread);
  279. ThreadCount::put(next_thread_count_value);
  280. Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id));
  281. Ok(thread_id)
  282. }
  283. /// Ensures thread can be created.
  284. /// Checks:
  285. /// - max allowed authors for the Closed thread mode
  286. pub fn ensure_can_create_thread(mode: &ThreadMode<MemberId<T>>) -> DispatchResult {
  287. if let ThreadMode::Closed(list) = mode {
  288. ensure!(
  289. list.len() <= (T::MaxWhiteListSize::get()).saturated_into(),
  290. Error::<T>::MaxWhiteListSizeExceeded
  291. );
  292. }
  293. Ok(())
  294. }
  295. }
  296. impl<T: Trait> Module<T> {
  297. // Wrapper-function over System::block_number()
  298. fn current_block() -> T::BlockNumber {
  299. <frame_system::Module<T>>::block_number()
  300. }
  301. fn ensure_thread_mode(
  302. origin: T::Origin,
  303. thread_author_id: MemberId<T>,
  304. thread_id: T::ThreadId,
  305. ) -> DispatchResult {
  306. let thread = Self::thread_by_id(thread_id);
  307. match thread.mode {
  308. ThreadMode::Open => Ok(()),
  309. ThreadMode::Closed(members) => {
  310. let is_thread_author = thread_author_id == thread.author_id;
  311. let is_councilor =
  312. T::CouncilOriginValidator::ensure_actor_origin(origin, thread_author_id)
  313. .is_ok();
  314. let is_allowed_member = members
  315. .iter()
  316. .any(|member_id| *member_id == thread_author_id);
  317. if is_thread_author || is_councilor || is_allowed_member {
  318. Ok(())
  319. } else {
  320. Err(Error::<T>::CannotPostOnClosedThread.into())
  321. }
  322. }
  323. }
  324. }
  325. }