🖼️

Discordの画像認証ボットを作る

2023/05/17に公開

discord.pyでcaptchaモジュールを使用した画像認証ボットを作ったのでせっかくならと紹介。

完成型

/panelというコマンドを送信すると認証パネルが送信され、チェックマークが書かれたボタンを押すとぼかしが入った認証コードが表示されるというものになっています。

「表示する」を押すとぼかしが消され、認証と書かれたボタンを押すと認証コードを入力するモーダルが出るというものになっています。

使うモジュール

  • discord.py
  • captcha
  • random (標準モジュール)
  • string (標準モジュール)
  • io (標準モジュール)
  • pillow

コード

準備

まずdiscord.pyなどのモジュールのインポートやインテントの設定をします

import discord
import random
import string
from PIL import Image
import io
from captcha.image import ImageCaptcha

intents = discord.Intents.all()
client = discord.Client(intents = intents)
tree = discord.app_commands.CommandTree(client)
gld = discord.Object(id=SERVER_ID_HERE) #tree.syncとスラッシュコマンドで使うため
ImageCaptcha = ImageCaptcha()

@client.event
async def on_ready():
    print("立ち上がったよ")
    await tree.sync(guild=gld)

ボタン検知

@tree.commandを使用しスラッシュコマンドを作成し、"image_au"というカスタムIDがついたボタンと認証パネルを送信します。

@tree.command(name="panel", description="AUTHENICATION PANEL",guild=gld)
async def panel_au(interaction: discord.Interaction):
    ch = interaction.channel
    embed = discord.Embed(title="認証をする",description="下の:white_check_mark:を押して認証を開始することができます。")
    button = discord.ui.Button(emoji="✅", style=discord.ButtonStyle.primary, custom_id="image_au")
    view = discord.ui.View()
    view.add_item(button)
    await interaction.response.send_message(":white_check_mark:", ephemeral=True)
    await ch.send(embed = embed, view = view)

その後on_interactionを使用しon_button_clickを作成しボタンの検知をしております。
ボタン検知はこの記事を参考にしました。

@client.event
async def on_interaction(inter:discord.Interaction):
    try:
        if inter.data['component_type'] == 2:
            await on_button_click(inter)
    except KeyError:
        pass

async def on_button_click(interaction:discord.Interaction):
    custom_id = interaction.data["custom_id"]
    if custom_id == "image_au":
    # コードずらーっと #

画像作成、ぼかし入れ

※埋め込みだとスポイラー画像が使えないのでぼかし入れをしています※
まず画像を作るために認証コードを作成する必要があります。
string、randomモジュールを使用し、

text = string.ascii_letters + string.digits
global captcha_text
captcha_text = random.choices(text, k=5)
captcha = "".join(captcha_text)

このようにstringで文字列等を持ってきたのちにrandomで乱数を生成しています。
次にcaptchaモジュールから引っ張ってきたImageCaptchaを使用し画像を生成したのち、まず画像を縮小させ、その後通常のサイズに戻しぼかし入れをします。
ぼかしについてはこちらの記事を参考にしました。

original = ImageCaptcha.generate(captcha_text)
intensity = 20
global img
img = Image.open(original)
small = img.resize(
    (round(img.width / intensity), round(img.height / intensity))
)
blur = small.resize(
    (img.width,img.height),
    resample=Image.BILINEAR
)

(globalが入っている変数はモーダルの際に使うため入れています)

画像送信、ぼかし除去

embed = discord.Embed()
button = discord.ui.Button(label="表示する", style=discord.ButtonStyle.primary, custom_id="picture")
view = discord.ui.View()
view.add_item(button)
with io.BytesIO() as image_binary:
    blur.save(image_binary, 'PNG')
    image_binary.seek(0)
    file = discord.File(fp=image_binary, filename="captcha_img.png")
    embed.set_image(url="attachment://captcha_img.png")
    await interaction.response.send_message(file = file, embed = embed, view = view, ephemeral=True)

生成されたぼかし画像を埋め込みに入れるためにioを使用してblur.saveしたのちにdiscord.Fileを使用します。そしてpictureというカスタムIDのボタンを入れて送信し、先ほどと同じようにon_button_clickで検知します。
埋め込みにリンク以外の画像を使うやり方はdiscord.pyのDocsに記載されています。

elif custom_id == "picture":
    embed = discord.Embed()

    button = discord.ui.Button(label="認証", style=discord.ButtonStyle.success, custom_id="phot_au")
    view = discord.ui.View()
    view.add_item(button)
        
    with io.BytesIO() as image_binary:
        img.save(image_binary, 'PNG')
        image_binary.seek(0)
        file = discord.File(image_binary,filename="captcha_img.png")
        embed.set_image(url="attachment://captcha_img.png")
        await interaction.response.edit_message(attachments=[file], view = view, embed = embed)

検知後に次はぼかしが入っていない画像を表示させるので、生成したCaptcha画像を埋め込みで使えるようにするために先ほどやったようなものと同じくimg.saveし、discord.Fileを使用します。
そして、認証コードを入力するためのモーダルを出現させるためのボタンを作成し、メッセージを編集します。

モーダルを作る

elif custom_id == "phot_au":
    class Questionnaire(discord.ui.Modal):
	auth_answer = discord.ui.TextInput(label=f'認証コードを入力してください', style=discord.TextStyle.short, min_length=4, max_length=7)

        def __init__(self):
            super().__init__(title='認証をする', timeout=None)

        async def on_submit(self, interaction: discord.Interaction):
            answer = self.auth_answer.value
            if answer == captcha_text:
                embed = discord.Embed(description="**認証に成功しました!**", title= None)
                await interaction.response.send_message(embed = embed, ephemeral=True)
	    else:
                embed = discord.Embed(description="**認証に失敗しました...**\n**TIP:** `全角でないと成功にはなりません。`", title= None)
                await interaction.response.send_message(embed = embed, ephemeral=True)
    await interaction.response.send_modal(Questionnaire())

ボタンが押されたのちにモーダルを出現させるため、classを使用してモーダルを作ります。
self.auth_answer.valueを使用して回答がcaptcha_textと一致しているかをif文を使い、elseも使い間違いだった場合の処理も入れ、最後にsend_modalを使用します。

これで完成ですが、最後に忘れずclient.runを入れます。

client.run("BOT_TOKEN_HERE")

コード全体

bot.py
import discord
import random
import string
from PIL import Image
import io
from captcha.image import ImageCaptcha

intents = discord.Intents.all()
client = discord.Client(intents = intents)
tree = discord.app_commands.CommandTree(client)
gld = discord.Object(id=SERVER_ID_HERE) #tree.syncとスラッシュコマンドで使うため
ImageCaptcha = ImageCaptcha()

@client.event
async def on_ready():
    print("立ち上がったよ")
    await tree.sync(guild=gld)

@tree.command(name="panel", description="AUTHENICATION PANEL",guild=gld)
async def panel_au(interaction: discord.Interaction):
    ch = interaction.channel
    embed = discord.Embed(title="認証をする",description="下の:white_check_mark:を押して認証を開始することができます。")
    button = discord.ui.Button(emoji="✅", style=discord.ButtonStyle.primary, custom_id="image_au")
    view = discord.ui.View()
    view.add_item(button)
    await interaction.response.send_message(":white_check_mark:", ephemeral=True)
    await ch.send(embed = embed, view = view)

@client.event
async def on_interaction(inter:discord.Interaction):
    try:
        if inter.data['component_type'] == 2:
            await on_button_click(inter)
    except KeyError:
        pass

async def on_button_click(interaction:discord.Interaction):
    custom_id = interaction.data["custom_id"]
    if custom_id == "image_au":
        text = string.ascii_letters + string.digits
        global captcha_text
        captcha_text = random.choices(text, k=5)
        captcha_text = "".join(captcha_text)
        original = ImageCaptcha.generate(captcha_text)
        intensity = 20
        global img
        img = Image.open(original)
        small = img.resize(
            (round(img.width / intensity), round(img.height / intensity))
        )
        blur = small.resize(
            (img.width,img.height),
            resample=Image.BILINEAR
        )
        embed = discord.Embed()
        button = discord.ui.Button(label="表示する", style=discord.ButtonStyle.primary, custom_id="picture")
        view = discord.ui.View()
        view.add_item(button)
        with io.BytesIO() as image_binary:
            blur.save(image_binary, 'PNG')
            image_binary.seek(0)
            file = discord.File(fp=image_binary, filename="captcha_img.png")
            embed.set_image(url="attachment://captcha_img.png")
            await interaction.response.send_message(file = file, embed = embed, view = view, ephemeral=True)
    elif custom_id == "picture":
        embed = discord.Embed()

        button = discord.ui.Button(label="認証", style=discord.ButtonStyle.success, custom_id="phot_au")
        view = discord.ui.View()
        view.add_item(button)
        
        with io.BytesIO() as image_binary:
            img.save(image_binary, 'PNG')
            image_binary.seek(0)
            file = discord.File(image_binary,filename="captcha_img.png")
            embed.set_image(url="attachment://captcha_img.png")
            await interaction.response.edit_message(attachments=[file], view = view, embed = embed)
    elif custom_id == "phot_au":
        class Questionnaire(discord.ui.Modal):
            auth_answer = discord.ui.TextInput(label=f'認証コードを入力してください', style=discord.TextStyle.short, min_length=4, max_length=7)

            def __init__(self):
                super().__init__(title='認証をする', timeout=None)

            async def on_submit(self, interaction: discord.Interaction):
                answer = self.auth_answer.value
                if answer == captcha_text:
                    embed = discord.Embed(description="**認証に成功しました!**", title= None)
                    await interaction.response.send_message(embed = embed, ephemeral=True)
                else:
                    embed = discord.Embed(description="**認証に失敗しました...**\n**TIP:** `全角でないと成功にはなりません。`", title= None)
                    await interaction.response.send_message(embed = embed, ephemeral=True)
        await interaction.response.send_modal(Questionnaire())

client.run("BOT_TOKEN_HERE")

振り返り

「認証ボット作ってみたいな」と思って初心者ながらも知恵を絞り、フレンドにも助けてもらいながら作りました。大体1~2時間かけました。
こちらのものを使っていただいてもかまいませんが、とりあえず見つけたやつを丸コピしてそっからいろいろ変化をつけたコードとなっています。
次リメイクするときには綺麗でよくできたコードを書きたいです。

参考にしたリンクやDocsまとめ

Discussion