Feed

在 Feed 模块,我们有以下几个文件:

types.mo 文件

定义了整个系统中使用的主要数据类型别名。它导入了一个叫 Types 的模块,然后重新导出该模块中定义的类型,如帖子(Post)、评论(Comment)、点赞(Like)等。

rootFeed.mo 文件

它负责统计和创建用户的 Feed 。

  • createFeedCanister 函数允许用户创建自己的信息流 Canister(容器),这是用户在平台上存储帖子和信息流的地方。
  • updateFetchUserToFeed 函数负责在用户创建新的信息流 Canister 时更新其他系统组件,如帖子、评论和点赞的抓取服务。
  • getUserFeedCanistergetAllUserFeedCanister 函数提供了检索用户信息流 Canister 的方式。

database.mo 文件

实现了一个帖子目录(PostDirectory)和一个信息流目录(FeedDirectory),它们是用于存储和管理帖子及其相关动作(如评论、点赞和转发)的数据库。

  • PostDirectory 类中有一个 postMap,它是一个使用 TrieMap 实现的键值对集合,用于存储帖子及其索引。
  • createPost 函数实现了发帖的功能。它创建一个新帖子并将其添加到 postMap 中,同时将帖子索引递增。
  • getPost 函数允许通过帖子 ID 检索帖子。
  • createCommentcreateLikecreateRepost 分别用于创建评论、点赞和转发。
  • getAllPost 函数可以获取所有帖子,并按创建时间排序。

FeedDirectory 类则负责管理用户的信息流。它使用 TrieMap 存储和检索用户信息流中的帖子。

feed.mo 文件

Feed 代表了社交媒体平台中的用户信息流。它用前面数据库文件定义的 PostDirectoryFeedDirectory 来管理帖子和信息流。

  • Feed 类中有多个函数,createPostcreateRepostcreateCommentcreateLike,它们实现了用户在社交媒体上的基本互动。
  • receiveFeedbatchReceiveFeedreceiveCommentbatchReceiveCommentreceiveLikebatchReceiveLike 函数用于接收其他用户的帖子、评论和点赞,并将这些活动加入到当前用户的信息流中。
  • getLatestFeed 函数允许用户检索他们信息流中的最新帖子。

创建FeedCanister:rootFeed.mo

管理用户的 Feed Canister :

  • 定义存储用户和对应 Feed Canister 的映射关系的 TrieMap - userFeedCanisterMap

  • 提供创建用户个人 Feed Canister 的方法 - createFeedCanister()

  • 提供用户 Feed Canister 的方法 - getUserFeedCanister()

  • 提供获取所有用户 Feed 映射的方法 - getAllUserFeedCanister()

  • 提供获取总共创建的 Feed Canister 数量的方法 - getTotalUserFeedCanisterNumber()

管理 Feed 系统的其他 Canister :

  • 存储和提供接口查询 / 更新 Post 、Comment 、Like 的 Fetch Canister

  • 在创建用户 Feed 时,同步更新这些 Fetch Canister 中的映射关系


首先依然是定义 Feed 系统需要的一些基础类型和数据结构,包括用于获取 Feed 的数据的 Actor ,用于存储映射关系的 TrieMap 。

// 自定义的Actor类型,用于获取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万亿个cycles,1T,方便后续操作

// 一个存储键值对的数组,键是用户Principal,值是对应的 feed canister 的Principal
stable var userFeedCanisterMapEntries: [(Principal, Principal)] = [];
// 一个从用户Principal到 feed canister Principal 的TrieMap
// 通过fromEntries构造,传入userFeedCanisterMapEntries
let userFeedCanisterMap = TrieMap.fromEntries<Principal, Principal>(userFeedCanisterMapEntries.vals(), Principal.equal, Principal.hash);
let ic: IC.Service = actor("aaaaa-aa");

TrieMap 是一种键值存储结构,它可以高效地插入和查找数据。

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

这里创建了一个 TrieMap,它用来存储帖子数据,其中 Nat 是键的类型,Post 是值的类型。


为用户创建自己的 Feed Canister 的逻辑:给用户创建自己的 Feed Canister ,并做身份验证、授权设置、数据映射关系同步等工作。

// 给用户创建一个用户自己的Canister
public shared({caller}) func createFeedCanister(): async ?Principal {
    // 检查调用者身份
    assert(_getUserFeedCanister(caller) == null);
    // 给每个新Canister分配 2T Cycles
    Cycles.add(2 * T_CYCLES);
    // 调用 `Feed.Feed` 方法创建用户的 Feed Canister
    let feedCanister = await Feed.Feed(
        caller, rootPostCanister, userCanister, 
        postFetchCanister,
        commentFetchCanister,
        likeFetchCanister
    );
    // 获取Canister的ID,存入映射TrieMap中
    let feedCanisterId = Principal.fromActor(feedCanister);
    userFeedCanisterMap.put(caller, feedCanisterId);
    // 调用ic方法更新该Canister的设置,授权Root Canister和用户自己作为controllers
    await ic.update_settings({
        canister_id = feedCanisterId;
        settings = {
            freezing_threshold = null;
            controllers = ?[Principal.fromActor(this), feedCanisterId];
            memory_allocation = null;
            compute_allocation = null;
        }
    });
    
    // 更新fetch中的信息,在 Post/Comment/Like Fetch Canister 中也更新该用户的映射关系
    ignore updateFetchUserToFeed((caller, feedCanisterId));
    
    // 返回新创建的 Feed Canister ID
    ?feedCanisterId
};

当有用户 Feed Canister 创建时,同步到其他依赖这个映射关系的 Canister ,保证 Feed 系统的数据一致性。

总共分 3 步:

  1. 从 Root Fetch Canister 获取所有 Post 、Comment 、Like Fetch Canister 的 ID 。
  2. 遍历每个 Fetch Canister ,创建 Actor 引用。
  3. 调用每个 Fetch Canister 的 addUserToFeedEntry 方法,传入用户和对应 Feed Canister 的映射关系。
func updateFetchUserToFeed(entry: (Principal, Principal)): async () {
    let rootFetchActor: RootFetchActor = actor(Principal.toText(rootFetchCanister));

    // 更新 postFetch 中的信息
    let postFetchCanisterArray = await rootFetchActor.getAllPostFetchCanister();
    for(_canister in postFetchCanisterArray.vals()) {
        let postFetchActor: PostFetchActor = actor(Principal.toText(_canister));
        ignore postFetchActor.addUserToFeedEntry(entry);
    };

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

这几个函数是用于查询用户 Feed Canister 映射关系的:

按用户查询、获取全部映射、获取总数等不同粒度的接口,可以查询用户 Feed Canister 的映射关系,用于读取当前的 Feed 系统内部状态。

// 接收一个用户Principal作为参数
public query func getUserFeedCanister(user: Principal): async ?Principal {
    // 调用私有函数,返回该用户对应的 Feed Canister Principal
    _getUserFeedCanister(user)
};

// return [(user, feedCanister)]
public query func getAllUserFeedCanister(): async [(Principal, Principal)] {
    // 将内部的userFeedCanisterMap转换成数组
    // 返回所有用户到Feed Canister的映射关系数组
    Iter.toArray(userFeedCanisterMap.entries())
};

// 总共创建了多少个Canister
public query func getTotalUserFeedCanisterNumber(): async Nat {
    // 返回userFeedCanisterMap的大小,也就是全部 Feed Canister 的数量
    userFeedCanisterMap.size()
};

// 内部私有方法,查询用户的Feed
private func _getUserFeedCanister(user: Principal): ?Principal {
    // 查询userFeedCanisterMap
    switch(userFeedCanisterMap.get(user)) {
        case(null) { return null;};
        // 根据给定用户Principal,返回对应的 Feed Canister Id
        case(?canister) { return ?canister;};
    };
};

在创建 Bucket 时,我们还得告诉 Bucket 系统中的 Post Fetch 、Comment Fetch 和 Like Fetch 的 Canister ID ,所以在 Root Post 中,我们还要记录、保存 Post Fetch 、Comment Fetch 和 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 := [];
};

最后是两个系统函数 preupgrade()postupgrade() ,用来在 Canister 升级前后保存数据。


存储数据:database.mo

database.mo 文件负责存储用户的 Feed Canister 里的数据。

首先还是定义帖子索引和映射关系的存储结构,以及相关的查询接口。

postIndex 用于生成帖子唯一 ID ;postMap 存储帖子数据。getPostIndexEntries 和 getPostMapEntries 用于查询帖子的索引范围和映射关系。

// 定义一些与帖子相关的类型别名
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;
// 一个从帖子索引到帖子的映射表,使用TrieMap实现
let postMap = TrieMap.fromEntries<Nat, Post>(postMapEntries.vals(), Nat.equal, Hash.hash); // postIndex -> Post

// 返回当前的最大帖子索引值
public func getPostIndexEntries(): Nat { postIndex };

// 返回postMap中的全部映射关系数组
public func getPostMapEntries(): [(Nat, Post)] { Iter.toArray(postMap.entries()) };

// 用于生成帖子的唯一id,格式为: bucket#user#index
private func _getPostId(bucket: Principal, user: Principal, index: Nat): Text {
    Principal.toText(bucket) # "#" # Principal.toText(user) # "#" # Nat.toText(index)
};

发帖

在 Feed Canister 中发帖的逻辑:

实现对帖子信息的构造,并存储到帖子映射表中。

同时返回了一个不可变的帖子对象,避免帖子被修改。

通过 postIndex 的自增可以保证每篇帖子拥有一个全局唯一的 ID 。

帖子数据被存储在 TrieMap 中,可以高效查询。

// 发帖
public func createPost(user: UserId, feedCanister: Principal, content: Text, time: Time, bucket: Principal): PostImmutable {
    let post: Post = {
        // 生成帖子的唯一id
        postId = _getPostId(bucket, user, postIndex);
        // 构造一个Post记录,包括帖子内容、用户信息、时间等字段
        feedCanister = feedCanister;
        index = postIndex;
        user = user;
        content = content;
        var repost = [];
        var like = [];
        var comment = [];
        createdAt = time;
    };
    
    // 将这个Post记录放入postMap中
    postMap.put(postIndex, post);
    // postIndex自增
    postIndex += 1;

    // 返回PostImmutable
    Utils._convertPostToImmutable(post)
};

提供外部查询帖子的基础接口。

实现获取帖子总数和根据 id 获取单个帖子的功能。

// 获取帖子总数
public func getPostNumber(): Nat {
    // 直接调用postMap的size()
    postMap.size()
};

// 根据id获取单个帖子
public func getPost(postId: Text): ?PostImmutable {
    // 接收帖子id作为参数,首先调用checkPostId()校验id格式
    let (bucket, user, index) = utils.checkPostId(postId);
    // 从postMap中根据索引取出帖子记录post
    switch(postMap.get(index)) {
        // 如果不存在,返回null
        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;
            }
        };
    };
};

评论

返回包含 Bucket 和更新后的帖子评论数组的元组。

所以这个函数会检查帖子是否存在,如果存在就添加新评论,并返回包含帖子所在桶和更新后的帖子信息的元组。

主要逻辑是检查参数,获取原帖子信息,更新帖子评论,返回更新后的帖子评论。

// 评论
public func createComment(commentUser: UserId, postId: Text, content: Text, createdAt: Time): ?(Principal, NewComment) {
    // 检查帖子ID是否有效,并返回帖子所在的bucket、用户和帖子在数组中的索引
    let (bucket, user, index) = utils.checkPostId(postId);
    // 获取要评论的帖子post,如果不存在则返回null
    switch(postMap.get(index)) {
        case(null) { return null;};
        case(?post) {
            // 如果帖子存在,则将新评论添加到帖子的comment数组中
            // 评论内容包含用户ID、评论内容和时间
            post.comment := Array.append(post.comment, [{
            user = commentUser; // 发表评论的用户ID
            content = content; // 评论内容
            createdAt = createdAt; // 评论时间
        }]);
            ?(bucket, post.comment)
        };
    };
};

点赞

其实和前面的评论差不多。都是通过 postMap.get(index) 实现。

// 点赞
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)
        };
    }
};

查询所有帖子

函数 getAllPost,该函数从一个映射 (postMap) 中获取所有的帖子 (Post),将它们转换成不可变的形式 (PostImmutable),并按照创建时间进行排序。最后,返回排序后的帖子数组。

  • TrieMap.map

    使用 TrieMap.mappostMap 中的每一对键值进行映射,将其转换为不可变的 PostImmutable 类型。这个映射是通过 Nat 类型的键来执行的,使用 Nat.equalHash.hash 来处理键的相等性和哈希。

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

    获取映射的所有值,返回一个包含所有转换后的 PostImmutable 的数组。

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

    对值数组进行排序,按照帖子的创建时间 (createdAt) 进行比较。使用 Order.Order 来指定排序的顺序,其中 #less 表示升序,#greater 表示降序,#equal 表示相等。

    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

    将排序后的帖子数组转换为一个 Motoko 数组,最终作为函数的返回值。

    Iter.toArray(...)
    

整个函数的目的是获取映射中的所有帖子,将它们转换为不可变的形式,并按照创建时间排序,最后返回排序后的帖子数组。

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 类

废话不用多说,直接看代码:

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

    // 使用TrieMap类型创建了一个映射,键是Text类型,值是PostImmutable类型
    // 用于存储帖子数据
    let feedMap = TrieMap.fromEntries<Text, PostImmutable>(feedMapEntries.vals(), Text.equal, Text.hash);

    // 返回包含所有映射项的数组
    public func getFeedMapEntries(): [(Text, PostImmutable)] { Iter.toArray(feedMap.entries()) };

    // 将帖子添加到映射中,键是帖子的postId
    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()
    };

    // 根据给定的postId返回相应的帖子
    // 如果帖子不存在,返回null
    public func getFeed(postId: Text): ?PostImmutable {
        switch(feedMap.get(postId)) {
            case(null) { return null; };
            case(?_feed) { return ?_feed; };
        };
    };

    // 返回最新的n个帖子,按照创建时间排序
    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())
        }
    };

};

database.mo 完整文件:

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())
      }
    };

  };
};

用户云终端:feed.mo


owner

Feed Canister 里需要存储 owner 是谁,以及后期可以转移自己的控制权。

stable var owner = _owner;

// 查询owner,允许合约的用户异步地获取当前的owner
// 由于是查询函数,它不修改合约状态,因此可以被任何用户调用而不需要经过共识
public query func getOwner(): async Principal { owner };

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

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

FetchCanister

同样,Feed Canister 里还需要记录各种 Fetch Canister 。

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

Feed Canister 里同样维护着一个粉丝列表。在用户发帖时,Feed Canister 会把帖子 ID 和粉丝发给 Fetch Canister ,告诉 Fetch 应该通知哪些人。

在紧急情况下,可以直接点对点向粉丝发送帖子,也可以用来点对点留言。

stable var followers: [Principal] = [];

// 接收 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));

// 更新当前feed去存储的bucket canister
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

然后是和帖子有关的功能。

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);

// 查询用户发了多少帖子(统计总数)
public query func getPostNumber(): async Nat {
    postDirectory.getPostNumber()
};

// 根据帖子ID查询用户发的某个帖子
public query func getPost(postId: Text): async  ?PostImmutable {
    postDirectory.getPost(postId)
};

// 查询所有帖子
public query func getAllPost(): async [PostImmutable] {
    postDirectory.getAllPost()
};

用户发帖的核心逻辑。

public shared({caller}) func createPost(title: Text, content: Text): async Text {
    // 检查是否由信息流的所有者调用,确保bucket(用于存储帖子的地方)是存在的
    assert(caller == owner and bucket != null);
    let _bucket = Option.unwrap(bucket);
    // 创建一个新帖子
    let post: PostImmutable = postDirectory.createPost(caller, Principal.fromActor(this), content, Time.now(), _bucket);

    // 将帖子内容发送给公共区的Bucket 
    let bucketActor: BucketActor = actor(Principal.toText(_bucket));
    assert(await bucketActor.storeFeed(post));

    // 通知PostFetch有新帖子发布
    let postFetchActor: PostFetchActor = actor(Principal.toText(postFetchCanister));
    await postFetchActor.receiveNotify(followers, post.postId);

    post.postId
};

创建转发。

public shared({caller}) func createRepost(postId: Text): async Bool {
    switch(postDirectory.createRepost(caller, postId, Time.now())) {
        case(null) { return false; };
        case(?(_bucket, _newRepost)) {
            // 通知bucket更新转发信息
            let bucketActor: BucketActor = actor(Principal.toText(_bucket));
            assert(await bucketActor.updatePostRepost(postId, _newRepost));

            // 获取转发者的粉丝
            let userActor: UserActor = actor(Principal.toText(userCanister));
            let _repostUserFollowers = await userActor.getFollowersList(caller);

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

评论与点赞。

我们使用 postDirectory 对象来创建评论或点赞。

// 共享函数,需要提供调用者的信息(caller)
public shared({caller}) func createComment(postId: Text, content: Text): async Bool {
    // 根据postDirectory.createComment的返回值进行匹配处理
    switch(postDirectory.createComment(caller, postId, content, Time.now())) {
        // 创建失败
        case(null) { return false; };
        // 如果返回一个包含_bucket和_newComment的元组,表示成功创建评论,进行以下处理
        case(?(_bucket, _newComment)) {
            // 通知对应的bucket更新评论
            let bucketActor: BucketActor = actor(Principal.toText(_bucket));
            // assert关键字用于确保更新操作成功
            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)) {
            // 通知bucket更新点赞信息
            let bucketActor: BucketActor = actor(Principal.toText(_bucket));
            assert(await bucketActor.updatePostLike(postId, _newLike));
            return true;
        };
    };
};

Feed

定义一些类型别名和变量。

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);

除了 Feed 发布帖子、评论、点赞以外,Feed 还需要接收其他 Fetch的通知,更新 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;
        };
    };
};

有时候 Feed 得一次性接收很多个帖子,所以我们还需要一个批量接收帖子函数。

接收一个包含多个帖子 ID 的数组,针对每个帖子 ID 从相应的 Bucket 中获取帖子信息,如果帖子存在,则将其存储到 feedDirectory 中。

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);
            };
        };
    };
};

接收评论,并根据帖子的转发信息通知相应的用户粉丝。如果帖子不存在,函数返回 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)) {
                // 如果该用户是此贴的转发者,则继续向自己的粉丝推流                    
                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;
        };
    };
};

批量接收评论。

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) {
                // Debug.print("Canister Feed, Func batchReceiveComment");
                feedDirectory.storeFeed(_post);

                if(Utils._isRepostUser(_post, owner)) {
                    // 如果该用户是此贴的转发者,则继续向自己的粉丝推流                
                    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);
                };
            };
        };
    };
};

接收点赞。

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)) {
                // 如果该用户是此贴的转发者,则继续向自己的粉丝推流                    
                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;
        };
    };
};

批量接收点赞。

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)) {
                    // 如果该用户是此贴的转发者,则继续向自己的粉丝推流                    
                    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);
                };
            };
        };
    };
};

最后是一些查询函数。

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)
};

这里可以查看 feed.mo 的完整文件。