> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sageprotocol.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Actions

> Handles creation, management, and updates to user-owned and shared identities, invites, and profile interactions.

```rust theme={null}
use std::string;
use sui::clock;
use sui::coin;
use sui::event;
use sui::sui;
use sage_admin::admin;
use sage_admin::admin_access;
use sage_admin::apps;
use sage_admin::fees;
use sage_analytics::analytics_actions;
use sage_post::post;
use sage_post::post_actions;
use sage_post::post_fees;
use sage_reward::reward_actions;
use sage_reward::reward_registry;
use sage_shared::membership;
use sage_trust::trust;
use sage_trust::trust_access;
use sage_user::user_fees;
use sage_user::user_invite;
use sage_user::user_owned;
use sage_user::user_registry;
use sage_user::user_shared;
use sage_user::user_witness;
use sage_utils::string_helpers;
```

<Tip>
  Explore this module further in the Mover Registry: @sage/user
</Tip>

## Events

### `ChannelFavoritesUpdate`

Emitted when a `User` favorites or unfavorites a `Channel`.

```rust theme={null}
public struct ChannelFavoritesUpdate has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    app_id: address,
    favorited_channel_id: address,
    message: u8,
    updated_at: u64,
    user_id: address
  ```
</Expandable>

### `InviteCreated`

Emitted when a new `Invite` is created.

```rust theme={null}
public struct InviteCreated has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    invite_code: std::string::String,
    invite_key: std::string::String,
    user_id: address
  ```
</Expandable>

### `PostFavoritesUpdate`

Emitted when a `User` favorites or unfavorites a `Post`.

```rust theme={null}
public struct PostFavoritesUpdate has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    app_id: address,
    favorited_post_id: address,
    message: u8,
    updated_at: u64,
    user_id: address
  ```
</Expandable>

### `UserCreated`

Emitted when a new `User` is created.

```rust theme={null}
public struct UserCreated has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    avatar: std::string::String,
    banner: std::string::String,
    created_at: u64,
    description: std::string::String,
    invited_by: Option<address>,
    user_id: address,
    user_owned_id: address,
    user_shared_id: address,
    user_key: std::string::String,
    user_name: std::string::String
  ```
</Expandable>

### `UserFavoritesUpdate`

Emitted when a `User` favorites or unfavorites a `User`.

```rust theme={null}
public struct UserFavoritesUpdate has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    app_id: address,
    favorited_user_id: address,
    message: u8,
    updated_at: u64,
    user_id: address
  ```
</Expandable>

### `UserFollowsUpdate`

Emitted when a `User` follows or unfollows another `User`.

```rust theme={null}
public struct UserFollowsUpdate has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    account_type: u8,
    app_id: address,
    followed_user_id: address,
    message: u8,
    updated_at: u64,
    user_id: address
  ```
</Expandable>

### `UserFriendUpdate`

Emitted when a `User` confirms friendship or unfriends another `User`.

```rust theme={null}
public struct UserFriendUpdate has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    account_type: u8,
    app_id: address,
    friended_user_id: address,
    message: u8,
    updated_at: u64,
    user_id: address
  ```
</Expandable>

### `UserFriendRequestUpdate`

Emitted when a `User` requests friendship with another `User`.

```rust theme={null}
public struct UserFriendRequestUpdate has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    account_type: u8,
    app_id: address,
    friended_user_id: address,
    message: u8,
    updated_at: u64,
    user_id: address
  ```
</Expandable>

### `UserPostCreated`

Emitted when a new `Post` is created for a `User`.

```rust theme={null}
public struct UserPostCreated has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    app_id: address,
    created_at: u64,
    created_by: address,
    data: std::string::String,
    description: std::string::String,
    post_id: address,
    title: std::string::String,
    user_id: address
  ```
</Expandable>

### `UserUpdated`

Emitted when a `User` is updated.

```rust theme={null}
public struct UserUpdated has copy, drop
```

<Expandable title="Fields">
  ```rust theme={null}
    avatar: std::string::String,
    banner: std::string::String,
    description: std::string::String,
    updated_at: u64,
    user_id: address,
    user_name: std::string::String
  ```
</Expandable>

## Constants

Maximum number of characters for `User` description.

```rust theme={null}
const DESCRIPTION_MAX_LENGTH: u64 = 370;
```

Minimum number of characters for `User` name.

```rust theme={null}
const USERNAME_MIN_LENGTH: u64 = 3;
```

Maximum number of characters for `User` name.

```rust theme={null}
const USERNAME_MAX_LENGTH: u64 = 20;
```

`User` actions that are tracked by `Analytics` that result in \$TRUST rewards.

```rust theme={null}
const METRIC_COMMENT_GIVEN: vector<u8> = b"comment-given";
const METRIC_COMMENT_RECEIVED: vector<u8> = b"comment-received";
const METRIC_FAVORITED_POST: vector<u8> = b"favorited-post";
const METRIC_FOLLOWED_USER: vector<u8> = b"followed-user";
const METRIC_LIKED_POST: vector<u8> = b"liked-post";
const METRIC_POST_FAVORITED: vector<u8> = b"post-favorited";
const METRIC_POST_LIKED: vector<u8> = b"post-liked";
const METRIC_USER_FOLLOWED: vector<u8> = b"user-followed";
const METRIC_USER_FRIENDS: vector<u8> = b"user-friends";
const METRIC_USER_TEXT_POST: vector<u8> = b"user-text-posts";
```

Error code for invalid `User` description.

```rust theme={null}
const EInvalidUserDescription: u64 = 370;
```

Error code for invalid `User` name.

```rust theme={null}
const EInvalidUsername: u64 = 371;
```

Error code for attempt to create a `User` without a (required) `Invite`.

```rust theme={null}
const EInviteRequired: u64 = 372;
```

Error code for attempting to friend or follow the `User`'s own `User`.

```rust theme={null}
const ENoSelfJoin: u64 = 373;
```

Error code for attempting to update metadata for another's `User`.

```rust theme={null}
const ENotSelf: u64 = 374;
```

Error code for supplying a `User` who is not the author of a `Post`.

```rust theme={null}
const ESuppliedAuthorMismatch: u64 = 375;
```

Error code for attempting to change the name for the `User`. Only the casing may be changed (e.g. "user" to "USER")

```rust theme={null}
const EUserNameMismatch: u64 = 376;
```

## Functions

### `add_favorite_channel`

Allows a `User` to add a favorite `Channel` within an `App`. Aborts with `ETypeMismatch` if `ChannelType` is not a `Channel`.

```rust theme={null}
public fun add_favorite_channel<ChannelType: key> (app: &App, clock: &Clock, channel: &ChannelType, channel_config: &ChannelConfig, owned_user: &mut UserOwned, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun add_favorite_channel<ChannelType: key> (
        app: &App,
        clock: &Clock,
        channel: &ChannelType,
        channel_config: &ChannelConfig,
        owned_user: &mut UserOwned,
        ctx: &mut TxContext
    ) {
        admin_access::assert_channel(
            channel_config,
            channel
        );

        let app_address = object::id_address(app);
        let timestamp = clock.timestamp_ms();

        let (
            message,
            self,
            favorited_channel_id,
            _count
        ) = user_owned::add_favorite_channel(
            channel,
            owned_user,
            app_address,
            timestamp,
            ctx
        );

        event::emit(ChannelFavoritesUpdate {
            app_id: app_address,
            favorited_channel_id,
            message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `add_favorite_post`

Allows a `User` to add a favorite `Post` within an `App`. Aborts with `ESuppliedAuthorMismatch` if the supplied `SharedUser` is not the author of the `Post`.

```rust theme={null}
public fun add_favorite_post(app: &App, clock: &Clock, owned_user: &mut UserOwned, post: &Post, reward_weights_registry: &RewardWeightsRegistry, user: &mut UserShared, user_witness_config: &UserWitnessConfig, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun add_favorite_post(
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        post: &Post,
        reward_weights_registry: &RewardWeightsRegistry,
        user: &mut UserShared,
        user_witness_config: &UserWitnessConfig,
        ctx: &mut TxContext
    ) {
        let favorite_post_author = post.get_author();
        let supplied_author_address = user.get_owner();

        assert!(favorite_post_author == supplied_author_address, ESuppliedAuthorMismatch);

        let app_address = object::id_address(app);
        let timestamp = clock.timestamp_ms();

        let (
            message,
            self,
            favorited_post_id,
            count
        ) = user_owned::add_favorite_post(
            post,
            owned_user,
            app_address,
            timestamp,
            ctx
        );

        let has_rewards_enabled = app.has_rewards_enabled();

        if (
            has_rewards_enabled &&
            favorite_post_author != self &&
            count == 1
        ) {
            let current_epoch = reward_registry::get_current(
                reward_weights_registry
            );

            let analytics_self = user_owned::borrow_or_create_analytics_mut(
                owned_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            let reward_weights = reward_weights_registry.borrow_current();

            let metric_author = utf8(METRIC_POST_FAVORITED);
            let metric_self = utf8(METRIC_FAVORITED_POST);

            let claim_author = reward_weights.get_weight(metric_author);
            let claim_self = reward_weights.get_weight(metric_self);
            
            let user_witness = user_witness::create_witness();

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_self,
                app,
                &user_witness,
                user_witness_config,
                claim_self,
                metric_self
            );

            let analytics_author = user_shared::borrow_or_create_analytics_mut(
                user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_author,
                app,
                &user_witness,
                user_witness_config,
                claim_author,
                metric_author
            );
        };

        event::emit(PostFavoritesUpdate {
            app_id: app_address,
            favorited_post_id,
            message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `add_favorite_user`

Allows a `User` to add a favorite `User` within an `App`.

```rust theme={null}
public fun add_favorite_user(app: &App, clock: &Clock, owned_user: &mut UserOwned, user: &UserShared, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun add_favorite_user(
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        user: &UserShared,
        ctx: &mut TxContext
    ) {
        let app_address = object::id_address(app);
        let timestamp = clock.timestamp_ms();

        let (
            message,
            self,
            favorited_user_id,
            _count
        ) = user_owned::add_favorite_user(
            owned_user,
            user,
            app_address,
            timestamp,
            ctx
        );

        event::emit(UserFavoritesUpdate {
            app_id: app_address,
            favorited_user_id,
            message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `assert_user_description`

Aborts with `EInvalidUserDescription` if the description is invalid.

```rust theme={null}
public fun assert_user_description(description: &String)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun assert_user_description(
        description: &String
    ) {
        let is_valid_description = is_valid_description(description);

        assert!(is_valid_description, EInvalidUserDescription);
    }
  ```
</Expandable>

### `assert_user_name`

Aborts with `EInvalidUsername` if the name is invalid.

```rust theme={null}
public fun assert_user_name(name: &String)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun assert_user_name(
        name: &String
    ) {
        let is_valid_name = string_helpers::is_valid_name(
            name,
            USERNAME_MIN_LENGTH,
            USERNAME_MAX_LENGTH
        );

        assert!(is_valid_name, EInvalidUsername);
    }
  ```
</Expandable>

### `claim_reward`

If `User` has an available claim on the `App` and for the specified epoch, then mints \$TRUST and transfers it to the `User`'s wallet. Aborts with `ENotSelf` if the `SharedUser` does not belong to the wallet address.

```rust theme={null}
public fun claim_reward(app: &App, mint_config: &MintConfig, reward_witness_config: &RewardWitnessConfig, treasury: &mut ProtectedTreasury, owned_user: &mut UserOwned, shared_user: &mut UserShared, user_witness_config: &UserWitnessConfig, epoch: u64, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun claim_reward(
        app: &App,
        mint_config: &MintConfig,
        reward_witness_config: &RewardWitnessConfig,
        treasury: &mut ProtectedTreasury,
        owned_user: &mut UserOwned,
        shared_user: &mut UserShared,
        user_witness_config: &UserWitnessConfig,
        epoch: u64,
        ctx: &mut TxContext
    ) {
        let self = tx_context::sender(ctx);
        let owner = shared_user.get_owner();

        assert!(owner == self, ENotSelf);

        let app_address = object::id_address(app);
        let user_witness = user_witness::create_witness();

        let mut total_coin_option: Option<Coin<TRUST>> = option::none();

        let has_owned_analytics = owned_user.has_analytics(
            app_address,
            epoch
        );

        if (has_owned_analytics) {
            let analytics = owned_user.borrow_analytics_mut(
                app_address,
                epoch
            );
            let (
                _amount,
                coin_option
            ) = reward_actions::claim_value_for_user<UserWitness>(
                analytics,
                app,
                mint_config,
                reward_witness_config,
                treasury,
                &user_witness,
                user_witness_config,
                ctx
            );

            if (coin_option.is_some()) {
                total_coin_option.destroy_none();
                total_coin_option = coin_option;
            } else {
                coin_option.destroy_none();
            };
        };

        let has_shared_analytics = shared_user.has_analytics(
            app_address,
            epoch
        );

        if (has_shared_analytics) {
            let analytics = shared_user.borrow_analytics_mut(
                app_address,
                epoch
            );
            let (
                _amount,
                coin_option
            ) = reward_actions::claim_value_for_user<UserWitness>(
                analytics,
                app,
                mint_config,
                reward_witness_config,
                treasury,
                &user_witness,
                user_witness_config,
                ctx
            );

            if (coin_option.is_some()) {
                let coin = coin_option.destroy_some();

                if (total_coin_option.is_some()) {
                    let mut total_coin = total_coin_option.destroy_some();
                    total_coin.join(coin);

                    total_coin_option = option::some(total_coin);
                } else {
                    total_coin_option.destroy_none();
                    total_coin_option = option::some(coin);
                };
            } else {
                coin_option.destroy_none();
            };
        };

        if (total_coin_option.is_some()) {
            let total_coin = total_coin_option.destroy_some();

            let balance = total_coin.balance();
            let amount = balance.value();

            owned_user.add_to_total_rewards(amount);

            transfer::public_transfer(total_coin, self);
        } else {
            total_coin_option.destroy_none();
        };
    }
  ```
</Expandable>

### `comment`

Create a comment on a `Post`. Aborts with `ESuppliedAuthorMismatch` if the supplied `SharedUser` is not the author of the parent `Post`.

```rust theme={null}
public fun comment<CoinType> (app: &App, clock: &Clock, owned_user: &mut UserOwned, parent_post: &mut Post, post_fees: &PostFees, reward_weights_registry: &RewardWeightsRegistry, shared_user: &mut UserShared, user_witness_config: &UserWitnessConfig, data: String, description: String, title: String, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext): (address, address, u64)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun comment<CoinType> (
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        parent_post: &mut Post,
        post_fees: &PostFees,
        reward_weights_registry: &RewardWeightsRegistry,
        shared_user: &mut UserShared,
        user_witness_config: &UserWitnessConfig,
        data: String,
        description: String,
        title: String,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ): (
        address,
        address,
        u64
    ) {
        let has_rewards_enabled = app.has_rewards_enabled();

        let parent_author = parent_post.get_author();
        let self = tx_context::sender(ctx);

        let user_witness = user_witness::create_witness();

        if (
            has_rewards_enabled &&
            parent_author != self
        ) {
            let supplied_author = shared_user.get_owner();

            assert!(parent_author == supplied_author, ESuppliedAuthorMismatch);

            let app_address = object::id_address(app);
            let current_epoch = reward_registry::get_current(
                reward_weights_registry
            );

            let reward_weights = reward_weights_registry.borrow_current();

            let metric_parent = utf8(METRIC_COMMENT_RECEIVED);
            let metric_self = utf8(METRIC_COMMENT_GIVEN);

            let claim_parent = reward_weights.get_weight(metric_parent);
            let claim_self = reward_weights.get_weight(metric_self);

            let analytics_self = user_owned::borrow_or_create_analytics_mut(
                owned_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_self,
                app,
                &user_witness,
                user_witness_config,
                claim_self,
                metric_self
            );

            let analytics_parent = user_shared::borrow_or_create_analytics_mut(
                shared_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_parent,
                app,
                &user_witness,
                user_witness_config,
                claim_parent,
                metric_parent
            );
        };

        post_actions::comment_for_user<CoinType, UserWitness>(
            app,
            clock,
            parent_post,
            post_fees,
            &user_witness,
            user_witness_config,
            data,
            description,
            title,
            custom_payment,
            sui_payment,
            ctx
        )
    }
  ```
</Expandable>

### `create`

Create a new `User`. Aborts with `EInvalidUserDescription` or `EInvalidUsername` if the description or name is invalid, `EInviteRequired` if an `Invite` is required and not supplied, `EInviteDoesNotExist` if invite info was supplied but does not exist in the registry, `EInviteInvalid` if invite info was supplied but is not valid, or `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun create<CoinType> (clock: &Clock, invite_config: &InviteConfig, user_registry: &mut UserRegistry, user_invite_registry: &mut UserInviteRegistry, user_fees: &UserFees, invite_code_option: Option<String>, invite_key_option: Option<String>, avatar: String, banner: String, description: String, name: String, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext): (address, address)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun create<CoinType> (
        clock: &Clock,
        invite_config: &InviteConfig,
        user_registry: &mut UserRegistry,
        user_invite_registry: &mut UserInviteRegistry,
        user_fees: &UserFees,
        invite_code_option: Option<String>,
        invite_key_option: Option<String>,
        avatar: String,
        banner: String,
        description: String,
        name: String,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ): (address, address) {
        assert_user_name(&name);
        assert_user_description(&description);

        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_create_user_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let is_invite_included = 
            option::is_some(&invite_code_option) &&
            option::is_some(&invite_key_option);

        let is_invite_required = user_invite::is_invite_required(
            invite_config
        );

        assert!(
            !is_invite_required || is_invite_included,
            EInviteRequired
        );

        let invited_by = if (is_invite_included) {
            let invite_code = option::destroy_some(invite_code_option);
            let invite_key = option::destroy_some(invite_key_option);

            user_invite::assert_invite_exists(
                user_invite_registry,
                invite_key
            );

            let (hash, user_address) = user_invite::get_destructured_invite(
                user_invite_registry,
                invite_key
            );

            user_invite::assert_invite_is_valid(
                invite_code,
                invite_key,
                hash
            );

            user_invite::delete_invite(
                user_invite_registry,
                invite_key
            );

            option::some(user_address)
        } else {
            option::none()
        };

        let created_at = clock.timestamp_ms();
        let self = tx_context::sender(ctx);
        let user_key = string_helpers::to_lowercase(
            &name
        );

        let (
            owned_user,
            owned_user_address
        ) = user_owned::create(
            avatar,
            banner,
            created_at,
            description,
            user_key,
            name,
            self,
            ctx
        );

        let shared_user_address = user_shared::create(
            created_at,
            user_key,
            owned_user_address,
            self,
            ctx
        );

        user_owned::set_shared_user(
            owned_user,
            self,
            shared_user_address
        );

        user_registry::add(
            user_registry,
            user_key,
            self,
            owned_user_address,
            shared_user_address
        );

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );

        event::emit(UserCreated {
            avatar,
            banner,
            created_at,
            description,
            invited_by,
            user_owned_id: owned_user_address,
            user_shared_id: shared_user_address,
            user_id: self,
            user_key,
            user_name: name
        });

        (
            owned_user_address,
            shared_user_address
        )
    }
  ```
</Expandable>

### `create_invite`

Create an invite for another wallet address. Aborts with `EInviteNotAllowed` if invites are required, or `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun create_invite<CoinType> (invite_config: &InviteConfig, user_fees: &UserFees, user_invite_registry: &mut UserInviteRegistry, _: &UserOwned, invite_code: String, invite_hash: vector<u8>, invite_key: String, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun create_invite<CoinType> (
        invite_config: &InviteConfig,
        user_fees: &UserFees,
        user_invite_registry: &mut UserInviteRegistry,
        _: &UserOwned,
        invite_code: String,
        invite_hash: vector<u8>,
        invite_key: String,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        user_invite::assert_invite_not_required(invite_config);

        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_create_invite_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let self = tx_context::sender(ctx);

        user_invite::create_invite(
            user_invite_registry,
            invite_hash,
            invite_key,
            self
        );

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );

        event::emit(InviteCreated {
            invite_code,
            invite_key,
            user_id: self
        });
    }
  ```
</Expandable>

### `follow`

`User` follows another `User` within an `App`. Aborts with `ENoSelfJoin` if attempting to follow one's self, `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun follow<CoinType> (app: &App, clock: &Clock, owned_user: &mut UserOwned, reward_weights_registry: &RewardWeightsRegistry, shared_user: &mut UserShared, user_fees: &UserFees, user_witness_config: &UserWitnessConfig, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun follow<CoinType> (
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        reward_weights_registry: &RewardWeightsRegistry,
        shared_user: &mut UserShared,
        user_fees: &UserFees,
        user_witness_config: &UserWitnessConfig,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_follow_user_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let self = tx_context::sender(ctx);
        let user_address = user_shared::get_owner(shared_user);

        assert!(self != user_address, ENoSelfJoin);

        let app_address = object::id_address(app);

        let follows = user_shared::borrow_follows_mut(
            shared_user,
            app_address,
            ctx
        );
        
        let timestamp = clock.timestamp_ms();

        let (
            membership_message,
            membership_type,
            membership_count
        ) = membership::wallet_join(
            follows,
            self,
            timestamp
        );

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );

        let has_rewards_enabled = apps::has_rewards_enabled(
            app
        );

        if (has_rewards_enabled && membership_count == 1) {
            let current_epoch = reward_registry::get_current(
                reward_weights_registry
            );

            let reward_weights = reward_weights_registry.borrow_current();

            let metric_followed = utf8(METRIC_USER_FOLLOWED);
            let metric_self = utf8(METRIC_FOLLOWED_USER);

            let claim_followed = reward_weights.get_weight(metric_followed);
            let claim_self = reward_weights.get_weight(metric_self);

            let analytics_self = user_owned::borrow_or_create_analytics_mut(
                owned_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            let user_witness = user_witness::create_witness();

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_self,
                app,
                &user_witness,
                user_witness_config,
                claim_self,
                metric_self
            );

            let analytics_followed = user_shared::borrow_or_create_analytics_mut(
                shared_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_followed,
                app,
                &user_witness,
                user_witness_config,
                claim_followed,
                metric_followed
            );
        };

        event::emit(UserFollowsUpdate {
            account_type: membership_type,
            app_id: app_address,
            followed_user_id: user_address,
            message: membership_message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `friend_user`

Creates a friend request between two `User`s if one does not exist, or creates a friendship if the other `User` had already requested to be friends. Aborts with `ENotSelf` if attempting to friend one's self, or `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun friend_user<CoinType> (app: &App, clock: &Clock, reward_weights_registry: &RewardWeightsRegistry, user_fees: &UserFees, user_friend: &mut UserShared, user_shared: &mut UserShared, user_witness_config: &UserWitnessConfig, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun friend_user<CoinType> (
        app: &App,
        clock: &Clock,
        reward_weights_registry: &RewardWeightsRegistry,
        user_fees: &UserFees,
        user_friend: &mut UserShared,
        user_shared: &mut UserShared,
        user_witness_config: &UserWitnessConfig,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_friend_user_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let friend_address = user_shared::get_owner(user_friend);
        let user_address = user_shared::get_owner(user_shared);

        let self = tx_context::sender(ctx);

        assert!(self == user_address, ENotSelf);

        let app_address = object::id_address(app);

        let friend_requests = user_shared::borrow_friend_requests_mut(
            user_shared,
            app_address,
            ctx
        );

        let already_requested = membership::is_member(
            friend_requests,
            friend_address
        );

        let timestamp = clock.timestamp_ms();

        if (already_requested) {
            membership::wallet_leave(
                friend_requests,
                friend_address,
                timestamp
            );

            let friends = user_shared::borrow_friends_mut(
                user_shared,
                app_address,
                ctx
            );
            membership::wallet_join(
                friends,
                friend_address,
                timestamp
            );

            let friends_friends = user_shared::borrow_friends_mut(
                user_friend,
                app_address,
                ctx
            );
            let (
                membership_message,
                membership_type,
                membership_count
            ) = membership::wallet_join(
                friends_friends,
                user_address,
                timestamp
            );

            let has_rewards_enabled = apps::has_rewards_enabled(
                app
            );

            if (has_rewards_enabled && membership_count == 1) {
                let current_epoch = reward_registry::get_current(
                    reward_weights_registry
                );

                let reward_weights = reward_weights_registry.borrow_current();

                let metric = utf8(METRIC_USER_FRIENDS);

                let claim = reward_weights.get_weight(metric);

                let analytics = user_shared::borrow_or_create_analytics_mut(
                    user_shared,
                    user_witness_config,
                    app_address,
                    current_epoch,
                    ctx
                );

                let user_witness = user_witness::create_witness();

                analytics_actions::increment_analytics_for_user<UserWitness>(
                    analytics,
                    app,
                    &user_witness,
                    user_witness_config,
                    claim,
                    metric
                );

                let friend_analytics = user_shared::borrow_or_create_analytics_mut(
                    user_friend,
                    user_witness_config,
                    app_address,
                    current_epoch,
                    ctx
                );

                analytics_actions::increment_analytics_for_user<UserWitness>(
                    friend_analytics,
                    app,
                    &user_witness,
                    user_witness_config,
                    claim,
                    metric
                );
            };

            event::emit(UserFriendUpdate {
                account_type: membership_type,
                app_id: app_address,
                friended_user_id: friend_address,
                message: membership_message,
                updated_at: timestamp,
                user_id: user_address
            });
        } else {
            let friends_friend_requests = user_shared::borrow_friend_requests_mut(
                user_friend,
                app_address,
                ctx
            );

            let (
                membership_message,
                membership_type,
                _membership_count
            ) = membership::wallet_join(
                friends_friend_requests,
                user_address,
                timestamp
            );

            event::emit(UserFriendRequestUpdate {
                account_type: membership_type,
                app_id: app_address,
                friended_user_id: friend_address,
                message: membership_message,
                updated_at: timestamp,
                user_id: user_address
            });
        };

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );
    }
  ```
</Expandable>

### `like_post`

`User` likes a `Post`. Aborts with `ESuppliedAuthorMismatch` if the `SharedUser` is not the author of the `Post`, or `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun like_post<CoinType> (app: &App, clock: &Clock, owned_user: &mut UserOwned, post: &mut Post, post_fees: &PostFees, reward_weights_registry: &RewardWeightsRegistry, royalties: &Royalties, shared_user: &mut UserShared, user_witness_config: &UserWitnessConfig, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun like_post<CoinType> (
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        post: &mut Post,
        post_fees: &PostFees,
        reward_weights_registry: &RewardWeightsRegistry,
        royalties: &Royalties,
        shared_user: &mut UserShared,
        user_witness_config: &UserWitnessConfig,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        let author_address = post.get_author();
        let shared_user_address = shared_user.get_owner();

        assert!(author_address == shared_user_address, ESuppliedAuthorMismatch);

        let user_witness = user_witness::create_witness();

        post_actions::like_for_user<CoinType, UserWitness>(
            clock,
            post,
            post_fees,
            royalties,
            &user_witness,
            user_witness_config,
            custom_payment,
            sui_payment,
            ctx
        );

        let has_rewards_enabled = app.has_rewards_enabled();
        let self = tx_context::sender(ctx);

        if (
            has_rewards_enabled &&
            author_address != self
        ) {
            let app_address = object::id_address(app);
            let current_epoch = reward_registry::get_current(
                reward_weights_registry
            );

            let analytics_self = user_owned::borrow_or_create_analytics_mut(
                owned_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            let reward_weights = reward_weights_registry.borrow_current();

            let metric_author = utf8(METRIC_POST_LIKED);
            let metric_self = utf8(METRIC_LIKED_POST);

            let claim_author = reward_weights.get_weight(metric_author);
            let claim_self = reward_weights.get_weight(metric_self);

            let user_witness = user_witness::create_witness();

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_self,
                app,
                &user_witness,
                user_witness_config,
                claim_self,
                metric_self
            );

            let analytics_author = user_shared::borrow_or_create_analytics_mut(
                shared_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics_author,
                app,
                &user_witness,
                user_witness_config,
                claim_author,
                metric_author
            );
        };
    }
  ```
</Expandable>

### `post`

Creates a `Post` on the `User` and returns the `Post` address and timestamp that it was made. Aborts with `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun post<CoinType> (app: &App, clock: &Clock, owned_user: &mut UserOwned, reward_weights_registry: &RewardWeightsRegistry, shared_user: &mut UserShared, user_fees: &UserFees, user_witness_config: &UserWitnessConfig, data: String, description: String, title: String, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext): (address, u64)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun post<CoinType> (
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        reward_weights_registry: &RewardWeightsRegistry,
        shared_user: &mut UserShared,
        user_fees: &UserFees,
        user_witness_config: &UserWitnessConfig,
        data: String,
        description: String,
        title: String,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ): (address, u64) {
        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_post_from_user_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let app_address = object::id_address(app);

        let mut posts = user_shared::take_posts(
            shared_user,
            app_address,
            ctx
        );

        let user_witness = user_witness::create_witness();

        let (
            post_address,
            self,
            timestamp
        ) = post_actions::create_for_user<UserWitness>(
            app,
            clock,
            &mut posts,
            &user_witness,
            user_witness_config,
            data,
            description,
            title,
            ctx
        );

        user_shared::return_posts(
            shared_user,
            app_address,
            posts
        );

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );

        let has_rewards_enabled = apps::has_rewards_enabled(
            app
        );

        if (has_rewards_enabled) {
            let current_epoch = reward_registry::get_current(
                reward_weights_registry
            );

            let reward_weights = reward_weights_registry.borrow_current();

            let metric = utf8(METRIC_USER_TEXT_POST);

            let claim = reward_weights.get_weight(metric);

            let analytics = user_owned::borrow_or_create_analytics_mut(
                owned_user,
                user_witness_config,
                app_address,
                current_epoch,
                ctx
            );

            let user_witness = user_witness::create_witness();

            analytics_actions::increment_analytics_for_user<UserWitness>(
                analytics,
                app,
                &user_witness,
                user_witness_config,
                claim,
                metric
            );
        };

        let user_id = shared_user.get_owner();

        event::emit(UserPostCreated {
            app_id: app_address,
            created_at: timestamp,
            created_by: self,
            data,
            description,
            post_id: post_address,
            title,
            user_id
        });

        (post_address, timestamp)
    }
  ```
</Expandable>

### `remove_favorite_channel`

Removes a `Channel` from being a favorite within an `App`.

```rust theme={null}
public fun remove_favorite_channel<ChannelType: key> (app: &App, clock: &Clock, channel: &ChannelType, owned_user: &mut UserOwned, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun remove_favorite_channel<ChannelType: key> (
        app: &App,
        clock: &Clock,
        channel: &ChannelType,
        owned_user: &mut UserOwned,
        ctx: &mut TxContext
    ) {
        let app_address = object::id_address(app);
        let timestamp = clock.timestamp_ms();

        let (
            message,
            self,
            favorited_channel_id,
            _count
        ) = user_owned::remove_favorite_channel(
            channel,
            owned_user,
            app_address,
            timestamp,
            ctx
        );

        event::emit(ChannelFavoritesUpdate {
            app_id: app_address,
            favorited_channel_id,
            message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `remove_favorite_post`

Removes a `Post` from being a favorite within an `App`.

```rust theme={null}
public fun remove_favorite_post(app: &App, clock: &Clock, owned_user: &mut UserOwned, post: &Post, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun remove_favorite_post(
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        post: &Post,
        ctx: &mut TxContext
    ) {
        let app_address = object::id_address(app);
        let timestamp = clock.timestamp_ms();

        let (
            message,
            self,
            favorited_post_id,
            _count
        ) = user_owned::remove_favorite_post(
            post,
            owned_user,
            app_address,
            timestamp,
            ctx
        );

        event::emit(PostFavoritesUpdate {
            app_id: app_address,
            favorited_post_id,
            message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `remove_favorite_user`

Removes a `User` from being a favorite within an `App`.

```rust theme={null}
public fun remove_favorite_user(app: &App, clock: &Clock, owned_user: &mut UserOwned, user: &UserShared, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun remove_favorite_user(
        app: &App,
        clock: &Clock,
        owned_user: &mut UserOwned,
        user: &UserShared,
        ctx: &mut TxContext
    ) {
        let app_address = object::id_address(app);
        let timestamp = clock.timestamp_ms();

        let (
            message,
            self,
            favorited_user_id,
            _count
        ) = user_owned::remove_favorite_user(
            owned_user,
            user,
            app_address,
            timestamp,
            ctx
        );

        event::emit(UserFavoritesUpdate {
            app_id: app_address,
            favorited_user_id,
            message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `remove_friend_request`

Removes a friend request to the `User` if they do not wish to connect to the requester. Aborts with `ENotSelf` if the `SharedUser` does not belong to the wallet address or if the request is not affiliated with the provided `SharedUser`.

```rust theme={null}
public fun remove_friend_request(app: &App, clock: &Clock, shared_user: &mut UserShared, removed_request: address, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun remove_friend_request(
        app: &App,
        clock: &Clock,
        shared_user: &mut UserShared,
        removed_request: address,
        ctx: &mut TxContext
    ) {
        let self = tx_context::sender(ctx);
        let user_address = user_shared::get_owner(shared_user);

        assert!(self == user_address || self == removed_request, ENotSelf);

        let app_address = object::id_address(app);

        let friend_requests = user_shared::borrow_friend_requests_mut(
            shared_user,
            app_address,
            ctx
        );
        
        let timestamp = clock.timestamp_ms();

        let (
            membership_message,
            membership_type,
            _membership_count
        ) = membership::wallet_leave(
            friend_requests,
            removed_request,
            timestamp
        );

        event::emit(UserFriendRequestUpdate {
            account_type: membership_type,
            app_id: app_address,
            friended_user_id: removed_request,
            message: membership_message,
            updated_at: timestamp,
            user_id: user_address
        });
    }
  ```
</Expandable>

### `unfollow`

Unfollow a `User`. Aborts with `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun unfollow<CoinType> (app: &App, clock: &Clock, shared_user: &mut UserShared, user_fees: &UserFees, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun unfollow<CoinType> (
        app: &App,
        clock: &Clock,
        shared_user: &mut UserShared,
        user_fees: &UserFees,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_unfollow_user_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let self = tx_context::sender(ctx);
        let user_address = user_shared::get_owner(shared_user);

        let app_address = object::id_address(app);

        let follows = user_shared::borrow_follows_mut(
            shared_user,
            app_address,
            ctx
        );
        
        let timestamp = clock.timestamp_ms();

        let (
            membership_message,
            membership_type,
            _membership_count
        ) = membership::wallet_leave(
            follows,
            self,
            timestamp
        );

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );

        event::emit(UserFollowsUpdate {
            account_type: membership_type,
            app_id: app_address,
            followed_user_id: user_address,
            message: membership_message,
            updated_at: timestamp,
            user_id: self
        });
    }
  ```
</Expandable>

### `unfriend_user`

Removes friendship between `User`s. Aborts with `ENotSelf` if the wallet does not own the `OwnedUser` or `SharedUser`, or `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun unfriend_user<CoinType> (app: &App, clock: &Clock, user_fees: &UserFees, user_friend: &mut UserShared, user_shared: &mut UserShared, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun unfriend_user<CoinType> (
        app: &App,
        clock: &Clock,
        user_fees: &UserFees,
        user_friend: &mut UserShared,
        user_shared: &mut UserShared,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_unfriend_user_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let friend_address = user_shared::get_owner(user_friend);
        let user_address = user_shared::get_owner(user_shared);

        let self = tx_context::sender(ctx);

        assert!(self == friend_address || self == user_address, ENotSelf);

        let app_address = object::id_address(app);
        let timestamp = clock.timestamp_ms();

        let friends = user_shared::borrow_friends_mut(
            user_shared,
            app_address,
            ctx
        );
        membership::wallet_leave(
            friends,
            friend_address,
            timestamp
        );

        let friends_friends = user_shared::borrow_friends_mut(
            user_friend,
            app_address,
            ctx
        );
        let (
            membership_message,
            membership_type,
            _membership_count
        ) = membership::wallet_leave(
            friends_friends,
            user_address,
            timestamp
        );

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );

        event::emit(UserFriendUpdate {
            account_type: membership_type,
            app_id: app_address,
            friended_user_id: friend_address,
            message: membership_message,
            updated_at: timestamp,
            user_id: user_address
        });
    }
  ```
</Expandable>

### `update`

Updates metadata on the `OwnedUser`. Aborts with `EUserNameMismatch` if attempting to change the name, or `EIncorrectCoinType`, `EIncorrectCustomPayment`, or `EIncorrectSuiPayment` if the payment was incorrect.

```rust theme={null}
public fun update<CoinType> (clock: &Clock, user_registry: &UserRegistry, user_fees: &UserFees, owned_user: &mut UserOwned, avatar: String, banner: String, description: String, name: String, custom_payment: Coin<CoinType>, sui_payment: Coin<SUI>, ctx: &mut TxContext)
```

<Expandable title="Implementation">
  ```rust theme={null}
    public fun update<CoinType> (
        clock: &Clock,
        user_registry: &UserRegistry,
        user_fees: &UserFees,
        owned_user: &mut UserOwned,
        avatar: String,
        banner: String,
        description: String,
        name: String,
        custom_payment: Coin<CoinType>,
        sui_payment: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        let (
            custom_payment,
            sui_payment
        ) = user_fees::assert_update_user_payment<CoinType>(
            user_fees,
            custom_payment,
            sui_payment
        );

        let self = tx_context::sender(ctx);

        let owned_user_key = user_registry::get_key_from_owner_address(
            user_registry,
            self
        );

        let lowercase_user_name = string_helpers::to_lowercase(
            &name
        );

        assert!(lowercase_user_name == owned_user_key, EUserNameMismatch);

        let updated_at = clock.timestamp_ms();

        user_owned::update(
            owned_user,
            avatar,
            banner,
            description,
            name,
            updated_at
        );

        fees::collect_payment<CoinType>(
            custom_payment,
            sui_payment
        );

        event::emit(UserUpdated {
            avatar,
            banner,
            user_id: self,
            user_name: name,
            description,
            updated_at
        });
    }
  ```
</Expandable>
