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
andgetAllUserFeedCanister
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 apostMap
, which is a key-value collection implemented usingTrieMap
to store posts and their indices. - The
createPost
function implements the posting feature. It creates a new post and adds it to thepostMap
while incrementing the post index. - The
getPost
function allows retrieving posts by post ID. - The
createComment
,createLike
, andcreateRepost
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 ascreatePost
,createRepost
,createComment
, andcreateLike
, which implement basic user interactions on social media. - Functions like
receiveFeed
,batchReceiveFeed
,receiveComment
,batchReceiveComment
,receiveLike
, andbatchReceiveLike
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:
- Retrieve the IDs of all Post, Comment, and Like Fetch Canisters from the Root Fetch Canister.
- Iterate over each Fetch Canister, create an actor reference.
- 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 inpostMap
, converting it to an immutable typePostImmutable
. This mapping is done using theNat
type for keys, handling key equality and hashing withNat.equal
andHash.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
). UsesOrder.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.