Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions modules/proposals/codex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@
// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
//#![warn(missing_docs)]

pub use proposal_types::{ProposalType, RuntimeUpgradeProposalExecutable, TextProposalExecutable};

mod proposal_types;
#[cfg(test)]
mod tests;

use codec::Encode;
use proposal_engine::*;
use rstd::clone::Clone;
use rstd::marker::PhantomData;
use rstd::prelude::*;
use rstd::vec::Vec;
use runtime_primitives::traits::One;
use srml_support::{decl_error, decl_module, decl_storage, ensure};
use system::RawOrigin;

use proposal_engine::*;
pub use proposal_types::{ProposalType, RuntimeUpgradeProposalExecutable, TextProposalExecutable};

/// 'Proposals codex' substrate module Trait
pub trait Trait: system::Trait + proposal_engine::Trait + proposal_discussion::Trait {
/// Defines max allowed text proposal length.
Expand Down Expand Up @@ -100,6 +101,7 @@ decl_module! {

let discussion_thread_id = <proposal_discussion::Module<T>>::create_thread(
cloned_origin1,
T::ThreadAuthorId::one(), //TODO: temporary stub, provide implementation
title.clone(),
)?;

Expand Down Expand Up @@ -142,6 +144,7 @@ decl_module! {

let discussion_thread_id = <proposal_discussion::Module<T>>::create_thread(
cloned_origin1,
T::ThreadAuthorId::one(), //TODO: temporary stub, provide implementation
title.clone(),
)?;

Expand Down
13 changes: 11 additions & 2 deletions modules/proposals/codex/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,31 @@ impl proposal_engine::Trait for Test {
type MaxActiveProposalLimit = MaxActiveProposalLimit;
}

impl proposal_discussion::ActorOriginValidator<Origin, u64> for () {
fn validate_actor_origin(_: Origin, _: u64) -> bool {
true
}
}

parameter_types! {
pub const MaxPostEditionNumber: u32 = 5;
pub const MaxThreadInARowNumber: u32 = 3;
pub const ThreadTitleLengthLimit: u32 = 200;
pub const PostLengthLimit: u32 = 2000;
}

impl proposal_discussion::Trait for Test {
type ThreadAuthorOrigin = system::EnsureSigned<Self::AccountId>;
type PostAuthorOrigin = system::EnsureSigned<Self::AccountId>;
type Event = ();
type ThreadAuthorOriginValidator = ();
type PostAuthorOriginValidator = ();
type ThreadId = u32;
type PostId = u32;
type ThreadAuthorId = u64;
type PostAuthorId = u64;
type MaxPostEditionNumber = MaxPostEditionNumber;
type ThreadTitleLengthLimit = ThreadTitleLengthLimit;
type PostLengthLimit = PostLengthLimit;
type MaxThreadInARowNumber = MaxThreadInARowNumber;
}

pub struct MockVotersParameters;
Expand Down
14 changes: 14 additions & 0 deletions modules/proposals/discussion/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pub(crate) const MSG_NOT_AUTHOR: &str = "Author should match the post creator";
pub(crate) const MSG_POST_EDITION_NUMBER_EXCEEDED: &str = "Post edition limit reached.";
pub(crate) const MSG_EMPTY_TITLE_PROVIDED: &str = "Discussion cannot have an empty title";
pub(crate) const MSG_TOO_LONG_TITLE: &str = "Title is too long";
pub(crate) const MSG_THREAD_DOESNT_EXIST: &str = "Thread doesn't exist";
pub(crate) const MSG_POST_DOESNT_EXIST: &str = "Post doesn't exist";
pub(crate) const MSG_EMPTY_POST_PROVIDED: &str = "Post cannot be empty";
pub(crate) const MSG_TOO_LONG_POST: &str = "Post is too long";
pub(crate) const MSG_MAX_THREAD_IN_A_ROW_LIMIT_EXCEEDED: &str =
"Max number of threads by same author in a row limit exceeded";
pub(crate) const MSG_INVALID_THREAD_AUTHOR_ORIGIN: &str =
"Invalid combination of the origin and thread_author_id";
pub(crate) const MSG_INVALID_POST_AUTHOR_ORIGIN: &str =
"Invalid combination of the origin and post_author_id";
165 changes: 120 additions & 45 deletions modules/proposals/discussion/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//!
//! Supported extrinsics:
//! - add_post - adds a post to existing discussion thread
//! - update_post - updates existing post
//!
//! Public API:
//! - create_discussion - creates a discussion
Expand All @@ -14,38 +15,53 @@
// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
//#![warn(missing_docs)]

mod errors;
#[cfg(test)]
mod tests;
mod types;

use rstd::clone::Clone;
use rstd::prelude::*;
use rstd::vec::Vec;
use runtime_primitives::traits::EnsureOrigin;
use srml_support::{decl_module, decl_storage, ensure, Parameter};
use srml_support::{decl_event, decl_module, decl_storage, ensure, Parameter};

use runtime_primitives::traits::SimpleArithmetic;
use srml_support::traits::Get;
use types::{Post, Thread};

// TODO: create events
// TODO: move errors to decl_error macro

pub(crate) const MSG_NOT_AUTHOR: &str = "Author should match the post creator";
pub(crate) const MSG_POST_EDITION_NUMBER_EXCEEDED: &str = "Post edition limit reached.";
pub(crate) const MSG_EMPTY_TITLE_PROVIDED: &str = "Discussion cannot have an empty title";
pub(crate) const MSG_TOO_LONG_TITLE: &str = "Title is too long";
pub(crate) const MSG_THREAD_DOESNT_EXIST: &str = "Thread doesn't exist";
pub(crate) const MSG_POST_DOESNT_EXIST: &str = "Post doesn't exist";
pub(crate) const MSG_EMPTY_POST_PROVIDED: &str = "Post cannot be empty";
pub(crate) const MSG_TOO_LONG_POST: &str = "Post is too long";
pub use types::ActorOriginValidator;
use types::{Post, Thread, ThreadCounter};

// TODO: move errors to decl_error macro (after substrate version upgrade)

decl_event!(
/// Proposals engine events
pub enum Event<T>
where
<T as Trait>::ThreadId,
<T as Trait>::ThreadAuthorId,
<T as Trait>::PostId,
<T as Trait>::PostAuthorId,
{
/// Emits on thread creation.
ThreadCreated(ThreadId, ThreadAuthorId),

/// Emits on post creation.
PostCreated(PostId, PostAuthorId),

/// Emits on post update.
PostUpdated(PostId, PostAuthorId),
}
);

/// 'Proposal discussion' substrate module Trait
pub trait Trait: system::Trait {
/// Origin from which thread author must come.
type ThreadAuthorOrigin: EnsureOrigin<Self::Origin, Success = Self::AccountId>;
/// Engine event type.
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;

/// Origin from which commenter must come.
type PostAuthorOrigin: EnsureOrigin<Self::Origin, Success = Self::AccountId>;
/// Validates thread author id and origin combination
type ThreadAuthorOriginValidator: ActorOriginValidator<Self::Origin, Self::ThreadAuthorId>;

/// Validates post author id and origin combination
type PostAuthorOriginValidator: ActorOriginValidator<Self::Origin, Self::PostAuthorId>;

/// Discussion thread Id type
type ThreadId: From<u32> + Into<u32> + Parameter + Default + Copy;
Expand All @@ -54,7 +70,7 @@ pub trait Trait: system::Trait {
type PostId: From<u32> + Parameter + Default + Copy;

/// Type for the thread author id. Should be authenticated by account id.
type ThreadAuthorId: From<Self::AccountId> + Parameter + Default;
type ThreadAuthorId: From<Self::AccountId> + Parameter + Default + SimpleArithmetic;

/// Type for the post author id. Should be authenticated by account id.
type PostAuthorId: From<Self::AccountId> + Parameter + Default;
Expand All @@ -67,6 +83,9 @@ pub trait Trait: system::Trait {

/// Defines post length limit.
type PostLengthLimit: Get<u32>;

/// Defines max thread by same author in a row number limit.
type MaxThreadInARowNumber: Get<u32>;
}

// Storage for the proposals discussion module
Expand All @@ -85,24 +104,37 @@ decl_storage! {

/// Count of all posts that have been created.
pub PostCount get(fn post_count): u32;

/// Last author thread counter (part of the antispam mechanism)
pub LastThreadAuthorCounter get(fn last_thread_author_counter):
Option<ThreadCounter<T::ThreadAuthorId>>;
}
}

decl_module! {
/// 'Proposal discussion' substrate module
pub struct Module<T: Trait> for enum Call where origin: T::Origin {

/// Adds a post with author origin check.
pub fn add_post(origin, thread_id : T::ThreadId, text : Vec<u8>) {
let account_id = T::ThreadAuthorOrigin::ensure_origin(origin)?;
let post_author_id = T::PostAuthorId::from(account_id);
/// Emits an event. Default substrate implementation.
fn deposit_event() = default;

ensure!(<ThreadById<T>>::exists(thread_id), MSG_THREAD_DOESNT_EXIST);
/// Adds a post with author origin check.
pub fn add_post(
origin,
post_author_id: T::PostAuthorId,
thread_id : T::ThreadId,
text : Vec<u8>
) {
ensure!(
T::PostAuthorOriginValidator::validate_actor_origin(origin, post_author_id.clone()),
errors::MSG_INVALID_POST_AUTHOR_ORIGIN
);
ensure!(<ThreadById<T>>::exists(thread_id), errors::MSG_THREAD_DOESNT_EXIST);

ensure!(!text.is_empty(), MSG_EMPTY_POST_PROVIDED);
ensure!(!text.is_empty(), errors::MSG_EMPTY_POST_PROVIDED);
ensure!(
text.len() as u32 <= T::PostLengthLimit::get(),
MSG_TOO_LONG_POST
errors::MSG_TOO_LONG_POST
);

// mutation
Expand All @@ -114,35 +146,44 @@ decl_module! {
text,
created_at: Self::current_block(),
updated_at: Self::current_block(),
author_id: post_author_id,
author_id: post_author_id.clone(),
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.
pub fn update_post(origin, thread_id: T::ThreadId, post_id : T::PostId, text : Vec<u8>) {
let account_id = T::ThreadAuthorOrigin::ensure_origin(origin)?;
let post_author_id = T::PostAuthorId::from(account_id);
/// Updates a post with author origin check. Update attempts number is limited.
pub fn update_post(
origin,
post_author_id: T::PostAuthorId,
thread_id: T::ThreadId,
post_id : T::PostId,
text : Vec<u8>
){
ensure!(
T::PostAuthorOriginValidator::validate_actor_origin(origin, post_author_id.clone()),
errors::MSG_INVALID_POST_AUTHOR_ORIGIN
);

ensure!(<ThreadById<T>>::exists(thread_id), MSG_THREAD_DOESNT_EXIST);
ensure!(<PostThreadIdByPostId<T>>::exists(thread_id, post_id), MSG_POST_DOESNT_EXIST);
ensure!(<ThreadById<T>>::exists(thread_id), errors::MSG_THREAD_DOESNT_EXIST);
ensure!(<PostThreadIdByPostId<T>>::exists(thread_id, post_id), errors::MSG_POST_DOESNT_EXIST);

ensure!(!text.is_empty(), MSG_EMPTY_POST_PROVIDED);
ensure!(!text.is_empty(), errors::MSG_EMPTY_POST_PROVIDED);
ensure!(
text.len() as u32 <= T::PostLengthLimit::get(),
MSG_TOO_LONG_POST
errors::MSG_TOO_LONG_POST
);

let post = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id);

ensure!(post.author_id == post_author_id, MSG_NOT_AUTHOR);
ensure!(post.author_id == post_author_id, errors::MSG_NOT_AUTHOR);
ensure!(post.edition_number < T::MaxPostEditionNumber::get(),
MSG_POST_EDITION_NUMBER_EXCEEDED);
errors::MSG_POST_EDITION_NUMBER_EXCEEDED);

let new_post = Post {
text,
Expand All @@ -154,6 +195,7 @@ decl_module! {
// mutation

<PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));
}
}
}
Expand All @@ -164,15 +206,30 @@ impl<T: Trait> Module<T> {
<system::Module<T>>::block_number()
}

/// Create the discussion thread
pub fn create_thread(origin: T::Origin, title: Vec<u8>) -> Result<T::ThreadId, &'static str> {
let account_id = T::ThreadAuthorOrigin::ensure_origin(origin)?;
let thread_author_id = T::ThreadAuthorId::from(account_id);
/// Create the discussion thread. Cannot add more threads than 'predefined limit = MaxThreadInARowNumber'
/// times in a row by the same author.
pub fn create_thread(
origin: T::Origin,
thread_author_id: T::ThreadAuthorId,
title: Vec<u8>,
) -> Result<T::ThreadId, &'static str> {
ensure!(
T::ThreadAuthorOriginValidator::validate_actor_origin(origin, thread_author_id.clone()),
errors::MSG_INVALID_THREAD_AUTHOR_ORIGIN
);

ensure!(!title.is_empty(), MSG_EMPTY_TITLE_PROVIDED);
ensure!(!title.is_empty(), errors::MSG_EMPTY_TITLE_PROVIDED);
ensure!(
title.len() as u32 <= T::ThreadTitleLengthLimit::get(),
MSG_TOO_LONG_TITLE
errors::MSG_TOO_LONG_TITLE
);

// get new 'threads in a row' counter for the author
let current_thread_counter = Self::get_updated_thread_counter(thread_author_id.clone());

ensure!(
current_thread_counter.counter as u32 <= T::MaxThreadInARowNumber::get(),
errors::MSG_MAX_THREAD_IN_A_ROW_LIMIT_EXCEEDED
);

let next_thread_count_value = Self::thread_count() + 1;
Expand All @@ -181,15 +238,33 @@ impl<T: Trait> Module<T> {
let new_thread = Thread {
title,
created_at: Self::current_block(),
author_id: thread_author_id,
author_id: thread_author_id.clone(),
};

// 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)
}

// returns incremented thread counter if last thread author equals with provided parameter
fn get_updated_thread_counter(
author_id: T::ThreadAuthorId,
) -> ThreadCounter<T::ThreadAuthorId> {
// 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)
}
}
Loading