Feed

In the Feed module, we have the following files:

types.mo File

Defines primary data type aliases used throughout the system. It imports a module named Types and re-exports the types defined in that module, such as Post, Comment, Like, etc.

rootFeed.mo File

Responsible for aggregating and creating users' feeds.

  • The createFeedCanister function allows users to create their own feed canister, which is the place where users store posts and feeds on the platform.
  • The updateFetchUserToFeed function is responsible for updating other system components, such as post, comment, and like fetching services when a user creates a new feed canister.
  • The getUserFeedCanister and getAllUserFeedCanister functions provide ways to retrieve user feed canisters.

database.mo File

Implements a post directory (PostDirectory) and a feed directory (FeedDirectory) used for storing and managing posts and related actions (such as comments, likes, and reposts) in the database.

  • The PostDirectory class has a postMap, which is a key-value collection implemented using TrieMap to store posts and their indices.
  • The createPost function implements the posting feature. It creates a new post and adds it to the postMap while incrementing the post index.
  • The getPost function allows retrieving posts by post ID.
  • The createComment, createLike, and createRepost functions are used for creating comments, likes, and reposts, respectively.
  • The getAllPost function retrieves all posts and sorts them by creation time.

The FeedDirectory class is responsible for managing users' feeds. It uses TrieMap to store and retrieve posts in the user's feed.

feed.mo File

Feed represents the user feed in a social media platform. It manages posts and feeds using the PostDirectory and FeedDirectory defined in the previous database files.

  • The Feed class has several functions, such as createPost, createRepost, createComment, and createLike, which implement basic user interactions on social media.
  • Functions like receiveFeed, batchReceiveFeed, receiveComment, batchReceiveComment, receiveLike, and batchReceiveLike are used to receive posts, comments, and likes from other users and add these activities to the current user's feed.
  • The getLatestFeed function allows users to retrieve the latest posts in their feed.

Creating Feed Canister: rootFeed.mo

Managing user feed canisters:

  • Defines a TrieMap (userFeedCanisterMap) that stores the mapping between users and their corresponding feed canisters.
  • Provides a method (createFeedCanister()) for users to create their personal feed canisters.
  • Provides methods (getUserFeedCanister(), getAllUserFeedCanister()) for retrieving user feed canisters.
  • Provides a method (getTotalUserFeedCanisterNumber()) for getting the total number of created feed canisters.

Managing other canisters in the feed system:

  • Stores and provides interfaces for querying/updating the Fetch Canisters of Post, Comment, and Like.
  • Synchronously updates the mapping in these Fetch Canisters when a user creates a new feed canister.

First, defining some basic types and data structures needed for the Feed system, including an Actor for fetching feed data and a TrieMap for storing mappings.

// Custom Actor types for fetching different parts of the Feed
type RootFetchActor = Types.RootFetchActor;
type PostFetchActor = Types.PostFetchActor;
type CommentFetchActor = Types.CommentFetchActor;
type LikeFetchActor = Types.LikeFetchActor;

stable let T_CYCLES = 1_000_000_000_000; // 1 trillion cycles for later operations

// An array storing key-value pairs, where keys are user Principals and values are corresponding feed canister Principals
stable var userFeedCanisterMapEntries: [(Principal, Principal)] = [];
// A TrieMap from user Principals to feed canister Principals, constructed using fromEntries with userFeedCanisterMapEntries
let userFeedCanisterMap = TrieMap.fromEntries<Principal, Principal>(userFeedCanisterMapEntries.vals(), Principal.equal, Principal.hash);
let ic: IC.Service = actor("aaaaa-aa");

TrieMap is a key-value storage structure that efficiently inserts and looks up data.

let postMap = TrieMap.fromEntries<Nat, Post>(postMapEntries.vals(), Nat.equal, Hash.hash);

Here, a TrieMap is created to store post data, where Nat is the key type, and Post is the value type.


Logic for creating a user's own feed canister: Create a feed canister for the user, perform identity verification, authorization settings, data mapping synchronization, etc.

// Create a feed canister for the user
public shared({caller}) func createFeedCanister(): async ?Principal {
    // Check the caller's identity
    assert(_getUserFeedCanister(caller) == null);
    // Allocate 2 trillion cycles for each new canister
    Cycles.add(2 * T_CYCLES);
    // Call the `Feed.Feed` method to create the user's feed canister
    let feedCanister = await Feed.Feed(
        caller, rootPostCanister, userCanister, 
        postFetchCanister,
        commentFetchCanister,
        likeFetchCanister
    );
    // Get the ID of the canister and store it in the mapping TrieMap
    let feedCanisterId = Principal.fromActor(feedCanister);
    userFeedCanisterMap.put(caller, feedCanisterId);
    // Call the ic method to update the settings of the canister, authorizing the Root Canister and the user as controllers
    await ic.update_settings({
        canister_id = feedCanisterId;
        settings = {
            freezing_threshold = null;
            controllers = ?[Principal.fromActor(this), feedCanisterId];
            memory_allocation = null;
            compute_allocation = null;
        }
    });
    
    // Update the information in fetch; also update the mapping in Post/Comment/Like Fetch Canisters for this user
    ignore updateFetchUserToFeed((caller, feedCanisterId));
    
    // Return the ID of the newly created feed canister
    ?feedCanisterId
};

When a user's feed canister is created, synchronize it with other canisters that depend on this mapping to ensure data consistency in the Feed system.

This is done in three steps:

  1. Retrieve the IDs of all Post, Comment, and Like Fetch Canisters from the Root Fetch Canister.
  2. Iterate over each Fetch Canister, create an actor reference.
  3. Call the addUserToFeedEntry method of each Fetch Canister, providing the mapping between the user and their corresponding feed canister.
func updateFetchUserToFeed(entry: (Principal, Principal)): async () {
    let rootFetchActor: RootFetchActor = actor(Principal.toText(rootFetchCanister));

    // Update information in postFetch
    let postFetchCanisterArray = await rootFetchActor.getAllPostFetchCanister();
    for(_canister in postFetchCanisterArray.vals()) {
        let postFetchActor: PostFetchActor = actor(Principal.toText(_canister));
        ignore postFetchActor.addUserToFeedEntry(entry);
    };

    // Update commentFetch
    let commentFetchCanisterArray = await rootFetchActor.getAllCommentFetchCanister();
    for(_canister in commentFetchCanisterArray.vals()) {
        let commentFetchActor: CommentFetchActor = actor(Principal.toText(_canister));
        ignore commentFetchActor.addUserToFeedEntry(entry);
    };
    
    // Update likeFetch
    let likeFetchCanisterArray = await rootFetchActor.getAllLikeFetchCanister();
    for(_canister in likeFetchCanisterArray.vals()) {
        let likeFetchActor: LikeFetchActor = actor(Principal.toText(_canister));
        ignore likeFetchActor.addUserToFeedEntry(entry);
    };
};

These several functions are used to query the mapping relationships of user Feed Canisters:

Different interfaces for user queries, getting all mappings, getting totals, etc., can be used to query the mapping relationships of user Feed Canisters for reading the current internal state of the Feed system.

// Receives a user Principal as a parameter
public query func getUserFeedCanister(user: Principal): async ?Principal {
    // Calls a private function, returns the Feed Canister Principal corresponding to the user
    _getUserFeedCanister(user)
};

// return [(user, feedCanister)]
public query func getAllUserFeedCanister(): async [(Principal, Principal)] {
    // Converts the internal userFeedCanisterMap to an array
    // Returns an array of all user-to-Feed Canister mapping relationships
    Iter.toArray(userFeedCanisterMap.entries())
};

// Total number of Canisters created
public query func getTotalUserFeedCanisterNumber(): async Nat {
    // Returns the size of userFeedCanisterMap, i.e., the total number of Feed Canisters
    userFeedCanisterMap.size()
};

// Internal private method, queries the user's Feed
private func _getUserFeedCanister(user: Principal): ?Principal {
    // Queries userFeedCanisterMap
    switch(userFeedCanisterMap.get(user)) {
        case(null) { return null;};
        // Returns the corresponding Feed Canister Id based on the given user Principal
        case(?canister) { return ?canister;};
    };
};

When creating a Bucket, we also need to specify the Canister IDs for Post Fetch, Comment Fetch, and Like Fetch in the Bucket system. Therefore, in the Root Post, we also need to record and save Post Fetch, Comment Fetch, and Like Fetch:

stable var postFetchCanister = _postFetchCanister;

public query func getPostFetchCanister(): async Principal { postFetchCanister };

public shared({caller}) func updatePostFetchCanister(
    newPostFetchCanister: Principal
): async () {
    postFetchCanister := newPostFetchCanister;
};

// CommentFetchCanister

stable var commentFetchCanister = _commentFetchCanister;
    
public query func getCommentFetchCanister(): async Principal { commentFetchCanister };

public shared({caller}) func updateCommentFetchCanister(
    newCommentFetchCanister: Principal
): async () {
    commentFetchCanister := commentFetchCanister;
};

// LikeFetchCanister

stable var likeFetchCanister = _likeFetchCanister;
    
public query func getLikeFetchCanister(): async Principal { likeFetchCanister };

public shared({caller}) func updateLikeFetchCanister(
    newLikeFetchCanister: Principal
): async () {
    likeFetchCanister := newLikeFetchCanister;
};

system func preupgrade() {
    userFeedCanisterMapEntries := Iter.toArray(userFeedCanisterMap.entries());
};

system func postupgrade() {
    userFeedCanisterMapEntries := [];
};

Finally, there are two system functions preupgrade() and postupgrade(), used to save data before and after Canister upgrades.


Storing Data: database.mo

The database.mo file is responsible for storing data inside the user's Feed Canister.

First, define the storage structure for post indexes and mapping relationships, as well as related query interfaces.

postIndex is used to generate a unique ID for posts; postMap stores post data. getPostIndexEntries and getPostMapEntries are used to query the index range and mapping relationships of posts.

// Define some type aliases related to posts
type Post = Types.Post;
type PostImmutable = Types.PostImmutable;
type Comment = Types.Comment;
type NewComment = Types.NewComment;
type UserId = Types.UserId;
type Time = Types.Time;
type Like = Types.Like;
type NewLike = Types.NewLike;
type Repost = Types.Repost;
type NewRepost = Types.NewRepost;

// An incrementing post index value
var postIndex: Nat = postIndexEntries;
// A mapping table from post index to post, implemented using TrieMap
let postMap = TrieMap.fromEntries<Nat, Post>(postMapEntries.vals(), Nat.equal, Hash.hash); // postIndex -> Post

// Returns the current maximum post index value
public func getPostIndexEntries(): Nat { postIndex };

// Returns an array of all mapping relationships in postMap
public func getPostMapEntries(): [(Nat, Post)] { Iter.toArray(postMap.entries()) };

// Generates a unique ID for posts, in the format: bucket#user#index
private func _getPostId(bucket: Principal, user: Principal, index: Nat): Text {
    Principal.toText(bucket) # "#" # Principal.toText(user) # "#" # Nat.toText(index)
};

Posting

The logic of posting in the Feed Canister:

Implement the construction of post information and store it in the post mapping table.

Also returns an immutable post object to prevent posts from being modified.

The incrementing postIndex ensures that each post has a globally unique ID.

Post data is stored in TrieMap for efficient querying.

// Posting
public func createPost(user: UserId, feedCanister: Principal, content: Text, time: Time, bucket: Principal): PostImmutable {
    let post: Post = {
        // Generate a unique ID for the post
        postId = _getPostId(bucket, user, postIndex);
        // Construct a Post record, including post content, user information, time, etc.
        feedCanister = feedCanister;
        index = postIndex;
        user = user;
        content = content;
        var repost = [];
        var like = [];
        var comment = [];
        createdAt = time;
    };
    
    // Put this Post record into postMap
    postMap.put(postIndex, post);
    // Increment postIndex
    postIndex += 1;

    // Return PostImmutable
    Utils._convertPostToImmutable(post)
};

Provides basic interfaces for external post queries.

Implements functionality to get the total number of posts and retrieve individual posts based on ID.

// Get the total number of posts
public func getPostNumber(): Nat {
    // Directly call postMap's size()
    postMap.size()
};

// Get a single post based on ID
public func getPost(postId: Text): ?PostImmutable {
    // Receive the post ID as a parameter, first call checkPostId() to validate the ID format
    let (bucket, user, index) = utils.checkPostId(postId);
    // Retrieve the post record post from postMap based on the index
    switch(postMap.get(index)) {
        // If it doesn't exist, return null
        case(null) { return null; };
        // If it exists, return the post
        case(?post) {
            return ?{
                postId = post.postId;
                feedCanister = post.feedCanister;
                index = post.index;
                user = post.user;
                repost = post.repost;
                content = post.content;
                like = post.like;
                comment = post.comment;
                createdAt = post.createdAt;
            }
        };
    };
};

Commenting

Returns a tuple containing the Bucket and the updated array of post comments.

Therefore, this function checks if the post exists, adds a new comment if it does, and returns a tuple containing the bucket where the post is located and the updated post information.

The main logic is to check parameters, retrieve the original post information, update the post comments, and return the updated post comments.

// Commenting
public func createComment(commentUser: UserId, postId: Text, content: Text, createdAt: Time): ?(Principal, NewComment) {
    // Check if the post ID is valid and return the bucket, user, and index in the array where the post is located
    let (bucket, user, index) = utils.checkPostId(postId);
    // Get the post post to be commented on, return null if it does not exist
    switch(postMap.get(index)) {
        case(null) { return null;};
        case(?post) {
            // If the post exists, add the new comment to the post's comment array
            // The comment content includes user ID, comment content, and time
            post.comment := Array.append(post.comment, [{
            user = commentUser; // User ID of the commenter
            content = content; // Comment content
            createdAt = createdAt; // Comment time
        }]);
            ?(bucket, post.comment)
        };
    };
};

Liking

Actually, it's similar to the comment function mentioned earlier. It also uses postMap.get(index).

// Liking
public func createLike(likeUser: UserId, postId: Text, createdAt: Time): ?(Principal, NewLike) {
    let (bucket, user, index) = utils.checkPostId(postId);
    switch(postMap.get(index)) {
        case(null) { return null; };
        case(?post) {
            for(like in post.like.vals()) {
                // Already liked
                if(like.user == likeUser) { return null;};
        };
        post.like := Array.append<Like>(post.like, [{
            user = likeUser;
            createdAt = createdAt;
        }]);
            ?(bucket, post.like)
        };
    }
};

Reposting

Similar to the above.

// Reposting
public func createRepost(repostUser: UserId, postId: Text, createdAt: Time): ?(Principal, NewRepost) {
    let (bucket, user, index) = utils.checkPostId(postId);
    switch(postMap.get(index)) {
        case(null) { return null; };
        case(?post) {
            for(repost in post.repost.vals()) {
                // Already reposted
                if(repost.user == repostUser) { return null;};
            };
        post.repost := Array.append<Repost>(post.repost, [{
            user = repostUser;
            createdAt = createdAt;
        }]);
            ?(bucket, post.repost)
        };
    }
};

Querying All Posts

The function getAllPost retrieves all posts (Post) from a mapping (postMap), converts them into an immutable form (PostImmutable), and sorts them by creation time. Finally, it returns the sorted array of posts.

  • TrieMap.map:

    Uses TrieMap.map to map each key-value pair in postMap, converting it to an immutable type PostImmutable. This mapping is done using the Nat type for keys, handling key equality and hashing with Nat.equal and Hash.hash.

    TrieMap.map<Nat, Post, PostImmutable>(
        postMap, Nat.equal, Hash.hash,
        func (k: Nat, v1: Post): PostImmutable {
            Utils._convertPostToImmutable(v1)
        }
    )
    
  • .vals():

    Retrieves all values of the mapping, returning an array containing all converted PostImmutable values.

    TrieMap.map<Nat, Post, PostImmutable>(...).vals()
    
  • Iter.sort:

    Sorts the array of values by comparing posts based on their creation time (createdAt). Uses Order.Order to specify the sorting order, where #less indicates ascending, #greater indicates descending, and #equal indicates equality.

    Iter.sort<PostImmutable>(
        TrieMap.map<Nat, Post, PostImmutable>(...).vals(),
        func (x: PostImmutable, y: PostImmutable): Order.Order {
            if(x.createdAt > y.createdAt) return #less
            else if(x.createdAt < y.createdAt) return #greater
            else return #equal
        }
    )
    
  • Iter.toArray:

    Converts the sorted array of posts into a Motoko array, ultimately serving as the return value of the function.

    Iter.toArray(...)
    

The purpose of the entire function is to retrieve all posts from the mapping, convert them into an immutable form, and sort them by creation time before returning the sorted array of posts.

public func getAllPost(): [PostImmutable] {
    Iter.toArray(
        Iter.sort<PostImmutable>(
            TrieMap.map<Nat, Post, PostImmutable>(
                postMap, Nat.equal, Hash.hash,
                func (k: Nat, v1: Post): PostImmutable {
                Utils._convertPostToImmutable(v1)
            }
        ).vals(),
        func (x: PostImmutable, y: PostImmutable): Order.Order {
              if(x.createdAt > y.createdAt) return #less
              else if(x.createdAt < y.createdAt) return #greater
              else return #equal
        }))
    };

};

FeedDirectory Class

Without further ado, let's dive into the code:

public class FeedDirectory(
    feedMapEntries: [(Text, PostImmutable)]
) {
    
    type PostImmutable = Types.PostImmutable;

    // Using the TrieMap type to create a mapping, where the key is of type Text and the value is of type PostImmutable
    // Used for storing post data
    let feedMap = TrieMap.fromEntries<Text, PostImmutable>(feedMapEntries.vals(), Text.equal, Text.hash);

    // Returns an array containing all mapping entries
    public func getFeedMapEntries(): [(Text, PostImmutable)] { Iter.toArray(feedMap.entries()) };

    // Adds a post to the mapping, with the key being the postId of the post
    public func storeFeed(post: PostImmutable) {
        feedMap.put(post.postId, post);
    };

    // Bulk adds all posts in the post array to the mapping
    public func batchStoreFeed(postArray: [PostImmutable]) {
        for(_post in postArray.vals()) {
            feedMap.put(_post.postId, _post);
        };
    };

    // Returns the number of posts stored in the mapping
    public func getFeedNumber(): Nat {
        feedMap.size()
    };

    // Returns the post corresponding to the given postId
    // Returns null if the post does not exist
    public func getFeed(postId: Text): ?PostImmutable {
        switch(feedMap.get(postId)) {
            case(null) { return null; };
            case(?_feed) { return ?_feed; };
        };
    };

    // Returns the latest n posts, sorted by creation time
    public func getLatestFeed(n: Nat): [PostImmutable] {
        let feedArray = Iter.toArray(
            Iter.sort<PostImmutable>(
            feedMap.vals(),
            func (x: PostImmutable, y: PostImmutable): Order.Order {
                if(x.createdAt > y.createdAt) return #less
                else if(x.createdAt < y.createdAt) return #greater
                else return #equal
        }));
        // If the requested quantity exceeds the actual number of posts, return all posts
        if(n <= feedArray.size()) {
            Array.subArray(feedArray, 0, n)
        } else {
            Array.subArray(feedArray, 0, feedArray.size())
        }
    };

};

database.mo complete file:

import Array "mo:base/Array";
import HashMap "mo:base/HashMap";
import Iter "mo:base/Iter";
import Option "mo:base/Option";
import Principal "mo:base/Principal";
import Types "./types";
import TrieMap "mo:base/TrieMap";
import TrieSet "mo:base/TrieSet";
import Hash "mo:base/Hash";
import Nat "mo:base/Nat";
import Time "mo:base/Time";
import utils "../utils";
import Text "mo:base/Text";
import Order "mo:base/Order";
import Utils "../utils";

module {
  type Post = Types.Post;
  public class PostDirectory(
    postIndexEntries: Nat,
    postMapEntries: [(Nat, Post)]
  ) {

    type Post = Types.Post;
    type PostImmutable = Types.PostImmutable;
    type Comment = Types.Comment;
    type NewComment = Types.NewComment;
    type UserId = Types.UserId;
    type Time = Types.Time;
    type Like = Types.Like;
    type NewLike = Types.NewLike;
    type Repost = Types.Repost;
    type NewRepost = Types.NewRepost;

    var postIndex: Nat = postIndexEntries;
    let postMap = TrieMap.fromEntries<Nat, Post>(postMapEntries.vals(), Nat.equal, Hash.hash); // postIndex -> Post

    public func getPostIndexEntries(): Nat { postIndex };

    public func getPostMapEntries(): [(Nat, Post)] { Iter.toArray(postMap.entries()) };

    private func _getPostId(bucket: Principal, user: Principal, index: Nat): Text {
      Principal.toText(bucket) # "#" # Principal.toText(user) # "#" # Nat.toText(index)
    };

    public func createPost(user: UserId, feedCanister: Principal, content: Text, time: Time, bucket: Principal): PostImmutable {
      let post: Post = {
        postId = _getPostId(bucket, user, postIndex);
        feedCanister = feedCanister;
        index = postIndex;
        user = user;
        content = content;
        var repost = [];
        var like = [];
        var comment = [];
        createdAt = time;
      };

      postMap.put(postIndex, post);
      postIndex += 1;

      Utils._convertPostToImmutable(post)
    };

    public func getPostNumber(): Nat {
      postMap.size()
    };

    public func getPost(postId: Text): ?PostImmutable {
      let (bucket, user, index) = utils.checkPostId(postId);
      switch(postMap.get(index)) {
        case(null) { return null; };
        case(?post) {
          return ?{
            postId = post.postId;
            feedCanister = post.feedCanister;
            index = post.index;
            user = post.user;
            repost = post.repost;
            content = post.content;
            like = post.like;
            comment = post.comment;
            createdAt = post.createdAt;
          }
        };
      };
    };


    public func createComment(commentUser: UserId, postId: Text, content: Text, createdAt: Time): ?(Principal, NewComment) {
      let (bucket, user, index) = utils.checkPostId(postId);
      switch(postMap.get(index)) {
        case(null) { return null;};
        case(?post) {
          post.comment := Array.append(post.comment, [{
            user = commentUser;
            content = content;
            createdAt = createdAt;
          }]);
          ?(bucket, post.comment)
        };
      };
    };


    public func createLike(likeUser: UserId, postId: Text, createdAt: Time): ?(Principal, NewLike) {
      let (bucket, user, index) = utils.checkPostId(postId);
      switch(postMap.get(index)) {
        case(null) { return null; };
        case(?post) {
          for(like in post.like.vals()) {
            if(like.user == likeUser) { return null;};
          };
          post.like := Array.append<Like>(post.like, [{
            user = likeUser;
            createdAt = createdAt;
          }]);
          ?(bucket, post.like)
        };
      }
    };


    public func createRepost(repostUser: UserId, postId: Text, createdAt: Time): ?(Principal, NewRepost) {
      let (bucket, user, index) = utils.checkPostId(postId);
      switch(postMap.get(index)) {
        case(null) { return null; };
        case(?post) {
          for(repost in post.repost.vals()) {
            if(repost.user == repostUser) { return null;};
          };
          post.repost := Array.append<Repost>(post.repost, [{
            user = repostUser;
            createdAt = createdAt;
          }]);
          ?(bucket, post.repost)
        };
      }
    };

    public func getAllPost(): [PostImmutable] {
      Iter.toArray(
        Iter.sort<PostImmutable>(
          TrieMap.map<Nat, Post, PostImmutable>(
            postMap, Nat.equal, Hash.hash,
            func (k: Nat, v1: Post): PostImmutable {
              Utils._convertPostToImmutable(v1)
            }
          ).vals(),
          func (x: PostImmutable, y: PostImmutable): Order.Order {
              if(x.createdAt > y.createdAt) return #less
              else if(x.createdAt < y.createdAt) return #greater
              else return #equal
          }))
    };

  };
  
  type PostImmutable = Types.PostImmutable;

  public class FeedDirectory(
    feedMapEntries: [(Text, PostImmutable)]
  ) {
    
    type PostImmutable = Types.PostImmutable;

    let feedMap = TrieMap.fromEntries<Text, PostImmutable>(feedMapEntries.vals(), Text.equal, Text.hash);

    public func getFeedMapEntries(): [(Text, PostImmutable)] { Iter.toArray(feedMap.entries()) };

    public func storeFeed(post: PostImmutable) {
      feedMap.put(post.postId, post);
    };

    public func batchStoreFeed(postArray: [PostImmutable]) {
      for(_post in postArray.vals()) {
        feedMap.put(_post.postId, _post);
      };
    };

    public func getFeedNumber(): Nat {
      feedMap.size()
    };

    public func getFeed(postId: Text): ?PostImmutable {
      switch(feedMap.get(postId)) {
        case(null) { return null; };
        case(?_feed) { return ?_feed; };
      };
    };

    public func getLatestFeed(n: Nat): [PostImmutable] {
      let feedArray = Iter.toArray(
        Iter.sort<PostImmutable>(
        feedMap.vals(),
        func (x: PostImmutable, y: PostImmutable): Order.Order {
            if(x.createdAt > y.createdAt) return #less
            else if(x.createdAt < y.createdAt) return #greater
            else return #equal
      }));
      if(n <= feedArray.size()) {
        Array.subArray(feedArray, 0, n)
      } else {
        Array.subArray(feedArray, 0, feedArray.size())
      }
    };

  };
};


User Cloud Services: feed.mo


Owner

In the Feed Canister, it is necessary to store information about who the owner is and allow the owner to transfer their control later.

stable var owner = _owner;

// Query owner, allowing contract users to asynchronously retrieve the current owner
// Since it is a query function, it does not modify the contract state, so it can be called by any user without consensus
public query func getOwner(): async Principal { owner };

// Update owner
public shared({caller}) func updateOwner(newOwner: Principal): async () {
    assert(caller == owner);
    owner := newOwner;
};

public query({caller}) func whoami(): async Principal { caller };

Fetch Canister

Similarly, the Feed Canister needs to keep records of various Fetch Canisters.

stable var postFetchCanister = _postFetchCanister;

public query func getPostFetchCanister(): async Principal { postFetchCanister };

public shared({caller}) func updatePostFetchCanister(
    newPostFetchCanister: Principal
): async () {
    postFetchCanister := newPostFetchCanister;
};

CommentFetchCanister:

stable var commentFetchCanister = _commentFetchCanister;

public query func getCommentFetchCanister(): async Principal { commentFetchCanister };

public shared({caller}) func updateCommentFetchCanister(
    newCommentFetchCanister: Principal
): async () {
    commentFetchCanister := commentFetchCanister;
};

LikeFetchCanister:

stable var likeFetchCanister = _likeFetchCanister;

public query func getLikeFetchCanister(): async Principal { likeFetchCanister };

public shared({caller}) func updateLikeFetchCanister(
    newLikeFetchCanister: Principal
): async () {
    likeFetchCanister := newLikeFetchCanister;
};

Followers

The Feed Canister also maintains a list of followers. When a user posts, the Feed Canister sends the post ID and followers to the Fetch Canister, indicating which followers should be notified.

In urgent situations, posts can be sent directly to followers in a peer-to-peer manner, also useful for peer-to-peer messaging.

stable var followers: [Principal] = [];

// Receive updates from the user canister
public shared({caller}) func updateFollowers(newFollowers: [Principal]): async () {
    followers := newFollowers;
};

public query func getFollowers(): async [Principal] {
    followers
};

Bucket

type RootPostActor = Types.RootPostActor;
stable var bucket: ?Principal = null;
stable let rootPostActor: RootPostActor = actor(Principal.toText(rootPostCanister));

// Update the current bucket canister to be used for storing the feed
public shared func checkAvailableBucket(): async Bool {
    switch((await rootPostActor.getAvailableBucket())) {
        case(null) { return false; };
        case(?_bucket) {
            bucket := ?_bucket;
            return true;
        };
    };
};

public query func getBucket(): async ?Principal { bucket };

Post

Next are the functionalities related to posts.

type Time = Types.Time;
type UserId = Types.UserId;
type BucketActor = Types.BucketActor;
type PostFetchActor = Types.PostFetchActor;
type Post = Types.Post;

stable var postIndexEntries: Nat = 0;
stable var postMapEntries: [(Nat, Post)] = [];
let postDirectory: Database.PostDirectory = Database.PostDirectory(postIndexEntries, postMapEntries);

// Query the number of posts a user has made (total count)
public query func getPostNumber(): async Nat {
    postDirectory.getPostNumber()
};

// Query a specific post made by the user based on the post ID
public query func getPost(postId: Text): async  ?PostImmutable {
    postDirectory.getPost(postId)
};

// Query all posts
public query func getAllPost(): async [PostImmutable] {
    postDirectory.getAllPost()
};

Core logic for users to create posts.

public shared({caller}) func createPost(title: Text, content: Text): async Text {
    // Check if it is called by the owner of the feed to ensure the existence of the bucket (where posts are stored)
    assert(caller == owner and bucket != null);
    let _bucket = Option.unwrap(bucket);
    // Create a new post
    let post: PostImmutable = postDirectory.createPost(caller, Principal.fromActor(this), content, Time.now(), _bucket);

    // Send the post content to the public area of the Bucket 
    let bucketActor: BucketActor = actor(Principal.toText(_bucket));
    assert(await bucketActor.storeFeed(post));

    // Notify PostFetch that there is a new post
    let postFetchActor: PostFetchActor = actor(Principal.toText(postFetchCanister));
    await postFetchActor.receiveNotify(followers, post.postId);

    post.postId
};

Creating reposts.

public shared({caller}) func createRepost(postId: Text): async Bool {
    switch(postDirectory.createRepost(caller, postId, Time.now())) {
        case(null) { return false; };
        case(?(_bucket, _newRepost)) {
            // Notify the bucket to update repost information
            let bucketActor: BucketActor = actor(Principal.toText(_bucket));
            assert(await bucketActor.updatePostRepost(postId, _newRepost));

            // Get the followers of the one who reposted
            let userActor: UserActor = actor(Principal.toText(userCanister));
            let _repostUserFollowers = await userActor.getFollowersList(caller);

            // Notify PostFetch
            let postFetchActor: PostFetchActor = actor(Principal.toText(postFetchCanister));
            await postFetchActor.receiveNotify(_repostUserFollowers, postId);
            return true;
        };
    };
};

Comments and likes.

We use the postDirectory object to create comments or likes.

// Shared function, caller information needs to be provided
public shared({caller}) func createComment(postId: Text, content: Text): async Bool {
    // Match the return value of postDirectory.createComment
    switch(postDirectory.createComment(caller, postId, content, Time.now())) {
        // Creation failed
        case(null) { return false; };
        // If it returns a tuple containing _bucket and _newComment, it means the comment was created successfully, and the following actions are performed
        case(?(_bucket, _newComment)) {
            // Notify the corresponding bucket to update comments
            let bucketActor: BucketActor = actor(Principal.toText(_bucket));
            // The assert keyword is used to ensure the success of the update operation
            assert(await bucketActor.updatePostComment(postId, _newComment));
            return true;
        };
    };
};

public shared({caller}) func createLike(postId: Text): async Bool {
    switch(postDirectory.createLike(caller, postId, Time.now())) {
        case(null) { return false; };
        case(?(_bucket, _newLike)) {
            // Notify the bucket to update like information
            let bucketActor: BucketActor = actor(Principal.toText(_bucket));
            assert(await bucketActor.updatePostLike(postId, _newLike));
            return true;
        };
    };
};

Feed

Define some type aliases and variables.

type PostImmutable = Types.PostImmutable;
type FeedActor = Types.FeedActor;
type UserActor = Types.UserActor;
type CommentFetchActor = Types.CommentFetchActor;
type LikeFetchActor = Types.LikeFetchActor;

stable var feedMapEntries: [(Text, PostImmutable)] = [];
let feedDirectory = Database.FeedDirectory(feedMapEntries);

In addition to publishing posts, comments, and likes, the Feed also needs to receive notifications from other Fetch Canisters to update the internal feed.

public shared({caller}) func receiveFeed(postId: Text): async Bool {
    let (_bucket, _, _) = Utils.checkPostId(postId);
    let bucketActor: BucketActor = actor(Principal.toText(_bucket));
    switch((await bucketActor.getPost(postId))) {
        case(null) { return false; };
        case(?_post) {
            feedDirectory.storeFeed(_post);
            return true;
        };
    };
};

Sometimes the Feed needs to receive a lot of posts at once, so we also need a function for batch receiving posts.

It receives an array containing multiple post IDs, retrieves post information from the corresponding Bucket for each post ID, and stores it in the feedDirectory if the post exists.

public shared({caller}) func batchReceiveFeed(postIdArray: [Text]): async () {
    for(_postId in postIdArray.vals()) {
        let (_bucket, _, _) = Utils.checkPostId(_postId);
        let bucketActor: BucketActor = actor(Principal.toText(_bucket));
        switch((await bucketActor.getPost(_postId))) {
            case(null) { };
            case(?_post) {
                feedDirectory.storeFeed(_post);
            };
        };
    };
};

Receive comments and notify the followers of the user based on the repost information of the post. If the post does not exist, the function returns false.

public shared({caller}) func receiveComment(postId: Text): async Bool {
    let (_bucket, _, _) = Utils.checkPostId(postId);
    let bucketActor: BucketActor = actor(Principal.toText(_bucket));
    switch((await bucketActor.getPost(postId))) {
        case(null) { return false; };
        case(?_post) {

            feedDirectory.storeFeed(_post);

            if(Utils._isRepostUser(_post, owner)) {
                // If the user is the repost user of this post, continue pushing to their followers                    
                let userActor: UserActor = actor(Principal.toText(userCanister));
                let repostUserFollowers = await userActor.getFollowersList(owner);

                let commentFetchActor: CommentFetchActor = actor(Principal.toText(commentFetchCanister));
                await commentFetchActor.receiveRepostUserNotify(repostUserFollowers, postId);
            };

            return true;
        };
    };
};

Batch receive comments.

public shared({caller}) func batchReceiveComment(postIdArray: [Text]): async () {
    for(_postId in postIdArray.vals()) {
        let (_bucket, _, _) = Utils.checkPostId(_postId);
        let bucketActor: BucketActor = actor(Principal.toText(_bucket));
        switch((await bucketActor.getPost(_postId))) {
            case(null) { };
            case(?_post) {
                feedDirectory.storeFeed(_post);

                if(Utils._isRepostUser(_post, owner)) {
                    // If the user is the repost user of this post, continue pushing to their followers                
                    let userActor: UserActor = actor(Principal.toText(userCanister));
                    let repostUserFollowers = await userActor.getFollowersList(owner);

                    let commentFetchActor: CommentFetchActor = actor(Principal.toText(commentFetchCanister));
                    await commentFetchActor.receiveRepostUserNotify(repostUserFollowers, _postId);
                };
            };
        };
    };
};

Receive likes.

public shared({caller}) func receiveLike(postId: Text): async Bool {
    let (_bucket, _, _) = Utils.checkPostId(postId);
    let bucketActor: BucketActor = actor(Principal.toText(_bucket));
    switch((await bucketActor.getPost(postId))) {
        case(null) { return false; };
        case(?_post) {

            feedDirectory.storeFeed(_post);

            if(Utils._isRepostUser(_post, owner)) {
                // If the user is the repost user of this post, continue pushing to their followers                    
                let userActor: UserActor = actor(Principal.toText(userCanister));
                let repostUserFollowers = await userActor.getFollowersList(owner);

                let likeFetchActor: LikeFetchActor = actor(Principal.toText(likeFetchCanister));
                await likeFetchActor.receiveRepostUserNotify(repostUserFollowers, postId);
            };

            return true;
        };
    };
};

Batch receive likes.

public shared({caller}) func batchReceiveLike(postIdArray: [Text]): async () {
    for(_postId in postIdArray.vals()) {
        let (_bucket,

 _, _) = Utils.checkPostId(_postId);
        let bucketActor: BucketActor = actor(Principal.toText(_bucket));
        switch((await bucketActor.getPost(_postId))) {
            case(null) {};
            case(?_post) {

                feedDirectory.storeFeed(_post);

                if(Utils._isRepostUser(_post, owner)) {
                    // If the user is the repost user of this post, continue pushing to their followers                    
                    let userActor: UserActor = actor(Principal.toText(userCanister));
                    let repostUserFollowers = await userActor.getFollowersList(owner);

                    let likeFetchActor: LikeFetchActor = actor(Principal.toText(likeFetchCanister));
                    await likeFetchActor.receiveRepostUserNotify(repostUserFollowers, _postId);
                };
            };
        };
    };
};

Finally, there are some query functions.

public query func getFeedNumber(): async Nat {
    feedDirectory.getFeedNumber()
};

public query func getFeed(postId: Text): async ?PostImmutable {
    feedDirectory.getFeed(postId)
};

public query func getLatestFeed(n: Nat): async [PostImmutable] {
    feedDirectory.getLatestFeed(n)
};

You can view the complete feed.mo file here.