Discord.pyでチケットBOTを作る

## チケットとは
今回はDiscord.pyでdiscordでのチケット機能を作って行きたいと思います。動作について参考にしたコードはなく、自力で１からつくったのでかなりシンプルな内容になっています。
チケットとは、 **「チケット作成者と管理者のみが閲覧、発言できるプライベートチャンネル」** のことです。 **第三者に公開されない** ため、サーバーの管理者へのお問い合わせなどに利用されます。

# bot動作のフローチャート
```mermaid
graph TD;
    スラッシュコマンドを実行し,チケット作成フォームを設置-->チケット作成ボタンが押されると,プライベートチャンネルが生成;
    チケット作成ボタンが押されると,プライベートチャンネルが生成-->チャンネルに,ガイドとチケット削除ボタンを表示;
    チャンネルに,ガイドとチケット削除ボタンを表示-->チケット削除ボタンが押されると,チケットチャンネルを削除;
```
----
### 注意するポイント
<dl>
  <dt>・荒らし対策</dt>
  <dd>botを利用してサーバーを荒らされることを防ぐ必要があります。今回は、チケット作成を制限する必要があります。</dd>
  <dt>・権限について</dt>
  <dd>チケットの閲覧は、チケット作成者と管理者のみが可能でなければなりません。また、スラッシュコマンドの利用や、チャンネルの編集権限などにも注意する必要があります。</dd>
</dl>



## 用意するもの
まだインストールしていない方は __pipなどを利用して__ すべてインストールしてください。
```:ターミナル
$ pip install discord
$ pip install discord.py
$ pip install discord-ui
```
discord.uiを利用する利点としては、記述しやすいのはもちろんですが、スラッシュコマンドによるBOTの発言を、ユーザーの返信へのインタラクション無しで行うことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500912/58c9ed30-5c6c-84e6-511c-21d330f8aabc.png)

↑このようにしてスラッシュコマンドを使用したログを消せる

# BOTの設定
 DiscordBOTの作成手順については、ググってください。
まずはBOTのログイン、起動までの動作、スラッシュコマンドを実装する引数を書きます。

```python:main.py
import discord
from discord import app_commands

TOKEN = "Your discord bot token"

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)

@client.event
async def on_ready():
    await tree.sync()

client.run(TOKEN)
```
## 1.スラッシュコマンドを実行し,チケット作成フォームを設置
discordのembedの機能を利用して、見た目を良くしました。
二行目のコードにより、スラッシュコマンドの利用を管理者のみ可能にしています。
```python:main.py
@tree.command(name="ticket", description="チケットを作成")
@discord.app_commands.default_permissions(
    administrator=True
)
async def create_ticket(interaction: discord.Interaction):
    channel = client.get_channel(interaction.channel_id)
    embed = discord.Embed(
                title="",
                color=0x178CE6,
                description="",                
        )
    embed.set_footer(text="Made by Spicy │2023/08/25")
    embed.add_field(name="チケット",value="お問い合わせはこちらからチケットを発行してください！")
    await channel.send(embed=embed)
    # チケット作成ボタンの表示
    view = discord.ui.View()
    button = discord.ui.Button(style=discord.ButtonStyle.primary, label="チケットを作成", custom_id="create_ticket")
    view.add_item(button)
    await interaction.channel.send("", view=view)
```
>まず、`@tree.command`デコレータを使用して、`create_ticket`関数が「ticket」という名前のコマンドとして登録されるようにしています。そのため、ユーザーは「/ticket」と入力することでこのコマンドを実行することができます。
次に、チケット作成ボタンを表示するために、`discord.ui.View`オブジェクトと`discord.ui.Button`オブジェクトを作成しています。ボタンにはスタイルやラベル、カスタムIDを設定することができます。この例では、スタイルをprimaryにし、ラベルを「チケットを作成」とし、カスタムIDを「create_ticket」と設定しています。

### Embedについて
`discord.Embed`クラスを使用して、Embedメッセージを作成しています。Embedメッセージには、タイトルや説明文、フッター、サムネイル、フィールドなどを設定することができます。ここでは、タイトルを空欄にし、説明文には「お問い合わせはこちらからチケットを発行してください！」と記載しています。

**Embedについてはこちらに詳しくのっています。**
https://qiita.com/hisuie08/items/5b63924156080694fc81

---
### ボタンについて
ボタンを押したときのインタラクションに対する動作はコードの一番下にまとめます。
```python:main.py
@client.event
async def on_interaction(inter: discord.Interaction):
    try:
        custom_id = inter.data["custom_id"]
        if custom_id == "create_ticket":
    #ここにチケットを作成ボタンを押したときの動作を記述
```

**ボタンについてはこちらに詳しくのっています。**
https://qiita.com/Broccolingual/items/b008421e535b96c05557

----

### スラッシュコマンドを実行した結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500912/e85ab6e9-c3c5-6676-b0ae-a21537af3c92.png)

チケット作成パネルがうまく表示されています。




# 2.チケット作成ボタンが押されたときの動作
「チケットを作成」ボタンが押されたときの処理を記述します。
今回は、ボタンが押されたときに、ボタンを押した人にしか見るkとができないメッセージで「チケットを作成しました！」というテキストと同時に、生成されたチケットチャンネルをメンションするようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500912/e4c8828b-3902-bf10-16b6-834be6354133.png)

## チャンネルの生成
以下のコードで管理者とチケット作成者しかアクセスできないプライベートチャンネルを生成します。プライベートチャンネルが生成されたときに、チケット作成者にメンションして通知し、さらにチケット削除ボタンを追加しています。また、荒らし対策のために、チケットを既に作成した人のidを保存し、チケットが既に存在している場合には作成できない旨のメッセージを送信して制限しています。

```python:main.py
ticket_owners = {}
@client.event
async def on_interaction(inter: discord.Interaction):
    try:
        custom_id = inter.data["custom_id"]
        if custom_id == "create_ticket":
            # create_ticketの処理
            server = inter.guild
            
            # ユーザーが既にチケットを持っている場合は処理を終了
            if inter.user.id in ticket_owners.values():
                await inter.response.send_message("既にチケットが存在します。", ephemeral=True)
                return
            
            # プライベートチャンネルを生成
            overwrites = {
                server.default_role: discord.PermissionOverwrite(read_messages=False),
                server.me: discord.PermissionOverwrite(read_messages=True),
                inter.user: discord.PermissionOverwrite(read_messages=True)
            }
            channel_name = f"チケット-{inter.user.name}"
            channel = await server.create_text_channel(name=channel_name, overwrites=overwrites)
            
            ticket_owners[channel.id] = inter.user.id

            await channel.send(f"{inter.user.mention} チケットが作成されました!")
            
            ticket_message = f"チケットが作成されました！\n{channel.mention}"
            
            await inter.response.send_message(ticket_message, ephemeral=True)
            
            # チケットチャンネルに削除ボタンを表示
            view = discord.ui.View()
            button = discord.ui.Button(style=discord.ButtonStyle.danger, label="チケットを削除", custom_id="delete_ticket")
            view.add_item(button)
            await channel.send("", view=view)
            ticket_owners[channel.id] = inter.user.id
```
>まず、`ticket_owners`という空の辞書を定義しています。この辞書は、各チケットチャンネルのIDをキーにし、そのチケットを持つユーザーのIDを値として保持します。
次に、ユーザーが既にチケットを持っているかどうかを確認しています。`inter.user.id`はユーザーのIDを取得するための属性です。`ticket_owners`の値として存在しているかどうかを確認することで、ユーザーがチケットを既に持っているかどうかを判断しています。もし既にチケットを持っている場合は、タイムリーなメッセージを送信して処理を終了します。
それ以外の場合、プライベートチャンネルを生成します。`overwrites`変数には、各役割やユーザーに対するパーミッションを設定する`discord.PermissionOverwrite`オブジェクトの辞書があります。デフォルトの役割やユーザーにはメッセージの読み取り権限を与えませんが、ボット自体とチケットを作成したユーザーにはメッセージの読み取り権限を与えます。
最後に、チケットチャンネルに削除ボタンを追加します。`discord.ui.View`オブジェクトを作成し、その中に`discord.ui.Button`オブジェクトを作成しています。ボタンのスタイルは`discord.ButtonStyle.danger`（赤色）であり、ラベルは"チケットを削除"です。ボタンをビューに追加してから、`await channel.send()`メソッドを使用してビューをチケットチャンネルに送信します。
最後に、チケットチャンネルのIDをキーにしてユーザーのIDを保持する`ticket_owners`に新しいエントリを追加します。

### チケットを２つ以上作ろうとしたときの動作
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500912/34c33cf3-13b0-33db-c370-a8d4567b5fb1.png)


### チケットチャンネルでの通知
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500912/93a2cd91-95e5-4e32-6836-2b6aad3300fa.png)


### チャンネル名
管理しやすいように、チケット作成者の名前を追加しています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500912/4939f4f9-426a-c587-a49c-e0402b8df0ed.png)


# 3.チケット削除ボタンが押されたときの処理
チケット削除ボタンは、管理者とチケット作成者の両方がいつでも押せるようになっています。今回は、シンプルに、削除ボタンが押されるとすぐにチャンネルが消えるようになっているので、コードは単純です。
以下がコードです。
```python:main.py
        elif custom_id == "delete_ticket":
            # delete_ticketの処理
            channel = inter.channel

                # チケットチャンネルを削除
            await channel.delete()

                # チケット保持者の辞書から削除
            del ticket_owners[channel.id]
```
>`channel.delete()`を使って、チケットチャンネルを削除しています。`delete()`メソッドは、Discord.pyで提供されるチャンネルオブジェクトのメソッドであり、チャンネルを削除するために使用されます。
最後に、`ticket_owners`辞書から`channel.id`をキーとするエントリを削除しています。`ticket_owners`は、チケットチャンネルの所有者を追跡するための辞書であり、キーはチャンネルのIDです。この行は、チケットチャンネルを削除した後、関連する所有者情報をデータ構造から削除するために使用されます。

# 最後に
いかがでしたか？これで一通りの単純なチケット機能ができました。この記事を高評価してもらえたらコードをそのまま使っても構いません。バグなどありましたらコメントしてください。
Discord.py、なかなか楽しいです。静的な処理から、スタイルなど色々な範囲を簡単に実装できるので、開発しててワクワクします。今後もBOT開発したら記事を投稿しようと思いますのでよろしくお願いします。