lib.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. //! # Proposals discussion module
  2. //! Proposals `discussion` module for the Joystream platform.
  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
  8. //! discussions about different proposals. It allows to create discussion threads and then add and
  9. //! update related 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
  15. //! - [delete_post](./struct.Module.html#method.delete_post) - Removes thread from storage
  16. //! permission mode
  17. //!
  18. //! ## Public API methods
  19. //! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread
  20. //! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures
  21. //! safe thread creation
  22. //!
  23. //! ## Usage
  24. //!
  25. //! ```
  26. //! use frame_support::decl_module;
  27. //! use frame_system::ensure_root;
  28. //! use pallet_proposals_discussion::{self as discussions, ThreadMode};
  29. //!
  30. //! pub trait Trait: discussions::Trait + common::membership::MembershipTypes {}
  31. //!
  32. //! decl_module! {
  33. //! pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  34. //! #[weight = 10_000_000]
  35. //! pub fn create_discussion(origin, title: Vec<u8>, author_id : T::MemberId) {
  36. //! ensure_root(origin)?;
  37. //! let thread_mode = ThreadMode::Open;
  38. //! <discussions::Module<T>>::ensure_can_create_thread(&thread_mode)?;
  39. //! <discussions::Module<T>>::create_thread(author_id, thread_mode)?;
  40. //! }
  41. //! }
  42. //! }
  43. //! # fn main() {}
  44. //! ```
  45. // Ensure we're `no_std` when compiling for Wasm.
  46. #![cfg_attr(not(feature = "std"), no_std)]
  47. // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
  48. //#![warn(missing_docs)]
  49. mod benchmarking;
  50. #[cfg(test)]
  51. mod tests;
  52. mod types;
  53. use frame_support::dispatch::{DispatchError, DispatchResult};
  54. use frame_support::sp_runtime::ModuleId;
  55. use frame_support::sp_runtime::SaturatedConversion;
  56. use frame_support::traits::Get;
  57. use frame_support::traits::{Currency, ExistenceRequirement};
  58. use frame_support::{
  59. decl_error, decl_event, decl_module, decl_storage, ensure, weights::Weight, Parameter,
  60. };
  61. use sp_runtime::traits::{AccountIdConversion, Saturating};
  62. use sp_std::clone::Clone;
  63. use sp_std::vec::Vec;
  64. use common::council::CouncilOriginValidator;
  65. use common::membership::MemberOriginValidator;
  66. use common::MemberId;
  67. use types::{DiscussionPost, DiscussionThread};
  68. pub use types::ThreadMode;
  69. /// Balance alias for `balances` module.
  70. pub type BalanceOf<T> = <T as balances::Trait>::Balance;
  71. type Balances<T> = balances::Module<T>;
  72. /// Proposals discussion WeightInfo.
  73. /// Note: This was auto generated through the benchmark CLI using the `--weight-trait` flag
  74. pub trait WeightInfo {
  75. fn add_post(j: u32) -> Weight;
  76. fn update_post(j: u32) -> Weight;
  77. fn delete_post() -> Weight;
  78. fn change_thread_mode(i: u32) -> Weight;
  79. }
  80. type WeightInfoDiscussion<T> = <T as Trait>::WeightInfo;
  81. decl_event!(
  82. /// Proposals engine events
  83. pub enum Event<T>
  84. where
  85. <T as Trait>::ThreadId,
  86. MemberId = MemberId<T>,
  87. <T as Trait>::PostId,
  88. {
  89. /// Emits on thread creation.
  90. ThreadCreated(ThreadId, MemberId),
  91. /// Emits on post creation.
  92. PostCreated(PostId, MemberId, ThreadId, Vec<u8>),
  93. /// Emits on post update.
  94. PostUpdated(PostId, MemberId, ThreadId, Vec<u8>),
  95. /// Emits on thread mode change.
  96. ThreadModeChanged(ThreadId, ThreadMode<MemberId>, MemberId),
  97. /// Emits on post deleted
  98. PostDeleted(MemberId, ThreadId, PostId, bool),
  99. }
  100. );
  101. /// Defines whether the member is an active councilor.
  102. pub trait CouncilMembership<AccountId, MemberId> {
  103. /// Defines whether the member is an active councilor.
  104. fn is_council_member(account_id: &AccountId, member_id: &MemberId) -> bool;
  105. }
  106. /// 'Proposal discussion' substrate module Trait
  107. pub trait Trait:
  108. frame_system::Trait + balances::Trait + common::membership::MembershipTypes
  109. {
  110. /// Discussion event type.
  111. type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
  112. /// Validates post author id and origin combination
  113. type AuthorOriginValidator: MemberOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
  114. /// Defines whether the member is an active councilor.
  115. type CouncilOriginValidator: CouncilOriginValidator<
  116. Self::Origin,
  117. MemberId<Self>,
  118. Self::AccountId,
  119. >;
  120. /// Discussion thread Id type
  121. type ThreadId: From<u64> + Into<u64> + Parameter + Default + Copy;
  122. /// Post Id type
  123. type PostId: From<u64> + Parameter + Default + Copy;
  124. /// Defines author list size limit for the Closed discussion.
  125. type MaxWhiteListSize: Get<u32>;
  126. /// Weight information for extrinsics in this pallet.
  127. type WeightInfo: WeightInfo;
  128. /// Fee for creating a post
  129. type PostDeposit: Get<Self::Balance>;
  130. /// The proposal_discussion module Id, used to derive the account Id to hold the thread bounty
  131. type ModuleId: Get<ModuleId>;
  132. /// Maximum number of blocks before a post can be erased by anyone
  133. type PostLifeTime: Get<Self::BlockNumber>;
  134. }
  135. decl_error! {
  136. /// Discussion module predefined errors
  137. pub enum Error for Module<T: Trait> {
  138. /// Thread doesn't exist
  139. ThreadDoesntExist,
  140. /// Post doesn't exist
  141. PostDoesntExist,
  142. /// Require root origin in extrinsics
  143. RequireRootOrigin,
  144. /// The thread has Closed mode. And post author doesn't belong to council or allowed members.
  145. CannotPostOnClosedThread,
  146. /// Should be thread author or councilor.
  147. NotAuthorOrCouncilor,
  148. /// Max allowed authors list limit exceeded.
  149. MaxWhiteListSizeExceeded,
  150. /// Account has insufficient balance to create a post
  151. InsufficientBalanceForPost,
  152. /// Account can't delete post at the moment
  153. CannotDeletePost,
  154. }
  155. }
  156. // Storage for the proposals discussion module
  157. decl_storage! {
  158. pub trait Store for Module<T: Trait> as ProposalDiscussion {
  159. /// Map thread identifier to corresponding thread.
  160. pub ThreadById get(fn thread_by_id): map hasher(blake2_128_concat)
  161. T::ThreadId => DiscussionThread<MemberId<T>, T::BlockNumber, MemberId<T>>;
  162. /// Count of all threads that have been created.
  163. pub ThreadCount get(fn thread_count): u64;
  164. /// Map thread id and post id to corresponding post.
  165. pub PostThreadIdByPostId:
  166. double_map hasher(blake2_128_concat) T::ThreadId, hasher(blake2_128_concat) T::PostId =>
  167. DiscussionPost<MemberId<T>, BalanceOf<T>, T::BlockNumber>;
  168. /// Count of all posts that have been created.
  169. pub PostCount get(fn post_count): u64;
  170. }
  171. }
  172. decl_module! {
  173. /// 'Proposal discussion' substrate module
  174. pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  175. /// Predefined errors
  176. type Error = Error<T>;
  177. /// Emits an event. Default substrate implementation.
  178. fn deposit_event() = default;
  179. /// Adds a post with author origin check.
  180. ///
  181. /// <weight>
  182. ///
  183. /// ## Weight
  184. /// `O (L)` where:
  185. /// - `L` is the length of `text`
  186. /// - DB:
  187. /// - O(1) doesn't depend on the state or parameters
  188. /// # </weight>
  189. #[weight = WeightInfoDiscussion::<T>::add_post(text.len().saturated_into())]
  190. pub fn add_post(
  191. origin,
  192. post_author_id: MemberId<T>,
  193. thread_id: T::ThreadId,
  194. text: Vec<u8>,
  195. editable: bool
  196. ) {
  197. let account_id = T::AuthorOriginValidator::ensure_member_controller_account_origin(
  198. origin.clone(),
  199. post_author_id,
  200. )?;
  201. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  202. Self::ensure_thread_mode(origin, post_author_id, thread_id)?;
  203. // Ensure account has enough funds
  204. if editable {
  205. ensure!(
  206. Balances::<T>::usable_balance(&account_id) >= T::PostDeposit::get(),
  207. Error::<T>::InsufficientBalanceForPost,
  208. );
  209. }
  210. // mutation
  211. if editable {
  212. Self::transfer_to_state_cleanup_treasury_account(
  213. T::PostDeposit::get(),
  214. thread_id,
  215. &account_id,
  216. )?;
  217. }
  218. let next_post_count_value = Self::post_count() + 1;
  219. let new_post_id = next_post_count_value;
  220. let post_id = T::PostId::from(new_post_id);
  221. if editable {
  222. let new_post = DiscussionPost {
  223. author_id: post_author_id,
  224. cleanup_pay_off: T::PostDeposit::get(),
  225. last_edited: frame_system::Module::<T>::block_number(),
  226. };
  227. <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
  228. }
  229. PostCount::put(next_post_count_value);
  230. Self::deposit_event(RawEvent::PostCreated(post_id, post_author_id, thread_id, text));
  231. }
  232. /// Remove post from storage, with the last parameter indicating whether to also hide it
  233. /// in the UI.
  234. ///
  235. /// <weight>
  236. ///
  237. /// ## Weight
  238. /// `O (1)`
  239. /// - DB:
  240. /// - O(1) doesn't depend on the state or parameters
  241. /// # </weight>
  242. #[weight = WeightInfoDiscussion::<T>::delete_post()]
  243. pub fn delete_post(
  244. origin,
  245. deleter_id: MemberId<T>,
  246. post_id : T::PostId,
  247. thread_id: T::ThreadId,
  248. hide: bool,
  249. ) {
  250. let account_id = T::AuthorOriginValidator::ensure_member_controller_account_origin(
  251. origin.clone(),
  252. deleter_id,
  253. )?;
  254. ensure!(
  255. <PostThreadIdByPostId<T>>::contains_key(thread_id, post_id),
  256. Error::<T>::PostDoesntExist
  257. );
  258. T::AuthorOriginValidator::ensure_member_controller_account_origin(
  259. origin,
  260. deleter_id,
  261. )?;
  262. let post = <PostThreadIdByPostId<T>>::get(thread_id, post_id);
  263. if !Self::anyone_can_delete_post(thread_id, post_id) {
  264. ensure!(
  265. post.author_id == deleter_id,
  266. Error::<T>::CannotDeletePost
  267. );
  268. }
  269. // mutation
  270. Self::pay_off(
  271. thread_id,
  272. T::PostDeposit::get(),
  273. &account_id,
  274. )?;
  275. <PostThreadIdByPostId<T>>::remove(thread_id, post_id);
  276. Self::deposit_event(RawEvent::PostDeleted(deleter_id, thread_id, post_id, hide));
  277. }
  278. /// Updates a post with author origin check. Update attempts number is limited.
  279. ///
  280. /// <weight>
  281. ///
  282. /// ## Weight
  283. /// `O (L)` where:
  284. /// - `L` is the length of `text`
  285. /// - DB:
  286. /// - O(1) doesn't depend on the state or parameters
  287. /// # </weight>
  288. #[weight = WeightInfoDiscussion::<T>::update_post(text.len().saturated_into())]
  289. pub fn update_post(
  290. origin,
  291. thread_id: T::ThreadId,
  292. post_id : T::PostId,
  293. text : Vec<u8>
  294. ){
  295. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  296. ensure!(
  297. <PostThreadIdByPostId<T>>::contains_key(thread_id, post_id),
  298. Error::<T>::PostDoesntExist
  299. );
  300. let post_author_id = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id).author_id;
  301. T::AuthorOriginValidator::ensure_member_controller_account_origin(
  302. origin,
  303. post_author_id,
  304. )?;
  305. // mutation
  306. <PostThreadIdByPostId<T>>::mutate(
  307. thread_id,
  308. post_id,
  309. |new_post| new_post.last_edited = frame_system::Module::<T>::block_number()
  310. );
  311. Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id, thread_id, text));
  312. }
  313. /// Changes thread permission mode.
  314. ///
  315. /// <weight>
  316. ///
  317. /// ## Weight
  318. /// `O (W)` if ThreadMode is close or O(1) otherwise where:
  319. /// - `W` is the number of whitelisted members in `mode`
  320. /// - DB:
  321. /// - O(1) doesn't depend on the state or parameters
  322. /// # </weight>
  323. #[weight = WeightInfoDiscussion::<T>::change_thread_mode(
  324. if let ThreadMode::Closed(ref list) = mode {
  325. list.len().saturated_into()
  326. } else {
  327. 0
  328. }
  329. )]
  330. pub fn change_thread_mode(
  331. origin,
  332. member_id: MemberId<T>,
  333. thread_id : T::ThreadId,
  334. mode : ThreadMode<MemberId<T>>
  335. ) {
  336. T::AuthorOriginValidator::ensure_member_controller_account_origin(origin.clone(), member_id)?;
  337. ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
  338. if let ThreadMode::Closed(ref list) = mode{
  339. ensure!(
  340. list.len() <= (T::MaxWhiteListSize::get()).saturated_into(),
  341. Error::<T>::MaxWhiteListSizeExceeded
  342. );
  343. }
  344. let thread = Self::thread_by_id(&thread_id);
  345. let is_councilor =
  346. T::CouncilOriginValidator::ensure_member_consulate(origin, member_id)
  347. .is_ok();
  348. let is_thread_author = thread.author_id == member_id;
  349. ensure!(is_thread_author || is_councilor, Error::<T>::NotAuthorOrCouncilor);
  350. // mutation
  351. <ThreadById<T>>::mutate(thread_id, |thread| {
  352. thread.mode = mode.clone();
  353. });
  354. Self::deposit_event(RawEvent::ThreadModeChanged(thread_id, mode, member_id));
  355. }
  356. }
  357. }
  358. impl<T: Trait> Module<T> {
  359. /// Create the discussion thread.
  360. /// times in a row by the same author.
  361. pub fn create_thread(
  362. thread_author_id: MemberId<T>,
  363. mode: ThreadMode<MemberId<T>>,
  364. ) -> Result<T::ThreadId, DispatchError> {
  365. Self::ensure_can_create_thread(&mode)?;
  366. let next_thread_count_value = Self::thread_count() + 1;
  367. let new_thread_id = next_thread_count_value;
  368. let new_thread = DiscussionThread {
  369. activated_at: Self::current_block(),
  370. author_id: thread_author_id,
  371. mode,
  372. };
  373. // mutation
  374. let thread_id = T::ThreadId::from(new_thread_id);
  375. <ThreadById<T>>::insert(thread_id, new_thread);
  376. ThreadCount::put(next_thread_count_value);
  377. Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id));
  378. Ok(thread_id)
  379. }
  380. /// Ensures thread can be created.
  381. /// Checks:
  382. /// - max allowed authors for the Closed thread mode
  383. pub fn ensure_can_create_thread(mode: &ThreadMode<MemberId<T>>) -> DispatchResult {
  384. if let ThreadMode::Closed(list) = mode {
  385. ensure!(
  386. list.len() <= (T::MaxWhiteListSize::get()).saturated_into(),
  387. Error::<T>::MaxWhiteListSizeExceeded
  388. );
  389. }
  390. Ok(())
  391. }
  392. }
  393. impl<T: Trait> Module<T> {
  394. // Wrapper-function over System::block_number()
  395. fn current_block() -> T::BlockNumber {
  396. <frame_system::Module<T>>::block_number()
  397. }
  398. fn anyone_can_delete_post(thread_id: T::ThreadId, post_id: T::PostId) -> bool {
  399. let thread_exists = <ThreadById<T>>::contains_key(thread_id);
  400. let post = <PostThreadIdByPostId<T>>::get(thread_id, post_id);
  401. !thread_exists
  402. && frame_system::Module::<T>::block_number().saturating_sub(post.last_edited)
  403. >= T::PostLifeTime::get()
  404. }
  405. fn pay_off(
  406. thread_id: T::ThreadId,
  407. amount: BalanceOf<T>,
  408. account_id: &T::AccountId,
  409. ) -> DispatchResult {
  410. let state_cleanup_treasury_account = T::ModuleId::get().into_sub_account(thread_id);
  411. <Balances<T> as Currency<T::AccountId>>::transfer(
  412. &state_cleanup_treasury_account,
  413. account_id,
  414. amount,
  415. ExistenceRequirement::AllowDeath,
  416. )
  417. }
  418. fn transfer_to_state_cleanup_treasury_account(
  419. amount: BalanceOf<T>,
  420. thread_id: T::ThreadId,
  421. account_id: &T::AccountId,
  422. ) -> DispatchResult {
  423. let state_cleanup_treasury_account = T::ModuleId::get().into_sub_account(thread_id);
  424. <Balances<T> as Currency<T::AccountId>>::transfer(
  425. account_id,
  426. &state_cleanup_treasury_account,
  427. amount,
  428. ExistenceRequirement::AllowDeath,
  429. )
  430. }
  431. fn ensure_thread_mode(
  432. origin: T::Origin,
  433. thread_author_id: MemberId<T>,
  434. thread_id: T::ThreadId,
  435. ) -> DispatchResult {
  436. let thread = Self::thread_by_id(thread_id);
  437. match thread.mode {
  438. ThreadMode::Open => Ok(()),
  439. ThreadMode::Closed(members) => {
  440. let is_thread_author = thread_author_id == thread.author_id;
  441. let is_councilor =
  442. T::CouncilOriginValidator::ensure_member_consulate(origin, thread_author_id)
  443. .is_ok();
  444. let is_allowed_member = members
  445. .iter()
  446. .any(|member_id| *member_id == thread_author_id);
  447. if is_thread_author || is_councilor || is_allowed_member {
  448. Ok(())
  449. } else {
  450. Err(Error::<T>::CannotPostOnClosedThread.into())
  451. }
  452. }
  453. }
  454. }
  455. }