Discord Bot

PY 小社課 12 / 4, 11

講師: 蘇西

Discord

Applications

Setup

Events

Deploy the Bot

Commands

Discord Bot

Discord 誕生於西元 2015 年,一開始是為了遊戲玩家而設計的免費網路即時通話軟體,經過多年來的發展,現今教育人士、學生、商業人士等等各式各樣的人都有在使用 Discord 作為辦公討論的工具。

Discord Bot 在 Discord 中扮演一個非常重要的角色,Bot 是機器人的意思,能夠自動化地幫助伺服器管理員執行繁忙瑣碎的任務,例如:歡迎新成員、自動分發身分組、點播音樂、管理伺服器秩序等等功能,最重要的是每位 Discord 使用者都能免費打造屬於自己獨一無二的機器人。

Discord Applications

沒伺服器的可以先建自己的伺服器

Discord 開發者專區

點選

Discord 開發者專區

幫機器人取名字

Discord 開發者專區

左邊選單 Bot -> Priviliged Gateway Intents 三個權限可以全開

Discord 開發者專區

左邊選單 OAuth2

-> 勾選 Bot

-> 複製並開啟下方 URL

-> 開啟後就可以把機器人加入自己的伺服器囉

OAuth 2.0 是一個授權協議,它允許軟體應用代表(而不是充當)資源擁有者去訪問資源擁
有者的資源。應用向資源擁有者請求授權,然後取得令牌(token),並用它來訪問資源。這一切
都不需要應用去充當資源擁有者的身份,因為令牌明確表示了被授予的訪問權。
 

補充: OAuth 2.0 是什麼

Setup

拿來存 token 用

指定需要下載的函式庫

discord.py
python-dotenv

requirements.txt

pip3 install -r requirements.txt
    python -m venv myenv #創虛擬環境
    myenv\Scripts\activate #啟用它 (windows)
    source myenv/bin/activate #啟用它 (mac/linux)

Events

import discord
from discord.ext import commands
import os
from dotenv import load_dotenv

把需要用的 Library 匯入

intents = discord.Intents.default()
intents.message_content = True

把權限開啟

bot.run(token)

讓 bot 開始跑

@bot.event
async def on_ready():
    print(f'We have logged in as {bot.user}')

跑成功時讓它說話

@bot.event
async def on_ready():
    print(f'We have logged in as {bot.user}')

"@" 是 Python 裝飾器 (Decorator)

是用於簡化語法的語法糖 (Syntax candy)

有點像是引用別人先寫好的 function 在自己的 function 外面

@bot.event
async def on_ready():
    print(f'We have logged in as {bot.user}')

async 是什麼?

asynchronous (非同步)就是讓 discord 在等待連線時,可以先去做別的事

@bot.event
async def on_ready():
    print(f'We have logged in as {bot.user}')

 event 是什麼?

就是事件,有很多種不同的event 可以用

on_ready()

on_message(message) on_message_edit(before, after) on_message_delete(message) on_member_join(member) on_member_remove(member) on_member_update(before, after) on_guild_join(guild) on_guild_remove(guild) on_reaction_add(reaction, user) on_reaction_remove(reaction, user)

events:

@bot.event
async def on_message(message):
    if message.author.bot:
        return

    if "bot" in message.content.lower() and "hi" in message.content.lower():
        await message.channel.send(f"你好 {message.author.display_name}!")
        
    await bot.process_commands(message)

範例:關鍵字偵測

Prefix commands

第一種:前綴指令 (Prefix commands)

就是讓 bot 判斷看到你指定的特定前綴(例:!, #, $ 之類)

才會把接下來的文字視為指令

Discord Bot 有兩種類型的指令

 

bot = commands.Bot(command_prefix='$', intents=intents)

先設定你想要什麼前綴

@bot.command()
async def hello(ctx):
  await ctx.send{f"你好啊{ctx.author.mention}"}
  • 記得 @bot.command ()
  • ctx 就是 context (前後文),會讓 bot 存取是誰發出指令等等資訊
  • ctx.author.mention 是 ctx 的功能之一,可以讓 bot 去標發出指令的人
await ctx.reply("收到!") #直接回覆使用者

user_name = ctx.author.display_name #抓使用者顯示名稱

server_name = ctx.guild.name #抓伺服器名稱

await ctx.trigger_typing() #讓 bot 顯示「打字中」(約10秒 (?))

ctx 一些其他用法

import asyncio

@bot.command()
async def hello(ctx):
    async with ctx.typing():
        await asyncio.sleep(5)   #如果想要指定打字打多久
    await ctx.send("Hello!")
@bot.command()
async def hello(ctx):
    user = ctx.author.mention
    server = ctx.guild.name
    utc_time = ctx.message.created_at
    taiwan_time = utc_time.astimezone(timezone(timedelta(hours=8)))
    
    async with ctx.typing():
        await asyncio.sleep(1)  
    await ctx.send(f"現在是 {taiwan_time}, {user} 跟 **{server}** 打招呼!")

ex:

@bot.command()
async def add(ctx, a: int, b: int):
    await ctx.send(a + b)

ctx 存取使用者發出的一些資訊

變數名稱

變數類型

@bot.command()
async def greet(ctx, receiver: discord.Member):
    sender = ctx.author
    await ctx.send(f"{sender.mention}向{receiver.mention}打招呼!")

另外有一些 discord 專用的變數型態

讓使用者可以在指令中標另一個人

@bot.command()
async def command(ctx, *members: discord.Member)
  for m in members:
      await ctx.send(f"Hello {m.mention}")

那如果想要一次標任意數量的人?

用 * args,會先自動把輸入的 members 變成一個 tuple

然後迴圈讓 bot 一個一個標人

Cogs

Cog檔是一種將機器人指令和功能模組化的方式。

優點包含:

1. 模組化命令與功能

把相關的命令和功能寫在一個 Cog 類別中,比起全部堆在 main.py 中好管理很多。

 

2. 容易擴充與維護

可以「加載」與「下載」,且每個 Cog 都視為獨立的, 所以一個功能故障較不會影響到其他功能

/資料夾
  ├── main.py              #主程式,啟動 bot
  ├── .env                 #存密碼(token)的地方
  └── /cogs                #把功能分類
       ├── moderation.py   
       ├── commands.py     
       ...

使用 Cog 之後的資料夾應該長這樣

import discord
import asyncio
import os
from discord.ext import commands
from dotenv import load_dotenv


load_dotenv()
token = os.getenv('TOKEN')

intents = discord.Intents.default()
intents.message_content = True 
intents.guild_messages = True

bot = commands.Bot(command_prefix='$', intents=intents)

main.py

(原本的初始化)

async def load_extensions():
    for filename in os.listdir('./cogs'):
        if filename.endswith('.py'):
            await bot.load_extension(f'cogs.{filename[:-3]}')
#跑一個迴圈把 cogs 資料夾裡面的 .py 都抓出來,然後變成 cogs

@bot.event
async def on_ready():
    print(f"{bot.user} logged in!")

main.py

load cogs 資料夾裡面的東西

然後 log in

async def main():
    async with bot:
        await load_extensions()
        await bot.start(token)

if __name__ == '__main__':
    asyncio.run(main())

main.py

啟動


import discord
from discord.ext import commands
from discord import app_commands

class OnMessage(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

  	# cog 裡面的 decorator 改成這個
    @commands.Cog.listener()
    async def on_message(self, message): #要把 self 給放進去
       
        # 後面這邊都是一樣的
        
        if message.author.bot:
            return
       
        if "bot" in message.content.lower() and "hi" in message.content.lower():
            await message.channel.send(f"你好 {message.author.display_name}!")
    
async def setup(bot):
  	await bot.add_cog(OnMessage(bot))

可以在 cog 裡面建立一個 msg.py

把 OnMessage 寫進去

await bot.process_commands(message)

注意如果寫進 cogs 裡就不用寫這段了

因為 @commands.Cogs.listener() 會自動在背景偵測關鍵字,不會影響到其他功能運行

Slash commands

如何製作像這樣的斜線提示呢(?

import discord
from discord.ext import commands
from discord import app_commands

class GeneralCommands(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @app_commands.command(name="你好", description="打招呼")
    async def hello(self, interaction: discord.Interaction):
        await interaction.response.send_message("你好,世界")

async def setup(bot):
    await bot.add_cog(GeneralCommands(bot))

一樣在 cogs 裡面建立一個 .py 檔案

注意是用 interaction 不是 context

寫在 cogs 裡面的時候都要有一個 self 喔

@commands.command(name="你好", help="說你好")
    async def hello(self, ctx):
        await ctx.send("你好,世界")

async def setup(bot):
    await bot.add_cog(GeneralCommands(bot))

/ 選單

前綴指令

@app_commands.command(name="你好", description="打招呼")
async def hello(self, interaction: discord.Interaction):
	await interaction.response.send_message("你好,世界")

差在哪

Embed messages

Discord Embed 是 Discord Bot 中一個可以嵌入內容的訊息,讓訊息能夠以卡片的方式呈現更豐富的內容,像是添加標題、敘述、顏色、連結、時間戳等等。


Discord Embed 本身分為好幾個區塊,每個區塊都有相對應的函式,而這些函式是要拿來設定該區塊的內容和格式

@commands.command()
async def sendembed(self, ctx):
    msg = discord.Embed(title = "標題", description = "說明", color = discord.Color.red())
    msg.add_field(name="名字", value = "數值", inline = True)
    msg.add_field(name="名字", value = "數值", inline = True)
    await ctx.send(embed=msg)

顏色可以替換成 HEX 代碼

增加一個區域

inline 的意思是會這樣並排

import discord
from discord.ext import commands

class EmbedCommands(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @commands.command()
    async def sendembed(self, ctx):
        msg = discord.Embed(title="標題", description="說明", color=discord.Color.red())
        msg.add_field(name="名字", value="數值", inline=True)
        msg.add_field(name="名字", value="數值", inline=True)
        
        if ctx.author.avatar:
            msg.set_image(url=ctx.author.avatar.url) 
        
        msg.set_thumbnail(url="https://cdn.discordapp.com/embed/avatars/0.png")
        msg.set_footer(text="文字", icon_url="https://cdn.discordapp.com/embed/avatars/0.png") 
        
        await ctx.send(embed=msg)

    @commands.command()
    async def embed_with_emoji(self, ctx):
        emoji = "<:MyCustomEmoji:123456789012345678>" 
        
        msg = discord.Embed(
            title=f"表符 {emoji}", 
            description="這是表符", 
            color=discord.Color.blue()
        )
        await ctx.send(embed=msg)

async def setup(bot):
    await bot.add_cog(EmbedCommands(bot))

一些其他範例

Moderation

Commands

Moderation = 管理

就是一些拿來管理伺服器的指令

例如踢人、禁言等等

我們需要先確認

1) 發送訊息使用者有管理權限

2) 機器人有管理權限

伺服器設定 > 身份組

注意如果是要踢人的話,機器人只能踢身份比自己低階的人

可以在 cogs 裡面建立一個 moderation.py

import discord
from discord.ext import commands
from discord import app_commands

class Moderation(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        
# 中間塞主程式
        
async def setup(bot):
    await bot.add_cog(Moderation(bot))
@app_commands.command(name="clear", description="指定要刪除幾則訊息")
@app_commands.checks.has_permissions(manage_messages=True)
   async def delete_msg(self, interaction: discord.Interaction, amount: int):
       if amount < 1:
           await interaction.response.send_message("重新輸入數量", ephemeral=True)
           return

       await interaction.response.defer(ephemeral=True)
       deleted = await interaction.channel.purge(limit=amount)
       await interaction.followup.send(f"已刪除 {len(deleted)} 則訊息", ephemeral=True)

 刪除訊息

ephermal = 短暫的,讓機器人的確認訊息只有發送指令者可以看到

defer 和 purge 是為了避免因為跑太久而 crash

@app_commands.command(name="kick", description="踢人")
    @app_commands.checks.has_permissions(kick_members=True)
    async def kick_member(self, interaction: discord.Interaction, member: discord.Member):
        await member.kick()
        await interaction.response.send_message(f"踢掉 {member.mention}了", ephemeral=True)

踢人

@app_commands.command(name="ban", description="叫他閉嘴")
@app_commands.checks.has_permissions(ban_members=True)
async def ban_member(self, interaction: discord.Interaction, member: discord.Member, reason: str = "沒理由"):
	await member.ban(reason=reason)
    await interaction.response.send_message(f"已禁言 **{member}**", ephemeral=True)

@app_commands.command(name="unban", description="讓他說話")
@app_commands.checks.has_permissions(ban_members=True)
async def unban_member(self, interaction: discord.Interaction, user: discord.User, reason: str = "沒理由"):
    await interaction.guild.unban(user, reason=reason)
    await interaction.response.send_message(f" **{user}**回來了", ephemeral=True)

禁言

Deploy the Bot

from threading import Thread
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return "Bot is alive!"

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

def keep_alive():
    t = Thread(target=run_flask)
    t.start()
discord.py

python-dotenv

Flask
import discord
from discord.ext import commands
import os
from dotenv import load_dotenv
from google import genai 

load_dotenv()
token = os.getenv('TOKEN')
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')


@bot.command()
async def talk(ctx, *, prompt: str):
    async with ctx.typing():
            response = await gemini_client.models.generate_content_async(
                model=GEMINI_MODEL,
                contents=prompt
            )

            ai_response_text = response.text
            
            await ctx.reply(ai_response_text, mention_author=False)

       

無聊的 gemini wrapper 範例

因為我沒創意

有個問題

一旦關掉電腦

bot  就沒辦法運作了

這時候我們需要把程式碼部署到網路上

Discord Bot

By Suzy Huang

Discord Bot

  • 65