Instantly share code, notes, and snippets.
Created
February 27, 2020 18:37
-
Star
1
(1)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save olehmell/70044482c3fff9f321d03d6befd9f3c5 to your computer and use it in GitHub Desktop.
Subsocial-pallet in ryst and types.json for registry types on UI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![cfg_attr(not(feature = "std"), no_std)] | |
/// For more guidance on FRAME pallets, see the example. | |
/// https://github.com/paritytech/substrate/blob/master/frame/example/src/lib.rs | |
pub mod defaults; | |
pub mod messages; | |
pub mod functions; | |
use sp_std::prelude::*; | |
use codec::{Encode, Decode}; | |
use frame_support::{decl_module, decl_storage, decl_event, ensure}; | |
use sp_runtime::{RuntimeDebug}; | |
use system::ensure_signed; | |
use pallet_timestamp; | |
use defaults::*; | |
use messages::*; | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct Change<T: Trait> { | |
pub account: T::AccountId, | |
pub block: T::BlockNumber, | |
pub time: T::Moment, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct Blog<T: Trait> { | |
pub id: BlogId, | |
pub created: Change<T>, | |
pub updated: Option<Change<T>>, | |
// Can be updated by the owner: | |
pub writers: Vec<T::AccountId>, | |
pub slug: Vec<u8>, | |
pub ipfs_hash: Vec<u8>, | |
pub posts_count: u16, | |
pub followers_count: u32, | |
pub edit_history: Vec<BlogHistoryRecord<T>>, | |
pub score: i32, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct BlogUpdate<AccountId> { | |
pub writers: Option<Vec<AccountId>>, | |
pub slug: Option<Vec<u8>>, | |
pub ipfs_hash: Option<Vec<u8>>, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct BlogHistoryRecord<T: Trait> { | |
pub edited: Change<T>, | |
pub old_data: BlogUpdate<T::AccountId>, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct Post<T: Trait> { | |
pub id: PostId, | |
pub blog_id: BlogId, | |
pub created: Change<T>, | |
pub updated: Option<Change<T>>, | |
pub extension: PostExtension, | |
// Next fields can be updated by the owner only: | |
pub ipfs_hash: Vec<u8>, | |
pub comments_count: u16, | |
pub upvotes_count: u16, | |
pub downvotes_count: u16, | |
pub shares_count: u16, | |
pub edit_history: Vec<PostHistoryRecord<T>>, | |
pub score: i32, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct PostUpdate { | |
pub blog_id: Option<BlogId>, | |
pub ipfs_hash: Option<Vec<u8>>, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct PostHistoryRecord<T: Trait> { | |
pub edited: Change<T>, | |
pub old_data: PostUpdate, | |
} | |
#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug)] | |
pub enum PostExtension { | |
RegularPost, | |
SharedPost(PostId), | |
SharedComment(CommentId), | |
} | |
impl Default for PostExtension { | |
fn default() -> Self { | |
PostExtension::RegularPost | |
} | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct Comment<T: Trait> { | |
pub id: CommentId, | |
pub parent_id: Option<CommentId>, | |
pub post_id: PostId, | |
pub created: Change<T>, | |
pub updated: Option<Change<T>>, | |
// Can be updated by the owner: | |
pub ipfs_hash: Vec<u8>, | |
pub upvotes_count: u16, | |
pub downvotes_count: u16, | |
pub shares_count: u16, | |
pub direct_replies_count: u16, | |
pub edit_history: Vec<CommentHistoryRecord<T>>, | |
pub score: i32, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct CommentUpdate { | |
pub ipfs_hash: Vec<u8>, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct CommentHistoryRecord<T: Trait> { | |
pub edited: Change<T>, | |
pub old_data: CommentUpdate, | |
} | |
#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug)] | |
pub enum ReactionKind { | |
Upvote, | |
Downvote, | |
} | |
impl Default for ReactionKind { | |
fn default() -> Self { | |
ReactionKind::Upvote | |
} | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct Reaction<T: Trait> { | |
pub id: ReactionId, | |
pub created: Change<T>, | |
pub updated: Option<Change<T>>, | |
pub kind: ReactionKind, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct SocialAccount<T: Trait> { | |
pub followers_count: u32, | |
pub following_accounts_count: u16, | |
pub following_blogs_count: u16, | |
pub reputation: u32, | |
pub profile: Option<Profile<T>>, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct Profile<T: Trait> { | |
pub created: Change<T>, | |
pub updated: Option<Change<T>>, | |
pub username: Vec<u8>, | |
pub ipfs_hash: Vec<u8>, | |
pub edit_history: Vec<ProfileHistoryRecord<T>>, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct ProfileUpdate { | |
pub username: Option<Vec<u8>>, | |
pub ipfs_hash: Option<Vec<u8>>, | |
} | |
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] | |
pub struct ProfileHistoryRecord<T: Trait> { | |
pub edited: Change<T>, | |
pub old_data: ProfileUpdate, | |
} | |
#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug)] | |
pub enum ScoringAction { | |
UpvotePost, | |
DownvotePost, | |
SharePost, | |
CreateComment, | |
UpvoteComment, | |
DownvoteComment, | |
ShareComment, | |
FollowBlog, | |
FollowAccount, | |
} | |
impl Default for ScoringAction { | |
fn default() -> Self { | |
ScoringAction::FollowAccount | |
} | |
} | |
pub type BlogId = u64; | |
pub type PostId = u64; | |
pub type CommentId = u64; | |
pub type ReactionId = u64; | |
/// The pallet's configuration trait. | |
pub trait Trait: system::Trait + pallet_timestamp::Trait { | |
/// The overarching event type. | |
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>; | |
} | |
// This pallet's storage items. | |
decl_storage! { | |
trait Store for Module<T: Trait> as TemplateModule { | |
pub SlugMinLen get(slug_min_len): u32 = DEFAULT_SLUG_MIN_LEN; | |
pub SlugMaxLen get(slug_max_len): u32 = DEFAULT_SLUG_MAX_LEN; | |
pub IpfsHashLen get(ipfs_hash_len): u32 = DEFAULT_IPFS_HASH_LEN; | |
pub UsernameMinLen get(username_min_len): u32 = DEFAULT_USERNAME_MIN_LEN; | |
pub UsernameMaxLen get(username_max_len): u32 = DEFAULT_USERNAME_MAX_LEN; | |
pub BlogMaxLen get(blog_max_len): u32 = DEFAULT_BLOG_MAX_LEN; | |
pub PostMaxLen get(post_max_len): u32 = DEFAULT_POST_MAX_LEN; | |
pub CommentMaxLen get(comment_max_len): u32 = DEFAULT_COMMENT_MAX_LEN; | |
pub UpvotePostActionWeight get (upvote_post_action_weight): i16 = DEFAULT_UPVOTE_POST_ACTION_WEIGHT; | |
pub DownvotePostActionWeight get (downvote_post_action_weight): i16 = DEFAULT_DOWNVOTE_POST_ACTION_WEIGHT; | |
pub SharePostActionWeight get (share_post_action_weight): i16 = DEFAULT_SHARE_POST_ACTION_WEIGHT; | |
pub CreateCommentActionWeight get (create_comment_action_weight): i16 = DEFAULT_CREATE_COMMENT_ACTION_WEIGHT; | |
pub UpvoteCommentActionWeight get (upvote_comment_action_weight): i16 = DEFAULT_UPVOTE_COMMENT_ACTION_WEIGHT; | |
pub DownvoteCommentActionWeight get (downvote_comment_action_weight): i16 = DEFAULT_DOWNVOTE_COMMENT_ACTION_WEIGHT; | |
pub ShareCommentActionWeight get (share_comment_action_weight): i16 = DEFAULT_SHARE_COMMENT_ACTION_WEIGHT; | |
pub FollowBlogActionWeight get (follow_blog_action_weight): i16 = DEFAULT_FOLLOW_BLOG_ACTION_WEIGHT; | |
pub FollowAccountActionWeight get (follow_account_action_weight): i16 = DEFAULT_FOLLOW_ACCOUNT_ACTION_WEIGHT; | |
pub BlogById get(blog_by_id): map BlogId => Option<Blog<T>>; | |
pub PostById get(post_by_id): map PostId => Option<Post<T>>; | |
pub CommentById get(comment_by_id): map CommentId => Option<Comment<T>>; | |
pub ReactionById get(reaction_by_id): map ReactionId => Option<Reaction<T>>; | |
pub SocialAccountById get(social_account_by_id): map T::AccountId => Option<SocialAccount<T>>; | |
pub BlogIdsByOwner get(blog_ids_by_owner): map T::AccountId => Vec<BlogId>; | |
pub PostIdsByBlogId get(post_ids_by_blog_id): map BlogId => Vec<PostId>; | |
pub CommentIdsByPostId get(comment_ids_by_post_id): map PostId => Vec<CommentId>; | |
pub ReactionIdsByPostId get(reaction_ids_by_post_id): map PostId => Vec<ReactionId>; | |
pub ReactionIdsByCommentId get(reaction_ids_by_comment_id): map CommentId => Vec<ReactionId>; | |
pub PostReactionIdByAccount get(post_reaction_id_by_account): map (T::AccountId, PostId) => ReactionId; | |
pub CommentReactionIdByAccount get(comment_reaction_id_by_account): map (T::AccountId, CommentId) => ReactionId; | |
pub BlogIdBySlug get(blog_id_by_slug): map Vec<u8> => Option<BlogId>; | |
pub BlogsFollowedByAccount get(blogs_followed_by_account): map T::AccountId => Vec<BlogId>; | |
pub BlogFollowers get(blog_followers): map BlogId => Vec<T::AccountId>; | |
pub BlogFollowedByAccount get(blog_followed_by_account): map (T::AccountId, BlogId) => bool; | |
pub AccountFollowedByAccount get(account_followed_by_account): map (T::AccountId, T::AccountId) => bool; | |
pub AccountsFollowedByAccount get(accounts_followed_by_account): map T::AccountId => Vec<T::AccountId>; | |
pub AccountFollowers get(account_followers): map T::AccountId => Vec<T::AccountId>; | |
pub NextBlogId get(next_blog_id): BlogId = 1; | |
pub NextPostId get(next_post_id): PostId = 1; | |
pub NextCommentId get(next_comment_id): CommentId = 1; | |
pub NextReactionId get(next_reaction_id): ReactionId = 1; | |
pub AccountReputationDiffByAccount get(account_reputation_diff_by_account): map (T::AccountId, T::AccountId, ScoringAction) => Option<i16>; // TODO shorten name (?refactor) | |
pub PostScoreByAccount get(post_score_by_account): map (T::AccountId, PostId, ScoringAction) => Option<i16>; | |
pub CommentScoreByAccount get(comment_score_by_account): map (T::AccountId, CommentId, ScoringAction) => Option<i16>; | |
pub PostSharesByAccount get(post_shares_by_account): map (T::AccountId, PostId) => u16; | |
pub SharedPostIdsByOriginalPostId get(shared_post_ids_by_original_post_id): map PostId => Vec<PostId>; | |
pub CommentSharesByAccount get(comment_shares_by_account): map (T::AccountId, CommentId) => u16; | |
pub SharedPostIdsByOriginalCommentId get(shared_post_ids_by_original_comment_id): map CommentId => Vec<PostId>; | |
pub AccountByProfileUsername get(account_by_profile_username): map Vec<u8> => Option<T::AccountId>; | |
} | |
} | |
// The pallet's dispatchable functions. | |
decl_module! { | |
pub struct Module<T: Trait> for enum Call where origin: T::Origin { | |
// Initializing events | |
// this is needed only if you are using events in your pallet | |
fn deposit_event() = default; | |
pub fn create_blog(origin, slug: Vec<u8>, ipfs_hash: Vec<u8>) { | |
let owner = ensure_signed(origin)?; | |
ensure!(slug.len() >= Self::slug_min_len() as usize, MSG_BLOG_SLUG_IS_TOO_SHORT); | |
ensure!(slug.len() <= Self::slug_max_len() as usize, MSG_BLOG_SLUG_IS_TOO_LONG); | |
ensure!(!BlogIdBySlug::exists(slug.clone()), MSG_BLOG_SLUG_IS_NOT_UNIQUE); | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
let blog_id = Self::next_blog_id(); | |
let ref mut new_blog: Blog<T> = Blog { | |
id: blog_id, | |
created: Self::new_change(owner.clone()), | |
updated: None, | |
writers: vec![], | |
slug: slug.clone(), | |
ipfs_hash, | |
posts_count: 0, | |
followers_count: 0, | |
edit_history: vec![], | |
score: 0 | |
}; | |
// Blog creator automatically follows their blog: | |
Self::add_blog_follower_and_insert_blog(owner.clone(), new_blog, true)?; | |
<BlogIdsByOwner<T>>::mutate(owner.clone(), |ids| ids.push(blog_id)); | |
BlogIdBySlug::insert(slug, blog_id); | |
NextBlogId::mutate(|n| { *n += 1; }); | |
} | |
pub fn update_blog(origin, blog_id: BlogId, update: BlogUpdate<T::AccountId>) { | |
let owner = ensure_signed(origin)?; | |
let has_updates = | |
update.writers.is_some() || | |
update.slug.is_some() || | |
update.ipfs_hash.is_some(); | |
ensure!(has_updates, MSG_NOTHING_TO_UPDATE_IN_BLOG); | |
let mut blog = Self::blog_by_id(blog_id).ok_or(MSG_BLOG_NOT_FOUND)?; | |
// TODO ensure: blog writers also should be able to edit this blog: | |
ensure!(owner == blog.created.account, MSG_ONLY_BLOG_OWNER_CAN_UPDATE_BLOG); | |
let mut fields_updated = 0; | |
let mut new_history_record = BlogHistoryRecord { | |
edited: Self::new_change(owner.clone()), | |
old_data: BlogUpdate {writers: None, slug: None, ipfs_hash: None} | |
}; | |
if let Some(writers) = update.writers { | |
if writers != blog.writers { | |
// TODO validate writers. | |
// TODO update BlogIdsByWriter: insert new, delete removed, update only changed writers. | |
new_history_record.old_data.writers = Some(blog.writers); | |
blog.writers = writers; | |
fields_updated += 1; | |
} | |
} | |
if let Some(ipfs_hash) = update.ipfs_hash { | |
if ipfs_hash != blog.ipfs_hash { | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
new_history_record.old_data.ipfs_hash = Some(blog.ipfs_hash); | |
blog.ipfs_hash = ipfs_hash; | |
fields_updated += 1; | |
} | |
} | |
if let Some(slug) = update.slug { | |
if slug != blog.slug { | |
let slug_len = slug.len(); | |
ensure!(slug_len >= Self::slug_min_len() as usize, MSG_BLOG_SLUG_IS_TOO_SHORT); | |
ensure!(slug_len <= Self::slug_max_len() as usize, MSG_BLOG_SLUG_IS_TOO_LONG); | |
ensure!(!BlogIdBySlug::exists(slug.clone()), MSG_BLOG_SLUG_IS_NOT_UNIQUE); | |
BlogIdBySlug::remove(blog.slug.clone()); | |
BlogIdBySlug::insert(slug.clone(), blog_id); | |
new_history_record.old_data.slug = Some(blog.slug); | |
blog.slug = slug; | |
fields_updated += 1; | |
} | |
} | |
// Update this blog only if at least one field should be updated: | |
if fields_updated > 0 { | |
blog.updated = Some(Self::new_change(owner.clone())); | |
blog.edit_history.push(new_history_record); | |
<BlogById<T>>::insert(blog_id, blog); | |
Self::deposit_event(RawEvent::BlogUpdated(owner.clone(), blog_id)); | |
} | |
} | |
pub fn follow_blog(origin, blog_id: BlogId) { | |
let follower = ensure_signed(origin)?; | |
let ref mut blog = Self::blog_by_id(blog_id).ok_or(MSG_BLOG_NOT_FOUND)?; | |
ensure!(!Self::blog_followed_by_account((follower.clone(), blog_id)), MSG_ACCOUNT_IS_FOLLOWING_BLOG); | |
Self::add_blog_follower_and_insert_blog(follower.clone(), blog, false)?; | |
} | |
pub fn unfollow_blog(origin, blog_id: BlogId) { | |
let follower = ensure_signed(origin)?; | |
let ref mut blog = Self::blog_by_id(blog_id).ok_or(MSG_BLOG_NOT_FOUND)?; | |
ensure!(Self::blog_followed_by_account((follower.clone(), blog_id)), MSG_ACCOUNT_IS_NOT_FOLLOWING_BLOG); | |
let mut social_account = Self::social_account_by_id(follower.clone()).ok_or(MSG_SOCIAL_ACCOUNT_NOT_FOUND)?; | |
social_account.following_blogs_count = social_account.following_blogs_count | |
.checked_sub(1) | |
.ok_or(MSG_UNDERFLOW_UNFOLLOWING_BLOG)?; | |
blog.followers_count = blog.followers_count.checked_sub(1).ok_or(MSG_UNDERFLOW_UNFOLLOWING_BLOG)?; | |
if blog.created.account != follower { | |
let author = blog.created.account.clone(); | |
if let Some(score_diff) = Self::account_reputation_diff_by_account((follower.clone(), author.clone(), ScoringAction::FollowBlog)) { | |
blog.score = blog.score.checked_sub(score_diff as i32).ok_or(MSG_OUT_OF_BOUNDS_UPDATING_BLOG_SCORE)?; | |
Self::change_social_account_reputation(author.clone(), follower.clone(), score_diff * -1, ScoringAction::FollowBlog)?; | |
} | |
} | |
<BlogsFollowedByAccount<T>>::mutate(follower.clone(), |blog_ids| Self::vec_remove_on(blog_ids, blog_id)); | |
<BlogFollowers<T>>::mutate(blog_id, |account_ids| Self::vec_remove_on(account_ids, follower.clone())); | |
<BlogFollowedByAccount<T>>::remove((follower.clone(), blog_id)); | |
<SocialAccountById<T>>::insert(follower.clone(), social_account); | |
<BlogById<T>>::insert(blog_id, blog); | |
Self::deposit_event(RawEvent::BlogUnfollowed(follower.clone(), blog_id)); | |
} | |
pub fn follow_account(origin, account: T::AccountId) { | |
let follower = ensure_signed(origin)?; | |
ensure!(follower != account, MSG_ACCOUNT_CANNOT_FOLLOW_ITSELF); | |
ensure!(!<AccountFollowedByAccount<T>>::exists((follower.clone(), account.clone())), MSG_ACCOUNT_IS_ALREADY_FOLLOWED); | |
let mut follower_account = Self::get_or_new_social_account(follower.clone()); | |
let mut followed_account = Self::get_or_new_social_account(account.clone()); | |
follower_account.following_accounts_count = follower_account.following_accounts_count | |
.checked_add(1).ok_or(MSG_OVERFLOW_FOLLOWING_ACCOUNT)?; | |
followed_account.followers_count = followed_account.followers_count | |
.checked_add(1).ok_or(MSG_OVERFLOW_FOLLOWING_ACCOUNT)?; | |
Self::change_social_account_reputation(account.clone(), follower.clone(), | |
Self::get_score_diff(follower_account.reputation.clone(), ScoringAction::FollowAccount), | |
ScoringAction::FollowAccount | |
)?; | |
<SocialAccountById<T>>::insert(follower.clone(), follower_account); | |
<SocialAccountById<T>>::insert(account.clone(), followed_account); | |
<AccountsFollowedByAccount<T>>::mutate(follower.clone(), |ids| ids.push(account.clone())); | |
<AccountFollowers<T>>::mutate(account.clone(), |ids| ids.push(follower.clone())); | |
<AccountFollowedByAccount<T>>::insert((follower.clone(), account.clone()), true); | |
Self::deposit_event(RawEvent::AccountFollowed(follower, account)); | |
} | |
pub fn unfollow_account(origin, account: T::AccountId) { | |
let follower = ensure_signed(origin)?; | |
ensure!(follower != account, MSG_ACCOUNT_CANNOT_UNFOLLOW_ITSELF); | |
let mut follower_account = Self::social_account_by_id(follower.clone()).ok_or(MSG_FOLLOWER_ACCOUNT_NOT_FOUND)?; | |
let mut followed_account = Self::social_account_by_id(account.clone()).ok_or(MSG_FOLLOWED_ACCOUNT_NOT_FOUND)?; | |
ensure!(<AccountFollowedByAccount<T>>::exists((follower.clone(), account.clone())), MSG_ACCOUNT_IS_NOT_FOLLOWED); | |
follower_account.following_accounts_count = follower_account.following_accounts_count | |
.checked_sub(1).ok_or(MSG_UNDERFLOW_UNFOLLOWING_ACCOUNT)?; | |
followed_account.followers_count = followed_account.followers_count | |
.checked_sub(1).ok_or(MSG_UNDERFLOW_UNFOLLOWING_ACCOUNT)?; | |
let reputation_diff = Self::account_reputation_diff_by_account( | |
(follower.clone(), account.clone(), ScoringAction::FollowAccount) | |
).ok_or(MSG_REPUTATION_DIFF_NOT_FOUND)?; | |
Self::change_social_account_reputation(account.clone(), follower.clone(), | |
reputation_diff, | |
ScoringAction::FollowAccount | |
)?; | |
<SocialAccountById<T>>::insert(follower.clone(), follower_account); | |
<SocialAccountById<T>>::insert(account.clone(), followed_account); | |
<AccountsFollowedByAccount<T>>::mutate(follower.clone(), |account_ids| Self::vec_remove_on(account_ids, account.clone())); | |
<AccountFollowers<T>>::mutate(account.clone(), |account_ids| Self::vec_remove_on(account_ids, follower.clone())); | |
<AccountFollowedByAccount<T>>::remove((follower.clone(), account.clone())); | |
Self::deposit_event(RawEvent::AccountUnfollowed(follower, account)); | |
} | |
pub fn create_profile(origin, username: Vec<u8>, ipfs_hash: Vec<u8>) { | |
let owner = ensure_signed(origin)?; | |
let mut social_account = Self::get_or_new_social_account(owner.clone()); | |
ensure!(social_account.profile.is_none(), MSG_PROFILE_ALREADY_EXISTS); | |
Self::is_username_valid(username.clone())?; | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
social_account.profile = Some( | |
Profile { | |
created: Self::new_change(owner.clone()), | |
updated: None, | |
username: username.clone(), | |
ipfs_hash, | |
edit_history: vec![] | |
} | |
); | |
<AccountByProfileUsername<T>>::insert(username.clone(), owner.clone()); | |
<SocialAccountById<T>>::insert(owner.clone(), social_account.clone()); | |
Self::deposit_event(RawEvent::ProfileCreated(owner.clone())); | |
} | |
pub fn update_profile(origin, update: ProfileUpdate) { | |
let owner = ensure_signed(origin)?; | |
let has_updates = | |
update.username.is_some() || | |
update.ipfs_hash.is_some(); | |
ensure!(has_updates, MSG_NOTHING_TO_UPDATE_IN_PROFILE); | |
let mut social_account = Self::social_account_by_id(owner.clone()).ok_or(MSG_SOCIAL_ACCOUNT_NOT_FOUND)?; | |
let mut profile = social_account.profile.ok_or(MSG_PROFILE_DOESNT_EXIST)?; | |
let mut is_update_applied = false; | |
let mut new_history_record = ProfileHistoryRecord { | |
edited: Self::new_change(owner.clone()), | |
old_data: ProfileUpdate {username: None, ipfs_hash: None} | |
}; | |
if let Some(ipfs_hash) = update.ipfs_hash { | |
if ipfs_hash != profile.ipfs_hash { | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
new_history_record.old_data.ipfs_hash = Some(profile.ipfs_hash); | |
profile.ipfs_hash = ipfs_hash; | |
is_update_applied = true; | |
} | |
} | |
if let Some(username) = update.username { | |
if username != profile.username { | |
Self::is_username_valid(username.clone())?; | |
<AccountByProfileUsername<T>>::remove(profile.username.clone()); | |
<AccountByProfileUsername<T>>::insert(username.clone(), owner.clone()); | |
new_history_record.old_data.username = Some(profile.username); | |
profile.username = username; | |
is_update_applied = true; | |
} | |
} | |
if is_update_applied { | |
profile.updated = Some(Self::new_change(owner.clone())); | |
profile.edit_history.push(new_history_record); | |
social_account.profile = Some(profile); | |
<SocialAccountById<T>>::insert(owner.clone(), social_account); | |
Self::deposit_event(RawEvent::ProfileUpdated(owner.clone())); | |
} | |
} | |
pub fn create_post(origin, blog_id: BlogId, ipfs_hash: Vec<u8>, extension: PostExtension) { | |
let owner = ensure_signed(origin)?; | |
let mut blog = Self::blog_by_id(blog_id).ok_or(MSG_BLOG_NOT_FOUND)?; | |
blog.posts_count = blog.posts_count.checked_add(1).ok_or(MSG_OVERFLOW_ADDING_POST_ON_BLOG)?; | |
let new_post_id = Self::next_post_id(); | |
// Sharing functions contain check for post/comment existance | |
match extension { | |
PostExtension::RegularPost => { | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
}, | |
PostExtension::SharedPost(post_id) => { | |
let post = Self::post_by_id(post_id).ok_or(MSG_ORIGINAL_POST_NOT_FOUND)?; | |
ensure!(post.extension == PostExtension::RegularPost, MSG_CANNOT_SHARE_SHARED_POST); | |
Self::share_post(owner.clone(), post_id, new_post_id)?; | |
}, | |
PostExtension::SharedComment(comment_id) => { | |
Self::share_comment(owner.clone(), comment_id, new_post_id)?; | |
}, | |
} | |
let new_post: Post<T> = Post { | |
id: new_post_id, | |
blog_id, | |
created: Self::new_change(owner.clone()), | |
updated: None, | |
extension, | |
ipfs_hash, | |
comments_count: 0, | |
upvotes_count: 0, | |
downvotes_count: 0, | |
shares_count: 0, | |
edit_history: vec![], | |
score: 0, | |
}; | |
<PostById<T>>::insert(new_post_id, new_post); | |
PostIdsByBlogId::mutate(blog_id, |ids| ids.push(new_post_id)); | |
NextPostId::mutate(|n| { *n += 1; }); | |
<BlogById<T>>::insert(blog_id, blog); | |
Self::deposit_event(RawEvent::PostCreated(owner.clone(), new_post_id)); | |
} | |
pub fn update_post(origin, post_id: PostId, update: PostUpdate) { | |
let owner = ensure_signed(origin)?; | |
let has_updates = | |
update.blog_id.is_some() || | |
update.ipfs_hash.is_some(); | |
ensure!(has_updates, MSG_NOTHING_TO_UPDATE_IN_POST); | |
let mut post = Self::post_by_id(post_id).ok_or(MSG_POST_NOT_FOUND)?; | |
// TODO ensure: blog writers also should be able to edit this post: | |
ensure!(owner == post.created.account, MSG_ONLY_POST_OWNER_CAN_UPDATE_POST); | |
let mut fields_updated = 0; | |
let mut new_history_record = PostHistoryRecord { | |
edited: Self::new_change(owner.clone()), | |
old_data: PostUpdate {blog_id: None, ipfs_hash: None} | |
}; | |
if let Some(ipfs_hash) = update.ipfs_hash { | |
if ipfs_hash != post.ipfs_hash { | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
new_history_record.old_data.ipfs_hash = Some(post.ipfs_hash); | |
post.ipfs_hash = ipfs_hash; | |
fields_updated += 1; | |
} | |
} | |
// Move this post to another blog: | |
if let Some(blog_id) = update.blog_id { | |
if blog_id != post.blog_id { | |
Self::ensure_blog_exists(blog_id)?; | |
// Remove post_id from its old blog: | |
PostIdsByBlogId::mutate(post.blog_id, |post_ids| Self::vec_remove_on(post_ids, post_id)); | |
// Add post_id to its new blog: | |
PostIdsByBlogId::mutate(blog_id.clone(), |ids| ids.push(post_id)); | |
new_history_record.old_data.blog_id = Some(post.blog_id); | |
post.blog_id = blog_id; | |
fields_updated += 1; | |
} | |
} | |
// Update this post only if at least one field should be updated: | |
if fields_updated > 0 { | |
post.updated = Some(Self::new_change(owner.clone())); | |
post.edit_history.push(new_history_record); | |
<PostById<T>>::insert(post_id, post); | |
Self::deposit_event(RawEvent::PostUpdated(owner.clone(), post_id)); | |
} | |
} | |
pub fn create_comment(origin, post_id: PostId, parent_id: Option<CommentId>, ipfs_hash: Vec<u8>) { | |
let owner = ensure_signed(origin)?; | |
let ref mut post = Self::post_by_id(post_id).ok_or(MSG_POST_NOT_FOUND)?; | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
let comment_id = Self::next_comment_id(); | |
let new_comment: Comment<T> = Comment { | |
id: comment_id, | |
parent_id, | |
post_id, | |
created: Self::new_change(owner.clone()), | |
updated: None, | |
ipfs_hash, | |
upvotes_count: 0, | |
downvotes_count: 0, | |
shares_count: 0, | |
direct_replies_count: 0, | |
edit_history: vec![], | |
score: 0, | |
}; | |
post.comments_count = post.comments_count.checked_add(1).ok_or(MSG_OVERFLOW_ADDING_COMMENT_ON_POST)?; | |
Self::change_post_score(owner.clone(), post, ScoringAction::CreateComment)?; | |
if let Some(id) = parent_id { | |
let mut parent_comment = Self::comment_by_id(id).ok_or(MSG_UNKNOWN_PARENT_COMMENT)?; | |
parent_comment.direct_replies_count = parent_comment.direct_replies_count.checked_add(1).ok_or(MSG_OVERFLOW_REPLYING_ON_COMMENT)?; | |
<CommentById<T>>::insert(id, parent_comment); | |
} | |
<CommentById<T>>::insert(comment_id, new_comment); | |
CommentIdsByPostId::mutate(post_id, |ids| ids.push(comment_id)); | |
NextCommentId::mutate(|n| { *n += 1; }); | |
<PostById<T>>::insert(post_id, post); | |
Self::deposit_event(RawEvent::CommentCreated(owner.clone(), comment_id)); | |
} | |
pub fn update_comment(origin, comment_id: CommentId, update: CommentUpdate) { | |
let owner = ensure_signed(origin)?; | |
let mut comment = Self::comment_by_id(comment_id).ok_or(MSG_COMMENT_NOT_FOUND)?; | |
ensure!(owner == comment.created.account, MSG_ONLY_COMMENT_AUTHOR_CAN_UPDATE_COMMENT); | |
let ipfs_hash = update.ipfs_hash; | |
ensure!(ipfs_hash != comment.ipfs_hash, MSG_NEW_COMMENT_HASH_DO_NOT_DIFFER); | |
Self::is_ipfs_hash_valid(ipfs_hash.clone())?; | |
let new_history_record = CommentHistoryRecord { | |
edited: Self::new_change(owner.clone()), | |
old_data: CommentUpdate {ipfs_hash: comment.ipfs_hash} | |
}; | |
comment.edit_history.push(new_history_record); | |
comment.ipfs_hash = ipfs_hash; | |
comment.updated = Some(Self::new_change(owner.clone())); | |
<CommentById<T>>::insert(comment_id, comment); | |
Self::deposit_event(RawEvent::CommentUpdated(owner.clone(), comment_id)); | |
} | |
pub fn create_post_reaction(origin, post_id: PostId, kind: ReactionKind) { | |
let owner = ensure_signed(origin)?; | |
ensure!( | |
!<PostReactionIdByAccount<T>>::exists((owner.clone(), post_id)), | |
MSG_ACCOUNT_ALREADY_REACTED_TO_POST | |
); | |
let ref mut post = Self::post_by_id(post_id).ok_or(MSG_POST_NOT_FOUND)?; | |
let reaction_id = Self::new_reaction(owner.clone(), kind.clone()); | |
let action: ScoringAction; | |
match kind { | |
ReactionKind::Upvote => { | |
post.upvotes_count = post.upvotes_count.checked_add(1).ok_or(MSG_OVERFLOW_UPVOTING_POST)?; | |
action = ScoringAction::UpvotePost; | |
}, | |
ReactionKind::Downvote => { | |
post.downvotes_count = post.downvotes_count.checked_add(1).ok_or(MSG_OVERFLOW_DOWNVOTING_POST)?; | |
action = ScoringAction::DownvotePost; | |
}, | |
} | |
if post.created.account != owner { | |
Self::change_post_score(owner.clone(), post, action)?; | |
} | |
else { | |
<PostById<T>>::insert(post_id, post); | |
} | |
ReactionIdsByPostId::mutate(post_id, |ids| ids.push(reaction_id)); | |
<PostReactionIdByAccount<T>>::insert((owner.clone(), post_id), reaction_id); | |
Self::deposit_event(RawEvent::PostReactionCreated(owner.clone(), post_id, reaction_id)); | |
} | |
pub fn update_post_reaction(origin, post_id: PostId, reaction_id: ReactionId, new_kind: ReactionKind) { | |
let owner = ensure_signed(origin)?; | |
ensure!( | |
<PostReactionIdByAccount<T>>::exists((owner.clone(), post_id)), | |
MSG_ACCOUNT_HAS_NOT_REACTED_TO_POST | |
); | |
let mut reaction = Self::reaction_by_id(reaction_id).ok_or(MSG_REACTION_NOT_FOUND)?; | |
let ref mut post = Self::post_by_id(post_id).ok_or(MSG_POST_NOT_FOUND)?; | |
ensure!(owner == reaction.created.account, MSG_ONLY_REACTION_OWNER_CAN_UPDATE_REACTION); | |
ensure!(reaction.kind != new_kind, MSG_NEW_REACTION_KIND_DO_NOT_DIFFER); | |
reaction.kind = new_kind; | |
reaction.updated = Some(Self::new_change(owner.clone())); | |
let action: ScoringAction; | |
let action_to_cancel: ScoringAction; | |
match new_kind { | |
ReactionKind::Upvote => { | |
post.upvotes_count += 1; | |
post.downvotes_count -= 1; | |
action_to_cancel = ScoringAction::DownvotePost; | |
action = ScoringAction::UpvotePost; | |
}, | |
ReactionKind::Downvote => { | |
post.downvotes_count += 1; | |
post.upvotes_count -= 1; | |
action_to_cancel = ScoringAction::UpvotePost; | |
action = ScoringAction::DownvotePost; | |
}, | |
} | |
Self::change_post_score(owner.clone(), post, action_to_cancel)?; | |
Self::change_post_score(owner.clone(), post, action)?; | |
<ReactionById<T>>::insert(reaction_id, reaction); | |
<PostById<T>>::insert(post_id, post); | |
Self::deposit_event(RawEvent::PostReactionUpdated(owner.clone(), post_id, reaction_id)); | |
} | |
pub fn delete_post_reaction(origin, post_id: PostId, reaction_id: ReactionId) { | |
let owner = ensure_signed(origin)?; | |
ensure!( | |
<PostReactionIdByAccount<T>>::exists((owner.clone(), post_id)), | |
MSG_NO_POST_REACTION_BY_ACCOUNT_TO_DELETE | |
); | |
let action_to_cancel: ScoringAction; | |
let reaction = Self::reaction_by_id(reaction_id).ok_or(MSG_REACTION_NOT_FOUND)?; | |
let ref mut post = Self::post_by_id(post_id).ok_or(MSG_POST_NOT_FOUND)?; | |
ensure!(owner == reaction.created.account, MSG_ONLY_REACTION_OWNER_CAN_UPDATE_REACTION); | |
match reaction.kind { | |
ReactionKind::Upvote => { | |
post.upvotes_count -= 1; | |
action_to_cancel = ScoringAction::UpvotePost; | |
}, | |
ReactionKind::Downvote => { | |
post.downvotes_count -= 1; | |
action_to_cancel = ScoringAction::DownvotePost; | |
}, | |
} | |
Self::change_post_score(owner.clone(), post, action_to_cancel)?; | |
<PostById<T>>::insert(post_id, post); | |
<ReactionById<T>>::remove(reaction_id); | |
ReactionIdsByPostId::mutate(post_id, |ids| Self::vec_remove_on(ids, reaction_id)); | |
<PostReactionIdByAccount<T>>::remove((owner.clone(), post_id)); | |
Self::deposit_event(RawEvent::PostReactionDeleted(owner.clone(), post_id, reaction_id)); | |
} | |
pub fn create_comment_reaction(origin, comment_id: CommentId, kind: ReactionKind) { | |
let owner = ensure_signed(origin)?; | |
ensure!( | |
!<CommentReactionIdByAccount<T>>::exists((owner.clone(), comment_id)), | |
MSG_ACCOUNT_ALREADY_REACTED_TO_COMMENT | |
); | |
let ref mut comment = Self::comment_by_id(comment_id).ok_or(MSG_COMMENT_NOT_FOUND)?; | |
let reaction_id = Self::new_reaction(owner.clone(), kind.clone()); | |
let action: ScoringAction; | |
match kind { | |
ReactionKind::Upvote => { | |
comment.upvotes_count = comment.upvotes_count.checked_add(1).ok_or(MSG_OVERFLOW_UPVOTING_COMMENT)?; | |
action = ScoringAction::UpvoteComment; | |
}, | |
ReactionKind::Downvote => { | |
comment.downvotes_count = comment.downvotes_count.checked_add(1).ok_or(MSG_OVERFLOW_DOWNVOTING_COMMENT)?; | |
action = ScoringAction::DownvoteComment; | |
}, | |
} | |
if comment.created.account != owner { | |
Self::change_comment_score(owner.clone(), comment, action)?; | |
} | |
else { | |
<CommentById<T>>::insert(comment_id, comment); | |
} | |
ReactionIdsByCommentId::mutate(comment_id, |ids| ids.push(reaction_id)); | |
<CommentReactionIdByAccount<T>>::insert((owner.clone(), comment_id), reaction_id); | |
Self::deposit_event(RawEvent::CommentReactionCreated(owner.clone(), comment_id, reaction_id)); | |
} | |
pub fn update_comment_reaction(origin, comment_id: CommentId, reaction_id: ReactionId, new_kind: ReactionKind) { | |
let owner = ensure_signed(origin)?; | |
ensure!( | |
<CommentReactionIdByAccount<T>>::exists((owner.clone(), comment_id)), | |
MSG_ACCOUNT_HAS_NOT_REACTED_TO_COMMENT | |
); | |
let mut reaction = Self::reaction_by_id(reaction_id).ok_or(MSG_REACTION_NOT_FOUND)?; | |
let ref mut comment = Self::comment_by_id(comment_id).ok_or(MSG_COMMENT_NOT_FOUND)?; | |
ensure!(owner == reaction.created.account, MSG_ONLY_REACTION_OWNER_CAN_UPDATE_REACTION); | |
ensure!(reaction.kind != new_kind, MSG_NEW_REACTION_KIND_DO_NOT_DIFFER); | |
reaction.kind = new_kind; | |
reaction.updated = Some(Self::new_change(owner.clone())); | |
let action: ScoringAction; | |
let action_to_cancel: ScoringAction; | |
match new_kind { | |
ReactionKind::Upvote => { | |
comment.upvotes_count += 1; | |
comment.downvotes_count -= 1; | |
action_to_cancel = ScoringAction::DownvoteComment; | |
action = ScoringAction::UpvoteComment; | |
}, | |
ReactionKind::Downvote => { | |
comment.downvotes_count += 1; | |
comment.upvotes_count -= 1; | |
action_to_cancel = ScoringAction::UpvoteComment; | |
action = ScoringAction::DownvoteComment; | |
}, | |
} | |
Self::change_comment_score(owner.clone(), comment, action_to_cancel)?; | |
Self::change_comment_score(owner.clone(), comment, action)?; | |
<ReactionById<T>>::insert(reaction_id, reaction); | |
<CommentById<T>>::insert(comment_id, comment); | |
Self::deposit_event(RawEvent::CommentReactionUpdated(owner.clone(), comment_id, reaction_id)); | |
} | |
pub fn delete_comment_reaction(origin, comment_id: CommentId, reaction_id: ReactionId) { | |
let owner = ensure_signed(origin)?; | |
ensure!( | |
<CommentReactionIdByAccount<T>>::exists((owner.clone(), comment_id)), | |
MSG_NO_COMMENT_REACTION_BY_ACCOUNT_TO_DELETE | |
); | |
let action_to_cancel: ScoringAction; | |
let reaction = Self::reaction_by_id(reaction_id).ok_or(MSG_REACTION_NOT_FOUND)?; | |
let ref mut comment = Self::comment_by_id(comment_id).ok_or(MSG_COMMENT_NOT_FOUND)?; | |
ensure!(owner == reaction.created.account, MSG_ONLY_REACTION_OWNER_CAN_UPDATE_REACTION); | |
match reaction.kind { | |
ReactionKind::Upvote => { | |
comment.upvotes_count -= 1; | |
action_to_cancel = ScoringAction::UpvoteComment | |
}, | |
ReactionKind::Downvote => { | |
comment.downvotes_count -= 1; | |
action_to_cancel = ScoringAction::DownvoteComment | |
}, | |
} | |
Self::change_comment_score(owner.clone(), comment, action_to_cancel)?; | |
<CommentById<T>>::insert(comment_id, comment); | |
ReactionIdsByCommentId::mutate(comment_id, |ids| Self::vec_remove_on(ids, reaction_id)); | |
<ReactionById<T>>::remove(reaction_id); | |
<CommentReactionIdByAccount<T>>::remove((owner.clone(), comment_id)); | |
Self::deposit_event(RawEvent::CommentReactionDeleted(owner.clone(), comment_id, reaction_id)); | |
} | |
} | |
} | |
decl_event!( | |
pub enum Event<T> where | |
<T as system::Trait>::AccountId, | |
{ | |
BlogCreated(AccountId, BlogId), | |
BlogUpdated(AccountId, BlogId), | |
BlogDeleted(AccountId, BlogId), | |
BlogFollowed(AccountId, BlogId), | |
BlogUnfollowed(AccountId, BlogId), | |
AccountReputationChanged(AccountId, ScoringAction, u32), | |
AccountFollowed(AccountId, AccountId), | |
AccountUnfollowed(AccountId, AccountId), | |
PostCreated(AccountId, PostId), | |
PostUpdated(AccountId, PostId), | |
PostDeleted(AccountId, PostId), | |
PostShared(AccountId, PostId), | |
CommentCreated(AccountId, CommentId), | |
CommentUpdated(AccountId, CommentId), | |
CommentDeleted(AccountId, CommentId), | |
CommentShared(AccountId, CommentId), | |
PostReactionCreated(AccountId, PostId, ReactionId), | |
PostReactionUpdated(AccountId, PostId, ReactionId), | |
PostReactionDeleted(AccountId, PostId, ReactionId), | |
CommentReactionCreated(AccountId, CommentId, ReactionId), | |
CommentReactionUpdated(AccountId, CommentId, ReactionId), | |
CommentReactionDeleted(AccountId, CommentId, ReactionId), | |
ProfileCreated(AccountId), | |
ProfileUpdated(AccountId), | |
} | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"Score": "i32", | |
"BlogId": "u64", | |
"PostId": "u64", | |
"CommentId": "u64", | |
"ReactionId": "u64", | |
"IpfsHash": "Text", | |
"OptionIpfsHash": "Option<IpfsHash>", | |
"ScoringAction": { | |
"_enum": [ | |
"UpvotePost", | |
"DownvotePost", | |
"SharePost", | |
"UpvoteComment", | |
"DownvoteComment", | |
"ShareComment", | |
"FollowBlog", | |
"FollowAccount" | |
] | |
}, | |
"PostExtension": { | |
"_enum": { | |
"RegularPost": "Null", | |
"SharedPost": "u64", | |
"SharedComment": "u64" | |
} | |
}, | |
"ReactionKind": { | |
"_enum": [ | |
"Upvote", | |
"Downvote" | |
] | |
}, | |
"Change": { | |
"account": "AccountId", | |
"block": "BlockNumber", | |
"time": "Moment" | |
}, | |
"VecAccountId": "Vec<AccountId>", | |
"OptionText": "Option<Text>", | |
"OptionChange": "Option<Change>", | |
"OptionBlogId": "Option<u64>", | |
"OptionCommentId": "Option<u64>", | |
"OptionVecAccountId": "Option<VecAccountId>", | |
"Blog": { | |
"id": "u64", | |
"created": "Change", | |
"updated": "Option<Change>", | |
"writers": "VecAccountId", | |
"slug": "Text", | |
"ipfs_hash": "IpfsHash", | |
"posts_count": "u16", | |
"followers_count": "u32", | |
"edit_history": "Vec<BlogHistoryRecord>", | |
"score": "Score" | |
}, | |
"BlogUpdate": { | |
"writers": "OptionVecAccountId", | |
"slug": "OptionText", | |
"ipfs_hash": "OptionIpfsHash" | |
}, | |
"Post": { | |
"id": "u64", | |
"blog_id": "u64", | |
"created": "Change", | |
"updated": "OptionChange", | |
"extension": "PostExtension", | |
"ipfs_hash": "IpfsHash", | |
"comments_count": "u16", | |
"upvotes_count": "u16", | |
"downvotes_count": "u16", | |
"shares_count": "u16", | |
"edit_history": "Vec<PostHistoryRecord>", | |
"score": "Score" | |
}, | |
"PostUpdate": { | |
"blog_id": "OptionBlogId", | |
"ipfs_hash": "OptionIpfsHash" | |
}, | |
"Comment": { | |
"id": "u64", | |
"parent_id": "OptionCommentId", | |
"post_id": "u64", | |
"created": "Change", | |
"updated": "OptionChange", | |
"ipfs_hash": "IpfsHash", | |
"upvotes_count": "u16", | |
"downvotes_count": "u16", | |
"shares_count": "u16", | |
"direct_replies_count": "u16", | |
"edit_history": "VecCommentHistoryRecord", | |
"score": "Score" | |
}, | |
"Reaction": { | |
"id": "u64", | |
"created": "Change", | |
"updated": "OptionChange", | |
"kind": "ReactionKind" | |
}, | |
"SocialAccount": { | |
"followers_count": "u32", | |
"following_accounts_count": "u16", | |
"following_blogs_count": "u16", | |
"reputation": "u32", | |
"profile": "OptionProfile" | |
}, | |
"Profile": { | |
"created": "Change", | |
"updated": "OptionChange", | |
"username": "Text", | |
"ipfs_hash": "IpfsHash", | |
"edit_history": "Vec<ProfileHistoryRecord>" | |
}, | |
"CommentUpdate": { | |
"ipfs_hash": "IpfsHash" | |
}, | |
"OptionProfile": "Option<Profile>", | |
"ProfileUpdate": { | |
"username": "OptionText", | |
"ipfs_hash": "OptionIpfsHash" | |
}, | |
"BlogHistoryRecord": { | |
"edited": "Change", | |
"old_data": "BlogUpdate" | |
}, | |
"PostHistoryRecord": { | |
"edited": "Change", | |
"old_data": "PostUpdate" | |
}, | |
"CommentHistoryRecord": { | |
"edited": "Change", | |
"old_data": "CommentUpdate" | |
}, | |
"VecCommentHistoryRecord": "Vec<CommentHistoryRecord>", | |
"ProfileHistoryRecord": { | |
"edited": "Change", | |
"old_data": "ProfileUpdate" | |
}, | |
"VecProfileHistoryRecord": "Vec<ProfileHistoryRecord>" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment