ทักทายชาว Discord ใน voice channel ด้วย Discord Bot + Python สุดเจ๋ง
สวัสดีมิตรรักนักอ่านทุกท่านค่ะ กลับมาพบกันอีกครั้งตามสัญญาว่าจะเอาน้องเหลือม (Python) มาต้มยำทำแกงให้ดูค่ะ วันนี้ จะมาสอนเขียน Discord bot ให้ทักทายชาว Discorder(?) ที่แวะเวียนเข้ามาใน voice channel ของเรากันค่ะ
ที่ไปที่มา 🤨
ก่อนจะลงมือทำก็ต้องบอก Pain Point กันก่อนนะคะ เรื่องมีอยู่ว่าภายในบริษัท Abbon Corporation เราทำงานกันแบบ Remote Working (a.k.a. Work from anywhere) ซึ่งการติดต่อสื่อสารภายในเราทำงานกันผ่าน Discord Application ที่แบ่งห้องไว้สำหรับพูดคุย (Voice channel) และสำหรับพิมพ์ข้อความ (Text channel) ได้ค่ะ
ในระหว่างวัน นัทก็จะอยู่ใน voice channel ตลอดเผื่อทีมติด issues จะได้เข้ามาคุยกันเพื่อแก้ไขปัญหาได้อย่างรวดเร็ว ทีนี้บางทีก็มีคนเข้าๆ ออกๆ บ่อยค่ะ แล้วเราก็เป็นคนดีซะด้วย แขกจะไปใครจะมา จะมาซ้ำ เข้าๆ ออกๆ เราก็ทักหมด ซึ่งบางครั้งก็เข้ามาในระหว่างที่เรากำลังเข้าภวังค์เขียนโค้ดทำให้เราไม่ได้ดูว่าใครเข้ามาหรือไม่ได้ทักด้วยก็มีค่ะ ไม่ได้การซะแล้ว! เราต้องได้ทักทุกคน! และเราต้องรู้ว่าใครเข้ามา! และเราต้องไม่ทักซ้ำ!
สร้าง Bot กันก่อน 🔥
แน่นอนว่าหัวใจของโปรเจคนี้ทุกคนต้องมี Discord bot กันก่อนค่ะ สร้างจากที่นี่
โดยจะขอเล่าเป็น steps คร่าวๆ นะคะ
- New Application > ตั้งชื่อ ใส่ภาพ Profile
- ไปที่หน้า OAuth เพื่อเอา Client ID
- ไปต่อกันที่หน้า Bot เพื่อเอา token โดยเราจะใช้ token นี้ใน secret environment นะคะ
- Generate URL เพื่อเพิ่ม bot บน Discord server ของเราค่ะ ซึ่งถ้าทำบนเว็บโดยตรงเลยเราว่ายุ่งยากค่ะ แนะนำให้ทุกคนไปทำผ่าน เว็บไซต์ นี้นะคะ
- เสร็จแล้วเราก็จิ้มที่ URL เพื่อเพิ่ม bot เข้า server เราได้เลยค่ะ
ป.ล. ใส่ permission เท่าที่จำเป็นนะคะ สำหรับเราเอาง่ายๆ ก่อนก็ให้บอทเป็นทุกอย่างไว้ก่อนค่ะ
ป.ล.2 ทุกครั้งที่มีการเปลี่ยน permission ต้องเตะ bot ออกจาก server ก่อนนะคะ แล้วค่อย add กลับเข้ามาใหม่ค่ะ
มาเขียนโค้ดกัน 🐍
- อันดับแรกคือ install libraries โดยการสร้าง requirements.txt แล้วใส่ชื่อ libraries ลงไปตาม list ข้างล่างค่ะ
// requirements.txt
discord.py[voice]
python-dotenv
gtts
ตามด้วย command
pip install --no-cache-dir -r requirements.txt
หากเราต้องการเล่นกับ audio มากๆ แนะนำให้ install ffmpeg เพิ่มด้วย ซึ่งเป็น library จัดการทั้งเสียงและวีดีโอครอบจักรวาลที่อยากป้ายยาให้ทุกคนได้รู้จักค่ะโดยการติดตั้งสามารถทำได้ผ่าน homebrew หรือดาวน์โหลดผ่านหน้าเว็บได้เลย
2. สร้าง .env ขึ้นมาโดยใส่ DISCORD_TOKEN
ไว้ข้างใน
DISCORD_TOKEN=<YOUR_DISCORD_BOT_TOKEN>
3. สร้าง bot.py ขึ้นมา โดยภายในเราจะมีการ setup discord bot event, command, env และอื่นๆ ทั้งหมดลงไป โดยใน Python Discord bot จะใช้ decorator ที่แตกต่างกันเพื่อจัดการกับ functions ต่างๆ ดังนี้
- `@bot.event`: เกิดขึ้นเสมอเป็น specific function name ของ Discord
- `@bot.tree.command()`: คือ function ที่จะถูก trigger ขึ้นเมื่อมี user มีการเรียก command ผ่านห้อง text channel
ในตัวอย่างจะเป็น command greeting
ที่จะเอาไว้ใช้ตอนที่เรียกน้องบอทเข้าห้อง
// bot.python
import os
import discord
import asyncio
from dotenv import load_dotenv
from discord.ext import commands
from speaker import Speaker
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
intents = discord.Intents.all()
// custom prefix เวลาต้องการเรียกใช้ bot command
bot = commands.Bot(intents=intents, command_prefix='!')
// จังหวะที่ bot เข้า voice channel สำเร็จ
@bot.event
async def on_ready():
// TODO:
// จังหวะที่ voice channel ที่ bot อยู่เกิดการเปลี่ยนแปลง เช่น มีคนเข้าห้อง
@bot.event
async def on_voice_state_update(
member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState
):
// TODO:
// จังหวะที่ bot ได้รับ command จาก user
@bot.event
async def on_command():
// TODO:
// ชื่อ command ของบอท
@bot.tree.command(name='greeting')
async def greeting(interaction: discord.Interaction):
// TODO:
bot.run(TOKEN)
4. สร้าง speaker.py ขึ้นมา โดยภายในเราจะเก็บ functions ที่เกี่ยวข้องกับการสร้างเสียงตอบกลับ เราจะใช้ Google text to speech มาเพื่อแปลงคำที่เราใช้ทักทายเป็น audio ก่อนส่งกลับไปที่ voice channel ค่ะ (แน่นอนว่า library ตัวนี้รองรับการ set up ภาษาอื่นๆ นอกจากภาษาไทยนะคะ)
from gtts import gTTS
from io import BytesIO
import random
class Speaker:
"""
A class for sending greeting message
"""
def __init__(self, name: str = ""):
self.name = name
def greeting_all(self):
"""Greeting all people in mp3 file object
Args:
None.
Returns:
BytesIO
"""
return self.generate_mp3_file_object("สวัสดีทุกคนค่ะ")
def greeting_by_person(self):
"""Random greeting by person in mp3 file object
Args:
text: word or sentence
Returns:
BytesIO
"""
greeting_list = [
'สวัสดี',
'หนีห่าว',
'บองชู้ว'
]
return self.generate_mp3_file_object(f'{random.choice(greeting_list)} {self.name}')
def generate_mp3_file_object(self, text: str, lang: str = "th"):
"""Convert text to mp3 file object
Args:
text: word or sentence
lang(optional): language
Returns:
BytesIO
"""
tts = gTTS(text=text, lang=lang)
mp3_file_object = BytesIO()
tts.write_to_fp(mp3_file_object)
mp3_file_object.seek(0)
return mp3_file_object
5. ตีบวก bot.py ด้วยการเรียก functions จาก speaker.py กันค่ะ เริ่มจากสวัสดีทุกคนจังหวะที่ bot เข้าห้องมาครั้งแรกก่อนค่ะ จะมีการเช็คห้องที่ต้องเข้าตาม user ที่เรียก bot และเช็คว่าน้องอยู่ในห้องนี้แล้วรึยังเพื่อป้องกันไม่ให้เรียก function เข้าห้องซ้ำ
...
@bot.tree.command(name='greeting')
async def greeting(interaction: discord.Interaction):
try:
user_voice = interaction.user.voice
if not user_voice:
await interaction.response.send_message("Sorry, you should be in voice channel.")
return
bot_voice = interaction.guild.voice_client
if bot_voice is not None:
if bot_voice.channel.id == user_voice.channel.id:
await interaction.response.send_message("I'm in your voice channel!")
return
await bot_voice.move_to(user_voice.channel)
await interaction.response.send_message("I'm coming to you")
return
await user_voice.channel.connect()
speaker = Speaker()
greeting_obj = speaker.greeting_all()
audio_source = discord.FFmpegPCMAudio(source=greeting_obj, pipe=True)
interaction.guild.voice_client.play(audio_source)
await interaction.response.send_message("I'm joining")
except Exception as e:
print(e)
await interaction.response.send_message('Joining failed')
...
หลังจากที่น้องเข้ามาแล้ว ก็ถึงตาน้องทักทาย Discorder คนใหม่ที่เข้ามาในห้องแล้วค่ะ
...
// จังหวะที่ voice channel ที่ bot อยู่เกิดการเปลี่ยนแปลง เช่น มีคนเข้าห้อง
@bot.event
async def on_voice_state_update(
member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState
):
if member.bot:
return
if before.channel == after.channel:
return
def is_connected(member: discord.Member):
client = discord.utils.get(bot.voice_clients, guild=member.guild)
return client and client.is_connected()
if is_connected(member):
if after.channel is not None:
if not any(
after.channel.id == voice_client.channel.id for voice_client in bot.voice_clients
):
return
current_channel_id = int(after.channel.id)
current_channel = bot.get_channel(current_channel_id)
name = member.nick if member.nick else member.name
print(f'{name} is entering')
greeting_item = Speaker(name)
greeting_obj = greeting_item.greeting_by_person()
voice_client = member.guild.voice_client if member.guild.voice_client else await current_channel.connect()
audio_source = discord.FFmpegPCMAudio(source=greeting_obj, pipe=True)
await asyncio.sleep(2)
voice_client.play(audio_source)
await asyncio.sleep(1)
...
6. ลอง run python ขึ้นมาแล้วทดลองใน server ของเราได้เลยค่ะ แต่ ๆ ๆ ถ้าหาก TODO ในข้อ 2 ไม่ได้ใช้ functions ไหนก็อย่าลืม comment ไว้นะคะ เดี๋ยวจะเกิด error ขึ้นมาซะก่อน
python bot.py
บทสรุป 🤘
และนี่ก็เป็นโปรเจคสนุก ๆ (?) โปรเจคแรกที่เราได้ลองทำ Discord Bot กับ Python ค่ะ ใครเข้าใจก็ขอให้ยกมือขึ้น! ใครไม่เข้าใจก็ขอให้ยกมือขึ้น! (ฮา) ถ้าหากมีจุดไหนที่ต้องการให้อธิบายเพิ่มตามก็สามารถทักหลังไมค์มาได้นะคะ และถ้าหากยังไม่จุใจสามารถไปตำ project เต็มๆ กันต่อได้ที่ repository นี้ค่ะ ส่วนในบทความถัดไปจะเกี่ยวกับอะไรนั้น… ให้คุกกี้ทำนายกัน~ 🥠