<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ 机器人 - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ 机器人 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 15 Jun 2026 05:22:51 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/robotics/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何用 Python 构建 Discord 机器人并免费托管 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Python Discord Bot Tutorial – Code a Discord Bot And Host it for Free [https://www.freecodecamp.org/news/create-a-discord-bot-with-python/]，作者：Beau Carnes [https://www.freecodecamp.org/news/author/beau/] 本教程将告诉你如何完全在云端建立你自己的 Discord 机器人。 你不需要在你的电脑上安装任何东西，也不需要支付任何费用来托管你的机器人。 我们将使用一些工具，包括 Discord API、Python 库和一个名为 Repl.it [https://www.repl.it]  的云计算平台。 这个书面教程也有一个视频版本 [https://www.youtube.com/watch?v=SPTfmiYiuok&feature=emb_imp_woyt]。 如何创建一个 Discord Bot 账户 为了使用 Python 库和 Discord API，我们必须首先创 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/create-a-discord-bot-with-python/</link>
                <guid isPermaLink="false">627a1d1fc9c067061df8b7c4</guid>
                
                    <category>
                        <![CDATA[ Discord ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 机器人 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Mon, 09 May 2022 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/05/1_NlqpTTAM8DbGl4paBmjE_g.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/create-a-discord-bot-with-python/">Python Discord Bot Tutorial – Code a Discord Bot And Host it for Free</a>，作者：<a href="https://www.freecodecamp.org/news/author/beau/">Beau Carnes</a></p><!--kg-card-begin: markdown--><p>本教程将告诉你如何完全在云端建立你自己的 Discord 机器人。</p>
<p>你不需要在你的电脑上安装任何东西，也不需要支付任何费用来托管你的机器人。</p>
<p>我们将使用一些工具，包括 Discord API、Python 库和一个名为 <a href="https://www.repl.it">Repl.it</a> 的云计算平台。</p>
<p>这个书面教程也有一个<a href="https://www.youtube.com/watch?v=SPTfmiYiuok&amp;feature=emb_imp_woyt">视频版本</a>。</p>
<h2 id="discordbot">如何创建一个 Discord Bot 账户</h2>
<p>为了使用 Python 库和 Discord API，我们必须首先创建一个 Discord Bot 账户。</p>
<p>以下是创建 Discord Bot 账户的步骤。</p>
<ol>
<li>
<p>确保你已经登录到 <a href="https://discord.com">Discord 网站</a></p>
</li>
<li>
<p>进入<a href="https://discord.com/developers/applications">应用程序页面</a></p>
</li>
<li>
<p>点击 <code>New Application</code> 按钮</p>
</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-117.png" alt="image-117" width="600" height="400" loading="lazy"></p>
<ol start="4">
<li>给应用程序一个名称，然后点击 <code>Create</code> 按钮</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-118.png" alt="image-118" width="600" height="400" loading="lazy"></p>
<ol start="5">
<li>进入 <code>Bot</code> 标签，然后点击 <code>Add Bot</code>。你必须点击 <code>Yes, do it!</code></li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-119.png" alt="image-119" width="600" height="400" loading="lazy"></p>
<p>保持默认设置 <strong>Public Bot</strong>（选中）和 <strong>Require OAuth2 Code Grant</strong>（未选中）。</p>
<p>你的机器人已经创建完毕。下一步是复制令牌（token）。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-122.png" alt="image-122" width="600" height="400" loading="lazy"></p>
<p>这个令牌是你的机器人的密码，所以不要与任何人分享它。它可以让别人登录到你的机器人并做各种坏事。</p>
<p>如果不小心被分享，你可以重新生成令牌。</p>
<h2 id="">如何邀请你的机器人接入一个服务器</h2>
<p>现在你必须让你的机器人用户接入一个服务器。要做到这一点，你应该为它创建一个邀请 URL。</p>
<p>转到 <code>OAuth2</code> 标签。然后在 <code>scopes</code> 部分选择 <code>bot</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-123.png" alt="image-123" width="600" height="400" loading="lazy"></p>
<p>现在为机器人选择你想要的权限。我们的机器人将主要使用文本信息，所以我们不需要很多的权限。你可能需要更多的权限，这取决于你希望你的机器人做什么。对 <code>Administrator</code> 的权限要小心。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-124.png" alt="image-124" width="600" height="400" loading="lazy"></p>
<p>选择适当的权限后，点击权限上方的 <code>copy</code> 按钮。这将复制一个 URL，可用于将机器人添加到一个服务器。</p>
<p>把这个 URL 粘贴到你的浏览器，选择一个服务器来接入机器人，然后点击 <code>Authorize</code>。</p>
<p>要添加机器人，你的账户需要 <code>Manage Server</code> 的权限。</p>
<p>现在你已经创建了机器人用户，我们将开始为机器人编写 Python 代码。</p>
<h2 id="discordpydiscord">如何用 discord.py 库编写一个基本的 Discord 机器人代码</h2>
<p>我们将使用 discord.py， 这个 Python 库来编写机器人的代码。discord.py 是 Discord 的一个 API 封装器，使在 Python 中更容易创建一个 Discord 机器人。</p>
<h3 id="repldiscordpy">如何创建一个 Repl 项目并安装 discord.py</h3>
<p>你可以在你的本地电脑上用任何代码编辑器开发机器人。然而，在本教程中，我们将使用 Repl.it，因为它操作起来比较简便。Repl.it 是一个在线 IDE，你可以在你的网络浏览器中使用。</p>
<p>首先进入 <a href="https://repl.it">Repl.it</a>。创建一个新的 Repl，选择 <code>Python</code> 作为语言。</p>
<p>要使用 discord.py 库，只要在 <code>main.py</code> 的顶部写上 <code>import discord</code>。当你按下 <code>run</code> 按钮时，Repl.it 会自动安装这个依赖。</p>
<p>如果你喜欢在本地编码机器人，你可以在 MacOS 上使用这个命令来安装 discord.py：</p>
<p><code>python3 -m pip install -U discord.py</code></p>
<p>你可能需要使用 <code>pip3</code> 而不是 <code>pip</code>。</p>
<p>如果你使用的是 Windows，那么你应该使用下面一行代码：</p>
<p><code>py -3 -m pip install -U discord.py</code></p>
<h3 id="discord">如何为你的机器人设置 Discord 事件</h3>
<p>discord.py 基于事件的概念。一个事件是你监听的东西，然后对其作出回应。例如，当一条消息发生时，你会收到一个关于它的事件，你可以对其作出回应。</p>
<p>让我们做一个机器人，回复一个特定的消息。这个简单的机器人代码，以及代码解释，来自于<a href="https://discordpy.readthedocs.io/en/latest/quickstart.html#a-minimal-bot">the discord.py documentation</a>。我们将在以后为机器人添加更多的功能。</p>
<p>将这段代码添加到 main.py 中。如果你愿意，你可以给这个文件起个别的名字，但不要叫 discord.py。我很快会解释这些代码的作用。</p>
<pre><code class="language-python">import discord
import os

client = discord.Client()

@client.event
async def on_ready():
    print('We have logged in as {0.user}'.format(client))

@client.event
async def on_message(message):
    if message.author == client.user:
        return

    if message.content.startswith('$hello'):
        await message.channel.send('Hello!')

client.run(os.getenv('TOKEN'))
</code></pre>
<p>当你在 Discord 上创建你的机器人用户时，你复制了一个令牌。现在我们要创建一个 <code>.env</code> 文件来存储该令牌。如果你在本地运行你的代码，你不需要 <code>.env</code> 文件。只要用令牌替换 <code>os.getenv('TOKEN')</code> 就可以了。</p>
<p><code>.env</code> 文件是用来声明环境变量的。在 Repl.it 上，你创建的大多数文件对任何人都是可见的，但 <code>.env</code> 文件只对你可见。 其他人将无法看到公共 repl 里的<code>.env</code>文件的内容。</p>
<p>因此，如果你在 Repl.it 上开发，应该只在 <code>.env</code> 文件中存储私人信息，如令牌或密钥。</p>
<p>点击 <code>Add file</code>按钮，创建一个名为 <code>.env</code> 的文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-19-1.png" alt="image-19-1" width="600" height="400" loading="lazy"></p>
<p>在文件中添加以下一行，包括你之前复制的令牌（token）：</p>
<pre><code class="language-python">TOKEN=[paste token here]
</code></pre>
<p>现在让我们来看看每一行代码在你的 Discord 机器人中的作用是什么。</p>
<ol>
<li>第一行是导入 discord.py 库。</li>
<li>第二行导入 os 库，但这只是用于从 <code>.env</code> 文件中获取 <code>TOKEN</code> 变量。如果你不使用 <code>.env</code> 文件，你不需要这一行。</li>
<li>接下来，我们创建一个 <a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.Client"><code>Client</code></a> 的实例。这是与 Discord 的连接。</li>
<li><a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.Client.event">@client.event()</a> 装饰器被用来注册一个事件。这是一个异步库，所以事情是通过回调完成的。回调是一个当其他事情发生时被调用的函数。在这段代码中，当机器人准备开始使用时，<a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.on_ready">on_ready()</a> 事件被调用。然后，当机器人收到一个消息时，<a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.on_message">on_message()</a> 事件被调用。</li>
<li><a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.on_message">on_message()</a> 事件在每次收到消息时都会触发，但如果消息是来自我们自己，我们不希望它做任何事情。因此，如果<a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.Message.author">Message.author</a> 与 <a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.Client.user">Client.user</a> 相同，代码只是返回。</li>
<li>接下来，我们检查 <a href="https://discordpy.readthedocs.io/en/latest/api.html#discord.Message.content"><code>Message.content</code></a> 是否以 <code>$hello</code> 开头。如果是，那么机器人就会向它所使用的频道回复 <code>Hello!</code>。</li>
<li>现在机器人已经设置好了，最后一行是用登录令牌运行机器人。它从 <code>.env</code> 文件中获取令牌。</li>
</ol>
<p>我们有了机器人的代码，现在我们只需要运行它。</p>
<h3 id="">如何运行机器人</h3>
<p>现在点击上面的 <code>run</code> 按钮，在 repl.it 中运行你的机器人。</p>
<p>如果你是在本地编写机器人，你可以在终端使用下面这些命令来运行该机器人。</p>
<p>在 Windows 系统：</p>
<p><code>py -3 main.py</code></p>
<p>在别的系统：</p>
<p><code>python3 main.py</code></p>
<p>现在去你的 Discord 房间，输入<code>$hello</code>。你的机器人应该返回 <code>Hello</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-141.png" alt="image-141" width="600" height="400" loading="lazy"></p>
<h2 id="">如何改进机器人</h2>
<p>现在我们有了一个基本的机器人工作，我们将改进它。它被称为 <code>鼓励机器人</code> 是有原因的。</p>
<p>每当有人发来含有悲伤或压抑字眼的信息时，这个机器人就会以鼓励的信息来回应。</p>
<p>任何人都可以为机器人添加鼓励信息，用户提交的信息将被储存在 Repl.it 数据库中。</p>
<p>当有人在聊天中输入 <code>$inspire</code> 信息时，机器人也会从 API 中随机返回一句鼓舞人心的话。</p>
<p>我们将从添加 <code>$inspire</code> 功能开始。</p>
<h3 id="">如何在机器人上添加鼓舞人心的名言</h3>
<p>我们将从一个名为 zenquotes.io 的 API 中获得鼓舞人心的语录。我们需要再导入几个 Python 模块，添加一个<code>get_quote()</code>函数，并更新我们的机器人代码以调用该函数。</p>
<p>下面是更新后的代码。在代码之后，我将解释新的部分。</p>
<pre><code class="language-python">import discord
import os
import requests
import json

client = discord.Client()

def get_quote():
  response = requests.get("https://zenquotes.io/api/random")
  json_data = json.loads(response.text)
  quote = json_data[0]['q'] + " -" + json_data[0]['a']
  return(quote)

@client.event
async def on_ready():
  print('We have logged in as {0.user}'.format(client))

@client.event
async def on_message(message):
  if message.author == client.user:
    return

  if message.content.startswith('$inspire'):
    quote = get_quote()
    await message.channel.send(quote)

client.run(os.getenv('TOKEN'))
</code></pre>
<p>我们现在必须导入 <code>requests</code> 模块。这个模块允许我们的代码进行 HTTP 请求，从 API 获得数据。API 返回 JSON，所以 <code>json</code> 模块使我们更容易处理返回的数据。</p>
<p><code>get_quote()</code> 函数是非常简单的。首先，它使用 requests 模块从 API URL 请求数据。API 会返回一个随机的鼓舞人心的引言。如果当前的 API 停止工作，这个函数可以很容易地被重写，以从不同的 API 获得引言。</p>
<p>接下来在这个函数中，我们使用 <code>json.load()</code> 将 API 的响应转换为 JSON。通过试验和错误，我找到了如何将 JSON 中的引言转换成我想要的字符串格式。引言被作为一个字符串从函数中返回。</p>
<p>代码中最后更新的部分是在最后。以前，它寻找以 <code>$hello</code> 开头的信息。现在它寻找的是 <code>$inspire</code>。它不再返回 <code>Hello!</code>，而是用 <code>quote = get_quote()</code> 来获取引言，并返回引言。</p>
<p>在这一点上，你可以运行你的代码并尝试一下。</p>
<h2 id="">如何向机器人添加鼓励性的信息</h2>
<p>现在我们要实现的功能是，当用户发布带有悲伤字眼的信息时，机器人会以鼓励性的信息进行回应。</p>
<h3 id="">如何在机器人中添加悲伤的词语</h3>
<p>首先，我们需要创建一个 Python 列表，其中包含机器人将回应的悲伤的词语。</p>
<p>在创建<code>client</code>变量后添加以下一行:</p>
<p><code>sad_words = ["sad", "depressed", "unhappy", "angry", "miserable"]</code></p>
<p>请随意在列表中添加更多的单词。</p>
<h3 id="">如何向机器人添加鼓励性的信息</h3>
<p>现在我们将添加一个鼓励性的信息列表，机器人将用这些信息来回应。</p>
<p>在你创建的<code>sad_words</code>列表后面添加以下列表:</p>
<pre><code class="language-python">starter_encouragements = [
  "Cheer up!",
  "Hang in there.",
  "You are a great person / bot!"
]
</code></pre>
<p>像以前一样，请随时在列表中添加更多你选择的短语。我现在只使用三个项目，因为以后我们会增加用户添加更多鼓励性短语的能力，供机器人使用。</p>
<h3 id="">如何回复留言</h3>
<p>现在我们需要更新我们的机器人来使用我们创建的两个列表。首先，导入随机模块，因为机器人将随机选择鼓励信息。在代码顶部的导入语句中添加以下一行。<code>import random</code>。</p>
<p>现在我们将更新<code>on_message()</code>函数，以检查所有信息，看它们是否包含<code>sad_words</code>列表中的一个词。如果发现一个悲伤的词，机器人将发送一条随机的鼓励信息。</p>
<p>以下是更新后的代码：</p>
<pre><code class="language-python">async def on_message(message):
  if message.author == client.user:
    return

  msg = message.content

  if msg.startswith('$inspire'):
    quote = get_quote()
    await message.channel.send(quote)
    
  if any(word in msg for word in sad_words):
    await message.channel.send(random.choice(starter_encouragements))
</code></pre>
<p>这是一个测试机器人的好时机。你现在知道的足够多，可以创建你自己的机器人。但接下来你将学习如何实现更高级的功能，并使用 Repl.it 数据库存储数据。</p>
<h3 id="">如何启用用户提交信息</h3>
<p>这个机器人是完全正常的，但现在让我们从 Discord 中直接更新机器人。用户应该能够添加更多的鼓励性信息，以便机器人在检测到一个悲伤的词时使用。</p>
<p>我们将使用 Repl.it 的内置数据库来存储用户提交的信息。这个数据库是一个键值存储，内置于每个 Repl.it 中。</p>
<p>在代码的顶部，在其他导入语句下，添加 <code>from replit import db</code>。这将使我们能够使用 Repl.it 数据库。</p>
<p>用户将能够直接从 Discord 聊天中为机器人添加自定义鼓励信息。在我们为机器人添加新的命令之前，让我们创建两个辅助函数，将自定义消息添加到数据库并删除它们。</p>
<p>在 <code>get_quote()</code> 函数后添加以下代码:</p>
<pre><code class="language-python">def update_encouragements(encouraging_message):
  if "encouragements" in db.keys():
    encouragements = db["encouragements"]
    encouragements.append(encouraging_message)
    db["encouragements"] = encouragements
  else:
    db["encouragements"] = [encouraging_message]

def delete_encouragment(index):
  encouragements = db["encouragements"]
  if len(encouragements) &gt; index:
    del encouragements[index]
  db["encouragements"] = encouragements
</code></pre>
<p><code>update_encouragements()</code>函数接受一个鼓励信息作为参数。</p>
<p>首先，它检查 <code>encouragements</code> 是否是数据库中的一个键（key）。如果是，它将获得已经在数据库中的鼓励信息列表，将新的鼓励信息添加到列表中，并将更新的列表存储在数据库中的 <code>encouragements</code> 键下。</p>
<p>如果数据库中还没有 <code>encouragements</code>，就用这个名字创建一个新的键，并将新的鼓励信息作为列表中的第一个元素加入。</p>
<p><code>delete_encouragement()</code> 函数接受一个索引作为参数。</p>
<p>它从数据库中获取存储在 <code>encouragements</code> 键下的鼓励信息列表。如果鼓励列表中的项目数量大于索引，则删除该索引处的列表项。</p>
<p>最后，更新后的列表被存储在数据库的 <code>encouragements</code> 键下。</p>
<p>下面是 <code>on_message()</code> 函数的更新代码。在代码之后，我将解释新的部分。</p>
<pre><code class="language-python">async def on_message(message):
  if message.author == client.user:
    return

  msg = message.content
 
  if msg.startswith("$inspire"):
    quote = get_quote()
    await message.channel.send(quote)

  options = starter_encouragements
  if "encouragements" in db.keys():
    options = options + db["encouragements"]

  if any(word in msg for word in sad_words):
    await message.channel.send(random.choice(options))

  if msg.startswith("$new"):
    encouraging_message = msg.split("$new ",1)[1]
    update_encouragements(encouraging_message)
    await message.channel.send("New encouraging message added.")

  if msg.startswith("$del"):
    encouragements = []
    if "encouragements" in db.keys():
      index = int(msg.split("$del",1)[1])
      delete_encouragment(index)
      encouragements = db["encouragements"]
    await message.channel.send(encouragements)
</code></pre>
<p>上面的第一行新代码是 <code>options = starter_encouragements</code>。我们复制了变量 <code>starter_encouragements</code>的值，因为我们要把用户提交的信息添加到该列表中，然后再为机器人选择一条随机信息来发送。</p>
<p>我们检查 <code>encouragements</code> 是否已经在数据库键中（意味着用户已经提交了至少一条自定义消息）。如果是的话，我们就把用户信息添加到启动器的鼓励信息中。</p>
<p>然后，机器人现在不是从 <code>starter_encouragements</code> 中随机发送消息，而是从 <code>options</code> 中随机发送消息。</p>
<p>下一段新的代码是将用户提交的新消息添加到数据库中。如果一条 Discord 信息以 <code>$new</code>开头，那么 <code>$new</code> 之后的文字将被用作新的鼓励信息。</p>
<p>代码<code>msg.split("$new",1)[1]</code>从 <code>$new</code> 命令中分离出信息，并将该信息存储在一个变量中。在这行代码中，注意"$new "中的空格。我们要的是空格之后的所有内容。</p>
<p>我们调用 <code>update_encouragements</code> 辅助函数处理新消息，然后机器人向 discord 聊天室发送一条消息，确认消息被添加。</p>
<p>第三个新部分（在上面代码的末尾）检查新的 discord 消息是否以<code>$del</code> 开头。这是删除数据库中 <code>encouragements</code> 列表中的一个项目的命令。</p>
<p>首先，一个名为 <code>encouragements</code> 的新变量被初始化为一个空数组。这样做的原因是，如果数据库中不包括 <code>encouragement</code> 键，这部分代码将发送一个空数组的信息。</p>
<p>如果 <code>encouragement</code> 键在数据库中，索引将从以 <code>$del</code> 开始的 Discord 消息中分离出来。然后，调用<code>delete_encouragement()</code>函数，传入要删除的索引。更新的鼓励列表被加载到 <code>encouragements</code> 变量中，然后机器人向 Discord 发送一条带有当前列表的消息。</p>
<h2 id="">最后要添加机器人的功能</h2>
<p>该机器人应该可以工作，所以现在是测试它的好时机。我们现在将添加一些最后的功能。</p>
<p>我们将增加从 Discord 中直接获得用户提交的信息列表的功能，并且我们将增加关闭和开启机器人是否对伤心话做出反应的功能。</p>
<p>我将给你们提供程序的全部最终代码，然后我将在下面讨论更新的代码。</p>
<pre><code class="language-python">import discord
import os
import requests
import json
import random
from replit import db

client = discord.Client()

sad_words = ["sad", "depressed", "unhappy", "angry", "miserable"]

starter_encouragements = [
  "Cheer up!",
  "Hang in there.",
  "You are a great person / bot!"
]

if "responding" not in db.keys():
  db["responding"] = True

def get_quote():
  response = requests.get("https://zenquotes.io/api/random")
  json_data = json.loads(response.text)
  quote = json_data[0]["q"] + " -" + json_data[0]["a"]
  return(quote)

def update_encouragements(encouraging_message):
  if "encouragements" in db.keys():
    encouragements = db["encouragements"]
    encouragements.append(encouraging_message)
    db["encouragements"] = encouragements
  else:
    db["encouragements"] = [encouraging_message]

def delete_encouragment(index):
  encouragements = db["encouragements"]
  if len(encouragements) &gt; index:
    del encouragements[index]
  db["encouragements"] = encouragements

@client.event
async def on_ready():
  print("We have logged in as {0.user}".format(client))

@client.event
async def on_message(message):
  if message.author == client.user:
    return

  msg = message.content

  if msg.startswith("$inspire"):
    quote = get_quote()
    await message.channel.send(quote)

  if db["responding"]:
    options = starter_encouragements
    if "encouragements" in db.keys():
      options = options + db["encouragements"]

    if any(word in msg for word in sad_words):
      await message.channel.send(random.choice(options))

  if msg.startswith("$new"):
    encouraging_message = msg.split("$new ",1)[1]
    update_encouragements(encouraging_message)
    await message.channel.send("New encouraging message added.")

  if msg.startswith("$del"):
    encouragements = []
    if "encouragements" in db.keys():
      index = int(msg.split("$del",1)[1])
      delete_encouragment(index)
      encouragements = db["encouragements"]
    await message.channel.send(encouragements)

  if msg.startswith("$list"):
    encouragements = []
    if "encouragements" in db.keys():
      encouragements = db["encouragements"]
    await message.channel.send(encouragements)
    
  if msg.startswith("$responding"):
    value = msg.split("$responding ",1)[1]

    if value.lower() == "true":
      db["responding"] = True
      await message.channel.send("Responding is on.")
    else:
      db["responding"] = False
      await message.channel.send("Responding is off.")

client.run(os.getenv("TOKEN"))
</code></pre>
<p>添加到代码中的第一个部分就在 <code>starter_encouragements</code> 列表下面：</p>
<pre><code class="language-python">if "responding" not in db.keys():
  db["responding"] = True
</code></pre>
<p>我们在数据库中创建一个名为 <code>responding</code> 的新键，并将其设置为 <code>true</code>。我们将用它来决定机器人是否应该对悲伤的话语做出反应。由于数据库即使在程序停止运行后也会被保存，所以我们只在新键不存在的情况下创建它。</p>
<p>代码的下一个新部分是，对伤心话做出反应的部分现在在这个 if 语句里面：<code>if db["responding"]:</code>。只有当<code>db["responing"] = True</code>时，机器人才会对悲伤的词语做出反应。更新这个值的动作是在下一节之后。</p>
<p>接下来，在使机器人响应 <code>$del</code> 命令的代码之后，有新的代码来响应作为 Discord 消息发送的 <code>$list</code> 命令。</p>
<p>这一部分首先是创建一个名为 <code>encouragements</code> 的空列表。然后，如果数据库中已经有鼓励语（encouragements），这些鼓励语将取代刚刚创建的空列表。</p>
<p>最后，机器人将鼓励（encouragements）列表作为 Discord 消息发送出去。</p>
<p>接下来是最后的新部分。这段代码使机器人对 <code>$responding</code> 命令作出反应。这个命令的参数是 <code>true</code> 或 <code>false</code>。下面是一个使用例子。<code>$responding true</code>。</p>
<p>代码首先用<code>value = msg.split("$responding",1)[1]</code> 获得一个值（和前面一样，注意<code>"$responding"</code>的空格）。然后有一个 if/else 语句，适当地设置数据库中的 <code>responding</code> 键，并向 Discord 发送一个通知信息。如果参数不是 <code>true</code>，代码就假定为 <code>false</code>。</p>
<p>该机器人的代码已经完成！你现在可以运行机器人并试用它了。但还有一个重要的步骤，我们接下来会讨论。</p>
<h2 id="">如何设置机器人连续运行</h2>
<p>如果你在 repl.it 中运行你的机器人，然后关闭它所运行的标签，你的机器人将停止运行。</p>
<p>但有两种方法可以让你的机器人持续运行，即使在你关闭你的网络浏览器之后。</p>
<p>第一个方法和最简单的方法是在 Repl.it 注册付费计划。他们最便宜的付费计划被称为黑客计划，它包括五个永远在线的 repls 项目。</p>
<p>你可以通过这个链接获得三个月的免费服务（限于前 1000 人）：&nbsp;<a href="https://repl.it/claim?code=tryalwayson2103">https://repl.it/claim?code=tryalwayson2103</a></p>
<p>当你注册了该计划，打开你的 Repl，点击顶部的项目名字。然后选择 <code>Always On</code> (永久在线) 选项。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-35-1.png" alt="image-35-1" width="600" height="400" loading="lazy"></p>
<p>还有一种方法可以使你的代码保持运行，即使是在免费计划，但它有点复杂。Repl.it 在标签关闭之后。但即使是网络服务器，也只能在没有任何使用的情况下运行一个小时。</p>
<p>下面是 repl.it 文档中的内容：</p>
<blockquote>
<p>当部署完，服务器将继续在后台运行，甚至在你关闭浏览器标签之后。服务器将保持运行和活跃，直到最后一次请求后一个小时，之后它将进入睡眠阶段。睡眠中的 Repls 一旦收到一个请求就会被唤醒；不需要重新运行。然而，如果你对你的服务器做了修改，你将需要重新启动 repl，以便看到这些修改反映在新版本中。</p>
</blockquote>
<p>为了保持机器人的持续运行，我们将使用另一项免费的服务，叫做 <a href="https://uptimerobot.com/">Uptime Robot</a>。</p>
<p>Uptime Robot 可以被设置为每 5 分钟 ping 一次在 repl.it 上机器人的网络服务器。有了持续的 ping，机器人将永远不会进入睡眠阶段，而会一直运行。</p>
<p>因此，我们必须再做两件事来使我们的机器人持续运行：</p>
<ol>
<li>在 repl.it 中创建一个网络服务器，然后</li>
<li>设置 Uptime Robot，以持续地 ping 该 Web 服务器。</li>
</ol>
<h3 id="replitweb">如何在 repl.it 中创建一个 Web 服务器</h3>
<p>创建一个网络服务器比你想象的要简单。</p>
<p>要做到这一点，在你的项目中创建一个新的文件，叫做 <code>keep_alive.py</code>。</p>
<p>然后添加以下代码:</p>
<pre><code class="language-python">from flask import Flask
from threading import Thread

app = Flask('')

@app.route('/')
def home():
    return "Hello. I am alive!"

def run():
  app.run(host='0.0.0.0',port=8080)

def keep_alive():
    t = Thread(target=run)
    t.start()
</code></pre>
<p>在这段代码中，我们使用 Flask 来启动一个网络服务器。该服务器向访问它的人返回 “Hello. I am alive.” 给任何访问它的人。该服务器将在与我们的机器人分开的线程上运行。我们不会在这里讨论所有的东西，因为其他的东西与我们的机器人并不相关。</p>
<p>现在我们只需要机器人来运行这个网络服务器。</p>
<p>在 <code>main.py</code> 的顶部添加以下一行来导入服务器。</p>
<pre><code class="language-python">from keep_alive import keep_alive
</code></pre>
<p>要在运行 <code>main.py</code> 时启动网络服务器，请在机器人运行前的倒数第二行添加以下一行。</p>
<p><code>keep_alive()</code></p>
<p>当你加入这段代码后在 repl.it 上运行机器人时，一个新的 Web 服务器窗口将被打开。这里有一个网络服务器的 URL。复制这个 URL，这样你就可以在下一节中使用它。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-20-1.png" alt="image-20-1" width="600" height="400" loading="lazy"></p>
<h3 id="">如何设置机器人运行</h3>
<p>现在，我们需要设置 Uptime Robot，使其每隔 5 分钟就对网络服务器进行一次 ping。这将使机器人持续运行。</p>
<p>在 <a href="https://uptimerobot.com/">https://uptimerobot.com/</a> 上创建一个免费账户。</p>
<p>一旦你登录到你的账户，点击 “Add New Monitor”。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-21-1.png" alt="image-21-1" width="600" height="400" loading="lazy"></p>
<p>对于新的监控器（monitor），选择 “HTTP(s)” 作为监控器类型，并命名为你喜欢的任何名字。然后，从 repl.it 中粘贴你的网络服务器的 URL。最后，点击 “Create Monitor”。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/image-22-1.png" alt="image-22-1" width="600" height="400" loading="lazy"></p>
<p>我们完成了！现在，这个机器人将持续运行，所以人们可以一直在 Repl.it 上 与它互动。</p>
<h2 id="">结语</h2>
<p>你现在知道如何用 Python 创建一个 Discord 机器人，并在云上持续运行。</p>
<p>discord.py 库还可以做很多其他的事情。因此，如果你想让 Discord 机器人拥有更多的功能，你的下一步是查看 <a href="https://discordpy.readthedocs.io/en/latest/index.html">discord.py 文档</a></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 使用 TypeScript、MongoDB 和 Discord.js 13 构建一个 100 天编程挑战的 Discord 机器人 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Build a 100 Days of Code Discord Bot with TypeScript, MongoDB, and Discord.js 13 [https://www.freecodecamp.org/news/build-a-100-days-of-code-discord-bot-with-typescript-mongodb-and-discord-js-13/] ，作者：Naomi Carrigan [https://www.freecodecamp.org/news/author/nhcarrigan/] 100 天编程挑战 [https://www.freecodecamp.org/news/the-crazy-history-of-the-100daysofcode-challenge-and-why-you-should-try-it-for-2018-6c89a76e298d/] 在希望提升技能的编程学习者和经验丰富的开发者中都非常流行。它是如此受欢迎，以至于我们的 Discord 服务器 [https://www.freecodecamp ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-a-100-days-of-code-discord-bot-with-typescript-mongodb-and-discord-js-13/</link>
                <guid isPermaLink="false">624fa4b299ec7406219e5f70</guid>
                
                    <category>
                        <![CDATA[ 机器人 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Fri, 08 Apr 2022 09:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/04/pexels-kindel-media-8566473.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/build-a-100-days-of-code-discord-bot-with-typescript-mongodb-and-discord-js-13/">Build a 100 Days of Code Discord Bot with TypeScript, MongoDB, and Discord.js 13</a>，作者：<a href="https://www.freecodecamp.org/news/author/nhcarrigan/">Naomi Carrigan</a></p><!--kg-card-begin: markdown--><p><a href="https://www.freecodecamp.org/news/the-crazy-history-of-the-100daysofcode-challenge-and-why-you-should-try-it-for-2018-6c89a76e298d/">100 天编程挑战</a>在希望提升技能的编程学习者和经验丰富的开发者中都非常流行。它是如此受欢迎，以至于我们的 <a href="https://www.freecodecamp.org/news/freecodecamp-discord-chat-room-server/">Discord 服务器</a>有一个频道专门讨论它。</p>
<p>应大家的要求，我们创建了一个 Discord 机器人，帮助人们跟踪他们在挑战中的进展。</p>
<p>今天我将向你展示如何创建你自己的 “100 天编程挑战”机器人。</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#create-a-discord-bot-application">创建一个 Discord 机器人应用程序</a></li>
<li><a href="#set-up-your-project">设置你的项目</a></li>
<li><a href="#create-the-discord-bot">创建 Discord 机器人</a></li>
<li><a href="#gateway-events-in-discord">Discord 中的网关事件</a></li>
<li><a href="#connect-to-the-database">连接到数据库</a></li>
<li><a href="#environment-variable-validation">环境变量验证</a></li>
<li><a href="#the-interaction-event">交互事件</a></li>
<li><a href="#prepare-for-commands">准备命令</a></li>
<li><a href="#database-model">数据库模型</a></li>
<li><a href="#write-bot-commands">编写机器人命令</a></li>
</ul>
<h2 id="create-a-discord-bot-application">创建一个 Discord 机器人应用程序</h2>
<p>你的第一步是设置一个 Discord 机器人应用程序。前往 <a href="https://discord.dev">Discord Developer Portal</a>，如果需要，请登录，并从侧边栏选择 “Applications”。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/01/image-76.png" alt="这是开发者门户网站的屏幕截图，如果这是你的第一个机器人，这里将不会有任何应用程序" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>这是开发者门户网站的屏幕截图，如果这是你的第一个机器人，这里将不会有任何应用程序</figcaption>
</figure>
<p>从侧边栏选择 “Bot”，然后点击 “Add Bot” 按钮。这将为你的应用程序创建一个 Discord Bot 账户。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/01/image-77.png" alt="这是机器人设置页面的屏幕截图，如果你没有设置头像，你会看到一个基于你的机器人名称的默认头像" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>这是机器人设置页面的屏幕截图，如果你没有设置头像，你会看到一个基于你的机器人名称的默认头像</figcaption>
</figure>
<p>这是你将获得的机器人令牌（token）的截图。保密这个令牌是非常重要的，因为它允许你的代码连接到你的机器人。请保证它的安全，不要与任何人分享它。</p>
<p>现在你需要将机器人添加到一个服务器上，与它进行交互。点击侧边栏上的 “OAuth2” 选项，然后选择 “URL Generator”。</p>
<p>在 “Scopes” 下，选择<code>bot</code>和<code>application.command</code>。<code>bot</code>范围允许你的机器人账户加入服务器，而<code>application.command</code>范围允许你更新斜线命令（后面会有更多介绍 slash commands）。</p>
<p>当你选择<code>bot</code>时，会出现一个新的 “Bot Permissions” 部分。选择以下权限：</p>
<ul>
<li>Send Messages</li>
<li>Embed Links</li>
<li>Read Messages/View Channels</li>
</ul>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/01/image-78.png" alt="带有所需设置的 OAuth 屏幕截图" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>带有所需设置的 OAuth 屏幕截图</figcaption>
</figure>
<p>复制生成的 URL，并将其粘贴到你的浏览器。这将使你通过 Discord 的程序，将你的新机器人添加到一个服务器。</p>
<p>注意，你必须在你想添加机器人的服务器中拥有管理服务器的权限。如果你没有这个权限，你可以创建一个服务器来测试你的机器人。</p>
<p>现在你已经准备好写一些代码了！</p>
<h2 id="set-up-your-project">设置你的项目</h2>
<p>你首先需要为你的项目创建基础设施和工具。</p>
<p>确保你安装了 Node.js <strong>版本 16</strong>和<code>npm</code>。注意，你将使用的软件包不支持早期版本的 Node。</p>
<h3 id="packagejson">准备<code>package.json</code>文件</h3>
<p>为你的项目创建一个目录，或文件夹。打开你的终端，指向这个新文件夹。运行命令<code>npm init</code>来设置你的<code>package.json</code>文件。在本教程中，默认值就足够了，但你可以根据你的需要自由编辑。</p>
<p>你最终应该得到一个类似于以下的<code>package.json</code>：</p>
<pre><code class="language-json">{
  "name": "100doc-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
  },
  "author": "",
  "license": "ISC"
}
</code></pre>
<p>现在你需要做一些改变，为 TypeScript 的实现做好准备。</p>
<p>首先，将<code>index.js</code>的<code>main</code>值替换为<code>./prod/index.js</code>——你将设置你的 TypeScript 编译到<code>prod</code>目录。</p>
<p>然后删除<code>test</code>脚本，添加以下两个脚本：</p>
<pre><code class="language-json">"build": "tsc",
"start": "node -r dotenv/config ./prod/index.js"
</code></pre>
<p><code>build</code>脚本将把你的 TypeScript 编译成 JavaScript，以便 Node 可以运行它，<code>start</code>脚本将运行<code>index.js</code>入口文件。</p>
<p>在这里添加<code>-r dotenv/config</code>将动态导入并运行<code>dotenv</code>包中的<code>config</code>方法，它从<code>.env</code>文件中加载你的环境变量。</p>
<p>你的下一步是安装依赖项。使用<code>npm install</code>，安装这些依赖项：</p>
<ul>
<li><code>discord.js</code> – 这是一个处理连接到网关和管理 Discord API 调用的库</li>
<li><code>@discordjs/builders</code> – 用于构建应用程序命令的 discord.js 包</li>
<li><code>@discordjs/rest</code> – 用于与 Discord REST API 互动的自定义 API 客户端</li>
<li><code>discord-api-types</code> – Discord REST API 的类型定义和处理程序</li>
<li><code>dotenv</code> – 一个将`.env'值加载到 Node 进程的包</li>
<li><code>mongoose</code> – MongoDB 连接的驱动，提供了结构化数据的工具</li>
</ul>
<p>最后，用<code>npm install --save-dev</code>安装开发依赖项。开发依赖是指在开发环境中处理你的项目所需要的包，但在生产中运行代码库时不需要。</p>
<ul>
<li><code>typescript</code> – 这是 TypeScript 语言的包，它包括用 TypeScript 编写代码并将其编译为 JavaScript 所需的一切。</li>
<li><code>@types/node</code> – TypeScript 依靠类型定义来理解你写的代码。这个包定义了 Node.js 运行环境的类型，例如<code>process.env</code>对象。</li>
</ul>
<p>安装了这些软件包后，你现在应该有一个类似于以下的<code>package.json</code>：</p>
<pre><code class="language-json">{
  "name": "100doc-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "./prod/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node -r dotenv/config ./prod/index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@discordjs/builders": "^0.11.0",
    "@discordjs/rest": "^0.2.0-canary.0",
    "discord.js": "^13.6.0",
    "dotenv": "^14.2.0",
    "mongoose": "^6.1.7"
  },
  "devDependencies": {
    "@types/node": "^17.0.10",
    "typescript": "^4.5.4"
  }
}
</code></pre>
<h3 id="typescript">准备好 TypeScript</h3>
<p>TypeScript 的编译器提供了许多不同的设置，以最大限度地提高你对生成的 JavaScript 的控制。</p>
<p>你通常可以通过项目根部的<code>tsconfig.json</code>文件修改编译器设置。你可以用<code>npx tsc --init</code>为这个文件生成默认的模板，如果你在其他项目中设置了一个模板，可以使用现有的模板，甚至可以从头开始写一个。</p>
<p>因为编译器的设置可以显著改变 TypeScript 的行为，所以在学习本教程时最好使用相同的设置。以下是你应该使用的设置：</p>
<pre><code class="language-json">{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "rootDir": "./src",
    "outDir": "./prod",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  }
}
</code></pre>
<p>这里最重要的设置是<code>rootDir</code>和<code>outDir</code>的设置。这些设置告诉编译器，你所有的代码都在<code>src</code>目录下，而生成的 JavaScript 应该在<code>prod</code>目录下。</p>
<p>如果你想测试你的设置，创建一个<code>src</code>目录并在里面放置一个<code>index.ts</code>文件。编写一些代码（如<code>console.log</code>语句）并在终端运行<code>npm run build</code>。你应该看到一个<code>prod</code>目录被创建，其中的<code>index.js</code>包含了你的编译代码。</p>
<h3 id="">其他设置说明</h3>
<p>如果你使用<code>git</code>作为版本控制，你要避免将秘密的和不必要的代码推送到你的仓库。在你的项目根目录下创建一个<code>.gitignore</code>文件，并添加以下内容：</p>
<pre><code class="language-txt">/node_modules/
/prod/
.env
</code></pre>
<p><code>.gitignore</code>文件告诉<code>git</code>不要追踪符合你输入的模式的文件/文件夹。忽略 <code>node_modules</code>文件夹可以防止你的仓库变得臃肿（node_modules 实在太大了，有黑洞之称）。</p>
<p>推送已编译的 JavaScript 也是不必要的，因为你的项目通常在运行前就已经在生产中编译了。<code>.env</code>文件包含秘密值，如 API 密钥和令牌，所以它们不应该被提交到版本库。</p>
<h2 id="create-the-discord-bot">创建 Discord 机器人</h2>
<p>你的下一步是准备初始的机器人连接。如果你之前没有这样做，创建一个<code>src</code>目录和一个<code>index.ts</code>文件。</p>
<p>从一个匿名的立即执行的函数表达式（IIFE）开始，以允许顶层的<code>await</code>使用：</p>
<pre><code class="language-ts">(async () =&gt; {

})();
</code></pre>
<p>在这个函数中，你将实例化你的 Discord 机器人。在文件的顶部，用<code>import { Client } from "discord.js";</code>导入<code>Client</code>类。<code>Client</code>类代表你的 Discord 机器人的会话。</p>
<p>在你的函数中，构建一个新的<code>Client</code>实例，并将其分配给<code>BOT</code>变量，<code>const BOT = new Client();</code>。现在，`BOT'变量将代表你的机器人。</p>
<p>为了将你的机器人连接到 Discord 网关并开始接收事件，你需要在你的机器人实例上使用<code>.login()</code>方法。<code>.login()</code>方法需要一个参数，即你之前创建的机器人应用程序的令牌（token）。</p>
<p><code>discord.js</code>中的许多方法是异步的，所以你需要在这里使用<code>await</code>。在你的IIFE中加入<code>await BOT.login(process.env.BOT_TOKEN);</code>这一行。</p>
<p>你的 <code>index.ts</code> 文件现在应该看起来像这样：</p>
<pre><code class="language-ts">import { Client } from "discord.js";

(async () =&gt; {
  const BOT = new Client();

  await BOT.login(process.env.BOT_TOKEN);
})();
</code></pre>
<p>如果你尝试运行<code>npm run build</code>，你会看到一个错误：<code>An argument for 'options' was not provided.</code>。</p>
<p>在 discord.js 13 中，当你实例化你的机器人时，你需要指定 Gateway Intents。Gateway Intents 告诉 Discord 你的机器人应该接收哪些事件。</p>
<p>在你的<code>src</code>文件夹中，创建一个<code>config</code>文件夹 - 然后在<code>config</code>中，创建一个<code>IntentOptions.ts</code>文件。</p>
<p>在这个新文件中，添加 “export const IntentOptions = ["GUILDS"]”一行。这将告诉 Discord 你的机器人应该接收公会事件（Guild events）。</p>
<p>然后，在你的<code>index.ts</code>文件中，给你的<code>new Client()</code>调用添加一个参数：<code>new Client({intents: IntentOptions})</code>。你需要在文件的顶部用 <code>import { IntentOptions } from "./config/IntentOptions;</code>，导入它。</p>
<p>看来你仍然有一个错误：<code>Type 'string' is not assignable to type 'number | `${bigint}` | IntentsString | Readonly&lt;BitField&lt;IntentsString, number&gt;&gt; | RecursiveReadonlyArray&lt;number | `${bigint}` | IntentsString | Readonly&lt;...&gt;&gt;'.</code></p>
<p>TypeScript 将你的<code>IntentOptions</code>数组推断为一个字符串，但<code>Client</code>构造函数期望的是更具体的类型。</p>
<p>回到你的<code>config/IntentOptions.ts</code>文件，添加另一个导入。<code>import { IntentsString } from "discord.js"</code>。然后用新的类型定义更新你的变量： <code>export const IntentOptions: IntentsString[] = ["GUILDS"];</code>。</p>
<p>现在<code>npm run build</code>应该成功了。如果你已经把你的新机器人（bot）添加到一个 Discord 服务器，运行<code>npm start</code>将显示你的机器人在该服务器中上线。然而，机器人还不会对任何事情做出反应，因为你还没有开始监听事件。</p>
<h2 id="gateway-events-in-discord">Discord 中的网关事件（Gateway Events）</h2>
<p>网关事件是在 Discord 上发生动作时产生的，通常以 JSON payloads（有效载荷）的形式发送到客户端（包括你的机器人）。你可以用<code>.on()</code>方法监听这些事件，允许你为你的机器人编写逻辑，以便在特定事件发生时执行。</p>
<p>第一个要监听的事件是 “ready” 事件。当你的机器人连接到网关并准备处理事件时，这个事件就会发生。在你的<code>.login()</code>调用上面，添加<code>BOT.on("ready", () =&gt; console.log("Connected to Discord!");</code>。</p>
<p>为了使你的修改生效，再次使用<code>npm run build</code>来编译新的代码。现在，如果你尝试<code>npm run start</code>，你应该看到 “Connected to Discord!” 打印在你的终端。</p>
<h2 id="connect-to-the-database">连接到数据库</h2>
<p>你将使用<code>mongoose</code>包来连接到 MongoDB 实例。如果你愿意，你可以在本地运行 MongoDB，或者你可以使用 MongoDB Atlas 免费层来实现基于云的解决方案。</p>
<p>如果你没有 MongoDB Atlas 账户，freeCodeCamp 有一个<a href="https://www.freecodecamp.org/news/get-started-with-mongodb-atlas/">关于设置一个账户的好教程</a>。</p>
<p>获得你的数据库的连接字符串，并将其添加到你的<code>.env</code>文件中，作为<code>MONGO_URI=""</code>，连接字符串要在引号之间。对于数据库的名称，使用<code>oneHundredDays</code>。</p>
<p>创建一个名为 “database” 的目录来存放包含数据库逻辑的文件。在这个目录中，创建一个名为<code>connectDatabase.ts</code>的文件。你将在这里编写启动数据库连接的逻辑。</p>
<p>从一个导出的函数声明开始：</p>
<pre><code class="language-ts">export const connectDatabase = async () =&gt; {

}
</code></pre>
<p>注意，你需要在这里使用 <code>async</code> 关键字，因为数据库连接方法是异步的。</p>
<p><code>mongoose</code> 提供了一个 <code>connect</code> 方法用于连接数据库。在你的文件顶部用 <code>import { connect } from "mongoose";</code> 导入它。</p>
<p>然后用 <code>await connect(process.env.MONGO_URI);</code> 在你的函数中使用该方法。在这之后添加一个 <code>console.log</code> 语句，这样你就可以确定你的机器人已经连接到了数据库。</p>
<p>你的 <code>connectDatabase.ts</code> 文件现在看起来应该是这样的：</p>
<pre><code class="language-ts">import { connect } from "mongoose";

export const connectDatabase = async () =&gt; {
    await connect(process.env.MONGO_URI);
    console.log("Database Connected!")
}
</code></pre>
<p>现在，在你的 <code>index.ts</code> 文件中，用 <code>import { connectDatabase } from "./database/connectDatabase"</code> 导入这个函数，并在你的 IIFE 中添加 <code>await connectDatabase()</code>，就在 <code>.login()</code> 方法之前。继续并再次运行 <code>npm run build</code>。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/06/image-157.png" alt="一个编译器错误表明：类型为字符串或未定义的参数不能分配给类型为字符串的参数" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>一个编译器错误表明：类型为字符串或未定义的参数不能分配给类型为字符串的参数</figcaption>
</figure>
<p>哦，不——一个错误！</p>
<h2 id="environment-variable-validation">环境变量验证</h2>
<p>环境变量的问题是，它们都可能是 <code>undefined</code>。如果你在环境变量名称中打错了字，或者把名称和其他名称混在一起，就会经常发生这种情况（我在写这个教程时犯了一个错误，在一些地方用<code>TOKEN</code>而不是<code>BOT_TOKEN</code>）。</p>
<p>TypeScript 警告你，<code>connect</code> 方法需要一个字符串，而 <code>undefined</code> 值会破坏事情。你可以解决这个问题，但首先你要写一个函数来处理验证你的环境变量。</p>
<p>在你的 <code>src</code> 目录下，创建一个 <code>utils</code> 目录，包含你的实用函数。在那里添加一个 <code>validateEnv.ts</code> 文件。</p>
<p>在该文件中创建一个名为 <code>validateEnv</code> 的函数。这个函数将是同步的，不需要 <code>async</code> 关键字。在这个函数中，添加条件来检查你的两个环境变量。如果缺少任何一个，返回 <code>false</code>。否则，返回 <code>true</code>。</p>
<p>你的代码可能看起来像这样：</p>
<pre><code class="language-ts">export const validateEnv = () =&gt; {
  if (!process.env.BOT_TOKEN) {
    console.warn("Missing Discord bot token.");
    return false;
  }

  if (!process.env.MONGO_URI) {
    console.warn("Missing MongoDB connection.");
    return false;
  }
  return true;
};

</code></pre>
<p>回到你的 <code>index.ts</code> 文件，用 <code>import { validateEnv } from "./utils/validateEnv"</code> 导入这个验证函数。然后在你的 IIFE 的开头，使用一个 if 语句，如果函数返回 false，就提前返回。你的 <code>index.ts</code> 应该看起来像：</p>
<pre><code class="language-ts">import { Client } from "discord.js";
import { connectDatabase } from "./database/connectDatabase";
import { validateEnv } from "./utils/validateEnv";

(async () =&gt; {
  if (!validateEnv()) return;

  const BOT = new Client();

  BOT.on("ready", () =&gt; console.log("Connected to Discord!"));

  await connectDatabase();

  await BOT.login(process.env.BOT_TOKEN);
})();
</code></pre>
<p>如果你再次尝试 <code>npm run build</code>，你会看到和之前一样的错误信息。这是因为虽然我们知道环境变量存在，但 TypeScript 仍然无法推断出它。验证函数被设置为在环境变量丢失时退出进程，所以我们要告诉 TypeScript 它肯定是一个字符串。</p>
<p>回到你的 <code>connectDatabase.ts</code> 文件中，在 <code>connect</code> 函数中使用 <code>process.env.MONGO_URI as string</code>来强迫类型为 <code>string</code>。错误应该消失了，你现在可以运行 <code>npm run build</code> 和 <code>npm start</code>。</p>
<p>你应该看到你为 Discord 和 MongoDB 连接写的信息在终端打印出来。</p>
<h2 id="the-interaction-event">交互事件</h2>
<p>虽然你的机器人取得了很大的进展，但它仍然没有做任何事情。为了接收命令，你将需要创建另一个事件监听器。</p>
<p>Discord推出了 <code>slash</code> 命令，具有一个新的用户界面和一个新的网关事件。当有人用你的机器人使用 <code>slash</code>命令时，<code>interactionCreate</code> 事件被触发。这是你想要监听的事件。因为逻辑比 <code>ready</code>事件更复杂，你将需要创建一个单独的文件。</p>
<p>在你的 <code>src</code> 目录下，创建一个 <code>events</code> 目录，并在其中创建 <code>onInteraction.ts</code> 文件。首先定义一个导出的函数 <code>onInteraction</code>。这应该是一个异步函数，有一个名为 <code>interaction</code> 的单一参数。</p>
<pre><code class="language-ts">export const onInteraction = async (interaction) =&gt; {

};
</code></pre>
<p>为了给你的参数提供一个类型定义，从<code>discord.js</code>导入<code>Interaction</code>类型。</p>
<pre><code class="language-ts">import { Interaction } from "discord.js";

export const onInteraction = async (interaction: Interaction) =&gt; {

};
</code></pre>
<p><code>interaction</code>事件实际上是在任何命令交互上触发的，这包括像按钮点击和选择菜单，以及我们想要的 <code>slash</code> 命令。</p>
<p>因为你将只为这个机器人编写 <code>slash</code> 命令，你可以过滤掉任何其他的交互类型，帮助 TypeScript 理解你正在处理的数据。</p>
<p>在你的新函数中，添加一个条件来检查<code>interaction.isCommand()</code>。稍后你将在这个块中编写逻辑。</p>
<pre><code class="language-ts">import { Interaction } from "discord.js";

export const onInteraction = async (interaction: Interaction) =&gt; {
  if (interaction.isCommand()) {
  }
};
</code></pre>
<p>现在，在你的 <code>index.ts</code> 文件中，你可以加载另一个监听器。在你的 <code>.on("ready")</code> 监听器旁边，添加一个<code>BOT.on("interactionCreate")</code> 监听器。对于这个事件，回调需要一个 <code>interaction</code> 参数，你可以把它传递给你新的 <code>onInteraction</code> 函数。</p>
<pre><code class="language-ts">  BOT.on(
    "interactionCreate",
    async (interaction) =&gt; await onInteraction(interaction)
  );
</code></pre>
<p>记住，你将需要导入你的 <code>onInteraction</code> 函数。</p>
<p>很好！你可以运行 <code>npm run build</code> 来确认 TypeScript 没有抛出任何错误，但如果没有实际的命令来使用，你还不能完全测试这段代码。</p>
<h2 id="prepare-for-commands">准备命令</h2>
<p>我维护了一些 Discord 机器人，我发现有一件事有助于保持代码的可维护性和可读性，那就是使组件模块化。</p>
<h3 id="">定义一个接口</h3>
<p>你将首先需要为你的命令定义一个共同的结构。在<code>src</code>中创建一个<code>interfaces</code>文件夹，然后在<code>interfaces</code>中创建一个名为<code>Command.ts</code>的文件。</p>
<p>现在你要创建一个接口。在 TypeScript 中，接口经常被用来定义对象的结构，也是众多用于声明变量类型的工具之一。</p>
<p>在你的<code>Command.ts</code>文件中，创建一个名为<code>Command</code>的导出接口：</p>
<pre><code class="language-ts">export interface Command {

}
</code></pre>
<p>你的接口将有两个属性——<code>data</code>，它将保存要发送给 Discord 的命令数据，以及 <code>run</code>，它将保存回调函数和命令逻辑。</p>
<p>对于 <code>data</code> 属性，从 <code>@discordjs/builders</code> 导入 <code>SlashCommandBuilder</code> 和<code>SlashCommandSubcommandsOnlyBuilder</code>。将 <code>data</code> 属性定义为这两种类型中的一种。</p>
<p>对于 <code>run</code> 属性，从 <code>discord.js</code> 导入 <code>CommandInteraction</code> 类型。将 <code>run</code> 定义为一个函数，接收一个 <code>CommandInteraction</code> 类型的参数并返回一个 <code>void</code> Promise。</p>
<pre><code class="language-ts">import {
  SlashCommandBuilder,
  SlashCommandSubcommandsOnlyBuilder,
} from "@discordjs/builders";
import { CommandInteraction } from "discord.js";

export interface CommandInt {
  data: SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder;
  run: (interaction: CommandInteraction) =&gt; Promise&lt;void&gt;;
}
</code></pre>
<h3 id="">创建一个命令列表</h3>
<p>接下来你需要一个地方来存储你所有的命令。在 <code>src</code> 目录下创建一个名为 <code>commands</code> 的文件夹，并添加一个名为 <code>_CommandList.ts</code> 的文件。这里的下划线将使这个文件保持在列表的顶部。</p>
<p><code>_CommandList.ts</code> 文件将需要两行。首先，导入你的 <code>Command</code> 接口，然后声明一个 <code>CommandList</code> 数组。这个数组现在是空的，但是要给它一个 <code>Command[]</code> 的类型，这样 TypeScript 就知道它最终会容纳你的命令对象。这个文件应该是这样的：</p>
<pre><code class="language-ts">import { Command } from "../interfaces/Command";

export const CommandList: Command[] = [];
</code></pre>
<p>这个文件的目的是创建一个你的机器人的命令数组，你将在交互事件监听器中进行迭代。<a href="https://github.com/BeccaLyria/discord-bot/blob/main/src/utils/readDirectory.ts">可以使之自动化</a>，但对于较小的机器人来说，它们往往是不必要的复杂。</p>
<h3 id="">检查命令的执行情况</h3>
<p>在你的 <code>onInteraction.ts</code> 文件中，你应该开始研究寻找和运行命令的逻辑。</p>
<p>在你的 <code>interaction.isCommand()</code> 条件块中，通过 <code>CommandList</code> 数组（记得要导入它！）进行 <code>for...of</code> 循环。</p>
<pre><code class="language-ts">for (const Command of CommandList) {

}
</code></pre>
<p>从 Discord 收到的交互 payload （有效载荷）包括一个 <code>commandName</code> 属性，你可以用它来查找用户选择的命令。要检查这一点，将 <code>interaction.commandName</code> 与 <code>Command.data.name</code> 属性进行比较。</p>
<pre><code class="language-ts">if (interaction.commandName === Command.data.name) {

}
</code></pre>
<p>现在，如果你已经找到了用户选择的命令，你需要运行该命令的逻辑。这是通过 <code>Command.run(interaction)</code> 的调用来实现的--将交互的 payload（有效载荷）传递给命令。</p>
<p>你的最终文件应该是这样的：</p>
<pre><code class="language-ts">import { Interaction } from "discord.js";
import { CommandList } from "../commands/_CommandList";

export const onInteraction = async (interaction: Interaction) =&gt; {
  if (interaction.isCommand()) {
    for (const Command of CommandList) {
      if (interaction.commandName === Command.data.name) {
        await Command.run(interaction);
        break;
      }
    }
  }
};
</code></pre>
<p>注意，在我们运行命令后，我们 <code>break</code> 循环，以避免不必要的搜索。</p>
<h2 id="database-model">数据库模型</h2>
<p>在你准备开始编写命令之前，还有一个步骤。这个机器人将跟踪你的社区成员的 100 天编程挑战的进展。而你需要将该进度存储在数据库中。</p>
<p><code>mongoose</code> 可以帮助你结构化你的 MongoDB 记录，以防止你将畸形或不完整的数据传入数据库。</p>
<p>首先，在你的 <code>database</code> 目录下创建一个 <code>models</code> 文件夹。在这个 <code>models</code> 文件夹中，创建一个 <code>CamperModel.ts</code> 文件。这将是你的用户对象的结构。</p>
<p>你首先需要从 <code>mongoose</code> 库中导入必要的值。在文件的顶部添加 <code>import { Document, model, Schema } from "mongoose";</code>。</p>
<p>因为你正在使用 TypeScript，你需要为你的数据库对象创建一个类型定义。创建另一个接口，就像你为你的命令所做的那样，名为<code>CamperInt</code>。</p>
<pre><code class="language-ts">export interface CamperInt extends Document {

}
</code></pre>
<p><code>extends</code> 关键字告诉TypeScript我们要在 <code>Document</code> 类型的基础上添加属性。</p>
<p>你的数据库模型将有四个属性。把这些添加到你的接口中：</p>
<ul>
<li><code>discordId: string;</code> - Discord 中的每个用户对象都有一个唯一的标识符，称为Snowflake，用于区分他们与其他用户。与用户名或判别符（用户名后的四位数）不同，<code>id</code>值不能被改变。这使得它成为将你的存储数据与 Discord 用户联系起来的理想值。</li>
<li><code>round: number;</code> - 这将代表用户在挑战中所处的“回合”。当某人完成了 100 天的挑战，他们可以选择再次进行挑战。当他们这样做的时候，他们通常称其为“第二轮”。</li>
<li><code>day: number;</code> - 这代表用户在挑战中的日期。</li>
<li><code>timestamp: number;</code> - 你将使用这个值来跟踪用户最后一次提交 100 天编程挑战帖子的时间。</li>
</ul>
<p>很好！现在你需要为你的数据库条目定义模式。<code>mongoose</code> 使用一个 Schema 对象来定义进入数据库集合的文件的形状。<code>Schema</code> 导入有一个构造函数，你将把它分配给一个变量。</p>
<pre><code class="language-ts">export const Camper = new Schema();
</code></pre>
<p>这个构造函数接受一个对象作为其参数，这个对象定义了数据库的键和类型。继续，传入一个与你的界面相似的对象。</p>
<pre><code class="language-ts">export const Camper = new Schema({
    discordId: String,
    round: Number,
    day: Number,
    timestamp: Number,
})
</code></pre>
<p>注意，我们使用的是<code>String</code>而不是<code>string</code>。<code>String</code>是指JavaScript的原始类型，而<code>string</code>是TypeScript的类型定义。</p>
<p>接下来你需要创建<code>model</code>。在 <code>mongoose</code> 中，<code>model</code> 对象的作用是在MongoDB数据库中创建、读取和更新你的文档。在你文件的底部添加 <code>export default model();</code>。</p>
<p><code>model</code> 函数需要几个参数。第一个是一个字符串，是你数据库中的文档（documents）的名称。对于这个集合（collection），使用 <code>"camper"</code>。第二个参数是用于数据的模式（schema）--使用你的 <code>Camper</code> 模式（schema）。</p>
<p>默认情况下，<code>mongoose</code> 将使用你的 <code>model</code> 名称的复数版本作为集合。在我们的例子中，这将是 "campers"。如果你想改变它，你可以传入第三个参数 <code>{集合: "name" }</code> 来设置集合为 <code>name</code>。</p>
<p>如果你使用的是 JavaScript，这就足以让你的数据库模型设置好。然而，由于你使用的是TypeScript，你应该利用类型安全的优势。<code>model()</code> 默认返回一个 <code>Document</code> 类型的 <code>any</code>。</p>
<p>为了解决这个问题，你可以在 <code>model</code> 函数中传递一个泛型。从某种意义上说，泛型可以作为类型定义的变量。你需要为你的 <code>model</code> 设置泛型以使用你的接口。通过将 <code>model</code> 改为 <code>model&lt;CamperInt&gt;</code>，来添加泛型。</p>
<p>这里只有一个步骤了。你的 <code>CamperInt</code> 接口只定义了你在 MongoDB 文档中设置的属性，但并不包括标准属性。</p>
<p>将你的 <code>export interface CamperInt</code> 改为 <code>export interface CamperInt extends Document</code>。这告诉 TypeScript，你的类型定义是现有 <code>Document</code> 类型定义的扩展——你基本上是在向该结构添加属性。</p>
<p>你的最终文件应该看起来像这样：</p>
<pre><code class="language-ts">import { Document, model, Schema } from "mongoose";

export interface CamperInt {
  discordId: string;
  round: number;
  day: number;
  timestamp: number;
}

export const Camper = new Schema({
  discordId: String,
  round: Number,
  day: Number,
  timestamp: Number,
});

export default model&lt;CamperInt&gt;("camper", Camper);
</code></pre>
<p>作为一个安全检查，再次使用<code>npm run build</code>。你不应该在终端看到任何错误。</p>
<h2 id="write-bot-commands">编写机器人命令</h2>
<p>你终于准备好开始编写一些命令了。由于这是一个 100 天代码机器人，你应该从创建 100 天代码更新的命令开始。</p>
<h3 id="100command">100 Command</h3>
<p>在你的 <code>commands</code> 文件夹中，创建一个 <code>oneHundred.ts</code> 文件。这将保存你的100天代码命令。用 <code>import { Command } from ".../interfaces/Command;</code>导入你的命令接口。</p>
<p>现在声明一个导出的变量<code>oneHundred</code>，并赋予它<code>Command</code>类型：</p>
<pre><code class="language-ts">import { Command } from "../interfaces/Command";

export const oneHundred: Command = {

};
</code></pre>
<p>首先，创建 <code>data</code> 属性。你将使用 <code>@discordjs/builders</code> 包来创建一个 <code>slash</code> 命令。</p>
<p>首先从 <code>@discordjs/builders</code> 包中导入 <code>SlashCommandBuilder()</code>。然后，用 <code>new SlashCommandBuilder()</code> 在<code>data</code> 属性中构建一个新实例。你将在这里使用一些方法来传递你想要的信息到构建器中。</p>
<p><code>.setName()</code> 方法允许你设置斜线命令的名称。设置名称为 <code>"100"</code>。<code>setDescription()</code> 选项允许你在 Discord 的用户界面中显示命令的描述。将描述设为 <code>"Check in for the 100 Days of Code challenge"</code>。</p>
<p>Slash 命令也可以接受 <code>option</code> 值。这些是用来接受用户的参数的，有各种类型。对于这个命令，你需要一个字符串选项，使用 <code>addStringOption()</code> 方法。选项方法需要一个回调函数，有一个 <code>option</code> 参数。</p>
<p>然后你可以在 <code>option</code> 参数上使用连锁方法来配置参数的信息。使用 <code>.setName()</code> 方法给选项取名为<code>"message"</code>，使用<code>.setDescription()</code>方法给它取名为<code>"The message to go in your 100 Days of Code update"</code>。最后，使用<code>.setRequired()</code>方法将该选项设置为必填。</p>
<p>以下是你现在应该有的东西：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { Command } from "../interfaces/Command";

export const oneHundred: Command = {
  data: new SlashCommandBuilder()
    .setName("100")
    .setDescription("Check in for the 100 Days of Code challenge.")
    .addStringOption((option) =&gt;
      option
        .setName("message")
        .setDescription("The message to go in your 100 Days of Code update.")
        .setRequired(true)
    ),
};
</code></pre>
<p>如果你在 IDE 中编码启用了智能提示，你可能已经注意到，这将在 <code>data</code> 属性上抛出一个类型错误（type error）。这是因为 <code>SlashCommandBuilder</code> 实际上返回了一个 <code>Omit</code> 类型！ <code>Omit</code> 类型是用来告诉 TypeScript，该类型和另一个类型几乎相同，但删除了特定属性。</p>
<p>前往你的 <code>interfaces/Command.ts</code> 文件，更新类型。用 <code>Omit&lt;SlashCommandBuilder, "addSubcommandGroup" | "addSubcommand"&gt;</code> 替换 <code>SlashCommandBuilder</code> 类型。这将告诉TypeScript，<code>data</code> 应该是一个<code>SlashCommandBuilder</code>，但没有那两个特定的属性。</p>
<pre><code class="language-ts">import {
  SlashCommandBuilder,
  SlashCommandSubcommandsOnlyBuilder,
} from "@discordjs/builders";
import { CommandInteraction } from "discord.js";

export interface Command {
  data:
    | Omit&lt;SlashCommandBuilder, "addSubcommandGroup" | "addSubcommand"&gt;
    | SlashCommandSubcommandsOnlyBuilder;
  run: (interaction: CommandInteraction) =&gt; Promise&lt;void&gt;;
}
</code></pre>
<p>很好！现在你的类型错误已经解决了，回到你的 <code>oneHundred.ts</code> 命令文件--是时候编写命令逻辑了。</p>
<p>你的机器人响应命令的所有逻辑将被放在 <code>run</code> 属性中。就像你在界面中做的那样，首先创建一个接受 <code>interaction</code> 参数的async函数。然后，让你的函数的第一行是 <code>await interaction.deferReply();</code>。</p>
<p>Discord 期望机器人在三秒内对一个命令做出反应。因为这个命令可能需要更长的时间来处理，使用 <code>.deferReply()</code> 方法会发送一个确认响应，让你有整整 15 分钟的时间来发送实际响应。</p>
<p>接下来，你需要从该命令中提取一些数据。首先，用 <code>const { user } = interaction;</code>将 <code>user</code> 对象从交互的有效载荷中解构出来。<code>user</code> 对象代表调用该命令的 Discord 用户。</p>
<p>然后用 <code>const text = interaction.options.getString("message", true);</code> 获得你发送的 <code>message</code> 选项。通过这一行，你正在访问交互的 <code>options</code> 属性。<code>.getString()</code> 方法专门抓取一个字符串选项（记得你在 <code>data</code> 中创建了这个选项），<code>"message"</code>是这个选项的<strong>name</strong>。<code>true</code>参数表示这是一个必选项，所以TypeScript不会认为它是空的。</p>
<p>你的文件应该看起来像这样：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { Command } from "../interfaces/Command";

export const oneHundred: Command = {
  data: new SlashCommandBuilder()
    .setName("100")
    .setDescription("Check in for the 100 Days of Code challenge.")
    .addStringOption((option) =&gt;
      option
        .setName("message")
        .setDescription("The message to go in your 100 Days of Code update.")
        .setRequired(true)
    ),
  run: async (interaction) =&gt; {
    await interaction.deferReply();
    const { user } = interaction;
    const text = interaction.options.getString("message", true);
  },
};
</code></pre>
<p>这个命令的下一步将是从你的数据库中获取数据。因为你的许多命令都需要这样做，你应该为它创建一个模块。</p>
<h3 id="">处理数据库逻辑</h3>
<p>创建一个 <code>src/modules</code> 目录，并在其中添加一个 <code>getCamperData.ts</code> 文件。创建一个导出的异步函数<code>getCamperData</code>，并给它一个名为 <code>id</code> 的字符串参数。然后，在该函数中，你可以查询数据库。</p>
<p>从 <code>database</code> 目录中导入你的 <code>CamperModel</code>，并使用 <code>findOne()</code> 方法来查询营员的 <code>id</code>：<code>const camperData = await CamperModel.findOne({ discordId: id });</code>。</p>
<pre><code class="language-ts">import CamperModel from "../database/models/CamperModel";

export const getCamperData = async (id: string) =&gt; {
  const camperData = await CamperModel.findOne({ id });
};
</code></pre>
<p>我们在这里还有一个步骤。如果 <code>camper</code> 以前没有使用过机器人，他们就不会有现有的数据库记录。<code>findOne()</code>在这种情况下会返回 <code>null</code>,所以你可以添加一个回退值。</p>
<pre><code class="language-ts">import CamperModel from "../database/models/CamperModel";

export const getCamperData = async (id: string) =&gt; {
  const camperData =
    (await CamperModel.findOne({ discordId: id })) ||
    (await CamperModel.create({
      discordId: id,
      round: 1,
      day: 0,
      date: Date.now(),
    }));
};
</code></pre>
<p>在这里，我们在第一轮第 0 天开始一个新的 <code>camper</code>，如果他们使用 <code>100 command</code>，这允许我们更新他们的状态。</p>
<p>最后，你需要 <code>返回（return）</code>你的数据。在函数的末尾添加 <code>return camperData</code>。为了额外的类型安全，将你的函数的返回类型定义为 <code>Promise&lt;CamperData&gt;</code>。</p>
<pre><code class="language-ts">import CamperModel, { CamperInt } from "../database/models/CamperModel";

export const getCamperData = async (id: string): Promise&lt;CamperInt&gt; =&gt; {
  const camperData =
    (await CamperModel.findOne({ discordId: id })) ||
    (await CamperModel.create({
      discordId: id,
      round: 1,
      day: 0,
      date: Date.now(),
    }));
  return camperData;
};
</code></pre>
<p>你现在有了从数据库中获取 <code>camper</code> 数据的方法，但你也需要一种方法来更新它。在你的<code>/src/modules</code>目录下创建另一个文件，叫做 <code>updateCamperData.ts</code>。这将处理增加 <code>camper</code> 的进度的逻辑。</p>
<p>从一个导出的异步函数开始，称为 <code>updateCamperData</code>。它应该接受一个 <code>Camper</code> 参数，这将是你从 MongoDB 获取的数据。</p>
<pre><code class="language-ts">import { CamperInt } from "../database/models/CamperModel";

export const updateCamperData = async (Camper: CamperInt) =&gt; {
    
};
</code></pre>
<p>你唯一要更新数据的时候是在 <code>/100</code> 命令中——在那里你要增加营员的日计数，检查他们是否开始了新的一轮（round），并更新时间戳。</p>
<p>首先，用 <code>Camper.day++;</code> 来增加日计数。根据 100 天编程挑战的工作方式，如果 <code>camper</code> 已经过了第 100 天，那么他们就开始了新的“一轮（round）”。你需要一个条件来检查 <code>Camper.day &gt; 100</code>，如果是的话，就把日子重置为 1，并增加一轮（round）。</p>
<p>在这个条件之后，用 <code>Camper.timestamp = Date.now();</code> 更新时间戳，用 <code>await Camper.save();</code> 保存数据。最后，返回修改后的数据对象，这样你就可以在命令中使用它。</p>
<p>你的最终文件应该是这样的：</p>
<pre><code class="language-ts">import { CamperInt } from "../database/models/CamperModel";

export const updateCamperData = async (Camper: CamperInt) =&gt; {
  Camper.day++;
  if (Camper.day &gt; 100) {
    Camper.day = 1;
    Camper.round++;
  }
  Camper.timestamp = Date.now();
  await Camper.save();
  return Camper;
};
</code></pre>
<h3 id="100commandcontinued">100 Command Continued</h3>
<p>现在你的数据库逻辑已经准备好了，返回到你的<code>oneHundred.ts</code>文件。作为提醒，该文件应该看起来像这样：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { Command } from "../interfaces/Command";

export const oneHundred: Command = {
  data: new SlashCommandBuilder()
    .setName("100")
    .setDescription("Check in for the 100 Days of Code challenge.")
    .addStringOption((option) =&gt;
      option
        .setName("message")
        .setDescription("The message to go in your 100 Days of Code update.")
        .setRequired(true)
    ),
  run: async (interaction) =&gt; {
    await interaction.deferReply();
    const { user } = interaction;
    const text = interaction.options.getString("message", true);
  },
};
</code></pre>
<p>在文件的顶部导入你的两个新模块（modules）。然后，在你从交互对象中提取数值的逻辑之后，用<code>const targetCamper = await getCamperData(user.id);</code>从数据库中获取 <code>camper</code> 的数据。用 <code>const updatedCamper = await updateCamperData(targetCamper);</code> 来更新数据。</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { Command } from "../interfaces/Command";
import { getCamperData } from "../modules/getCamperData";
import { updateCamperData } from "../modules/updateCamperData";

export const oneHundred: Command = {
  data: new SlashCommandBuilder()
    .setName("100")
    .setDescription("Check in for the 100 Days of Code challenge.")
    .addStringOption((option) =&gt;
      option
        .setName("message")
        .setDescription("The message to go in your 100 Days of Code update.")
        .setRequired(true)
    ),
  run: async (interaction) =&gt; {
    await interaction.deferReply();
    const { user } = interaction;
    const text = interaction.options.getString("message", true);

    const targetCamper = await getCamperData(user.id);
    const updatedCamper = await updateCamperData(targetCamper);
  },
};
</code></pre>
<p>现在你需要构建响应，以便在 <code>camper</code> 使用该命令时将其送回。</p>
<p>为此，你将使用Discord的消息嵌入功能。首先从 discord.js 导入 <code>MessageEmbed</code> 构造函数，然后用 <code>const oneHundredEmbed = new MessageEmbed();</code> 创建一个新的嵌入。<code>MessageEmbed</code> 类有几个方法可以用来创建嵌入的内容。</p>
<p>使用 <code>.setTitle()</code> 方法来设置嵌入的标题为<code>"100 Days of Code"</code>。</p>
<p>使用<code>.setDescription()</code> 方法将嵌入的描述设置为 <code>camper</code> 在命令中提供的信息（记得你之前将其提取到<code>text</code>变量）。嵌入的作者可以被设置，并将显示在嵌入的顶部。</p>
<p>使用 <code>.setAuthor()</code> 方法传递一个对象，其 <code>name</code> 属性设置为 <code>user.tag</code>（将显示 <code>camper</code> 的用户名和判别符，如<code>nhcarrigan#0001</code>），<code>iconURL</code>属性设置为<code>user.displayAvatarUrl()</code>（将 <code>camper</code> 的头像附加到嵌入文件上）。</p>
<p>嵌入（Embeds）也接受字段，它是较小的文本块，有自己的标题和描述。<code>.addField()</code> 方法需要两个或三个参数，第一个是字段的标题，第二个是字段的描述，第三个是可选的布尔值，将字段设置为内联（inline）。</p>
<p>使用<code>.addField()</code>方法来添加两个字段。第一个字段的标题应该设置为 "Round"，描述设置为 <code>updatedCamper.round.toString()</code>。第二个字段的标题应该设置为 <code>"Day"</code>，描述设置为 <code>updatedCamper.day.toString()</code>。这两个字段都应该是内联的（inline）。</p>
<p>对于你嵌入的最后一部分，使用 <code>.setFooter()</code> 方法来添加小的页脚文本。传递一个对象，其 <code>text</code> 属性设置为<code>"Day completed:" + new Date(upedCamer.timestamp).toLocaleDateString()</code>以显示 <code>camper</code> 报告他们进展的时间。</p>
<p>最后，你需要把这个新嵌入的内容发回给 <code>camper</code>。因为你已经用<code>interaction.deferReply()</code> 调用发送了一个响应，你不能再发送一个响应。相反，你需要编辑你发送的那个。</p>
<p>使用<code>await interaction.editReply()</code>来编辑响应。<code>.editReply()</code>方法接收一个具有各种属性的对象,在这种情况下，你正在发送一个嵌入（embed）。传递一个对象，其 <code>embeds</code> 属性设置为<code>[oneHundredEmbed]</code>。</p>
<p>注意，这是一个包含你的嵌入的数组。Discord 消息最多可以包含 10 个嵌入物（embeds），API 希望有一个嵌入对象（embed objects）的数组来匹配。</p>
<p>你的最终命令文件应该是这样的：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { MessageEmbed } from "discord.js";
import { Command } from "../interfaces/Command";
import { getCamperData } from "../modules/getCamperData";
import { updateCamperData } from "../modules/updateCamperData";

export const oneHundred: Command = {
  data: new SlashCommandBuilder()
    .setName("100")
    .setDescription("Check in for the 100 Days of Code challenge.")
    .addStringOption((option) =&gt;
      option
        .setName("message")
        .setDescription("The message to go in your 100 Days of Code update.")
        .setRequired(true)
    ),
  run: async (interaction) =&gt; {
    await interaction.deferReply();
    const { user } = interaction;
    const text = interaction.options.getString("message", true);

    const targetCamper = await getCamperData(user.id);
    const updatedCamper = await updateCamperData(targetCamper);

    const oneHundredEmbed = new MessageEmbed();
    oneHundredEmbed.setTitle("100 Days of Code");
    oneHundredEmbed.setDescription(text);
    oneHundredEmbed.setAuthor({
      name: user.tag,
      iconURL: user.displayAvatarURL(),
    });
    oneHundredEmbed.addField("Round", updatedCamper.round.toString(), true);
    oneHundredEmbed.addField("Day", updatedCamper.day.toString(), true);
    oneHundredEmbed.setFooter({
      text:
        "Day completed: " +
        new Date(updatedCamper.timestamp).toLocaleDateString(),
    });

    await interaction.editReply({ embeds: [oneHundredEmbed] });
  },
};
</code></pre>
<h3 id="">注册命令</h3>
<p>如果你运行<code>npm run build</code>和<code>npm start</code>，一切都会启动——但你没有办法实际使用你的新命令。这是因为 Discord 要求命令被注册，以便它们在应用程序的用户界面上可用。要做到这一点，我们需要采取几个步骤。</p>
<p>首先，前往你的<code>_CommandList.ts</code>文件，导入你的<code>oneHundred</code>命令。把它添加到你的<code>CommandList</code>数组中，这样它就可以在其他地方使用。</p>
<pre><code class="language-ts">import { Command } from "../interfaces/Command";
import { oneHundred } from "./oneHundred";

export const CommandList: Command[] = [oneHundred];
</code></pre>
<p>现在是时候添加逻辑来发送命令信息给 Discord 了。在你的<code>src/events</code>目录下，添加一个<code>onReady.ts</code>文件。我们将在<code>"ready"</code>事件中使用它。</p>
<p>创建一个名为<code>onReady</code>的出口异步函数，并给它一个名为<code>BOT</code>的参数。从 discord.js 导入<code>Client</code>类型，并将<code>BOT</code>类型定义设为<code>Client</code>。</p>
<pre><code class="language-ts">import { Client } from "discord.js";

export const onReady = async (BOT: Client) =&gt; {};
</code></pre>
<p>现在从<code>@discordjs/rest</code>导入<code>REST</code>模块。这将允许你实例化一个 API 客户端，你将用它来发送命令。用<code>const rest = new REST();</code>构建一个新的实例。</p>
<p>你需要对你的 REST 客户端进行一些配置。首先，向<code>REST()</code>构造函数传递一个对象，并将<code>version</code>属性设置为<code>"9"</code>。这告诉客户端使用 Discord 的 API 版本 9，这是目前最新的版本。</p>
<p>然后，在构造函数上链接一个<code>.setToken()</code>调用，将API token（口令）设置为<code>process.env.BOT_TOKEN</code>, 你必须将其强制为一个<code>字符串</code>。</p>
<pre><code class="language-ts">import { REST } from "@discordjs/rest";
import { Client } from "discord.js";

export const onReady = async (BOT: Client) =&gt; {
  const rest = new REST({ version: "9" }).setToken(
    process.env.BOT_TOKEN as string
  );
};
</code></pre>
<p>API 希望命令数据以特定的 JSON 格式发送，但值得庆幸的是，我们使用的 slash 命令生成器有一个方法专门用于此。导入你的<code>CommandList</code>，然后创建一个新的数组并映射你的命令数据。</p>
<pre><code class="language-ts">const commandData = CommandList.map((command) =&gt; command.data.toJSON());
</code></pre>
<p>在你向 Discord 发送命令之前，有必要注意有两种类型的命令。全局命令Global Commands 在你的机器人被使用的所有地方都可用，但需要一个小时左右的时间来更新。Guild Commands 只在单个服务器中可用，但会立即更新。因为这个机器人被设计为在单个服务器中运行，所以我们要使用 Guild Commands。</p>
<p>你需要获得你使用该机器人的服务器的 ID。要做到这一点，确保你在你的 Discord 应用程序中启用了开发者模式，然后右击你的服务器图标并选择 “Copy ID”。在你的 <code>.env</code> 文件中，添加一个 <code>GUILD_ID</code> 变量，并将你复制的 ID 分配给它。它应该看起来像 <code>GUILD_ID="778130114772598785"</code>。</p>
<p>回到你的 <code>onReady.ts</code> 文件中，用 <code>await rest.put()</code> 开始你的 API 调用。发送一个 <code>PUT</code> 请求将更新任何现有的命令，而 <code>POST</code> 将试图创建新的命令，如果命令共享一个名字就会出错。从<code>discord-api-types/v9</code>导入<code>Routes</code>，并在<code>rest.put()</code> 调用中传递 <code>Routes.applicationGuildCommands()</code> 调用。这将被用来构建 API 端点以发送命令。</p>
<p>调用 <code>applicationGuildCommands()</code> 时，将接受两个参数。</p>
<p>首先是应用程序的 ID，以便将这些命令与之联系起来。你可以从 <code>BOT.user.id</code> 的值中得到它，但 <code>user</code> 有可能是未定义的，所以你需要选择性地把它连起来。使用 <code>BOT.user?.id || "missing id"</code> 来添加一个会出错的后备值——这将允许我们知道机器人的 ID 是否丢失。</p>
<p>第二个参数是服务器 ID，你把它设置为<code>process.env.GUILD_ID</code>（记得要强制使用这个类型！）。</p>
<p><code>.put()</code>调用也需要第二个参数，这是你要发送的数据。以<code>{ body: commandData }</code>的形式传递，以符合预期格式。</p>
<p>最后，在文件末尾添加一个<code>console.log("Discord ready!")</code>，以表明你的机器人已经上线。</p>
<pre><code class="language-ts">import { REST } from "@discordjs/rest";
import { Routes } from "discord-api-types/v9";
import { Client } from "discord.js";
import { CommandList } from "../commands/_CommandList";

export const onReady = async (BOT: Client) =&gt; {
  const rest = new REST({ version: "9" }).setToken(
    process.env.BOT_TOKEN as string
  );

  const commandData = CommandList.map((command) =&gt; command.data.toJSON());

  await rest.put(
    Routes.applicationGuildCommands(
      BOT.user?.id || "missing id",
      process.env.GUILD_ID as string
    ),
    { body: commandData }
  );

  console.log("Discord ready!");
};
</code></pre>
<p>切换到你的 <code>index.ts</code> 文件，找到你的 <code>"ready"</code> 事件监听器。用新的 <code>onReady</code> 函数替换 <code>console.log</code>调用--记得导入它，回调中异步调用。</p>
<pre><code class="language-ts">import { Client } from "discord.js";
import { IntentOptions } from "./config/IntentOptions";
import { connectDatabase } from "./database/connectDatabase";
import { onInteraction } from "./events/onInteraction";
import { onReady } from "./events/onReady";
import { validateEnv } from "./utils/validateEnv";

(async () =&gt; {
  if (!validateEnv()) return;
  const BOT = new Client({ intents: IntentOptions });

  BOT.on("ready", async () =&gt; await onReady(BOT));

  BOT.on(
    "interactionCreate",
    async (interaction) =&gt; await onInteraction(interaction)
  );

  await connectDatabase();

  await BOT.login(process.env.BOT_TOKEN);
})();
</code></pre>
<p>现在运行<code>npm run build</code>和<code>npm start</code>，并在Discord中连接你的服务器。如果你输入<code>/</code>，你应该看到你的新<code>/100</code>命令显示出来。尝试使用该命令并检查响应。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/01/image-122.png" alt="如果你看到这个响应，那么你已经成功创建了你的第一个命令！" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>如果你看到这个响应，那么你已经成功创建了你的第一个命令</figcaption>
</figure>
<p>祝贺你！你有了你的第一个成功的命令。有了你所创建的所有基础结构，添加其他的命令就会顺利得多。让我们现在就去做吧。</p>
<h3 id="">编辑命令</h3>
<p>如果成员在他们的 <code>/100</code> 信息中出现了错误，会发生什么？因为机器人会发送回复，camper 无法编辑它（Discord 不允许你编辑你没有发送的信息）。你应该创建一个命令，允许 camper 这样做。</p>
<p>在你的 <code>src/commands</code> 目录下创建一个 <code>edit.ts</code> 文件。就像你对 <code>/100</code> 命令所做的那样，导入你的<code>SlashCommandBuilder</code> 和 <code>Command</code> 接口，并导出一个 <code>edit</code> 对象，其类型为 <code>Command</code>。</p>
<p>使用 <code>SlashCommandBuilder</code> 来准备 <code>data</code> 属性。给这个命令取名为 <code>edit</code>，描述为<code>Edit a previous 100 days of code post</code>，然后添加两个字符串选项。第一个字符串选项应该有一个名字 <code>embed-id</code> 和一个描述<code>"ID of the message to edit"</code>，第二个应该有一个名字<code>message</code>和一个描述 <code>"The message to go in your 100 Days of Code update"</code> 。这两个选项都应该是必需的。</p>
<p>你的代码应该是这样的：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { Command } from "../interfaces/Command";

export const edit: Command = {
    data: new SlashCommandBuilder()
    .setName("edit")
    .setDescription("Edit a previous 100 days of code post.")
    .addStringOption((option) =&gt;
      option
        .setName("embed-id")
        .setDescription("ID of the message to edit.")
        .setRequired(true)
    )
    .addStringOption((option) =&gt;
      option
        .setName("message")
        .setDescription("The message to go in your 100 Days of Code update.")
        .setRequired(true)
    ),
}
</code></pre>
<p>用一个异步函数和一个 <code>interaction（交互）</code>参数创建你的<code>run</code>属性。从交互中获得 <code>channel</code> 和 <code>user</code>，并抓住 <code>embed-id</code>和 <code>message</code>选项。不要忘记延迟响应！</p>
<pre><code class="language-js">    run: async (interaction) =&gt; {
        await interaction.deferReply();
        const { channel, user } = interaction;
        const targetId = interaction.options.getString("embed-id", true);
        const text = interaction.options.getString("message", true);
    }
</code></pre>
<p><code>channle</code>属性是 nullable（例如，在通过 DM 发送互动的情况下），所以你要检查它是否存在。如果它不存在，则回应一个命令缺少参数的消息。</p>
<pre><code class="language-ts">    if (!channel) {
      await interaction.editReply({
        content: "Missing channel parameter.",
      });
      return;
    }
</code></pre>
<p>现在你知道了这个 <code>channel</code> 的存在，你可以根据 <code>camper</code> 提供的ID来获取他们想要编辑的信息。使用<code>channel.messages.fetch()</code> 来做这件事，把 <code>targetId</code> 作为参数传入。</p>
<p>因为目标 <code>message</code> 有可能不存在，你需要在你的代码中考虑到这一点。添加一个条件来检查这一点，如果没有找到消息，则回应一个解释。</p>
<pre><code class="language-ts">    const targetMessage = await channel.messages.fetch(targetId);

    if (!targetMessage) {
      await interaction.editReply({
        content:
          "That does not appear to be a valid message ID. Be sure that you are using this command in the same channel as the message.",
      });
      return;
    }
</code></pre>
<p>你需要检查的最后一件事是，<code>camper</code> 正在编辑的信息实际上属于他们。你可以用<code>.embeds</code>属性来访问嵌入（embed），和你发送的方式一样，该属性以嵌入对象（embed objects）数组的形式返回。</p>
<p>从数组中抓取第一个嵌入对象（embed objects），然后检查嵌入作者是否与用户的标签相符。如果不是，让他们知道他们不能编辑这个帖子。</p>
<pre><code class="language-ts">    const targetEmbed = targetMessage.embeds[0];

    if (targetEmbed.author?.name !== user.tag) {
        await interaction.editReply({
            content: "This does not appear to be your 100 Days of Code post. You cannot edit it."
        })
    }
</code></pre>
<p>现在你已经确认一切都正确了，你可以在嵌入（embed）上使用 <code>.setDescription()</code> 来更新文本。然后，用新的嵌入（embed）来编辑消息，并对互动作出确认。</p>
<p>你的完整代码应该是这样的：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { Command } from "../interfaces/Command";

export const edit: Command = {
  data: new SlashCommandBuilder()
    .setName("edit")
    .setDescription("Edit a previous 100 days of code post.")
    .addStringOption((option) =&gt;
      option
        .setName("embed-id")
        .setDescription("ID of the message to edit.")
        .setRequired(true)
    )
    .addStringOption((option) =&gt;
      option
        .setName("message")
        .setDescription("The message to go in your 100 Days of Code update.")
        .setRequired(true)
    ),
  run: async (interaction) =&gt; {
    await interaction.deferReply();
    const { channel, user } = interaction;
    const targetId = interaction.options.getString("embed-id", true);
    const text = interaction.options.getString("message", true);

    if (!channel) {
      await interaction.editReply({
        content: "Missing channel parameter.",
      });
      return;
    }

    const targetMessage = await channel.messages.fetch(targetId);

    if (!targetMessage) {
      await interaction.editReply({
        content:
          "That does not appear to be a valid message ID. Be sure that you are using this command in the same channel as the message.",
      });
      return;
    }

    const targetEmbed = targetMessage.embeds[0];

    if (targetEmbed.author?.name !== user.tag) {
      await interaction.editReply({
        content:
          "This does not appear to be your 100 Days of Code post. You cannot edit it.",
      });
    }

    targetEmbed.setDescription(text);
    await targetMessage.edit({ embeds: [targetEmbed] });
    await interaction.editReply({ content: "Updated!" });
  },
};
</code></pre>
<p>将新的 <code>edit</code> 命令添加到你的 <code>CommandList</code> 数组中，然后创建并运行你的机器人，你应该看到新的命令。尝试编辑你之前发送的嵌入文件（embed）。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/01/image-123.png" alt="你应该看到你的嵌入更新，以及来自机器人的确认！" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>你应该看到你的嵌入更新，以及来自机器人的确认</figcaption>
</figure>
<h3 id="">查看命令</h3>
<p><code>Campers</code> 应该有办法查看他们当前的进度，因此我们需要创建一个命令来执行此操作。到目前为止，你应该对命令结构感到满意。我们鼓励你遵循这些说明，尝试编写代码。</p>
<p>在你的命令目录下创建一个<code>view.ts</code>文件，并设置好你的命令变量。在<code>data</code>属性中创建一个命令，其名称为<code>view</code>，描述为 <code>Shows your latest 100 days of code check in</code>。这个命令不需要任何选项。</p>
<p>在 <code>run</code> 属性中设置你的异步函数，并推迟交互响应。从交互中提取 <code>user</code> 对象。使用你的 <code>getCamperData</code> 模块从数据库中获取 <code>camper</code> 的数据。然后，检查数据的 <code>day</code> 属性是否有一个非零值。如果没有，让 <code>camper</code> 知道他们还没有开始100天的编程挑战，可以用 <code>/100</code> 命令来做。</p>
<p>创建一个嵌入，标题设置为 <code>My 100DoC Progress</code>。将描述设置为<code>Here is my 100 Days of Code progress. I last reported an update on:</code>, 并添加 <code>camper</code> 的时间戳。添加一个 <code>Round</code> 和 <code>Day</code> 字段，并设置嵌入的作者。然后在交互响应中发送嵌入的内容。</p>
<p>记得把你的新命令添加到 <code>CommandList</code> 中，然后尝试创建和启动你的机器人。你应该看到这个命令是可用的，并且能够从它那里得到一个响应。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-125.png" alt="image-125" width="600" height="400" loading="lazy"></p>
<p>如果你没有得到回应，以下是你的代码应该是这样的：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { MessageEmbed } from "discord.js";
import { Command } from "../interfaces/Command";
import { getCamperData } from "../modules/getCamperData";

export const view: Command = {
  data: new SlashCommandBuilder()
    .setName("view")
    .setDescription("Shows your latest 100 Days of Code check in."),
  run: async (interaction) =&gt; {
    await interaction.deferReply();
    const { user } = interaction;
    const targetCamper = await getCamperData(user.id);

    if (!targetCamper.day) {
      await interaction.editReply({
        content:
          "It looks like you have not started the 100 Days of Code challenge yet. Use `/100` and add your message to report your first day!",
      });
      return;
    }

    const camperEmbed = new MessageEmbed();
    camperEmbed.setTitle("My 100DoC Progress");
    camperEmbed.setDescription(
      `Here is my 100 Days of Code progress. I last reported an update on ${new Date(
        targetCamper.timestamp
      ).toLocaleDateString()}.`
    );
    camperEmbed.addField("Round", targetCamper.round.toString(), true);
    camperEmbed.addField("Day", targetCamper.day.toString(), true);
    camperEmbed.setAuthor({
      name: user.tag,
      iconURL: user.displayAvatarURL(),
    });

    await interaction.editReply({ embeds: [camperEmbed] });
  },
};
</code></pre>
<h3 id="">帮助命令</h3>
<p>你需要创建的最后一件事是一个帮助命令，它将向<code>camper</code> 解释如何与机器人互动。</p>
<p>在 <code>command</code> 目录下创建你的 <code>help.ts</code> 文件，并创建你的 <code>data</code> 属性。给该命令起名为 <code>help</code>，并说明<code>Provides information on using this bot（提供关于使用该机器人的信息）</code>。</p>
<p>用 async 函数设置你的<code>run</code>属性，并记住推迟回复的时间。创建一个嵌入，并使用描述和字段来提供你想与 <code>camper</code> 分享的信息。在交互响应中发送嵌入的信息。</p>
<p>将你的新帮助命令加载到 <code>CommandList</code> 中，并创建启动你的机器人来测试它。你应该看到一个带有你创建的嵌入的响应。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-126.png" alt="image-126" width="600" height="400" loading="lazy"></p>
<p>你的嵌入可能看起来不同，这取决于你选择分享什么信息。以下是我们用于上述嵌入的代码：</p>
<pre><code class="language-ts">import { SlashCommandBuilder } from "@discordjs/builders";
import { MessageEmbed } from "discord.js";
import { Command } from "../interfaces/Command";

export const help: Command = {
  data: new SlashCommandBuilder()
    .setName("help")
    .setDescription("Provides information on using this bot."),
  run: async (interaction) =&gt; {
    await interaction.deferReply();
    const helpEmbed = new MessageEmbed();
    helpEmbed.setTitle("100 Days of Code Bot!");
    helpEmbed.setDescription(
      "This discord bot is designed to help you track and share your 100 Days of Code progress."
    );
    helpEmbed.addField(
      "Create today's update",
      "Use the `/100` command to create your update for today. The `message` will be displayed in your embed."
    );
    helpEmbed.addField(
      "Edit today's update",
      "Do you see a typo in your embed? Right click it and copy the ID (you may need developer mode on for this), and use the `/edit` command to update that embed with a new message."
    );
    helpEmbed.addField(
      "Show your progress",
      "To see your current progress in the challenge, and the day you last checked in, use `/view`."
    );
    helpEmbed.setFooter({ text: `Version ${process.env.npm_package_version}` });
    await interaction.editReply({ embeds: [helpEmbed] });
    return;
  },
};
</code></pre>
<p>注意：我们应该使用 <code>npm_package_version</code> 来显示机器人的当前版本。</p>
<h2 id="">结语</h2>
<p>恭喜！你已成功为 100 天编程挑战构建了一个 Discord 机器人。</p>
<p>如果你有兴趣进一步探索，可以查看本教程参考的实时机器人的<a href="https://github.com/nhcarrigan/100-days-of-code-bot">源代码</a>，其中包括自定义错误、日志记录、外部错误报告和文档站点。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python 基础教程：这个自动化程序让你的工作更高效 ]]>
                </title>
                <description>
                    <![CDATA[ 很多日常工作可以通过自动化的方式来帮你节省一点宝贵的时间。这也使得掌握自动化技术成为关键。 只需要一小部分熟练的自动化工程师和领域专家就有可能将整个团队中的最繁琐的工作实现自动化。 在这篇文章中，我们将探讨基于 Python（一种强大的且简单易学的编程语言）来实现自动化工作流程的一些基础知识。我们将使用 Python 来编写一个简单有用的小型自动化脚本，这个脚本能够整理指定文件夹中的文件，并将其放到对应的文件夹中。 我们的目标不是在一开始就编写完美的代码以及构建一套理想的自动化体系。 当然我们也不会创建任何“非法”的脚本。相反的，我们将研究如何创建一个能够自动整理文件夹内容的脚本。 目录  1. 自动化的领域 * 简单的自动化      * 公共 API（Application Program Interface 应用程序接口）的自动化      * API 的逆向工程            2. 自动化的道德考量  3. 创建整理文件夹脚本  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/building-bots-in-python/</link>
                <guid isPermaLink="false">5fe4701a39641a0517d525dd</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 自动化 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 机器人 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jiawei Pan ]]>
                </dc:creator>
                <pubDate>Wed, 05 May 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/freecodecamp_cover.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>很多日常工作可以通过自动化的方式来帮你节省一点宝贵的时间。这也使得掌握自动化技术成为关键。</p>
<p>只需要一小部分熟练的自动化工程师和领域专家就有可能将整个团队中的最繁琐的工作实现自动化。</p>
<p>在这篇文章中，我们将探讨基于 Python（一种强大的且简单易学的编程语言）来实现自动化工作流程的一些基础知识。我们将使用 Python 来编写一个简单有用的小型自动化脚本，这个脚本能够整理指定文件夹中的文件，并将其放到对应的文件夹中。</p>
<p>我们的目标不是在一开始就编写完美的代码以及构建一套理想的自动化体系。</p>
<p>当然我们也不会创建任何“非法”的脚本。相反的，我们将研究如何创建一个能够自动整理文件夹内容的脚本。</p>
<h1 id="">目录</h1>
<ol>
<li>自动化的领域
<ul>
<li>简单的自动化</li>
<li>公共 API（Application Program Interface 应用程序接口）的自动化</li>
<li>API 的逆向工程</li>
</ul>
</li>
<li>自动化的道德考量</li>
<li>创建整理文件夹脚本</li>
<li>一份自动化程序的完整指南</li>
</ol>
<h2 id="">自动化的领域</h2>
<p>让我们从定义哪种类型的自动化开始。</p>
<p>自动化技术适用于大多数领域。从初级的，它可以帮你从一堆文档中提取邮箱地址，这样你就可以群发邮件了。到复杂一点的，优化大型公司的内部工作流程。</p>
<p>当然，从小型的个人脚本到可以替代人工的大型自动化系统，这中间需要一个学习的过程。所以让我们来看看你适合从哪个领域开始自动化之旅。</p>
<h3 id="">简单的自动化</h3>
<p>直接针对工作中的某一点流程实现简单的自动化。这其中可以是某些小型的独立的步骤，例如整理项目并重新编排目录中的文件，也可以是整个工作流程中一部分，例如自动调整已经保存文件的大小。</p>
<h3 id="api">公共 API 的自动化</h3>
<p>现如今我们可以通过 HTTP（Hypertext transfer protocol 超文本传输协议）请求对应的 API 来实现绝大部分程序的功能，因此自动调用公共 API 是最常见的自动化程序。例如，如果你需要给你家的花园实现自动化灌溉。</p>
<p>为此，你需要根据当天天气来决定是要浇水还是有雨要来。</p>
<h3 id="api">API 的逆向工程</h3>
<p>在实际的程序中，基于 API 逆向工程的自动化程序更为常见。在下文的“自动化的道德考量”中也会探讨机器人冒名顶替真是人类的问题</p>
<p>通过对一个 API 进行逆向工程，我们可以了解用户在某个应用中的操作流程。例如登录一个网页游戏的步骤。</p>
<p>通过理解登录和身份验证的逻辑，我们可以使用脚本来复制这一动作。然后即使他们不对外提供应用界面，我们也可以创建自己的接口脚本来使用他们的应用。</p>
<p>无论你是出什么目的，请考虑一下它是否合法。</p>
<p>你也不想惹麻烦，对吧？😁</p>
<h2 id="">道德考量</h2>
<p>GitHub 上有个人联系到我说：</p>
<blockquote>
<p>“点赞数和订阅数就是数字时代的货币，但是你们正在让它贬值。”</p>
</blockquote>
<p>This stuck with me and made me question the tool I've built for exactly that purpose.<br>
我一直思考着这个问题，并开始质疑我构建程序真正的目的是什么。</p>
<p>事实是，这些互动和点赞可以通过自动化的方式进行伪造，这种伪造越来越多，导致扭曲和破坏了社交媒体的正常运行。</p>
<p>如果不使用机器人或者其他的刷赞系统，用户和广告公司将看不到那些真正产出好内容并创造价值的人</p>
<p>我的一个朋友借助但丁《神曲》中《地狱篇》的“九层地狱”的情景，来向我解释随着你社会影响力的一步步扩大，你越来越难以意识到这个系统实际的破败之处。</p>
<p>我想和你分享这个观点，因为我认为这能非常准确的描述在我使用 InstaPy 工具与网红合作期间所看到的一切。</p>
<p><strong>第一层：灵薄狱</strong></p>
<p>假如你不使用机器人。</p>
<p><strong>第二层：纵欲</strong></p>
<p>当你手动的开始点赞和关注尽可能多的人，并让他们也点赞和关注你的文章。</p>
<p><strong>第三层：暴食</strong></p>
<p>当你加入一个 Telegram 群，大家点赞并评论 10 张照片，那么接下来的 10 个人也会点赞并评论你的照片。</p>
<p><strong>第四层：贪婪</strong></p>
<p>当你使用低成本的虚拟助手帮你点赞和关注。</p>
<p><strong>第五层：愤怒</strong></p>
<p>当你使用机器人去点赞，但是没有任何点赞的回馈（但是你不用付费，例如 Chrome 游览器中的扩展程序）。</p>
<p><strong>第六层：异端</strong></p>
<p>当你使用机器人给出 50+个点赞并获得 50+个点赞（但是你不用付费，例如 Chrome 游览器中的扩展程序）。</p>
<p><strong>第七层：施暴</strong></p>
<p>当你使用机器人点赞、关注、评论 200-700 张照片，并无视禁言的警告。</p>
<p><strong>第八层：欺诈</strong></p>
<p>当你付费给未知的第三方服务去自动的帮你获得点赞和关注，同时也用你的账号去点赞和关注别人。</p>
<p><strong>第九层：背叛者</strong></p>
<p>当你付费购买点赞和关注数，试图去包装自己成为一个网红。</p>
<p>在社交媒体上面使用机器人非常常见，以致于<strong>如果你不包装，你会被卡在第一层，即灵薄狱</strong>，与你的同行比起来你的关注人数没有增长，订阅量也低。</p>
<p>从经济型理论看，这叫<strong>囚徒困境和零和博弈</strong>。如果我不使用机器人，但是你用了，你就赢了。如果你没有用，但我用了，我就赢了。如果我们都不用，那么我们就能共赢。但是对于那些没有使用机器人的人是没有奖励的，大家就都在用，那么没有人会赢。</p>
<blockquote>
<p>请警惕这点，不要忘记整个工具对社交媒体的影响。</p>
</blockquote>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/07/spectrum-bot-intent-ebook.png" alt="spectrum-bot-intent-ebook" width="600" height="400" loading="lazy"></p>
<p>来源：SignalSciences.com</p>
<p>我们希望规避道德问题，并继续开展一个自动化的项目。这就是我们为什么只是创建一个简单的目录整理脚本来帮祝你整理乱糟糟的文件。</p>
<h2 id="">创建整理目录脚本</h2>
<p>现在让我们来看一个非常简单的脚本。它会自动整理指定的目录，将其中的文件根据文件后缀名移动到对于的文件夹。</p>
<p>下图就是我们将要做的事情：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/directory_clean_img.png" alt="directory_clean_img" width="600" height="400" loading="lazy"></p>
<h3 id="">设置参数解析器</h3>
<p>由于我们会用到操作系统的功能，比如移动文件，所以我们需要导入 <code>os</code> 库。除此之外，我们还希望用户能够控制要整理的文件夹，因此我们还会用到 <code>argparse</code> 库。</p>
<pre><code class="language-python">import os
import argparse
</code></pre>
<p>导入了这两个库之后，我们首先要设置参数解析器。确保为每个参数有对应的描述和帮助文本，以便用户在输入 <code>--help</code> 时得到帮助。</p>
<p>我们的参数会被命名为 <code>--path</code>。 参数名前面的双破折号告诉库这是一个可选参数。默认情况下，我们使用当前目录，因此将默认值设为 <code>.</code>。</p>
<pre><code class="language-python">parser = argparse.ArgumentParser(
    description="Clean up directory and put files into according folders."
)

parser.add_argument(
    "--path",
    type=str,
    default=".",
    help="Directory path of the to be cleaned directory",
)
# 解析用户提供的path参数的值
args = parser.parse_args()
path = args.path

print(f"Cleaning up directory {path}")
</code></pre>
<p>这就完成了参数解析的部分，非常简单易读，对吧？</p>
<p>让我们执行一下脚本，检查是否有报错。</p>
<pre><code class="language-bash">python directory_clean.py --path ./test

=&gt; Cleaning up directory ./test

</code></pre>
<p>一旦执行，我们可以在控制台中看到目录名称被打印出来，完美。</p>
<p>现在让我们来使用 <code>os</code> 库来获取给定路径下的文件。</p>
<h3 id="">从文件夹中获取文件列表</h3>
<p>通过给 <code>os.listdir(path)</code> 方法提供一个有效的路径，我们就能获得该目录内所有文件和文件夹的列表。</p>
<p>在列出了文件夹内所有的元素后，我们需要对文件和文件夹进行区分，因为我们只需要整理文件而不是文件夹。</p>
<p>我们使用 Python 的列表来遍历所有元素，根据是否满足是文件还是文件夹的条件将他们放到新的列表中。</p>
<pre><code class="language-python"># 获取目录中的所有文件
dir_content = os.listdir(path)
# 根据文件和文件名创建相对路径
path_dir_content = [os.path.join(path, doc) for doc in dir_content]
# 过滤目录内容到文档或文件夹列表
docs = [doc for doc in path_dir_content if os.path.isfile(doc)]
folders = [folder for folder in path_dir_content if os.path.isdir(folder)]
# 记录文件移动的数量
# 列出已经创建的文件夹以免出现重复
moved = 0
created_folders = []

print(f"Cleaning up {len(docs)} of {len(dir_content)} elements.")
</code></pre>
<p>和之前一样，让我们确保用户能够得到反馈。所以需要打印一个告知用户多少文件被移动的信息。</p>
<pre><code class="language-bash">python directory_clean.py --path ./test

=&gt; Cleaning up directory ./test
=&gt; Cleaning up 60 of 60 elements.
</code></pre>
<p>当我们再次执行 python 脚本之后，我们可以看到在 <code>/test</code> 文件夹下将会出现 60 个被移动的文件。</p>
<h3 id="">根据文件后缀名创建文件夹</h3>
<p>接下来也是最重要的一步是根据每个文件的后缀名创建文件夹。我们希望通过遍历所有已经过滤好的文件，如果没有创建对应后缀名的文件夹，就创建一个。</p>
<p><code>os</code> 库能给我们提供非常友好的功能，例如拆分给定文件的类型和路径，提取文件路径和文件名。</p>
<pre><code class="language-python"># 遍历所有的文件，并移动到对应的文件夹中
for doc in docs:
    # 提取文件后缀名
    full_doc_path, filetype = os.path.splitext(doc)
    doc_path = os.path.dirname(full_doc_path)
    doc_name = os.path.basename(full_doc_path)

    print(filetype)
    print(full_doc_path)
    print(doc_path)
    print(doc_name)

    break
</code></pre>
<p>如果我们的目录包含成堆的文件，那么上面代码末尾的 break 语句用于确保我们的终端不会列满所有的文件信息。</p>
<p>我们设置好这一步之后，让我们执行一下脚本，看到的内容可能类似这样的：</p>
<pre><code class="language-python">python directory_clean.py --path ./test

=&gt; ...
=&gt; .pdf
=&gt; ./test/test17
=&gt; ./test
=&gt; test17
</code></pre>
<p>我们可以通过上面的实现，分离文件类型，然后从完整路径中提取部分内容。</p>
<p>现在我们有了文件类型，我们就能检查拥有这个文件类型的同名文件夹是否已经存在。</p>
<p>在开始这一步之前，我们需要跳过一些文件。如果我们使用当前目录 <code>.</code> 作为路径，我们需要避免 python 脚本也被移动。可以通过一个简单的 if 条件语句来解决这个问题。</p>
<p>另外，我们也不希望移动<a href="https://www.lifewire.com/what-is-a-hidden-file-2625898">隐藏文件</a>，所有 <code>.</code> 开头的文件也要跳过。 macOS 上的 <code>.DS_Store</code> 就是一个例子。</p>
<pre><code class="language-python">    # 当在目录中存在 名为 directory_clean 的文件或 . 开头的文件在时跳过
    if doc_name == "directory_clean" or doc_name.startswith('.'):
        continue
    # 获得子文件夹的名称，创建其中不存在的文件夹名
    subfolder_path = os.path.join(path, filetype[1:].lower())

    if subfolder_path not in folders:
        # 创建文件夹

</code></pre>
<p>处理完 python 脚本路径和隐藏文件后，我们可以继续在系统中创建文件夹了。</p>
<p>除了我们的判断之外，如果一开始读取目录时发现同名文件夹已经存在，我们需要一种能够检测已创建文件夹的方法。这就是我们要声明数组变量 <code>create_folders = []</code> 的原因。它将用来存储已被检测过的文件名。</p>
<p><code>os</code> 库中提供了一个 <code>os.mkdir(folder_path)</code> 的方法用于根据路径创建新文件夹。</p>
<p>这个方法可能会抛出异常，告诉我们文件夹已经存在。所以我们需要确保异常被捕获。</p>
<pre><code class="language-python">if subfolder_path not in folders and subfolder_path not in created_folders:
    try:
        os.mkdir(subfolder_path)
        created_folders.append(subfolder_path)
        print(f"Folder {subfolder_path} created.")
    except FileExistsError as err:
        print(f"Folder already exists at {subfolder_path}... {err}")

</code></pre>
<p>在编写完创建文件夹的代码后，让我们重新运行一遍脚本。</p>
<pre><code class="language-python">python directory_clean.py --path ./test 

=&gt; ...
=&gt; Folder ./test/pdf created.
</code></pre>
<p>在第一次执行时，我们从日志列表看到，我们已经创建了以文件后缀名命名的文件夹。</p>
<p>最后一步就是移动文件到它们对应的父文件夹中。</p>
<p>当使用 <code>os</code> 库时需要明白的最重要的一点是，有些操作无法撤销。例如，删除文件的情况。因此，在执行脚本前我们需要先注释掉部分内容</p>
<p>这也是为什么我们的 <code>os.rename(...)</code> 方法会在这里被注释掉。</p>
<pre><code class="language-python"># 获取新文件路径，移动文件
    new_doc_path = os.path.join(subfolder_path, doc_name) + filetype
    # os.rename(doc, new_doc_path)
    moved += 1

    print(f"Moved file {doc} to {new_doc_path}")
</code></pre>
<p>运行完我们的脚本并看到正确的日志之后，我们可以撤销 <code>os.rename()</code> 方法的注释，并最终写成这样的。</p>
<pre><code class="language-python"># 获取新文件路径，移动文件
    new_doc_path = os.path.join(subfolder_path, doc_name) + filetype
    os.rename(doc, new_doc_path)
    moved += 1

    print(f"Moved file {doc} to {new_doc_path}")

print(f"Renamed {moved} of {len(docs)} files.")
</code></pre>
<pre><code class="language-bash">python directory_clean.py --path ./test

=&gt; ...
=&gt; Moved file ./test/test17.pdf to ./test/pdf/test17.pdf
=&gt; ...
=&gt; Renamed 60 of 60 files.
</code></pre>
<p>最后的这个运行结果会将所有文件移动到对应的文件夹中，我们的目录不需要手动操作也能很好的被整理。</p>
<p>下一步，我们可以利用我们上面创建好的脚本去做更多是事情，例如，安排脚本在每个星期一定时清理 Downloads 文件夹，以便让其看起来更整洁有序。</p>
<p><strong>这也是我们在 Udemy 中创建的名为<a href="https://www.udemy.com/course/the-complete-guide-to-bot-creation/?referralCode=7418EBB47E11E34D86C9">创建自动化程序</a>的后续课程</strong></p>
<h2 id=""><a href="https://www.udemy.com/course/the-complete-guide-to-bot-creation/?referralCode=7418EBB47E11E34D86C9">一份完整的日常自动化程序指南</a></h2>
<p>Felix 和我在学习 <strong>InstaPy</strong> 库和他的 <strong>Travian-Bot</strong> 库的基础上创建了一个<strong>教你如何创建自己的机器人</strong>的在线视频。实际上，他也因为这个库太火爆导致想关闭这个项目。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/building-bots/">How to Build a Bot and Automate your Everyday Work</a>，作者：<a href="https://www.freecodecamp.org/news/author/timgrossmann/">Tim Grossmann</a></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
