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 的完整文件。