123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- //! # Proposals discussion module
- //! Proposals `discussion` module for the Joystream platform. Version 2.
- //! It contains discussion system of the proposals.
- //!
- //! ## Overview
- //!
- //! The proposals discussion module is used by the codex module to provide a platform for discussions
- //! about different proposals. It allows to create discussion threads and then add and update related
- //! posts.
- //!
- //! ## Supported extrinsics
- //! - [add_post](./struct.Module.html#method.add_post) - adds a post to an existing discussion thread
- //! - [update_post](./struct.Module.html#method.update_post) - updates existing post
- //!
- //! ## Public API methods
- //! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread
- //! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures safe thread creation
- //!
- //! ## Usage
- //!
- //! ```
- //! use frame_support::decl_module;
- //! use frame_system::ensure_root;
- //! use pallet_proposals_discussion::{self as discussions};
- //!
- //! pub trait Trait: discussions::Trait + membership::Trait {}
- //!
- //! decl_module! {
- //! pub struct Module<T: Trait> for enum Call where origin: T::Origin {
- //! #[weight = 10_000_000]
- //! pub fn create_discussion(origin, title: Vec<u8>, author_id : T::MemberId) {
- //! ensure_root(origin)?;
- //! <discussions::Module<T>>::ensure_can_create_thread(author_id, &title)?;
- //! <discussions::Module<T>>::create_thread(author_id, title)?;
- //! }
- //! }
- //! }
- //! # fn main() {}
- //! ```
- // Ensure we're `no_std` when compiling for Wasm.
- #![cfg_attr(not(feature = "std"), no_std)]
- // Internal Substrate warning (decl_event).
- #![allow(clippy::unused_unit)]
- // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
- //#![warn(missing_docs)]
- #[cfg(test)]
- mod tests;
- mod types;
- use frame_support::dispatch::{DispatchError, DispatchResult};
- use frame_support::traits::Get;
- use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
- use sp_std::clone::Clone;
- use sp_std::vec::Vec;
- use common::origin::ActorOriginValidator;
- use types::{DiscussionPost, DiscussionThread, ThreadCounter};
- type MemberId<T> = <T as common::MembershipTypes>::MemberId;
- decl_event!(
- /// Proposals engine events
- pub enum Event<T>
- where
- <T as Trait>::ThreadId,
- MemberId = MemberId<T>,
- <T as Trait>::PostId,
- {
- /// Emits on thread creation.
- ThreadCreated(ThreadId, MemberId),
- /// Emits on post creation.
- PostCreated(PostId, MemberId),
- /// Emits on post update.
- PostUpdated(PostId, MemberId),
- }
- );
- /// 'Proposal discussion' substrate module Trait
- pub trait Trait: frame_system::Trait + membership::Trait {
- /// Discussion event type.
- type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
- /// Validates post author id and origin combination
- type PostAuthorOriginValidator: ActorOriginValidator<
- Self::Origin,
- MemberId<Self>,
- Self::AccountId,
- >;
- /// Discussion thread Id type
- type ThreadId: From<u64> + Into<u64> + Parameter + Default + Copy;
- /// Post Id type
- type PostId: From<u64> + Parameter + Default + Copy;
- /// Defines post edition number limit.
- type MaxPostEditionNumber: Get<u32>;
- /// Defines thread title length limit.
- type ThreadTitleLengthLimit: Get<u32>;
- /// Defines post length limit.
- type PostLengthLimit: Get<u32>;
- /// Defines max thread by same author in a row number limit.
- type MaxThreadInARowNumber: Get<u32>;
- }
- decl_error! {
- /// Discussion module predefined errors
- pub enum Error for Module<T: Trait> {
- /// Author should match the post creator
- NotAuthor,
- /// Post edition limit reached
- PostEditionNumberExceeded,
- /// Discussion cannot have an empty title
- EmptyTitleProvided,
- /// Title is too long
- TitleIsTooLong,
- /// Thread doesn't exist
- ThreadDoesntExist,
- /// Post doesn't exist
- PostDoesntExist,
- /// Post cannot be empty
- EmptyPostProvided,
- /// Post is too long
- PostIsTooLong,
- /// Max number of threads by same author in a row limit exceeded
- MaxThreadInARowLimitExceeded,
- /// Require root origin in extrinsics
- RequireRootOrigin,
- }
- }
- // Storage for the proposals discussion module
- decl_storage! {
- pub trait Store for Module<T: Trait> as ProposalDiscussion {
- /// Map thread identifier to corresponding thread.
- pub ThreadById get(fn thread_by_id): map hasher(blake2_128_concat)
- T::ThreadId => DiscussionThread<MemberId<T>, T::BlockNumber>;
- /// Count of all threads that have been created.
- pub ThreadCount get(fn thread_count): u64;
- /// Map thread id and post id to corresponding post.
- pub PostThreadIdByPostId:
- double_map hasher(blake2_128_concat) T::ThreadId, hasher(blake2_128_concat) T::PostId =>
- DiscussionPost<MemberId<T>, T::BlockNumber, T::ThreadId>;
- /// Count of all posts that have been created.
- pub PostCount get(fn post_count): u64;
- /// Last author thread counter (part of the antispam mechanism)
- pub LastThreadAuthorCounter get(fn last_thread_author_counter):
- Option<ThreadCounter<MemberId<T>>>;
- }
- }
- decl_module! {
- /// 'Proposal discussion' substrate module
- pub struct Module<T: Trait> for enum Call where origin: T::Origin {
- /// Predefined errors
- type Error = Error<T>;
- /// Emits an event. Default substrate implementation.
- fn deposit_event() = default;
- /// Exports post edition number limit const.
- const MaxPostEditionNumber: u32 = T::MaxPostEditionNumber::get();
- /// Exports thread title length limit const.
- const ThreadTitleLengthLimit: u32 = T::ThreadTitleLengthLimit::get();
- /// Exports post length limit const.
- const PostLengthLimit: u32 = T::PostLengthLimit::get();
- /// Exports max thread by same author in a row number limit const.
- const MaxThreadInARowNumber: u32 = T::MaxThreadInARowNumber::get();
- /// Adds a post with author origin check.
- #[weight = 10_000_000] // TODO: adjust weight
- pub fn add_post(
- origin,
- post_author_id: MemberId<T>,
- thread_id : T::ThreadId,
- text : Vec<u8>
- ) {
- T::PostAuthorOriginValidator::ensure_actor_origin(
- origin,
- post_author_id,
- )?;
- ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
- ensure!(!text.is_empty(),Error::<T>::EmptyPostProvided);
- ensure!(
- text.len() as u32 <= T::PostLengthLimit::get(),
- Error::<T>::PostIsTooLong
- );
- // mutation
- let next_post_count_value = Self::post_count() + 1;
- let new_post_id = next_post_count_value;
- let new_post = DiscussionPost {
- text,
- created_at: Self::current_block(),
- updated_at: Self::current_block(),
- author_id: post_author_id,
- edition_number : 0,
- thread_id,
- };
- let post_id = T::PostId::from(new_post_id);
- <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
- PostCount::put(next_post_count_value);
- Self::deposit_event(RawEvent::PostCreated(post_id, post_author_id));
- }
- /// Updates a post with author origin check. Update attempts number is limited.
- #[weight = 10_000_000] // TODO: adjust weight
- pub fn update_post(
- origin,
- post_author_id: MemberId<T>,
- thread_id: T::ThreadId,
- post_id : T::PostId,
- text : Vec<u8>
- ){
- T::PostAuthorOriginValidator::ensure_actor_origin(
- origin,
- post_author_id,
- )?;
- ensure!(<ThreadById<T>>::contains_key(thread_id), Error::<T>::ThreadDoesntExist);
- ensure!(<PostThreadIdByPostId<T>>::contains_key(thread_id, post_id), Error::<T>::PostDoesntExist);
- ensure!(!text.is_empty(), Error::<T>::EmptyPostProvided);
- ensure!(
- text.len() as u32 <= T::PostLengthLimit::get(),
- Error::<T>::PostIsTooLong
- );
- let post = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id);
- ensure!(post.author_id == post_author_id, Error::<T>::NotAuthor);
- ensure!(post.edition_number < T::MaxPostEditionNumber::get(),
- Error::<T>::PostEditionNumberExceeded);
- let new_post = DiscussionPost {
- text,
- updated_at: Self::current_block(),
- edition_number: post.edition_number + 1,
- ..post
- };
- // mutation
- <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
- Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));
- }
- }
- }
- impl<T: Trait> Module<T> {
- /// Create the discussion thread. Cannot add more threads than 'predefined limit = MaxThreadInARowNumber'
- /// times in a row by the same author.
- pub fn create_thread(
- thread_author_id: MemberId<T>,
- title: Vec<u8>,
- ) -> Result<T::ThreadId, DispatchError> {
- Self::ensure_can_create_thread(thread_author_id, &title)?;
- let next_thread_count_value = Self::thread_count() + 1;
- let new_thread_id = next_thread_count_value;
- let new_thread = DiscussionThread {
- title,
- created_at: Self::current_block(),
- author_id: thread_author_id,
- };
- // get new 'threads in a row' counter for the author
- let current_thread_counter = Self::get_updated_thread_counter(thread_author_id);
- // mutation
- let thread_id = T::ThreadId::from(new_thread_id);
- <ThreadById<T>>::insert(thread_id, new_thread);
- ThreadCount::put(next_thread_count_value);
- <LastThreadAuthorCounter<T>>::put(current_thread_counter);
- Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id));
- Ok(thread_id)
- }
- /// Ensures thread can be created.
- /// Checks:
- /// - title is valid
- /// - max thread in a row by the same author
- pub fn ensure_can_create_thread(thread_author_id: MemberId<T>, title: &[u8]) -> DispatchResult {
- ensure!(!title.is_empty(), Error::<T>::EmptyTitleProvided);
- ensure!(
- title.len() as u32 <= T::ThreadTitleLengthLimit::get(),
- Error::<T>::TitleIsTooLong
- );
- // get new 'threads in a row' counter for the author
- let current_thread_counter = Self::get_updated_thread_counter(thread_author_id);
- ensure!(
- current_thread_counter.counter as u32 <= T::MaxThreadInARowNumber::get(),
- Error::<T>::MaxThreadInARowLimitExceeded
- );
- Ok(())
- }
- }
- impl<T: Trait> Module<T> {
- // Wrapper-function over frame_system::block_number()
- fn current_block() -> T::BlockNumber {
- <frame_system::Module<T>>::block_number()
- }
- // returns incremented thread counter if last thread author equals with provided parameter
- fn get_updated_thread_counter(author_id: MemberId<T>) -> ThreadCounter<MemberId<T>> {
- // if thread counter exists
- if let Some(last_thread_author_counter) = Self::last_thread_author_counter() {
- // if last(previous) author is the same as current author
- if last_thread_author_counter.author_id == author_id {
- return last_thread_author_counter.increment();
- }
- }
- // else return new counter (set with 1 thread number)
- ThreadCounter::new(author_id)
- }
- }
|