为 Bluesky Social 创建机器人
机器人将如何运作
我们将为社交网络Bluesky开发一个机器人,我们将使用 Golang 来实现这一点。该机器人将通过 websocket 监控一些话题标签,
如果找到其中一个话题标签,它将转发并点赞原帖。
我们将介绍一些非常酷的东西,例如 websocket、AT(bluesky 使用的协议)、CAR(内容可寻址归档)和CBOR(简洁二进制对象表示)是两种用于高效存储和传输数据的格式。
项目结构
该项目将采用简单的结构,其中包含internal一个名为 `<package_name>` 的包,该包bot包含运行机器人所需的所有代码,
其中utils还包含一些帮助我们的函数。
文件.env中将包含访问API 的bluesky 凭据。
设置凭据
要对 bluesky API 进行身份验证,我们需要提供一个账号identifier和一个密码password,但是我们不能使用密码来访问我们的账号,
为此,我们将创建一个应用密码,只需访问 bluesky 中的账号,访问设置,然后选择应用密码。
将生成的密码放入.env文件中,如下所示:
BLUESKY_IDENTIFIER=<your_identifier>
BLUESKY_PASSWORD=<your_app_password>
生成 API 令牌
每当我们的机器人识别出我们正在监控的新话题标签时,它都会做出回复,但是我们需要 Bearer 令牌才能进行转发,
我们将创建一个生成令牌的函数,我们将在get-token.go文件中执行此操作。
首先,我们定义一个用于存储 API URL 的全局变量。
var (
API_URL = "https://bsky.social/xrpc"
)
现在我们定义一个结构体,其中包含 API 将返回的数据。
type DIDDoc struct {
Context []string `json:"@context"`
ID string `json:"id"`
AlsoKnownAs []string `json:"alsoKnownAs"`
VerificationMethod []struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
} `json:"verificationMethod"`
Service []struct {
ID string `json:"id"`
Type string `json:"type"`
ServiceEndpoint string `json:"serviceEndpoint"`
} `json:"service"`
}
type DIDResponse struct {
DID string `json:"did"`
DIDDoc DIDDoc `json:"didDoc"`
Handle string `json:"handle"`
Email string `json:"email"`
EmailConfirmed bool `json:"emailConfirmed"`
EmailAuthFactor bool `json:"emailAuthFactor"`
AccessJwt string `json:"accessJwt"`
RefreshJwt string `json:"refreshJwt"`
Active bool `json:"active"`
}
现在我们将创建一个getToken返回 a 的函数DIDResponse(你可以给它起任何你想要的名字)。
func getToken() (*DIDResponse, error) {
requestBody, err := json.Marshal(map[string]string{
"identifier": os.Getenv("BLUESKY_IDENTIFIER"),
"password": os.Getenv("BLUESKY_PASSWORD"),
})
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
url := fmt.Sprintf("%s/com.atproto.server.createSession", API_URL)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var tokenResponse DIDResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &tokenResponse, nil
}
该函数调用 bluesky 端点com.atproto.server.createSession,我们将收到一些数据,但现在重要的是accessJwt我们需要通过 Bearer 授权我们的机器人,有了它,生成令牌的函数就准备就绪了。
创建 WebSocket
这将是机器人最复杂的功能,我们需要调用 bluesky 端点。
首先,我们创建一个变量来保存端点,更多信息请参见文档。
var (
wsURL = "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos"
)
现在让我们创建结构体:
type RepoCommitEvent struct {
Repo string `cbor:"repo"`
Rev string `cbor:"rev"`
Seq int64 `cbor:"seq"`
Since string `cbor:"since"`
Time string `cbor:"time"`
TooBig bool `cbor:"tooBig"`
Prev interface{} `cbor:"prev"`
Rebase bool `cbor:"rebase"`
Blocks []byte `cbor:"blocks"`
Ops []RepoOperation `cbor:"ops"`
}
type RepoOperation struct {
Action string `cbor:"action"`
Path string `cbor:"path"`
Reply *Reply `cbor:"reply"`
Text []byte `cbor:"text"`
CID interface{} `cbor:"cid"`
}
type Reply struct {
Parent Parent `json:"parent"`
Root Root `json:"root"`
}
type Parent struct {
Cid string `json:"cid"`
Uri string `json:"uri"`
}
type Root struct {
Cid string `json:"cid"`
Uri string `json:"uri"`
}
type Post struct {
Type string `json:"$type"`
Text string `json:"text"`
Reply *Reply `json:"reply"`
}
我们还将使用Gorilla Websocket软件包,请使用以下命令下载该软件包:
go get github.com/gorilla/websocket
该Websocket函数最初的样子是这样的:
func Websocket() error {
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
slog.Error("Failed to connect to WebSocket", "error", err)
return err
}
defer conn.Close()
for {
_, message, err := conn.ReadMessage()
if err != nil {
slog.Error("Error reading message from WebSocket", "error", err)
continue
}
}
}
这样我们现在就可以读取通过 websocket 接收的无限长的消息for,但是这些消息是用CBOR编码的。
CBOR是什么?
CBOR(简洁二进制对象表示法)是一种二进制数据格式,用于以紧凑高效的方式表示数据。
它类似于JSON,但它不使用人类可读的文本,而是使用二进制字节,这使得数据体积更小,传输和处理速度更快。
要解码它,我们需要使用这个软件包。
decoder := cbor.NewDecoder(bytes.NewReader(message))
只需将其message转换为reader,像这样:
func Websocket() error {
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
slog.Error("Failed to connect to WebSocket", "error", err)
return err
}
defer conn.Close()
slog.Info("Connected to WebSocket", "url", wsURL)
for {
_, message, err := conn.ReadMessage()
if err != nil {
slog.Error("Error reading message from WebSocket", "error", err)
continue
}
decoder := cbor.NewDecoder(bytes.NewReader(message))
for {
var evt RepoCommitEvent
err := decoder.Decode(&evt)
if err == io.EOF {
break
}
if err != nil {
slog.Error("Error decoding CBOR message", "error", err)
break
}
}
}
}
-
decoder.Decode(&evt)解码器负责读取接收到的数据,并将其从 CBOR 格式解码为目标RepoCommitEvent类型。解码器evt存储解码后的数据。 -
if err == io.EOF { break }如果解码器到达数据末尾(没有更多消息),它将返回io.EOF(文件结束)。此时,循环将被中断break,因为没有更多数据需要处理。
创建 handleEvent
让我们创建一个函数来处理该事件:
func handleEvent(evt RepoCommitEvent) error {
for _, op := range evt.Ops {
if op.Action == "create" {
if len(evt.Blocks) > 0 {
err := handleCARBlocks(evt.Blocks, op)
if err != nil {
slog.Error("Error handling CAR blocks", "error", err)
return err
}
}
}
}
return nil
}
-
evt参数:该函数接收一个evt参数,该参数是一个事件类型RepoCommitEvent。此事件包含一个操作列表Ops,以及可能Blocks与这些操作相关的数据块。 -
循环遍历
Ops:evt事件可以包含多个操作。代码使用循环遍历每个操作for _, op := range evt.Ops。 -
检查
op.Action == "create"操作:对于每个操作,代码都会检查关联的操作是否为create,也就是说,该操作是否在 bluesky 中创建新内容,例如帖子或其他类型的内容。 -
如果存在数据块
len(evt.Blocks) > 0:如果检测到创建操作,代码会检查事件是否包含Blocks数据块。这些数据块包含可能与该操作相关的附加信息。 -
处理
handleCARBlocks数据块:如果存在数据块,handleCARBlocks则会调用该函数来处理这些数据块。该函数负责解释数据块内的数据(我们将在下文介绍 CAR)。
CAR是什么?
CAR(内容寻址归档)是一种归档格式,它利用内容寻址高效安全地存储数据。这意味着每条数据都由其内容而非特定位置来标识。
以下是一个简单的解释:
内容通过哈希值标识:CAR 文件中的每个数据块都由一个哈希值(根据数据内容生成的唯一标识符)标识。这确保了同一段数据始终具有相同的标识符。
CAR 广泛应用于 IPFS 和 IPLD 等系统中:CAR 广泛应用于 IPFS(星际文件系统)和 IPLD(星际链接数据)等系统中,在这些系统中,数据是根据内容而不是像 bluesky 那样根据位置在网络上进行分发和检索的。
数据块:CAR 文件可以存储多个数据块,并且可以使用其内容标识符 (CID) 单独检索每个数据块。
高效且安全:由于区块的标识符取决于其内容,因此很容易验证数据是否正确且未被篡改。
这是一个非常简单的解释,如果您想深入了解,我建议您访问这里。
创建 handleCARBlocks
这将是机器人最复杂的功能:
func handleCARBlocks(blocks []byte, op RepoOperation) error {
if len(blocks) == 0 {
return errors.New("no blocks to process")
}
reader, err := carv2.NewBlockReader(bytes.NewReader(blocks))
if err != nil {
slog.Error("Error creating CAR block reader", "error", err)
return err
}
for {
block, err := reader.Next()
if err == io.EOF {
break
}
if err != nil {
slog.Error("Error reading CAR block", "error", err)
break
}
if opTag, ok := op.CID.(cbor.Tag); ok {
if cidBytes, ok := opTag.Content.([]byte); ok {
c, err := decodeCID(cidBytes)
if err != nil {
slog.Error("Error decoding CID from bytes", "error", err)
continue
}
if block.Cid().Equals(c) {
var post Post
err := cbor.Unmarshal(block.RawData(), &post)
if err != nil {
slog.Error("Error decoding CBOR block", "error", err)
continue
}
if post.Text == "" || post.Reply == nil {
continue
}
if utils.FilterTerms(post.Text) {
repost(&post) // we will still create
}
}
}
}
}
return nil
}
我们仍然会创建该repost()函数,并将指向它的指针*Post作为参数传递。
请记住,我们的机器人只监控帖子评论。如果创建了一个帖子,并且插入了我们正在监控的标签,则不会进行转发。此
验证if post.Text == "" || post.Reply == nil将阻止转发,必须有标签,reply并且这种情况只会在帖子评论时发生。
该handleCARBlocks函数处理 CAR 格式的数据块。让我们一步一步地用简单的方式了解该函数的工作原理:
- 初始区块验证:
if len(blocks) == 0 {
return errors.New("no blocks to process")
}
如果数据块为空,则该函数返回错误,提示没有数据块要处理。
- 创建 CAR 块读取器:
reader, err := carv2.NewBlockReader(bytes.NewReader(blocks))
该函数创建一个块读取器来解析 CAR 文件中包含的数据,我们使用了carV2和go-cid软件包。
安装方法:
go install github.com/ipld/go-car/cmd/car@latest
go get github.com/ipfs/go-cid
- 阅读积木:
for {
block, err := reader.Next()
if err == io.EOF {
break
}
}
该函数进入循环,逐个读取所有数据块。当所有数据块都被读取完毕(即到达末尾)后,循环停止。
- 检查 CID:
if opTag, ok := op.CID.(cbor.Tag); ok {
if cidBytes, ok := opTag.Content.([]byte); ok {
c, err := decodeCID(cidBytes)
该函数检查操作中是否包含可解码的 CID(内容标识符)。此 CID 用于标识数据块的具体内容。
- 比较和解码积木:
if block.Cid().Equals(c) {
var post Post
err := cbor.Unmarshal(block.RawData(), &post)
如果读取的块与操作具有相同的 CID,则将块内容解码为函数可以理解的格式,例如“Post”。
- 筛选帖子:
if post.Text == "" || post.Reply == nil {
continue
}
if utils.FilterTerms(post.Text) {
repost(&post)
}
如果帖子包含文本和标签reply,则会使用名为 `filter` 的函数进行过滤FilterTerms。如果通过过滤,则会重新发布。
创建decodeCID
该decodeCID函数负责从一组字节中解码内容标识符 (CID)。它接收这些字节,并尝试将它们转换为可用于识别数据块的 CID。
func decodeCID(cidBytes []byte) (cid.Cid, error) {
var c cid.Cid
c, err := cid.Decode(string(cidBytes))
if err != nil {
return c, fmt.Errorf("error decoding CID: %w", err)
}
return c, nil
}
这样,我们就准备Websocket就绪了。
创建话题标签过滤器
让我们在内部创建以下utils内容filter-terms.go:
var (
terms = []string{"#hashtag2", "#hashtag1"}
)
func FilterTerms(text string) bool {
for _, term := range terms {
if strings.Contains(strings.ToLower(text), strings.ToLower(term)) {
return true
}
}
return false
}
在这个函数中,我们定义了要监控的标签,简单来说,我们接收text来自 websocket 的消息,并根据该消息进行过滤terms。
创建记录
让我们createRecord在create-record.go文件中创建一个名为 `repost` 的函数,该函数将负责根据$type通过参数发送的值来创建转发或点赞。
首先,我们创建一个包含所需参数的结构体:
type CreateRecordProps struct {
DIDResponse *DIDResponse
Resource string
URI string
CID string
}
DIDResponse我们将使用它来提取授权令牌。Resource它将用于决定我们是点赞还是转发。URI它将用于告知原始帖子的 URI。CID这是我们从 CAR 中提取出来用作标识符的信息。
最终函数将如下所示:
func createRecord(r *CreateRecordProps) error {
body := map[string]interface{}{
"$type": r.Resource,
"collection": r.Resource,
"repo": r.DIDResponse.DID,
"record": map[string]interface{}{
"subject": map[string]interface{}{
"uri": r.URI,
"cid": r.CID,
},
"createdAt": time.Now(),
},
}
jsonBody, err := json.Marshal(body)
if err != nil {
slog.Error("Error marshalling request", "error", err, "resource", r.Resource)
return err
}
url := fmt.Sprintf("%s/com.atproto.repo.createRecord", API_URL)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
slog.Error("Error creating request", "error", err, "r.Resource", r.Resource)
return nil
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.DIDResponse.AccessJwt))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
slog.Error("Error sending request", "error", err, "r.Resource", r.Resource)
return nil
}
if resp.StatusCode != http.StatusOK {
slog.Error("Unexpected status code", "status", resp, "r.Resource", r.Resource)
return nil
}
slog.Info("Published successfully", "resource", r.Resource)
return nil
}
很容易理解,我们POST向API_URL/com.atproto.repo.createRecord端点发送一个请求,告知我们将要创建一个记录,在请求中body我们$type告知 Bluesky API 我们将要创建的记录类型,然后我们组装请求,插入 bearer token,并进行一些错误处理,很简单,不是吗?
这样我们就可以使用该createRecord函数创建多个记录,只需更改其中的一个参数即可$type。
转发并点赞 Bluesky
准备就绪后createRecord,创建就很简单了repost,让我们在repost.go文件中执行此操作:
func repost(p *Post) error {
token, err := getToken()
if err != nil {
slog.Error("Error getting token", "error", err)
return err
}
resource := &CreateRecordProps{
DIDResponse: token,
Resource: "app.bsky.feed.repost",
URI: p.Reply.Root.Uri,
CID: p.Reply.Root.Cid,
}
err = createRecord(resource)
if err != nil {
slog.Error("Error creating record", "error", err, "resource", resource.Resource)
return err
}
resource.Resource = "app.bsky.feed.like"
err = createRecord(resource)
if err != nil {
slog.Error("Error creating record", "error", err, "resource", resource.Resource)
return err
}
return nil
}
*Post我们从函数中收到一个指向的指针Websocket(),我们设置通知,CreateRecordProps表明我们将通过该app.bsky.feed.repost资源进行重新发布,最后我们调用createRecord。
创建帖子后,我们会给它点赞(可选),只需createRecord再次调用,但这次要使用app.bsky.feed.like资源,因为我们在一个变量中创建了资源,只需设置一个新值,这就是我们所做的resource.Resource = "app.bsky.feed.like"。
这样一来,我们现在就可以进行转发和点赞了。
创建健康检查
这部分是可选的,它仅用于部署,托管服务会使用它来检查我们的机器人是否仍在运行,这是一个非常简单的端点,只返回一个状态码200。
让我们在文件中操作health-check.go:
func HealthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
该HealthCheck函数只返回一个w.WriteHeader(http.StatusOK),这可以直接在文件中完成main.go,我们将在该文件中启动我们的 Web 服务器,但我选择将其分开。
启动并运行机器人
好了,现在我们只需要让一切运行起来,让我们在……中完成main.go:
func main() {
slog.Info("Starting bot")
err := godotenv.Load()
if err != nil {
slog.Error("Error loading .env file")
}
go func() {
http.HandleFunc("/health", bot.HealthCheck)
slog.Info("Starting health check server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("Failed to start health check server:", err)
}
}()
err = bot.Websocket()
if err != nil {
log.Fatal(err)
}
}
非常简单:
err := godotenv.Load()我们使用godotenv包来访问本地变量.env。go func()我们HealthCheck在 goroutine 中启动我们的 web 服务器。err = bot.Websocket()最后我们开始Websocket。
现在,我们开始跑吧:
go run cdm/main.go
我们将启动机器人:
2024/09/13 09:11:31 INFO Starting bot
2024/09/13 09:11:31 INFO Starting health check server on :8080
2024/09/13 09:11:32 INFO Connected to WebSocket url=wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos
我们可以在 Bluesky 上进行测试,我使用了#bot-teste标签进行测试,我们来创建一个帖子并发表评论:
请看,转发已成功,现在已有点赞,终端中也显示了日志:
2024/09/13 09:14:16 INFO Published successfully resource=app.bsky.feed.repost
2024/09/13 09:14:16 INFO Published successfully resource=app.bsky.feed.like
最后考虑因素
我们已经介绍了如何使用 Golang 和各种技术(例如 Websockets、AT 协议、CAR 和 CBOR)为 Bluesky 社交网络创建一个机器人。
该机器人负责监控特定的话题标签,当它发现其中一个话题标签时,它会转发并点赞原帖。
这只是我们能用这个机器人实现的功能之一,Bluesky API 非常完善,提供了多种可能性,你可以使用这个机器人并添加新功能🐹。
链接
请点击此处查看我博客上的文章。
订阅并接收新帖子的通知,参与互动
项目仓库
文章来源:https://dev.to/wiliamvj/creating-a-bot-for-bluesky-social-496p

