// Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] #![recursion_limit = "256"] // Internal Substrate warning (decl_event). #![allow(clippy::unused_unit, clippy::all)] #[cfg(test)] mod tests; mod errors; mod permissions; pub use errors::*; pub use permissions::*; use core::hash::Hash; use codec::Codec; use codec::{Decode, Encode}; pub use storage::{ BagIdType, DataObjectCreationParameters, DataObjectStorage, DynamicBagIdType, UploadParameters, UploadParametersRecord, }; use frame_support::{ decl_event, decl_module, decl_storage, dispatch::DispatchResult, ensure, traits::Get, Parameter, }; use frame_system::ensure_signed; #[cfg(feature = "std")] pub use serde::{Deserialize, Serialize}; use sp_arithmetic::traits::{BaseArithmetic, One, Zero}; use sp_runtime::traits::{MaybeSerializeDeserialize, Member}; use sp_std::collections::btree_set::BTreeSet; use sp_std::vec::Vec; pub use common::{ currency::{BalanceOf, GovernanceCurrency}, working_group::WorkingGroup, AssetUrls, MembershipTypes, StorageOwnership, }; type Storage = storage::Module; /// Type, used in diffrent numeric constraints representations pub type MaxNumber = u32; /// A numeric identifier trait pub trait NumericIdentifier: Parameter + Member + BaseArithmetic + Codec + Default + Copy + Clone + Hash + MaybeSerializeDeserialize + Eq + PartialEq + Ord + Zero { } impl NumericIdentifier for u64 {} /// Module configuration trait for Content Directory Module pub trait Trait: frame_system::Trait + ContentActorAuthenticator + Clone + GovernanceCurrency + storage::Trait { /// The overarching event type. type Event: From> + Into<::Event>; /// Channel Transfer Payments Escrow Account seed for ModuleId to compute deterministic AccountId type ChannelOwnershipPaymentEscrowId: Get<[u8; 8]>; /// Type of identifier for Videos type VideoId: NumericIdentifier; /// Type of identifier for Video Categories type VideoCategoryId: NumericIdentifier; /// Type of identifier for Channel Categories type ChannelCategoryId: NumericIdentifier; /// Type of identifier for Playlists type PlaylistId: NumericIdentifier; /// Type of identifier for Persons type PersonId: NumericIdentifier; /// Type of identifier for Channels type SeriesId: NumericIdentifier; /// Type of identifier for Channel transfer requests type ChannelOwnershipTransferRequestId: NumericIdentifier; /// The maximum number of curators per group constraint type MaxNumberOfCuratorsPerGroup: Get; /// The storage type used type DataObjectStorage: storage::DataObjectStorage; } /// The owner of a channel, is the authorized "actor" that can update /// or delete or transfer a channel and its contents. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub enum ChannelOwner { /// A Member owns the channel Member(MemberId), /// A specific curation group owns the channel CuratorGroup(CuratorGroupId), } // simplification type pub(crate) type ActorToChannelOwnerResult = Result< ChannelOwner< ::MemberId, ::CuratorGroupId, >, Error, >; // Default trait implemented only because its used in a Channel which needs to implement a Default trait // since it is a StorageValue. impl Default for ChannelOwner { fn default() -> Self { ChannelOwner::Member(MemberId::default()) } } /// A category which channels can belong to. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct ChannelCategory { // No runtime information is currently stored for a Category. } /// Information on the category being created. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct ChannelCategoryCreationParameters { /// Metadata for the category. meta: Vec, } /// Information on the category being updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct ChannelCategoryUpdateParameters { // as this is the only field it is not an Option /// Metadata update for the category. new_meta: Vec, } /// Type representing an owned channel which videos, playlists, and series can belong to. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct ChannelRecord { /// The owner of a channel owner: ChannelOwner, /// The videos under this channel num_videos: u64, /// If curators have censored this channel or not is_censored: bool, /// Reward account where revenue is sent if set. reward_account: Option, /// Account for withdrawing deletion prize funds deletion_prize_source_account_id: AccountId, } // Channel alias type for simplification. pub type Channel = ChannelRecord< ::MemberId, ::CuratorGroupId, ::AccountId, >; /// A request to buy a channel by a new ChannelOwner. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct ChannelOwnershipTransferRequestRecord< ChannelId, MemberId, CuratorGroupId, Balance, AccountId, > { channel_id: ChannelId, new_owner: ChannelOwner, payment: Balance, new_reward_account: Option, } // ChannelOwnershipTransferRequest type alias for simplification. pub type ChannelOwnershipTransferRequest = ChannelOwnershipTransferRequestRecord< ::ChannelId, ::MemberId, ::CuratorGroupId, BalanceOf, ::AccountId, >; /// Information about channel being created. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub struct ChannelCreationParametersRecord { /// Asset collection for the channel, referenced by metadata assets: Option, /// Metadata about the channel. meta: Option>, /// optional reward account reward_account: Option, } type ChannelCreationParameters = ChannelCreationParametersRecord, ::AccountId>; /// Information about channel being updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct ChannelUpdateParametersRecord { /// Asset collection for the channel, referenced by metadata assets_to_upload: Option, /// If set, metadata update for the channel. new_meta: Option>, /// If set, updates the reward account of the channel reward_account: Option>, /// assets to be removed from channel assets_to_remove: BTreeSet, } type ChannelUpdateParameters = ChannelUpdateParametersRecord< StorageAssets, ::AccountId, DataObjectId, >; /// A category that videos can belong to. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct VideoCategory { // No runtime information is currently stored for a Category. } /// Information about the video category being created. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct VideoCategoryCreationParameters { /// Metadata about the video category. meta: Vec, } /// Information about the video category being updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct VideoCategoryUpdateParameters { // Because it is the only field it is not an Option /// Metadata update for the video category. new_meta: Vec, } /// Information regarding the content being uploaded #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub struct StorageAssetsRecord { /// Data object parameters. pub object_creation_list: Vec, /// Expected data size fee value for this extrinsic call. pub expected_data_size_fee: Balance, } type StorageAssets = StorageAssetsRecord<::Balance>; /// Information about the video being created. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub struct VideoCreationParametersRecord { /// Asset collection for the video assets: Option, /// Metadata for the video. meta: Option>, } type VideoCreationParameters = VideoCreationParametersRecord>; /// Information about the video being updated #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct VideoUpdateParametersRecord { /// Assets referenced by metadata assets_to_upload: Option, /// If set, metadata update for the video. new_meta: Option>, /// video assets to be removed from channel assets_to_remove: BTreeSet, } type VideoUpdateParameters = VideoUpdateParametersRecord, DataObjectId>; /// A video which belongs to a channel. A video may be part of a series or playlist. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct VideoRecord { pub in_channel: ChannelId, // keep track of which season the video is in if it is an 'episode' // - prevent removing a video if it is in a season (because order is important) pub in_series: Option, /// Whether the curators have censored the video or not. pub is_censored: bool, } type Video = VideoRecord<::ChannelId, ::SeriesId>; /// Information about the plyalist being created. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct PlaylistCreationParameters { /// Metadata about the playlist. meta: Vec, } /// Information about the playlist being updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct PlaylistUpdateParameters { // It is the only field so its not an Option /// Metadata update for the playlist. new_meta: Vec, } /// A playlist is an ordered collection of videos. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct Playlist { /// The channel the playlist belongs to. in_channel: ChannelId, } /// Information about the episode being created or updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub enum EpisodeParameters { /// A new video is being added as the episode. NewVideo(VideoCreationParametersRecord), /// An existing video is being made into an episode. ExistingVideo(VideoId), } /// Information about the season being created or updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct SeasonParameters { /// Season assets referenced by metadata assets: Option, // ?? It might just be more straighforward to always provide full list of episodes at cost of larger tx. /// If set, updates the episodes of a season. Extends the number of episodes in a season /// when length of new_episodes is greater than previously set. Last elements must all be /// 'Some' in that case. /// Will truncate existing season when length of new_episodes is less than previously set. episodes: Option>>>, meta: Option>, } /// Information about the series being created or updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct SeriesParameters { /// Series assets referenced by metadata assets: Option, // ?? It might just be more straighforward to always provide full list of seasons at cost of larger tx. /// If set, updates the seasons of a series. Extend a series when length of seasons is /// greater than previoulsy set. Last elements must all be 'Some' in that case. /// Will truncate existing series when length of seasons is less than previously set. seasons: Option>>>, meta: Option>, } /// A season is an ordered list of videos (episodes). #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct Season { episodes: Vec, } /// A series is an ordered list of seasons that belongs to a channel. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct Series { in_channel: ChannelId, seasons: Vec>, } /// The actor the caller/origin is trying to act as for Person creation and update and delete calls. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub enum PersonActor { Member(MemberId), Curator(CuratorId), } /// The authorized actor that may update or delete a Person. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub enum PersonController { /// Member controls the person Member(MemberId), /// Any curator controls the person Curators, } /// Default trait implemented only because its used in Person which needs to implement a Default trait /// since it is a StorageValue. impl Default for PersonController { fn default() -> Self { PersonController::Member(MemberId::default()) } } /// Information for Person being created. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] pub struct PersonCreationParameters { /// Assets referenced by metadata assets: StorageAssets, /// Metadata for person. meta: Vec, } /// Information for Persion being updated. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct PersonUpdateParameters { /// Assets referenced by metadata assets: Option, /// Metadata to update person. new_meta: Option>, } /// A Person represents a real person that may be associated with a video. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] pub struct Person { /// Who can update or delete this person. controlled_by: PersonController, } type DataObjectId = ::DataObjectId; decl_storage! { trait Store for Module as Content { pub ChannelById get(fn channel_by_id): map hasher(blake2_128_concat) T::ChannelId => Channel; pub ChannelCategoryById get(fn channel_category_by_id): map hasher(blake2_128_concat) T::ChannelCategoryId => ChannelCategory; pub VideoById get(fn video_by_id): map hasher(blake2_128_concat) T::VideoId => Video; pub VideoCategoryById get(fn video_category_by_id): map hasher(blake2_128_concat) T::VideoCategoryId => VideoCategory; pub PlaylistById get(fn playlist_by_id): map hasher(blake2_128_concat) T::PlaylistId => Playlist; pub SeriesById get(fn series_by_id): map hasher(blake2_128_concat) T::SeriesId => Series; pub PersonById get(fn person_by_id): map hasher(blake2_128_concat) T::PersonId => Person; pub ChannelOwnershipTransferRequestById get(fn channel_ownership_transfer_request_by_id): map hasher(blake2_128_concat) T::ChannelOwnershipTransferRequestId => ChannelOwnershipTransferRequest; pub NextChannelCategoryId get(fn next_channel_category_id) config(): T::ChannelCategoryId; pub NextChannelId get(fn next_channel_id) config(): T::ChannelId; pub NextVideoCategoryId get(fn next_video_category_id) config(): T::VideoCategoryId; pub NextVideoId get(fn next_video_id) config(): T::VideoId; pub NextPlaylistId get(fn next_playlist_id) config(): T::PlaylistId; pub NextPersonId get(fn next_person_id) config(): T::PersonId; pub NextSeriesId get(fn next_series_id) config(): T::SeriesId; pub NextChannelOwnershipTransferRequestId get(fn next_channel_transfer_request_id) config(): T::ChannelOwnershipTransferRequestId; pub NextCuratorGroupId get(fn next_curator_group_id) config(): T::CuratorGroupId; /// Map, representing CuratorGroupId -> CuratorGroup relation pub CuratorGroupById get(fn curator_group_by_id): map hasher(blake2_128_concat) T::CuratorGroupId => CuratorGroup; } } decl_module! { pub struct Module for enum Call where origin: T::Origin { /// Predefined errors type Error = Error; /// Initializing events fn deposit_event() = default; /// Exports const - max number of curators per group const MaxNumberOfCuratorsPerGroup: MaxNumber = T::MaxNumberOfCuratorsPerGroup::get(); // ====== // Next set of extrinsics can only be invoked by lead. // ====== /// Add new curator group to runtime storage #[weight = 10_000_000] // TODO: adjust weight pub fn create_curator_group( origin, ) { // Ensure given origin is lead ensure_is_lead::(origin)?; // // == MUTATION SAFE == // let curator_group_id = Self::next_curator_group_id(); // Insert empty curator group with `active` parameter set to false >::insert(curator_group_id, CuratorGroup::::default()); // Increment the next curator curator_group_id: >::mutate(|n| *n += T::CuratorGroupId::one()); // Trigger event Self::deposit_event(RawEvent::CuratorGroupCreated(curator_group_id)); } /// Set `is_active` status for curator group under given `curator_group_id` #[weight = 10_000_000] // TODO: adjust weight pub fn set_curator_group_status( origin, curator_group_id: T::CuratorGroupId, is_active: bool, ) { // Ensure given origin is lead ensure_is_lead::(origin)?; // Ensure curator group under provided curator_group_id already exist Self::ensure_curator_group_under_given_id_exists(&curator_group_id)?; // // == MUTATION SAFE == // // Set `is_active` status for curator group under given `curator_group_id` >::mutate(curator_group_id, |curator_group| { curator_group.set_status(is_active) }); // Trigger event Self::deposit_event(RawEvent::CuratorGroupStatusSet(curator_group_id, is_active)); } /// Add curator to curator group under given `curator_group_id` #[weight = 10_000_000] // TODO: adjust weight pub fn add_curator_to_group( origin, curator_group_id: T::CuratorGroupId, curator_id: T::CuratorId, ) { // Ensure given origin is lead ensure_is_lead::(origin)?; // Ensure curator group under provided curator_group_id already exist, retrieve corresponding one let curator_group = Self::ensure_curator_group_exists(&curator_group_id)?; // Ensure that curator_id is infact a worker in content working group ensure_is_valid_curator_id::(&curator_id)?; // Ensure max number of curators per group limit not reached yet curator_group.ensure_max_number_of_curators_limit_not_reached()?; // Ensure curator under provided curator_id isn`t a CuratorGroup member yet curator_group.ensure_curator_in_group_does_not_exist(&curator_id)?; // // == MUTATION SAFE == // // Insert curator_id into curator_group under given curator_group_id >::mutate(curator_group_id, |curator_group| { curator_group.get_curators_mut().insert(curator_id); }); // Trigger event Self::deposit_event(RawEvent::CuratorAdded(curator_group_id, curator_id)); } /// Remove curator from a given curator group #[weight = 10_000_000] // TODO: adjust weight pub fn remove_curator_from_group( origin, curator_group_id: T::CuratorGroupId, curator_id: T::CuratorId, ) { // Ensure given origin is lead ensure_is_lead::(origin)?; // Ensure curator group under provided curator_group_id already exist, retrieve corresponding one let curator_group = Self::ensure_curator_group_exists(&curator_group_id)?; // Ensure curator under provided curator_id is CuratorGroup member curator_group.ensure_curator_in_group_exists(&curator_id)?; // // == MUTATION SAFE == // // Remove curator_id from curator_group under given curator_group_id >::mutate(curator_group_id, |curator_group| { curator_group.get_curators_mut().remove(&curator_id); }); // Trigger event Self::deposit_event(RawEvent::CuratorRemoved(curator_group_id, curator_id)); } // TODO: Add Option to ChannelCreationParameters ? #[weight = 10_000_000] // TODO: adjust weight pub fn create_channel( origin, actor: ContentActor, params: ChannelCreationParameters, ) { ensure_actor_authorized_to_create_channel::( origin.clone(), &actor, )?; // channel creator account let sender = ensure_signed(origin)?; // The channel owner will be.. let channel_owner = Self::actor_to_channel_owner(&actor)?; // next channel id let channel_id = NextChannelId::::get(); // atomically upload to storage and return the # of uploaded assets if let Some(upload_assets) = params.assets.as_ref() { Self::upload_assets_to_storage( upload_assets, &channel_id, &sender, )?; } // // == MUTATION SAFE == // // Only increment next channel id if adding content was successful NextChannelId::::mutate(|id| *id += T::ChannelId::one()); // channel creation let channel: Channel = ChannelRecord { owner: channel_owner, // a newly create channel has zero videos ?? num_videos: 0u64, is_censored: false, reward_account: params.reward_account.clone(), // setting the channel owner account as the prize funds account deletion_prize_source_account_id: sender, }; // add channel to onchain state ChannelById::::insert(channel_id, channel.clone()); Self::deposit_event(RawEvent::ChannelCreated(actor, channel_id, channel, params)); } // Include Option in ChannelUpdateParameters to update reward_account #[weight = 10_000_000] // TODO: adjust weight pub fn update_channel( origin, actor: ContentActor, channel_id: T::ChannelId, params: ChannelUpdateParameters, ) { // check that channel exists let channel = Self::ensure_channel_exists(&channel_id)?; ensure_actor_authorized_to_update_channel::( origin, &actor, &channel.owner, )?; Self::remove_assets_from_storage(¶ms.assets_to_remove, &channel_id, &channel.deletion_prize_source_account_id)?; // atomically upload to storage and return the # of uploaded assets if let Some(upload_assets) = params.assets_to_upload.as_ref() { Self::upload_assets_to_storage( upload_assets, &channel_id, &channel.deletion_prize_source_account_id )?; } // // == MUTATION SAFE == // let mut channel = channel; // Maybe update the reward account if let Some(reward_account) = ¶ms.reward_account { channel.reward_account = reward_account.clone(); } // Update the channel ChannelById::::insert(channel_id, channel.clone()); Self::deposit_event(RawEvent::ChannelUpdated(actor, channel_id, channel, params)); } // extrinsics for channel deletion #[weight = 10_000_000] // TODO: adjust weight pub fn delete_channel( origin, actor: ContentActor, channel_id: T::ChannelId, num_objects_to_delete: u64, ) -> DispatchResult { // check that channel exists let channel = Self::ensure_channel_exists(&channel_id)?; // ensure permissions ensure_actor_authorized_to_update_channel::( origin, &actor, &channel.owner, )?; // check that channel videos are 0 ensure!(channel.num_videos == 0, Error::::ChannelContainsVideos); // get bag id for the channel let dyn_bag = DynamicBagIdType::::Channel(channel_id); let bag_id = storage::BagIdType::from(dyn_bag.clone()); // channel has a dynamic bag associated to it -> remove assets from storage if let Ok(bag) = T::DataObjectStorage::ensure_bag_exists(&bag_id) { // ensure that bag size provided is valid ensure!( bag.objects_number == num_objects_to_delete, Error::::InvalidBagSizeSpecified ); // construct collection of assets to be removed let assets_to_remove = T::DataObjectStorage::get_data_objects_id(&bag_id); // remove specified assets from storage Self::remove_assets_from_storage( &assets_to_remove, &channel_id, &channel.deletion_prize_source_account_id )?; // delete channel dynamic bag Storage::::delete_dynamic_bag( channel.deletion_prize_source_account_id, dyn_bag )?; } // // == MUTATION SAFE == // // remove channel from on chain state ChannelById::::remove(channel_id); // deposit event Self::deposit_event(RawEvent::ChannelDeleted(actor, channel_id)); Ok(()) } #[weight = 10_000_000] // TODO: adjust weight pub fn update_channel_censorship_status( origin, actor: ContentActor, channel_id: T::ChannelId, is_censored: bool, rationale: Vec, ) { // check that channel exists let channel = Self::ensure_channel_exists(&channel_id)?; if channel.is_censored == is_censored { return Ok(()) } ensure_actor_authorized_to_censor::( origin, &actor, &channel.owner, )?; // // == MUTATION SAFE == // ChannelById::::mutate(channel_id, |channel| { channel.is_censored = is_censored }); // TODO: unset the reward account ? so no revenue can be earned for censored channels? Self::deposit_event(RawEvent::ChannelCensorshipStatusUpdated(actor, channel_id, is_censored, rationale)); } #[weight = 10_000_000] // TODO: adjust weight pub fn create_channel_category( origin, actor: ContentActor, params: ChannelCategoryCreationParameters, ) { ensure_actor_authorized_to_manage_categories::( origin, &actor )?; // // == MUTATION SAFE == // let category_id = Self::next_channel_category_id(); NextChannelCategoryId::::mutate(|id| *id += T::ChannelCategoryId::one()); let category = ChannelCategory {}; ChannelCategoryById::::insert(category_id, category.clone()); Self::deposit_event(RawEvent::ChannelCategoryCreated(category_id, category, params)); } #[weight = 10_000_000] // TODO: adjust weight pub fn update_channel_category( origin, actor: ContentActor, category_id: T::ChannelCategoryId, params: ChannelCategoryUpdateParameters, ) { ensure_actor_authorized_to_manage_categories::( origin, &actor )?; Self::ensure_channel_category_exists(&category_id)?; Self::deposit_event(RawEvent::ChannelCategoryUpdated(actor, category_id, params)); } #[weight = 10_000_000] // TODO: adjust weight pub fn delete_channel_category( origin, actor: ContentActor, category_id: T::ChannelCategoryId, ) { ensure_actor_authorized_to_manage_categories::( origin, &actor )?; Self::ensure_channel_category_exists(&category_id)?; ChannelCategoryById::::remove(&category_id); Self::deposit_event(RawEvent::ChannelCategoryDeleted(actor, category_id)); } #[weight = 10_000_000] // TODO: adjust weight pub fn request_channel_transfer( _origin, _actor: ContentActor, _request: ChannelOwnershipTransferRequest, ) { // requester must be new_owner Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn cancel_channel_transfer_request( _origin, _request_id: T::ChannelOwnershipTransferRequestId, ) { // origin must be original requester (ie. proposed new channel owner) Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn accept_channel_transfer( _origin, _actor: ContentActor, _request_id: T::ChannelOwnershipTransferRequestId, ) { // only current owner of channel can approve Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn create_video( origin, actor: ContentActor, channel_id: T::ChannelId, params: VideoCreationParameters, ) { // check that channel exists let channel = Self::ensure_channel_exists(&channel_id)?; ensure_actor_authorized_to_update_channel::( origin, &actor, &channel.owner, )?; // next video id let video_id = NextVideoId::::get(); // atomically upload to storage and return the # of uploaded assets if let Some(upload_assets) = params.assets.as_ref() { Self::upload_assets_to_storage( upload_assets, &channel_id, &channel.deletion_prize_source_account_id )?; } // // == MUTATION SAFE == // // create the video struct let video: Video = VideoRecord { in_channel: channel_id, // keep track of which season the video is in if it is an 'episode' // - prevent removing a video if it is in a season (because order is important) in_series: None, /// Whether the curators have censored the video or not. is_censored: false, }; // add it to the onchain state VideoById::::insert(video_id, video); // Only increment next video id if adding content was successful NextVideoId::::mutate(|id| *id += T::VideoId::one()); // Add recently added video id to the channel ChannelById::::mutate(channel_id, |channel| { channel.num_videos = channel.num_videos.saturating_add(1); }); Self::deposit_event(RawEvent::VideoCreated(actor, channel_id, video_id, params)); } #[weight = 10_000_000] // TODO: adjust weight pub fn update_video( origin, actor: ContentActor, video_id: T::VideoId, params: VideoUpdateParameters, ) { // check that video exists, retrieve corresponding channel id. let video = Self::ensure_video_exists(&video_id)?; let channel_id = video.in_channel; let channel = ChannelById::::get(&channel_id); ensure_actor_authorized_to_update_channel::( origin, &actor, &channel.owner, )?; // remove specified assets from channel bag in storage Self::remove_assets_from_storage(¶ms.assets_to_remove, &channel_id, &channel.deletion_prize_source_account_id)?; // atomically upload to storage and return the # of uploaded assets if let Some(upload_assets) = params.assets_to_upload.as_ref() { Self::upload_assets_to_storage( upload_assets, &channel_id, &channel.deletion_prize_source_account_id )?; } // // == MUTATION SAFE == // Self::deposit_event(RawEvent::VideoUpdated(actor, video_id, params)); } #[weight = 10_000_000] // TODO: adjust weight pub fn delete_video( origin, actor: ContentActor, video_id: T::VideoId, assets_to_remove: BTreeSet>, ) { // check that video exists let video = Self::ensure_video_exists(&video_id)?; // get information regarding channel let channel_id = video.in_channel; let channel = ChannelById::::get(channel_id); ensure_actor_authorized_to_update_channel::( origin, &actor, // The channel owner will be.. &channel.owner, )?; // ensure video can be removed Self::ensure_video_can_be_removed(&video)?; // remove specified assets from channel bag in storage Self::remove_assets_from_storage(&assets_to_remove, &channel_id, &channel.deletion_prize_source_account_id)?; // // == MUTATION SAFE == // // Remove video VideoById::::remove(video_id); // Decrease video count for the channel ChannelById::::mutate(channel_id, |channel| { channel.num_videos = channel.num_videos.saturating_sub(1) }); Self::deposit_event(RawEvent::VideoDeleted(actor, video_id)); } #[weight = 10_000_000] // TODO: adjust weight pub fn create_playlist( _origin, _actor: ContentActor, _channel_id: T::ChannelId, _params: PlaylistCreationParameters, ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn update_playlist( _origin, _actor: ContentActor, _playlist: T::PlaylistId, _params: PlaylistUpdateParameters, ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn delete_playlist( _origin, _actor: ContentActor, _channel_id: T::ChannelId, _playlist: T::PlaylistId, ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn set_featured_videos( origin, actor: ContentActor, list: Vec ) { // can only be set by lead ensure_actor_authorized_to_set_featured_videos::( origin, &actor, )?; // // == MUTATION SAFE == // Self::deposit_event(RawEvent::FeaturedVideosSet(actor, list)); } #[weight = 10_000_000] // TODO: adjust weight pub fn create_video_category( origin, actor: ContentActor, params: VideoCategoryCreationParameters, ) { ensure_actor_authorized_to_manage_categories::( origin, &actor )?; // // == MUTATION SAFE == // let category_id = Self::next_video_category_id(); NextVideoCategoryId::::mutate(|id| *id += T::VideoCategoryId::one()); let category = VideoCategory {}; VideoCategoryById::::insert(category_id, category); Self::deposit_event(RawEvent::VideoCategoryCreated(actor, category_id, params)); } #[weight = 10_000_000] // TODO: adjust weight pub fn update_video_category( origin, actor: ContentActor, category_id: T::VideoCategoryId, params: VideoCategoryUpdateParameters, ) { ensure_actor_authorized_to_manage_categories::( origin, &actor )?; Self::ensure_video_category_exists(&category_id)?; Self::deposit_event(RawEvent::VideoCategoryUpdated(actor, category_id, params)); } #[weight = 10_000_000] // TODO: adjust weight pub fn delete_video_category( origin, actor: ContentActor, category_id: T::VideoCategoryId, ) { ensure_actor_authorized_to_manage_categories::( origin, &actor )?; Self::ensure_video_category_exists(&category_id)?; VideoCategoryById::::remove(&category_id); Self::deposit_event(RawEvent::VideoCategoryDeleted(actor, category_id)); } #[weight = 10_000_000] // TODO: adjust weight pub fn create_person( _origin, _actor: PersonActor, _params: PersonCreationParameters>, ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn update_person( _origin, _actor: PersonActor, _person: T::PersonId, _params: PersonUpdateParameters>, ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn delete_person( _origin, _actor: PersonActor, _person: T::PersonId, ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn add_person_to_video( _origin, _actor: ContentActor, _video_id: T::VideoId, _person: T::PersonId ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn remove_person_from_video( _origin, _actor: ContentActor, _video_id: T::VideoId ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn update_video_censorship_status( origin, actor: ContentActor, video_id: T::VideoId, is_censored: bool, rationale: Vec, ) { // check that video exists let video = Self::ensure_video_exists(&video_id)?; if video.is_censored == is_censored { return Ok(()) } ensure_actor_authorized_to_censor::( origin, &actor, // The channel owner will be.. &Self::channel_by_id(video.in_channel).owner, )?; // // == MUTATION SAFE == // // update VideoById::::mutate(video_id, |video| { video.is_censored = is_censored; }); Self::deposit_event(RawEvent::VideoCensorshipStatusUpdated(actor, video_id, is_censored, rationale)); } #[weight = 10_000_000] // TODO: adjust weight pub fn create_series( _origin, _actor: ContentActor, _channel_id: T::ChannelId, _params: SeriesParameters> ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn update_series( _origin, _actor: ContentActor, _channel_id: T::ChannelId, _params: SeriesParameters> ) { Self::not_implemented()?; } #[weight = 10_000_000] // TODO: adjust weight pub fn delete_series( _origin, _actor: ContentActor, _series: T::SeriesId, ) { Self::not_implemented()?; } } } impl Module { /// Ensure `CuratorGroup` under given id exists fn ensure_curator_group_under_given_id_exists( curator_group_id: &T::CuratorGroupId, ) -> Result<(), Error> { ensure!( >::contains_key(curator_group_id), Error::::CuratorGroupDoesNotExist ); Ok(()) } /// Ensure `CuratorGroup` under given id exists, return corresponding one fn ensure_curator_group_exists( curator_group_id: &T::CuratorGroupId, ) -> Result, Error> { Self::ensure_curator_group_under_given_id_exists(curator_group_id)?; Ok(Self::curator_group_by_id(curator_group_id)) } fn ensure_channel_exists(channel_id: &T::ChannelId) -> Result, Error> { ensure!( ChannelById::::contains_key(channel_id), Error::::ChannelDoesNotExist ); Ok(ChannelById::::get(channel_id)) } fn ensure_video_exists(video_id: &T::VideoId) -> Result, Error> { ensure!( VideoById::::contains_key(video_id), Error::::VideoDoesNotExist ); Ok(VideoById::::get(video_id)) } // Ensure given video is not in season fn ensure_video_can_be_removed(video: &Video) -> DispatchResult { ensure!(video.in_series.is_none(), Error::::VideoInSeason); Ok(()) } fn ensure_channel_category_exists( channel_category_id: &T::ChannelCategoryId, ) -> Result> { ensure!( ChannelCategoryById::::contains_key(channel_category_id), Error::::CategoryDoesNotExist ); Ok(ChannelCategoryById::::get(channel_category_id)) } fn ensure_video_category_exists( video_category_id: &T::VideoCategoryId, ) -> Result> { ensure!( VideoCategoryById::::contains_key(video_category_id), Error::::CategoryDoesNotExist ); Ok(VideoCategoryById::::get(video_category_id)) } fn pick_upload_parameters_from_assets( assets: &StorageAssets, channel_id: &T::ChannelId, prize_source_account: &T::AccountId, ) -> UploadParameters { // dynamic bag for a media object let dyn_bag = DynamicBagIdType::::Channel(*channel_id); let bag_id = BagIdType::from(dyn_bag.clone()); if T::DataObjectStorage::ensure_bag_exists(&bag_id).is_err() { // create_dynamic_bag checks automatically satifsfied with None as second parameter Storage::::create_dynamic_bag(dyn_bag, None).unwrap(); } UploadParametersRecord { bag_id, object_creation_list: assets.object_creation_list.clone(), deletion_prize_source_account_id: prize_source_account.clone(), expected_data_size_fee: assets.expected_data_size_fee, } } fn actor_to_channel_owner( actor: &ContentActor, ) -> ActorToChannelOwnerResult { match actor { // Lead should use their member or curator role to create channels ContentActor::Lead => Err(Error::::ActorCannotOwnChannel), ContentActor::Curator(curator_group_id, _curator_id) => { Ok(ChannelOwner::CuratorGroup(*curator_group_id)) } ContentActor::Member(member_id) => Ok(ChannelOwner::Member(*member_id)), } } fn bag_id_for_channel(channel_id: &T::ChannelId) -> storage::BagId { // retrieve bag id from channel id let dyn_bag = DynamicBagIdType::::Channel(*channel_id); BagIdType::from(dyn_bag) } fn not_implemented() -> DispatchResult { Err(Error::::FeatureNotImplemented.into()) } fn upload_assets_to_storage( assets: &StorageAssets, channel_id: &T::ChannelId, prize_source_account: &T::AccountId, ) -> DispatchResult { // construct upload params let upload_params = Self::pick_upload_parameters_from_assets(assets, channel_id, prize_source_account); // attempt to upload objects att Storage::::upload_data_objects(upload_params.clone())?; Ok(()) } fn remove_assets_from_storage( assets: &BTreeSet>, channel_id: &T::ChannelId, prize_source_account: &T::AccountId, ) -> DispatchResult { // remove assets if any if !assets.is_empty() { Storage::::delete_data_objects( prize_source_account.clone(), Self::bag_id_for_channel(&channel_id), assets.clone(), )?; } Ok(()) } } decl_event!( pub enum Event where ContentActor = ContentActor< ::CuratorGroupId, ::CuratorId, ::MemberId, >, CuratorGroupId = ::CuratorGroupId, CuratorId = ::CuratorId, VideoId = ::VideoId, VideoCategoryId = ::VideoCategoryId, ChannelId = ::ChannelId, ChannelCategoryId = ::ChannelCategoryId, ChannelOwnershipTransferRequestId = ::ChannelOwnershipTransferRequestId, PlaylistId = ::PlaylistId, SeriesId = ::SeriesId, PersonId = ::PersonId, ChannelOwnershipTransferRequest = ChannelOwnershipTransferRequest, Series = Series<::ChannelId, ::VideoId>, Channel = Channel, DataObjectId = DataObjectId, IsCensored = bool, ChannelCreationParameters = ChannelCreationParameters, ChannelUpdateParameters = ChannelUpdateParameters, VideoCreationParameters = VideoCreationParameters, VideoUpdateParameters = VideoUpdateParameters, StorageAssets = StorageAssets, { // Curators CuratorGroupCreated(CuratorGroupId), CuratorGroupStatusSet(CuratorGroupId, bool /* active status */), CuratorAdded(CuratorGroupId, CuratorId), CuratorRemoved(CuratorGroupId, CuratorId), // Channels ChannelCreated(ContentActor, ChannelId, Channel, ChannelCreationParameters), ChannelUpdated(ContentActor, ChannelId, Channel, ChannelUpdateParameters), ChannelAssetsRemoved(ContentActor, ChannelId, BTreeSet, Channel), ChannelCensorshipStatusUpdated( ContentActor, ChannelId, IsCensored, Vec, /* rationale */ ), // Channel Ownership Transfers ChannelOwnershipTransferRequested( ContentActor, ChannelOwnershipTransferRequestId, ChannelOwnershipTransferRequest, ), ChannelOwnershipTransferRequestWithdrawn(ContentActor, ChannelOwnershipTransferRequestId), ChannelOwnershipTransferred(ContentActor, ChannelOwnershipTransferRequestId), // Channel Categories ChannelCategoryCreated( ChannelCategoryId, ChannelCategory, ChannelCategoryCreationParameters, ), ChannelCategoryUpdated( ContentActor, ChannelCategoryId, ChannelCategoryUpdateParameters, ), ChannelCategoryDeleted(ContentActor, ChannelCategoryId), // Videos VideoCategoryCreated( ContentActor, VideoCategoryId, VideoCategoryCreationParameters, ), VideoCategoryUpdated(ContentActor, VideoCategoryId, VideoCategoryUpdateParameters), VideoCategoryDeleted(ContentActor, VideoCategoryId), VideoCreated(ContentActor, ChannelId, VideoId, VideoCreationParameters), VideoUpdated(ContentActor, VideoId, VideoUpdateParameters), VideoDeleted(ContentActor, VideoId), VideoCensorshipStatusUpdated( ContentActor, VideoId, IsCensored, Vec, /* rationale */ ), // Featured Videos FeaturedVideosSet(ContentActor, Vec), // Video Playlists PlaylistCreated(ContentActor, PlaylistId, PlaylistCreationParameters), PlaylistUpdated(ContentActor, PlaylistId, PlaylistUpdateParameters), PlaylistDeleted(ContentActor, PlaylistId), // Series SeriesCreated( ContentActor, SeriesId, StorageAssets, SeriesParameters, Series, ), SeriesUpdated( ContentActor, SeriesId, StorageAssets, SeriesParameters, Series, ), SeriesDeleted(ContentActor, SeriesId), // Persons PersonCreated( ContentActor, PersonId, StorageAssets, PersonCreationParameters, ), PersonUpdated( ContentActor, PersonId, StorageAssets, PersonUpdateParameters, ), PersonDeleted(ContentActor, PersonId), ChannelDeleted(ContentActor, ChannelId), } );