Skip to main content

Profile

//! # Profile Pallet
//! <!-- Original author of paragraph: @faiz
//!
//! ## Overview
//!
//! Pallet that allows users to create and update profiles, establish backup accounts, follow and unfollow other users,
//! and exchange messages within the system.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create a profile.
//! * Create a backup account for a profile.
//! * Update a profile.
//! * Follow a profile.
//! * Unfollow a profile.
//! * Send a message to another user.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! `create_user`: This function generates a new profile by utilizing a provided username and the inviter's account ID.
//!
//! `create_user_backup`: This function generates a new backup profile by utilizing a provided username and
//! the inviter's account ID.
//!
//! `update_user`: This function is responsible for updating user profile settings.
//!
//! `follow_user`: This function enables a user to follow another user's profile.
//!
//! `unfollow_user`: This function enables a user to unfollow another user's profile.
//!
//! `send_message`: This function allows a user to send a message to another user.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.

// Ensure we're `no_std` when compiling for Wasm.
#![cfg_attr(not(feature = "std"), no_std)]

// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;

mod types;
pub use types::*;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
use frame_support::BoundedVec;
use sp_runtime::traits::Hash;
use sp_std::{ vec, vec::Vec };

use traits::{
asset::{ AssetInterface, TokenType },
profile::{ ProfileInspect, ProfileInterface },
newsfeed::{ NewsfeedInterface, StatusType },
subaccounts::{ SubAccounts, AccountOrigin },
};

pub mod weights;
pub use weights::WeightInfo;

#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{ pallet_prelude::*, traits::{ fungibles::{ self, Inspect }, UnixTime } };
use frame_system::pallet_prelude::*;

// Balance type alias.
pub type BalanceOf<T> =
<<T as Config>::AssetsFungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;

/// Asset id type alias.
pub type AssetIdOf<T> =
<<T as Config>::AssetsFungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;

pub type Decimals = u8;

#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);

#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

// Provides actual time
type TimeProvider: UnixTime;

/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;

/// Type to access the Assets Pallet.
type AssetsFungibles: fungibles::Inspect<Self::AccountId> + //, AssetId = u32> // Hash this
fungibles::Mutate<Self::AccountId> +
fungibles::Create<Self::AccountId> +
fungibles::roles::Inspect<Self::AccountId> +
AssetInterface<AssetIdOf<Self>, BalanceOf<Self>, Self::AccountId, Decimals, TokenType>;

/// Newsfeed Interface type.
type Newsfeed: NewsfeedInterface<
Self::AccountId,
StatusType,
<Self::AssetsFungibles as Inspect<Self::AccountId>>::AssetId,
BalanceOf<Self>
>;

/// The maximum length of Strings
#[pallet::constant]
type StringLimit: Get<u32>;

// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}

/// Structs representing user profiles.
#[pallet::storage]
#[pallet::getter(fn user_item)]
pub type UserItem<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
Users<T::AccountId, T::StringLimit>,
OptionQuery
>;

/// Store the accountID associated with a username.
#[pallet::storage]
#[pallet::getter(fn get_user_address)]
pub type UserAddress<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::Hash,
T::AccountId,
OptionQuery
>;

/// All the followers of an accountID.
#[pallet::storage]
#[pallet::getter(fn get_followers)]
pub type Followers<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Blake2_128Concat,
T::AccountId,
u64,
OptionQuery
>;

/// All the followings of an accountID.
#[pallet::storage]
#[pallet::getter(fn get_followings)]
pub type Followings<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Blake2_128Concat,
T::AccountId,
u64,
OptionQuery
>;

// all messages of a account sent to another account
// this make a conversation being associated with two vectors of messages
#[pallet::storage]
#[pallet::getter(fn all_messages)]
pub type AllMessage<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId, // sender
Blake2_128Concat,
T::AccountId, // receiver
Vec<Message<T::AccountId, T::StringLimit>> // It doesn´t make sense to use the StringLimit here. TODO: change its name.
>;

// all connection of account
#[pallet::storage]
#[pallet::getter(fn all_connections)]
pub type AllConnection<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
Vec<Connections<T::AccountId, T::StringLimit>>
>;

/// All accountIDs invited by another accountID.
#[pallet::storage]
#[pallet::getter(fn team)]
pub type Team<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
Vec<T::AccountId>,
ValueQuery
>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Profile created.
ProfileCreated {
user: T::AccountId,
},
/// Profile updated.
ProfileUpdated {
user: T::AccountId,
},
/// Profile followed by another.
ProfileFollowed {
follower: T::AccountId,
following: T::AccountId,
},
/// Profile unfollowed by another.
ProfileUnfollowed {
follower: T::AccountId,
following: T::AccountId,
},
/// Profile sent a message.
ProfileSentMessage {
sender: T::AccountId,
receiver: T::AccountId,
},
}

#[pallet::error]
pub enum Error<T> {
/// The user does not exist
UserNotFound,
/// The accountID is already being used by another profile.
AccountAlreadyBeingUsed,
/// The account doesn't have permission.
NoPermission,
/// The username is not valid, use only letters.
UsernameNotValid,
/// The username used to create a profile is already being used by another profile.
UsernameAlreadyBeingUsed,
/// The username length is too big.
UsernameLengthTooLong,
/// The username length is too small.
UsernameLengthTooSmall,
/// The first name length is too small.
FistNameLengthTooSmall,
/// The last name length is too small.
LastNameLengthTooSmall,
/// The profile is already being followed.
ProfileAlreadyFollowed,
/// The profile is not followed.
ProfileNotFollowed,
/// The accountID can't follow it self.
CannotFollowSelf,
/// Invalid data
InvalidData,
/// Cannot chat with self
CannotChatWithSelf,
}

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
// Account, Username
pub profiles: Vec<(T::AccountId, BoundedVec<u8, T::StringLimit>)>,
}

impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self { profiles: Vec::new() }
}
}

#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
// users account created at genisis
for profile in &self.profiles {
let new_profile = Users {
connected_accounts: [profile.0.clone()].to_vec(),
user_address: profile.0.clone(),
first_name: BoundedVec::try_from("Unit".as_bytes().to_vec()).unwrap(),
last_name: BoundedVec::try_from("Network".as_bytes().to_vec()).unwrap(),
bio: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
created_at: 0,
updated_at: 0,
username: profile.1.clone(),
location: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
language_code: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
invited_by_user_id: profile.0.clone(),
hourly_rate: 0,
website: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
linkedin: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
twitter: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
instagram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
telegram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
youtube_url: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
facebook: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
vision: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
tag_line: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
city_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
industry_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
personal_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
project_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
profile_picture_colour_hex: BoundedVec::try_from(
"#35364b".as_bytes().to_vec()
).unwrap(),
profile_picture_initials: BoundedVec::try_from(
"UNIT".as_bytes().to_vec()
).unwrap(),
chat_public_key: None,
};

// add to user item
UserItem::<T>::insert(new_profile.user_address.clone(), new_profile.clone());

let username_hash = T::Hashing::hash(&new_profile.username);

UserAddress::<T>::insert(username_hash, new_profile.user_address.clone());

// add account to subaccounts
let _ = T::SubAccounts::add_sub_account(
new_profile.user_address.clone(),
new_profile.user_address,
AccountOrigin::PolkadotJS
);
}
}
}

#[pallet::call(weight(<T as Config>::WeightInfo))]
impl<T: Config> Pallet<T> {
/// The origin can create a new profile.
///
/// Parameters:
/// - `username`: The username of the profile.
/// - `inviter`: The inviter accountID of the profile.
///
/// Emits `ProfileCreated` event when successful.
#[pallet::call_index(0)]
#[pallet::weight((0, Pays::No))]
pub fn create_user(
origin: OriginFor<T>,
username: BoundedVec<u8, T::StringLimit>,
mut inviter: T::AccountId,
account_origin: AccountOrigin
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;

let mut good_username = true;

let mut new_username = username.clone();
// Due to username being a vector of u8, I'll use ASCII Table to see if
// it's not alphanumeric and doesn't have space.
for letter in &mut new_username {
if 65 <= *letter && *letter <= 90 {
*letter += 32;
} else if 97 <= *letter && *letter <= 122 {
continue;
} else if *letter >= 48 && *letter <= 57 {
continue;
} else {
good_username = false;
break;
}
}

// Ensure that the username length is greater than 0 characters.
ensure!(new_username.len() > 1, Error::<T>::UsernameLengthTooSmall);

// Ensure that the username length is less than or equal to 30 characters.
ensure!(new_username.len() <= 30, Error::<T>::UsernameLengthTooLong);

// Check if the username contains only letters.
ensure!(good_username, Error::<T>::UsernameNotValid);

// Check if the username is being used by another profile using it's hash to verify.
let username_hash = T::Hashing::hash(&new_username);

ensure!(
!UserAddress::<T>::contains_key(username_hash),
Error::<T>::UsernameAlreadyBeingUsed
);

// Check if this accountID is already being used by another profile.
ensure!(!UserItem::<T>::contains_key(&who), Error::<T>::AccountAlreadyBeingUsed);

// Check if the account is already a sub account of another profile
T::SubAccounts::already_sub_account(who.clone())?;

// Check if inviter accountId is being used by a profile.
inviter = T::SubAccounts::get_main_account(inviter)?;

Team::<T>::mutate(&inviter, |n| n.push(who.clone()));

let current_time = T::TimeProvider::now().as_secs();

// Create user struct.
let user = Users {
user_address: who.clone(),
connected_accounts: [who.clone()].to_vec(),
first_name: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
last_name: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
bio: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
created_at: current_time,
updated_at: current_time,
username: new_username,
location: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
language_code: BoundedVec::try_from("en".as_bytes().to_vec()).unwrap(),
invited_by_user_id: inviter,
hourly_rate: 0,
website: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
linkedin: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
twitter: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
instagram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
telegram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
youtube_url: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
facebook: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
vision: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
tag_line: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
city_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
industry_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
personal_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
project_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
profile_picture_colour_hex: BoundedVec::try_from(
"#007bff".as_bytes().to_vec()
).unwrap(),
profile_picture_initials: BoundedVec::try_from(username[0..2].to_vec()).unwrap(),
chat_public_key: None,
};

UserAddress::<T>::insert(username_hash, who.clone());

// Add userItem struct to the user item storage.
UserItem::<T>::insert(who.clone(), user.clone());

// Add address to sub accounts.
T::SubAccounts::add_sub_account(who.clone(), who.clone(), account_origin)?;

Self::deposit_event(Event::ProfileCreated { user: who });

Ok(Pays::No.into())
}

/// The origin can create a new profile while also establishing a backup account.
///
/// Parameters:
/// - `backup_account`: The accountID intended for use as a backup account.
/// - `first_name`: The first name of the profile.
/// - `last_name`: The last name of the profile.
/// - `username`: The username of the profile.
/// - `inviter`: The inviter accountID of the profile.
///
/// Emits `ProfileCreated` event when successful.
#[pallet::call_index(1)]
#[pallet::weight((0, Pays::No))]
pub fn create_user_backup(
origin: OriginFor<T>,
backup_account: T::AccountId,
username: BoundedVec<u8, T::StringLimit>,
mut inviter: T::AccountId,
chat_public_key: BoundedVec<u8, T::StringLimit>
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;

let mut good_username = true;

let mut new_username = username.clone();
// Due to username being a vector of u8, I'll use ASCII Table to see if
// it's not alphanumeric and doesn't have space.
for letter in &mut new_username {
if 65 <= *letter && *letter <= 90 {
*letter += 32;
} else if 97 <= *letter && *letter <= 122 {
continue;
} else if *letter >= 48 && *letter <= 57 {
continue;
} else {
good_username = false;
break;
}
}

// Ensure that the username length is greater than 0 characters.
ensure!(new_username.len() > 0, Error::<T>::UsernameLengthTooSmall);

// Ensure that the username length is less than or equal to 30 characters.
ensure!(new_username.len() <= 30, Error::<T>::UsernameLengthTooLong);

// Check if the username contains only letters.
ensure!(good_username, Error::<T>::UsernameNotValid);

// Check if the username is being used by another profile using it's hash to verify.
let username_hash = T::Hashing::hash(&new_username);

ensure!(
!UserAddress::<T>::contains_key(username_hash),
Error::<T>::UsernameAlreadyBeingUsed
);

// Check if this accountID is already being used by another profile.
ensure!(!UserItem::<T>::contains_key(&who), Error::<T>::AccountAlreadyBeingUsed);

// Check if the account is already a sub account of another profile.
T::SubAccounts::already_sub_account(who.clone())?;

// Check if the backup account is already a sub account of another profile
T::SubAccounts::already_sub_account(backup_account.clone())?;

// Check if inviter accountId is being used by a profile.
inviter = T::SubAccounts::get_main_account(inviter)?;

Team::<T>::mutate(&inviter, |n| n.push(who.clone()));

let current_time = T::TimeProvider::now().as_secs();

let profile_picture_initials = vec![new_username[0], new_username[1]];

// Create user struct.
let user = Users {
user_address: who.clone(),
connected_accounts: [who.clone(), backup_account.clone()].to_vec(),
first_name: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
last_name: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
bio: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
created_at: current_time,
updated_at: current_time,
username: new_username,
location: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
language_code: BoundedVec::try_from("en".as_bytes().to_vec()).unwrap(),
invited_by_user_id: inviter,
hourly_rate: 0,
website: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
linkedin: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
twitter: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
instagram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
telegram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
youtube_url: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
facebook: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
vision: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
tag_line: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
city_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
industry_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
personal_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
project_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
profile_picture_colour_hex: BoundedVec::try_from(
"#007bff".as_bytes().to_vec()
).unwrap(),
profile_picture_initials: BoundedVec::try_from(profile_picture_initials).unwrap(),
chat_public_key: Some(chat_public_key),
};

UserAddress::<T>::insert(username_hash, who.clone());

// Add userItem struct to the user item storage.
UserItem::<T>::insert(who.clone(), user.clone());

// Add address to sub accounts.
T::SubAccounts::add_sub_account(who.clone(), who.clone(), AccountOrigin::Credentials)?;
T::SubAccounts::add_sub_account(
who.clone(),
backup_account,
AccountOrigin::BackupPhrase
)?;

Self::deposit_event(Event::ProfileCreated { user: who });

Ok(Pays::No.into())
}

/// The origin can update a profile if it is the owner of that profile.
///
/// Parameters:
/// `first_name`: The updated first name for the profile.
/// `last_name`: The updated last name of the profile.
/// `bio`: The updated bio of the profile.
/// `username`: The updated username of the profile.
/// `website`: The updated website of the profile.
/// `linkedin`: The updated linkedin of the profile.
/// `twitter`: The updated twitter of the profile.
/// `instagram`: The updated instagram of the profile.
/// `telegram`: The updated telegram of the profile.
/// `youtube_url`: The updated youtube url of the profile.
/// `facebook`: The updated facebook of the profile.
/// `vision`: The updated vision of the profile.
/// `tag_line`: The updated tag line of the profile.
/// `location`: The updated location of the profile.
/// `city_token_symbol`: The updated city token symbol of the profile.
/// `industry_token_symbol`: The updated industry token symbol of the profile.
/// `personal_token_symbol`: The updated personal token symbol of the profile.
/// `project_token_symbol`: The updated project token symbol of the profile.
/// `profile_picture_colour_hex`: The updated picture colour in hexadecimal of the profile.
/// `profile_picture_initials`: The updated pricture initials of the profile.
/// `hourly_rate`: The updated hourly rate of the profile.
///
/// Emits `ProfileUpdated` event when successful.
#[pallet::call_index(2)]
pub fn update_user(
origin: OriginFor<T>,
first_name: BoundedVec<u8, T::StringLimit>,
last_name: BoundedVec<u8, T::StringLimit>,
bio: BoundedVec<u8, T::StringLimit>,
username: BoundedVec<u8, T::StringLimit>,
website: BoundedVec<u8, T::StringLimit>,
linkedin: BoundedVec<u8, T::StringLimit>,
twitter: BoundedVec<u8, T::StringLimit>,
instagram: BoundedVec<u8, T::StringLimit>,
telegram: BoundedVec<u8, T::StringLimit>,
youtube_url: BoundedVec<u8, T::StringLimit>,
facebook: BoundedVec<u8, T::StringLimit>,
vision: BoundedVec<u8, T::StringLimit>,
tag_line: BoundedVec<u8, T::StringLimit>,
location: BoundedVec<u8, T::StringLimit>,
city_token_symbol: BoundedVec<u8, T::StringLimit>,
industry_token_symbol: BoundedVec<u8, T::StringLimit>,
personal_token_symbol: BoundedVec<u8, T::StringLimit>,
project_token_symbol: BoundedVec<u8, T::StringLimit>,
profile_picture_colour_hex: BoundedVec<u8, T::StringLimit>,
profile_picture_initials: BoundedVec<u8, T::StringLimit>,
hourly_rate: u32
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;

// Check if this accountID is being used by a profile.
let user_details = UserItem::<T>::get(who.clone()).ok_or(Error::<T>::UserNotFound)?;

// Check if the origin has permission to update a profile.
ensure!(user_details.user_address == who.clone(), Error::<T>::NoPermission);

let mut good_username = true;

let mut new_username = username.clone();
// Due to username being a vector of u8, I'll use ASCII Table to see if
// it's not alphanumeric and doesn't have space.
for letter in &mut new_username {
if 65 <= *letter && *letter <= 90 {
*letter += 32;
} else if 97 <= *letter && *letter <= 122 {
continue;
} else if *letter >= 48 && *letter <= 57 {
continue;
} else {
good_username = false;
break;
}
}

// Ensure that the username length is greater than 0 characters.
ensure!(new_username.len() > 0, Error::<T>::UsernameLengthTooSmall);

// Ensure that the username length is less than or equal to 30 characters.
ensure!(new_username.len() <= 30, Error::<T>::UsernameLengthTooLong);

// Check if the username contains only letters.
ensure!(good_username, Error::<T>::UsernameNotValid);

// Ensure that the first name length is less than or equal to 1 characters.
ensure!(first_name.len() > 1, Error::<T>::FistNameLengthTooSmall);

// Ensure that the last name length is less than or equal to 1 characters.
ensure!(last_name.len() > 1, Error::<T>::LastNameLengthTooSmall);

// Check if the username is being used by another profile using it's hash to verify.
let username_hash = T::Hashing::hash(&new_username);

if let Some(username_owner) = UserAddress::<T>::get(username_hash) {
ensure!(username_owner == who, Error::<T>::UsernameAlreadyBeingUsed);
}

let current_time = T::TimeProvider::now().as_secs();

let mut old_username = Default::default();

UserItem::<T>::try_mutate(&who, |maybe_user| -> DispatchResult {
if let Some(user) = maybe_user {
// Get the old username
old_username = user.username.clone();

*maybe_user = Some(Users {
user_address: user.user_address.clone(),
connected_accounts: user.connected_accounts.clone(),
first_name,
last_name,
bio,
created_at: user.created_at,
updated_at: current_time,
username: new_username,
location,
language_code: user.language_code.clone(),
invited_by_user_id: user.invited_by_user_id.clone(),
hourly_rate,
website,
linkedin,
twitter,
instagram,
telegram,
youtube_url,
facebook,
vision,
tag_line,
city_token_symbol,
industry_token_symbol,
personal_token_symbol,
project_token_symbol,
profile_picture_colour_hex,
profile_picture_initials,
chat_public_key: user.chat_public_key.clone(),
});

Ok(())
} else {
Err(Error::<T>::UserNotFound.into())
}
})?;

// Hash the old username to delete if the new one is different of it.
let old_username_hash = T::Hashing::hash(&old_username);

if old_username_hash != username_hash {
UserAddress::<T>::remove(old_username_hash);
UserAddress::<T>::insert(username_hash, who.clone());
}

// ProfileUpdated
Self::deposit_event(Event::ProfileUpdated { user: who });

Ok(())
}

/// The origin can follow another profile with the accountID 'user'.
///
/// Parameters:
/// - `user`: The accountID of the profile to be followed.
///
/// Emits `ProfileFollowed` event when successful.
#[pallet::call_index(3)]
pub fn follow_user(origin: OriginFor<T>, user: T::AccountId) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;

ensure!(who != user, Error::<T>::CannotFollowSelf);
ensure!(
!Followings::<T>::contains_key(&who, &user),
Error::<T>::ProfileAlreadyFollowed
);

let current_time = T::TimeProvider::now().as_secs();

Followings::<T>::insert(&who, &user, current_time);
Followers::<T>::insert(&user, &who, current_time);

Self::deposit_event(Event::ProfileFollowed {
follower: who,
following: user,
});

Ok(())
}

/// The origin can unfollow another profile with the accountID 'user'.
///
/// Parameters:
/// - `user`: The accountID of the profile to be unfollowed.
///
/// Emits `ProfileUnfollowed` event when successful.
#[pallet::call_index(4)]
pub fn unfollow_user(origin: OriginFor<T>, user: T::AccountId) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;

ensure!(Followings::<T>::contains_key(&who, &user), Error::<T>::ProfileNotFollowed);

Followings::<T>::remove(&who, &user);
Followers::<T>::remove(&user, &who);

Self::deposit_event(Event::ProfileUnfollowed {
follower: who,
following: user,
});

Ok(())
}

/// The origin can send a message to another profile with the accountID 'receiver'.
///
/// Parameters:
/// `receiver`: The accountID of the receiver profile,
/// `encrypted_message_from`: The encrypted message using the origin chat public key,
/// `encrypted_message_to`: The encrypted message using the receiver chat public key,
/// `nonce_from`: ,
/// `nonce_to`: ,
/// Emits `ProfileSentMessage` event when successful.
#[pallet::call_index(5)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn send_message(
origin: OriginFor<T>,
mut receiver: T::AccountId,
encrypted_message_from: BoundedVec<u8, T::StringLimit>,
encrypted_message_to: BoundedVec<u8, T::StringLimit>,
nonce_from: Option<BoundedVec<u8, T::StringLimit>>,
nonce_to: Option<BoundedVec<u8, T::StringLimit>>
) -> DispatchResult {
let who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
let sender = T::SubAccounts::get_main_account(who)?;

receiver = T::SubAccounts::get_main_account(receiver)?;

// check both accounts have profile
UserItem::<T>::get(sender.clone()).ok_or(Error::<T>::UserNotFound)?;
UserItem::<T>::get(receiver.clone()).ok_or(Error::<T>::UserNotFound)?;

// can't send message to self
ensure!(sender != receiver, Error::<T>::CannotChatWithSelf);

// ensure encrypted message length is greater than 0
ensure!(encrypted_message_from.len() > 0, Error::<T>::InvalidData);
ensure!(encrypted_message_to.len() > 0, Error::<T>::InvalidData);

// ensure nonce length is greater than 0 only if it is not null
if nonce_from.is_some() {
ensure!(nonce_from.clone().unwrap().len() > 0, Error::<T>::InvalidData);
}
if nonce_to.is_some() {
ensure!(nonce_to.clone().unwrap().len() > 0, Error::<T>::InvalidData);
}

let current_time = T::TimeProvider::now().as_secs();

let _message = Message {
message_from: sender.clone(), // change this to sender
message_to: receiver.clone(),
encrypted_message_from: encrypted_message_from.clone(),
encrypted_message_to: encrypted_message_to.clone(),
nonce_from: nonce_from.clone(),
nonce_to: nonce_to.clone(),
created_at: current_time,
};

// making sure storage value is not null
if AllMessage::<T>::get(sender.clone(), receiver.clone()).is_none() {
let empty_vec: Vec<Message<T::AccountId, T::StringLimit>> = Vec::new();
AllMessage::<T>::insert(sender.clone(), receiver.clone(), empty_vec);
}

let mut all_message = AllMessage::<T>
::get(sender.clone(), receiver.clone())
.ok_or(Error::<T>::UserNotFound)?;
all_message.push(_message);
AllMessage::<T>::insert(sender.clone(), receiver.clone(), all_message);

// making sure storage value is not null
if AllConnection::<T>::get(receiver.clone()).is_none() {
let empty_vec: Vec<Connections<T::AccountId, T::StringLimit>> = Vec::new();
AllConnection::<T>::insert(receiver.clone(), empty_vec);
}

let mut _all_connection = AllConnection::<T>
::get(receiver.clone())
.ok_or(Error::<T>::UserNotFound)?;
let connection = _all_connection
.iter()
.find(|x| {
x.connection_from_user_address == sender &&
x.connection_to_user_address == receiver
});

if connection.is_none() {
let _connection1 = Connections {
connection_from_user_address: sender.clone(),
connection_to_user_address: receiver.clone(),
last_message: encrypted_message_to.clone(),
message_nonce: nonce_to.clone(),
last_seen_at: current_time,
created_at: current_time,
updated_at: current_time,
last_message_time: current_time,
};
let _connection2 = Connections {
connection_from_user_address: receiver.clone(),
connection_to_user_address: sender.clone(),
last_message: encrypted_message_from.clone(),
message_nonce: nonce_from.clone(),
last_seen_at: current_time,
created_at: current_time,
updated_at: current_time,
last_message_time: current_time,
};
_all_connection.push(_connection1);
_all_connection.push(_connection2);
AllConnection::<T>::insert(receiver.clone(), _all_connection);
} else {
let mut connection1 = _all_connection
.clone()
.into_iter()
.find(|x| {
x.connection_from_user_address == sender.clone() &&
x.connection_to_user_address == receiver
})
.ok_or(Error::<T>::UserNotFound)?;
let mut connection2 = _all_connection
.clone()
.into_iter()
.find(|x| {
x.connection_from_user_address == receiver &&
x.connection_to_user_address == sender.clone()
})
.ok_or(Error::<T>::UserNotFound)?;
_all_connection.retain(|x| *x != connection1);
_all_connection.retain(|x| *x != connection2);
connection1.last_seen_at = current_time;
connection1.updated_at = current_time;
connection1.last_message_time = current_time;
connection1.last_message = encrypted_message_from.clone();
connection1.message_nonce = nonce_from.clone();

connection2.last_seen_at = current_time;
connection2.updated_at = current_time;
connection2.last_message_time = current_time;
connection2.last_message = encrypted_message_to.clone();
connection2.message_nonce = nonce_to.clone();
_all_connection.push(connection1);
_all_connection.push(connection2);
AllConnection::<T>::insert(receiver.clone(), _all_connection);
}

Self::deposit_event(Event::ProfileSentMessage {
sender,
receiver,
});

Ok(())
}
}

impl<T: Config> ProfileInspect<T::AccountId, T::StringLimit> for Pallet<T> {
fn get_inviter(who: T::AccountId) -> Result<T::AccountId, DispatchError> {
let user = UserItem::<T>::get(who).ok_or(Error::<T>::UserNotFound)?;
Ok(user.invited_by_user_id)
}

fn has_profile(who: T::AccountId) -> bool {
UserItem::<T>::contains_key(who)
}

fn get_profile_address(who: T::AccountId) -> Result<T::AccountId, DispatchError> {
let user = UserItem::<T>::get(who).ok_or(Error::<T>::UserNotFound)?;
Ok(user.user_address)
}

fn get_address(
username: BoundedVec<u8, T::StringLimit>
) -> Result<T::AccountId, DispatchError> {
let username_hash = T::Hashing::hash(&username);
let address = UserAddress::<T>::get(username_hash).ok_or(Error::<T>::UserNotFound)?;
Ok(address)
}

fn add_sub_account(
main: T::AccountId,
sub_account: T::AccountId
) -> Result<(), DispatchError> {
UserItem::<T>::try_mutate(
&main,
|maybe_user| -> Result<(), DispatchError> {
let user = maybe_user.as_mut().ok_or(Error::<T>::UserNotFound)?;
user.connected_accounts.push(sub_account);
Ok(())
}
)?;

Ok(())
}

fn remove_sub_account(
main: T::AccountId,
sub_account: T::AccountId
) -> Result<(), DispatchError> {
UserItem::<T>::try_mutate(
&main,
|maybe_user| -> Result<(), DispatchError> {
let user = maybe_user.as_mut().ok_or(Error::<T>::UserNotFound)?;
user.connected_accounts.retain(|x| *x != sub_account);
Ok(())
}
)?;

Ok(())
}

fn get_sub_accounts(main: T::AccountId) -> Result<Vec<T::AccountId>, DispatchError> {
let profile = UserItem::<T>::get(main).ok_or(Error::<T>::UserNotFound)?;
Ok(profile.connected_accounts)
}
}
}

impl<T: Config> ProfileInterface<T::AccountId, T::StringLimit> for Pallet<T> {
type StringLimit = T::StringLimit;

fn create_user(
who: T::AccountId,
first_name: BoundedVec<u8, T::StringLimit>,
last_name: BoundedVec<u8, T::StringLimit>,
bio: BoundedVec<u8, T::StringLimit>,
username: BoundedVec<u8, T::StringLimit>,
website: BoundedVec<u8, T::StringLimit>,
linkedin: BoundedVec<u8, T::StringLimit>,
twitter: BoundedVec<u8, T::StringLimit>,
instagram: BoundedVec<u8, T::StringLimit>,
telegram: BoundedVec<u8, T::StringLimit>,
youtube_url: BoundedVec<u8, T::StringLimit>,
facebook: BoundedVec<u8, T::StringLimit>,
vision: BoundedVec<u8, T::StringLimit>,
tag_line: BoundedVec<u8, T::StringLimit>,
inviter: T::AccountId,
city_token_symbol: BoundedVec<u8, T::StringLimit>,
industry_token_symbol: BoundedVec<u8, T::StringLimit>,
personal_token_symbol: BoundedVec<u8, T::StringLimit>,
project_token_symbol: BoundedVec<u8, T::StringLimit>,
profile_picture_colour_hex: BoundedVec<u8, T::StringLimit>,
profile_picture_initials: BoundedVec<u8, T::StringLimit>
) {
// creating user object
let user = Users {
user_address: who.clone(),
connected_accounts: [who.clone()].to_vec(),
first_name,
last_name,
bio,
created_at: 0,
updated_at: 0,
username: username.clone(),
location: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
language_code: BoundedVec::try_from("en".as_bytes().to_vec()).unwrap(),
invited_by_user_id: inviter,
hourly_rate: 0,
website,
linkedin,
twitter,
instagram,
telegram,
youtube_url,
facebook,
vision,
tag_line,
city_token_symbol,
industry_token_symbol,
personal_token_symbol,
project_token_symbol,
profile_picture_colour_hex,
profile_picture_initials,
chat_public_key: None,
};

let username_hash = T::Hashing::hash(&username);

UserAddress::<T>::insert(username_hash, who.clone());

UserItem::<T>::insert(&who, user);

let _ = T::SubAccounts::add_sub_account(
who.clone(),
who.clone(),
AccountOrigin::PolkadotJS
);

Self::deposit_event(Event::ProfileCreated { user: who });
}

fn create_user_account(who: T::AccountId) {
let username: BoundedVec<u8, T::StringLimit> = BoundedVec::try_from(
"unit".as_bytes().to_vec()
).unwrap();

let profile = Users {
connected_accounts: [who.clone()].to_vec(),
user_address: who.clone(),
first_name: BoundedVec::try_from("Unit".as_bytes().to_vec()).unwrap(),
last_name: BoundedVec::try_from("Network".as_bytes().to_vec()).unwrap(),
bio: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
created_at: 0,
updated_at: 0,
username,
location: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
language_code: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
invited_by_user_id: who.clone(),
hourly_rate: 0,
website: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
linkedin: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
twitter: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
instagram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
telegram: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
youtube_url: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
facebook: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
vision: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
tag_line: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
city_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
industry_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
personal_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
project_token_symbol: BoundedVec::try_from("".as_bytes().to_vec()).unwrap(),
profile_picture_colour_hex: BoundedVec::try_from(
"#35364b".as_bytes().to_vec()
).unwrap(),
profile_picture_initials: BoundedVec::try_from("UNIT".as_bytes().to_vec()).unwrap(),
chat_public_key: None,
};

// add to user item
UserItem::<T>::insert(profile.user_address.clone(), profile.clone());

let username_hash = T::Hashing::hash(&profile.username);

UserAddress::<T>::insert(username_hash, profile.user_address.clone());

// add account to subaccounts
let _ = T::SubAccounts::add_sub_account(
profile.user_address.clone(),
profile.user_address,
AccountOrigin::PolkadotJS
);
}
}