Feed
在 Feed 模块,我们有以下几个文件:
types.mo 文件
定义了整个系统中使用的主要数据类型别名。它导入了一个叫 Types 的模块,然后重新导出该模块中定义的类型,如帖子(Post)、评论(Comment)、点赞(Like)等。
rootFeed.mo 文件
它负责统计和创建用户的 Feed 。
createFeedCanister函数允许用户创建自己的信息流 Canister(容器),这是用户在平台上存储帖子和信息流的地方。updateFetchUserToFeed函数负责在用户创建新的信息流 Canister 时更新其他系统组件,如帖子、评论和点赞的抓取服务。getUserFeedCanister和getAllUserFeedCanister函数提供了检索用户信息流 Canister 的方式。
database.mo 文件
实现了一个帖子目录(PostDirectory)和一个信息流目录(FeedDirectory),它们是用于存储和管理帖子及其相关动作(如评论、点赞和转发)的数据库。
PostDirectory类中有一个postMap,它是一个使用TrieMap实现的键值对集合,用于存储帖子及其索引。createPost函数实现了发帖的功能。它创建一个新帖子并将其添加到postMap中,同时将帖子索引递增。getPost函数允许通过帖子 ID 检索帖子。createComment、createLike和createRepost分别用于创建评论、点赞和转发。getAllPost函数可以获取所有帖子,并按创建时间排序。
FeedDirectory 类则负责管理用户的信息流。它使用 TrieMap 存储和检索用户信息流中的帖子。
feed.mo 文件
Feed 代表了社交媒体平台中的用户信息流。它用前面数据库文件定义的 PostDirectory 和 FeedDirectory 来管理帖子和信息流。
Feed类中有多个函数,createPost、createRepost、createComment和createLike,它们实现了用户在社交媒体上的基本互动。receiveFeed、batchReceiveFeed、receiveComment、batchReceiveComment、receiveLike和batchReceiveLike函数用于接收其他用户的帖子、评论和点赞,并将这些活动加入到当前用户的信息流中。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 步:
- 从 Root Fetch Canister 获取所有 Post 、Comment 、Like Fetch Canister 的 ID 。
- 遍历每个 Fetch Canister ,创建 Actor 引用。
- 调用每个 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.map对postMap中的每一对键值进行映射,将其转换为不可变的PostImmutable类型。这个映射是通过Nat类型的键来执行的,使用Nat.equal和Hash.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 的完整文件。