”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 为 Bluesky Social 创建机器人

为 Bluesky Social 创建机器人

发布于2024-09-16
浏览:669

How the bot will work

We will develop a bot for the social network Bluesky, we will use Golang for this, this bot will monitor some hashtags via websocket,
if it finds one of these hashtags it will make a repost and like the original post.

We will cover some really cool things like, websocket, AT (protocol used by bluesky), CAR (Content Addressable aRchive) and CBOR (Concise Binary Object Representation) are two formats used to store and transmit data efficiently.

Project structure

The project will have a simple structure, within internal we will have a package called bot with all the code to run the bot,
within utils we will have some functions to help us.

In the .env file we will have the bluesky credentials to access api.

Creating a Bot for Bluesky Social

Setting up credentials

To authenticate to the bluesky API we need to provide an identifier and a password, but we cannot use the password to access our account,
to do this we will create an App Passwords, just access your account in bluesky, access settings and then App Passwords.

With this generated password, place it inside the .env file, like this:

BLUESKY_IDENTIFIER=
BLUESKY_PASSWORD=

Generating the API token

Whenever our bot identifies a new hashtag that we are monitoring, a reply will be made, but we need a Bearer token to be able to make the repost,
we will create a function that generates the token, we will do this in the get-token.go file.

First we define a global variable for the API url.

var (
  API_URL = "https://bsky.social/xrpc"
)

Now we define our struct with the data that will be returned by the 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"`
}

Now we will create the getToken function that returns a DIDResponse (you can give it whatever name you want).

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
}

This function calls the bluesky endpoint com.atproto.server.createSession, we will receive some data, but what matters for now is the accessJwt which is what we will need to authorize our bot via Bearer, with that the function to generate the token is ready.

Creating the Websocket

This will be the most complex function of the bot, we will need to consume the bluesky endpoint.

First, let's create a variable to save the endpoint, see more in the docs

var (
  wsURL = "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos"
)

Now let's create the structs:

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"`
}

We will also use the Gorilla Websocket package, download the package with:

go get github.com/gorilla/websocket

the Websocket function initially looks like this:

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

With this we can now read messages received via websocket with an infinite for, but the messages are encoded in CBOR.

What is CBOR?

CBOR (Concise Binary Object Representation) is a binary data format that is used to represent data in a compact and efficient way.
It is similar to JSON, but instead of using human-readable text, it uses binary bytes, which makes it smaller and faster to transmit and process.

To decode it we will need to use this package.

decoder := cbor.NewDecoder(bytes.NewReader(message))

Just turn the message into a reader, like this:

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): The decoder is responsible for reading the received data and decoding it from the CBOR format to the RepoCommitEvent type. The evt stores the decoded data.

  • if err == io.EOF { break }: If the decoder reaches the end of the data (there are no more messages), it returns io.EOF (end of file). When this happens, the loop is interrupted with break, because there is no more data to process.

Creating the handleEvent

Let's create a function to process the event:

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 parameter: The function receives an evt parameter, which is an event of type RepoCommitEvent. This event contains a list of Ops operations and possibly Blocks data blocks related to these operations.

  • Loop over Ops: The evt event can contain multiple operations. The code iterates through each of these operations using the for _, op := range evt.Ops loop.

  • Checking the op.Action == "create" action: For each operation, the code checks if the associated action is create, that is, if the operation is creating something new in bluesky, such as a post or other type of content.

  • If there are Blocks len(evt.Blocks) > 0: If the create operation is detected, the code checks if the event contains Blocks data blocks. These blocks contain additional information that may be related to the operation.

  • Processing handleCARBlocks Blocks: If blocks are present, the handleCARBlocks function is called to process these blocks. This function is responsible for interpreting the data within the blocks (We will cover CAR below).

What is CAR?

CAR (Content Addressable Archive) is an archive format that stores data efficiently and securely using content addressing. This means that each piece of data is identified by its content rather than a specific location.

Here is a simple explanation:

Content Identified by Hash: Each block of data in a CAR file is identified by a hash (a unique identifier generated from the content of the data). This ensures that the same piece of data always has the same identifier.

Used in IPFS and IPLD: CAR is widely used in systems such as IPFS (InterPlanetary File System) and IPLD (InterPlanetary Linked Data), where data is distributed and retrieved over the network based on content rather than location like bluesky.

Data Blocks: A CAR file can store multiple blocks of data, and each block can be retrieved individually using its content identifier (CID).

Efficient and Safe: Since a block's identifier depends on its content, it is easy to verify that the data is correct and has not been altered.

This is a very simple explanation, if you want to go deeper, I recommend accessing this.

Creating the handleCARBlocks

This will be the most complex function of the bot:

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
}

We will still create the repost() function, we will pass a pointer to *Post as a parameter.

Remember that our bot only monitors post comments, if a post is created and the hashtag we are monitoring is inserted, the repost will not be made, this
validation if post.Text == "" || post.Reply == nil will prevent it, it is necessary to have a reply and this only happens if it is a comment on a post.

The handleCARBlocks function processes data blocks in CAR format. Let's understand step by step what the function does in a simple way:

  • Initial Block Verification:
if len(blocks) == 0 {
  return errors.New("no blocks to process")
}

If the blocks are empty, the function returns an error saying that there are no blocks to process.

  • Creating a CAR Block Reader:
reader, err := carv2.NewBlockReader(bytes.NewReader(blocks))

The function creates a block reader to interpret the data contained in the CAR file, we are using the packages carV2 and go-cid

To install, run:

  go install github.com/ipld/go-car/cmd/car@latest
  go get github.com/ipfs/go-cid
  • Reading the Blocks:
for {
  block, err := reader.Next()
    if err == io.EOF {
      break
    }
}

The function enters a loop to read all data blocks one by one. When all blocks are read (i.e. the end is reached), the loop stops.

  • Checking the CID:
if opTag, ok := op.CID.(cbor.Tag); ok {
  if cidBytes, ok := opTag.Content.([]byte); ok {
    c, err := decodeCID(cidBytes)

The function checks whether the operation contains a CID (Content Identifier) ​​that can be decoded. This CID identifies the specific content of the block.

  • Comparing and Decoding the Block:
if block.Cid().Equals(c) {
  var post Post
  err := cbor.Unmarshal(block.RawData(), &post)

If the block read has the same CID as the operation, the block content is decoded into a format that the function understands, such as a "Post".

  • Filtering the Post:
if post.Text == "" || post.Reply == nil {
  continue
}
if utils.FilterTerms(post.Text) {
  repost(&post)
}

If the post has text and a reply, it is filtered with a function called FilterTerms. If it passes the filter, it is reposted.

Creating decodeCID

The decodeCID function is responsible for decoding a content identifier (CID) from a set of bytes. It takes these bytes and tries to transform them into a CID that can be used to identify blocks of data.

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
}

With that, we have the Websocket ready.

Creating the Hashtag Filter

Let's create the following within utils in 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
}

It is in this function that we define the hashtags to be monitored, in a simple way we receive a text that comes from the websocket and filter it based on the terms.

Creating createRecord

Let's create a function called createRecord in the create-record.go file, which will be responsible for creating a repost or a like, depending on the $type that is sent via parameter.

First, let's create a struct with the parameters we will need:

type CreateRecordProps struct {
  DIDResponse *DIDResponse
  Resource    string
  URI         string
  CID         string
}
  • DIDResponse: We will use it to extract the authorization token.
  • Resource: It will be used to inform whether we are going to do a like or repost.
  • URI: It will be used to inform the uri of the original post.
  • CID: This is what we extracted from the CAR, used as an identifier.

The final function will look like this:

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
}

It's simple to understand, we make a POST to the API_URL/com.atproto.repo.createRecord endpoint, informing that we are going to create a record, in the body we inform the $type, which informs the bluesky API the type of record we are going to create, then we assemble the request, inserting the bearer token and we do some error handling, simple, isn't it?

This way we can use the createRecord function to create several records, changing only the $type.

Sending the repost and like to Bluesky

With createRecord ready, it's simple to create the repost, let's do this in the repost.go file:

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
}

We receive a pointer to the *Post from the Websocket() function, we set up the CreateRecordProps informing that we are going to make a repost through the app.bsky.feed.repost resource, and finally we call createRecord.

After creating the post, we will give it a like (optional), just call createRecord again, but now with the app.bsky.feed.like resource, since we created the resource in a variable, just set a new value, which is what we do resource.Resource = "app.bsky.feed.like".

With that, we can now make the repost and the like.

Creating a health check

This part is optional, it will be used only for deployment, it will be used by the hosting service to check if our bot is still working, it is a very simple endpoint that only returns a status code 200.

Let's do it in the health-check.go file:

func HealthCheck(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(http.StatusOK)
}

The HealthCheck function returns only a w.WriteHeader(http.StatusOK), this could be done directly in the main.go file, which is where we will start our web server, but I chose to separate it.

Getting the bot up and running

Well, now we just need to get everything running, let's do that in 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)
  }
}

Very simple too:

  • err := godotenv.Load(): We use the godotenv package to be able to access the variables of the .env locally.
  • go func(): We start our webserver for the HealthCheck in a goroutine.
  • err = bot.Websocket(): Finally we start the Websocket.

Now, let's run:

go run cdm/main.go

We will have the bot running:

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

We can test it on Bluesky, I used the hashtag #bot-teste for testing purposes, let's create a post and comment on it:

Creating a Bot for Bluesky Social

See that the repost was made and now it has the like, and in the terminal we have the logs:

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

Final considerations

We have covered how to create a bot for the Bluesky social network, using Golang and various technologies such as Websockets, AT Protocol, CAR and CBOR.

The bot is responsible for monitoring specific hashtags and, when it finds one of them, it reposts and likes the original post.

This is just one of the features we can do with the bot, the Bluesky API is very complete and allows for several possibilities, you can use this bot and add new features ?.

Links

See the post on my blog here

Subscribe and receive notification of new posts, participate

repository of the project

bot profile on Bluesky

Bluesky documentation

Gopher credits

版本声明 本文转载于:https://dev.to/wiliamvj/creating-a-bot-for-bluesky-social-496p?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 掌握 TypeScript:了解扩展的力量
    掌握 TypeScript:了解扩展的力量
    TypeScript 中的 extends 关键字就像一把瑞士军刀。它用于多种上下文,包括继承、泛型和条件类型。了解如何有效地使用扩展可以生成更健壮、可重用和类型安全的代码。 使用扩展进行继承 extends 的主要用途之一是继承,允许您创建基于现有接口或类的新接口或类。 inter...
    编程 发布于2024-11-06
  • 如何将具有组计数的列添加到 Pandas 中的分组数据框?
    如何将具有组计数的列添加到 Pandas 中的分组数据框?
    如何在Pandas中向分组数据框中添加列在数据分析中,经常需要对数据进行分组并进行计算每组。 Pandas 通过其 groupby 函数提供了一种便捷的方法来做到这一点。一个常见的任务是计算每个组中某一列的值,并将包含这些计数的列添加到数据帧中。考虑数据帧 df:df = pd.DataFrame(...
    编程 发布于2024-11-06
  • 破解编码面试的热门必备书籍(从初级到高级排名)
    破解编码面试的热门必备书籍(从初级到高级排名)
    准备编码面试可能是一个充满挑战的旅程,但拥有正确的资源可以让一切变得不同。无论您是从算法开始的初学者、专注于系统设计的中级开发人员,还是完善编码实践的高级工程师,这份按难度排名的前 10 本书列表都将为您提供成功所需的知识和技能。你的软件工程面试。这些书籍涵盖了从基本算法到系统设计和简洁编码原则的所...
    编程 发布于2024-11-06
  • Java 字符串实习初学者指南
    Java 字符串实习初学者指南
    Java String Interning 引入了通过在共享池中存储唯一字符串来优化内存的概念,减少重复对象。它解释了 Java 如何自动实习字符串文字以及开发人员如何使用 intern() 方法手动将字符串添加到池中。 通过掌握字符串驻留,您可以提高 Java 应用程序的性能和内存效率。要深入...
    编程 发布于2024-11-06
  • 如何在 GUI 应用程序中的不同页面之间共享变量数据?
    如何在 GUI 应用程序中的不同页面之间共享变量数据?
    如何从类中获取变量数据在 GUI 编程环境中,单个应用程序窗口中包含多个页面是很常见的。每个页面可能包含各种小部件,例如输入字段、按钮或标签。当与这些小部件交互时,用户提供输入或做出需要在不同页面之间共享的选择。这就提出了如何从一个类访问另一个类的变量数据的问题,特别是当这些类代表不同的页面时。利用...
    编程 发布于2024-11-06
  • React 中的动态路由
    React 中的动态路由
    React 中的动态路由允许您基于动态数据或参数创建路由,从而在应用程序中实现更灵活、更强大的导航。这对于需要根据用户输入或其他动态因素呈现不同组件的应用程序特别有用。 使用 React Router 设置动态路由 您通常会使用react-router-dom库在React中实现动态路由。这是分步指...
    编程 发布于2024-11-06
  • 大批
    大批
    方法是可以在对象上调用的 fns 数组是对象,因此它们在 JS 中也有方法。 slice(begin):将数组的一部分提取到新数组中,而不改变原始数组。 let arr = ['a','b','c','d','e']; // Usecase: Extract till index p...
    编程 发布于2024-11-06
  • WPF中延迟操作时如何避免UI冻结?
    WPF中延迟操作时如何避免UI冻结?
    WPF 中的延迟操作WPF 中的延迟操作对于增强用户体验和确保平滑过渡至关重要。一种常见的情况是在导航到新窗口之前添加延迟。为了实现此目的,经常使用 Thread.Sleep,如提供的代码片段中所示。但是,在延迟过程中,使用 Thread.Sleep 阻塞 UI 线程会导致 UI 无响应。这表现为在...
    编程 发布于2024-11-06
  • 利用 Java 进行实时数据流和处理
    利用 Java 进行实时数据流和处理
    In today's data-driven world, the ability to process and analyze data in real-time is crucial for businesses to make informed decisions swiftly. Java...
    编程 发布于2024-11-06
  • 如何修复损坏的 InnoDB 表?
    如何修复损坏的 InnoDB 表?
    从 InnoDB 表损坏中恢复灾难性事件可能会导致数据库表严重损坏,特别是 InnoDB 表。遇到这种情况时,了解可用的修复选项就变得至关重要。InnoDB Table Corruption Symptoms查询中描述的症状,包括事务日志中的时间戳错误InnoDB 表的修复策略虽然已经有修复 MyI...
    编程 发布于2024-11-06
  • JavaScript 数组和对象中是否正式允许使用尾随逗号?
    JavaScript 数组和对象中是否正式允许使用尾随逗号?
    数组和对象中的尾随逗号:标准还是容忍?数组和对象中尾随逗号的存在引发了一些关于它们的争论JavaScript 的标准化。这个问题源于在不同浏览器中观察到的不一致行为,特别是旧版本的 Internet Explorer。规范状态根据 ECMAScript 5 规范(第 11.1.5 节) ),对象字面...
    编程 发布于2024-11-06
  • 最佳引导模板生成器
    最佳引导模板生成器
    在当今快速发展的数字环境中,速度和效率是关键,网页设计师和开发人员越来越依赖 Bootstrap 构建器来简化他们的工作流程。这些工具可以快速创建响应灵敏、具有视觉吸引力的网站,使团队能够比以往更快地将他们的想法变为现实。 Bootstrap 构建器真正改变了网站的构建方式,使该过程更加易于访问和高...
    编程 发布于2024-11-06
  • 简化 NestJS 中的文件上传:无需磁盘存储即可高效内存中解析 CSV 和 XLSX
    简化 NestJS 中的文件上传:无需磁盘存储即可高效内存中解析 CSV 和 XLSX
    Effortless File Parsing in NestJS: Manage CSV and XLSX Uploads in Memory for Speed, Security, and Scalability Introduction Handling file uploa...
    编程 发布于2024-11-06
  • 使用 SubDomainRadar.io 和 Python 轻松发现隐藏子域
    使用 SubDomainRadar.io 和 Python 轻松发现隐藏子域
    作为网络安全专业人员、漏洞赏金猎人或渗透测试人员,发现隐藏的子域对于识别至关重要域中的潜在漏洞。子域通常托管可能容易受到攻击的被遗忘的服务或测试环境。 在这篇文章中,我将向您介绍 SubDomainRadar.io 及其 Python API 包装器 — 自动化子域枚举的终极工具 和 安全工作流程...
    编程 发布于2024-11-06
  • Python 中的 HackerRank 问题 - 基本数据类型列表
    Python 中的 HackerRank 问题 - 基本数据类型列表
    此 Python 代码旨在根据用户提供的命令对列表执行一系列操作。让我们一步步分析代码,了解其工作原理: if __name__ == '__main__': N = int(input()) l = [] while(N>0): cmd_l = inp...
    编程 发布于2024-11-06

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3