2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

itohalAdvent Calendar 2022

Day 21

discord botでメッセージを転送する

Posted at

初めに

この記事は,discord botでメッセージを転送しようという目的の記事です.また,discord botでminecraft serverを操作したいアドベントカレンダーの記事ですが,ネタ切れで趣旨とは若干それています

また,

  • WSL, Ubuntu22.04.1
  • Python 3.10.6
  • discord.py 2.1.0

という環境にて実施しています.

やりたいこと

誰かのメッセージをどこかのチャンネルへと転送したいです.また,そのときに誰がそのメッセージを転送したかを表示させたいです.

作る

転送に利用するコマンドは,特定のメッセージに対してコマンドを利用するためcontext_menuを利用します.

python
import os
from discord import (
    Interaction,
    Intents,
    Client,
    Message,
)
from discord.app_commands import CommandTree
import pprint


class MyClient(Client):
    def __init__(self, *, intents: Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        synced_commands = await self.tree.sync()
        pprint.pprint(synced_commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.context_menu()
async def message_forward(interaction: Interaction, message: Message):
    await interaction.response.send_message(
        f"this message is: {message.content}", ephemeral=True
    )


client.run(os.getenv("TOKEN"))

image.png

転送というメッセージに対してコマンドを実行して,中身を送信してもらうようにしました.

メッセージの埋め込み

次に誰がどんなことを言ったのかを表示させる必要があります.その表示にはEmbedというクラスを使います.

Embedにはtitleやfooter,authorやtimestampなどを設定できます.ただしtitleは初期化時のみ設定可能です.
今回は転送したいメッセージにおいて,その送信者,内容,時間,および転送する人を埋め込みます.titleは特に必要ないと思ったので入れていません.

画像だけ貼り付けた場合などはメッセージのcontentがありませんので確認しています.1そしてメッセージの送信時刻をtimestampに,メッセージの送信者の名前とアイコンの画像をset_authorで,誰が転送しようとしているのかをset_footerで設定しています.

あとはそれを送信時に指定してあげます.

python
import os
from discord import (
    Interaction,
    Intents,
    Client,
    Message,
+   Embed,
)
from discord.app_commands import CommandTree
import pprint


class MyClient(Client):
    def __init__(self, *, intents: Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        synced_commands = await self.tree.sync()
        pprint.pprint(synced_commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.context_menu()
async def message_forward(interaction: Interaction, message: Message):
-   await interaction.response.send_message(
-       f"this message is: {message.content}", ephemeral=True
-   )

+   embed = discord.Embed(title=None)
+   if message.content:
+       embed.description = message.content
+   embed.timestamp = message.created_at
+
+   embed.set_author(
+       name=message.author.display_name, 
+       icon_url=message.author.display_avatar.url,
+   ).set_footer(
+       text=f"forwarded from {interaction.user.display_name}",
+       icon_url=interaction.user.display_avatar.url,
+   )
+
+   await interaction.response.send_message("embed", embed=embed, ephemeral=True)


client.run(os.getenv("TOKEN"))

image.png

無事に埋め込めました.

メッセージの場所に移動

今度はメッセージの場所まで移動します.つまり,どんな文脈からそのメッセージが出てきたのかも見れる感じです.
ボタンを使って飛べるようにしましょう.つまりViewとButtonです.

python
import os
from discord import (
    Interaction,
    Intents,
    Client,
    Message,
    Embed,
+   ui,
+   ButtonStyle
)
from discord.app_commands import CommandTree
import pprint


class MyClient(Client):
    def __init__(self, *, intents: Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        synced_commands = await self.tree.sync()
        pprint.pprint(synced_commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.context_menu()
async def message_forward(interaction: Interaction, message: Message):
    embed = Embed(title=None)
    if message.content:
        embed.description = message.content
    embed.timestamp = message.created_at

    embed.set_author(
        name=message.author.display_name, icon_url=message.author.display_avatar.url
    ).set_footer(
        text=f"forwarded from {interaction.user.display_name}",
        icon_url=interaction.user.display_avatar.url,
    )

+   url_view = ui.View()
+   url_view.add_item(
+       ui.Button(
+           label="Go to Message", style=ButtonStyle.url, url=message.jump_url
+       )
+   )

-   await interaction.response.send_message("embed", embed=embed, ephemeral=True)
+   await interaction.response.send_message("embed", embed=embed, view=url_view)


client.run(os.getenv("TOKEN"))

image.png

使ってみるとわかりますが,ちゃんとそのメッセージの場所に移動してくれます.

チャンネルの選択

続いてどのチャンネルに送信するかのドロップダウンメニューを作ります.これにはViewのサブクラスでdiscord.ui.selectデコレータを利用します.これのclsui.ChannelSelectのクラスを指定することでチャンネルのドロップダウンメニューを作ってくれます.しかし,そのままだとチャンネルのカテゴリやボイスチャンネルも含まれるのでchannel_types=[ChannelType.text]としてテキストチャンネルだけ表示させるようにしています.

pyhon
import os
from discord import (
    Interaction,
    Intents,
    Client,
    Message,
    Embed,
    ui,
    ButtonStyle,
+   ChannelType,
)
from discord.app_commands import CommandTree
import pprint


class MyClient(Client):
    def __init__(self, *, intents: Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        synced_commands = await self.tree.sync()
        pprint.pprint(synced_commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


intents = Intents.default()
client = MyClient(intents=intents)


+ class SendChannelView(ui.View):
+     @ui.select(
+         cls=ui.ChannelSelect,
+         placeholder="select channel",
+         channel_types=[ChannelType.text],
+     )
+     async def set_channel(self, interaction: Interaction, select: ui.ChannelSelect):
+         await interaction.response.send_message(f"you select {select.values[0]}")



@client.tree.context_menu()
async def message_forward(interaction: Interaction, message: Message):
    embed = Embed(title=None)
    if message.content:
        embed.description = message.content
    embed.timestamp = message.created_at

    embed.set_author(
        name=message.author.display_name, icon_url=message.author.display_avatar.url
    ).set_footer(
        text=f"forwarded from {interaction.user.display_name}",
        icon_url=interaction.user.display_avatar.url,
    )

    url_view = ui.View()
    url_view.add_item(
        ui.Button(label="Go to Message", style=ButtonStyle.url, url=message.jump_url)
    )

+   send_channel_view = SendChannelView()

    await interaction.response.send_message("embed", embed=embed, view=url_view)
+   await interaction.followup.send(view=send_channel_view)


client.run(os.getenv("TOKEN"))

image.png

ボタンを押して送信

続いて,ボタンを押したときに選択されたチャンネルに送信するように変更します.先ほど作ったSendChannelViewにボタンを追加するbuttonデコレータを追加します.また,埋め込みはこのときに送信したいのでSendChannelViewの引数に渡しておきます.

pyhon
import os
from discord import (
    Interaction,
    Intents,
    Client,
    Message,
    Embed,
    ui,
    ButtonStyle,
    ChannelType,
)
from discord.app_commands import CommandTree
import pprint


class MyClient(Client):
    def __init__(self, *, intents: Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        synced_commands = await self.tree.sync()
        pprint.pprint(synced_commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


intents = Intents.default()
client = MyClient(intents=intents)


class SendChannelView(ui.View):
+   def __init__(self, embed: Embed, url_view: ui.View):
+       super().__init__()
+       self.send_embed = embed
+       self.send_url_view = url_view

    @ui.select(
        cls=ui.ChannelSelect,
        placeholder="select channel",
        channel_types=[ChannelType.text],
    )
    async def set_channel(self, interaction: Interaction, select: ui.ChannelSelect):
        await interaction.response.send_message(f"you select {select.values[0]}")


+   @ui.button(label="Send")
+   async def send_button(self, interaction: Interaction, button: ui.Button):
+       await interaction.response.send_message("send!")
+       await self.fav_channel.send(embed=self.send_embed, view=self.send_url_view)


@client.tree.context_menu()
async def message_forward(interaction: Interaction, message: Message):
    embed = Embed(title=None)
    if message.content:
        embed.description = message.content
    embed.timestamp = message.created_at

    embed.set_author(
        name=message.author.display_name, icon_url=message.author.display_avatar.url
    ).set_footer(
        text=f"forwarded from {interaction.user.display_name}",
        icon_url=interaction.user.display_avatar.url,
    )

    url_view = ui.View()
    url_view.add_item(
        ui.Button(label="Go to Message", style=ButtonStyle.url, url=message.jump_url)
    )

-   send_channel_view = SendChannelView()
+   send_channel_view = SendChannelView(embed=embed, url_view=url_view)

client.run(os.getenv("TOKEN"))

image.png

image.png

転送されましたね.

細かいところ

転送自体は現段階でできています.しかし問題があります.

  • チャンネルが指定されていないときにボタンを押すとエラーが発生する
  • 何回でも転送できる
  • 邪魔

というわけでこれらを片付けます.

チャンネルの指定を必須化,および一度のみの転送

チャンネルの指定を必須にするためには,そもそも送信できなくしてしまえばいいです.Buttonにはdisabledというプロパティがあります.このプロパティを最初はTrueにして,一度でも選択されたらFalseに変更させます.このとき,見た目を変えると押せるようになった感が出ると思いますので見た目の変更もします.

また,転送したらButtonやSelectを使えなくしましょう.Viewにはchildrenというプロパティがありますので,Buttonが押されたときの処理でfor文を回してdisabledにしましょう.ただし,childreの返り値はItemのリストです.これはButtonやSelectなどのスーパークラスですが,disabledというプロパティを持っていません.そのため,vscodeなどを使って入力補完が効かないです.まあButtonやSelectは持っていますから,一応大丈夫です.2

python
import os
from discord import (
    Interaction,
    Intents,
    Client,
    Message,
    Embed,
    ui,
    ButtonStyle,
    ChannelType,
)
from discord.app_commands import CommandTree
import pprint


class MyClient(Client):
    def __init__(self, *, intents: Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        synced_commands = await self.tree.sync()
        pprint.pprint(synced_commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


intents = Intents.default()
client = MyClient(intents=intents)


class SendChannelView(ui.View):
    def __init__(self, embed: Embed, url_view: ui.View):
        super().__init__()
        self.send_embed = embed
        self.send_url_view = url_view

    @ui.select(
        cls=ui.ChannelSelect,
        placeholder="select channel",
        channel_types=[ChannelType.text],
    )
    async def set_channel(self, interaction: Interaction, select: ui.ChannelSelect):
-       await interaction.response.send_message(f"you select {select.values[0]}")

+       self.send_button.disabled = False
+       self.send_button.style = ButtonStyle.green
+
+       await interaction.response.edit_message(view=self)

        self.fav_channel = await select.values[0].fetch()

    @ui.button(label="Send", disabled=True, style=ButtonStyle.gray)
    async def send_button(self, interaction: Interaction, button: ui.Button):
-       await interaction.response.send_message("send!")

+       for item in self.children:
+           item.disabled = True
+       self.send_button.style = ButtonStyle.gray
+       await interaction.response.edit_message(view=self)

        await self.fav_channel.send(embed=self.send_embed, view=self.send_url_view)


@client.tree.context_menu()
async def message_forward(interaction: Interaction, message: Message):
    embed = Embed(title=None)
    if message.content:
        embed.description = message.content
    embed.timestamp = message.created_at

    embed.set_author(
        name=message.author.display_name, icon_url=message.author.display_avatar.url
    ).set_footer(
        text=f"forwarded from {interaction.user.display_name}",
        icon_url=interaction.user.display_avatar.url,
    )

    url_view = ui.View()
    url_view.add_item(
        ui.Button(label="Go to Message", style=ButtonStyle.url, url=message.jump_url)
    )

    send_channel_view = SendChannelView(embed=embed, url_view=url_view)

    await interaction.response.send_message("where?", view=send_channel_view)


client.run(os.getenv("TOKEN"))

image.png

image.png

image.png

こんな感じでボタンの色が変わったり,使えなくなったりしました.使えなくなる話の詳しいことは,書いた記事で触れているので見ていただけるとありがたいです.

邪魔

さて,次の問題です.
履歴に残るのが邪魔です.残す理由もないですし消しましょう.また,そのままメッセージを送信すると通知も来るので自分だけにしか見えなくしましょう.

削除するメッセージはチャンネルの選択などをするメッセージですから,ボタンを押したときのインタラクションにあります.このインタラクションに対してdelete_original_response()メソッドを利用します.

python
import os
from discord import (
    Interaction,
    Intents,
    Client,
    Message,
    Embed,
    ui,
    ButtonStyle,
    ChannelType,
)
from discord.app_commands import CommandTree
import pprint


class MyClient(Client):
    def __init__(self, *, intents: Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        synced_commands = await self.tree.sync()
        pprint.pprint(synced_commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


intents = Intents.default()
client = MyClient(intents=intents)


class SendChannelView(ui.View):
    def __init__(self, embed: Embed, url_view: ui.View):
        super().__init__()
        self.send_embed = embed
        self.send_url_view = url_view

    @ui.select(
        cls=ui.ChannelSelect,
        placeholder="select channel",
        channel_types=[ChannelType.text],
    )
    async def set_channel(self, interaction: Interaction, select: ui.ChannelSelect):
        self.send_button.disabled = False
        self.send_button.style = ButtonStyle.green

        await interaction.response.edit_message(view=self)

        self.fav_channel = await select.values[0].fetch()

    @ui.button(label="Send", disabled=True, style=ButtonStyle.gray)
    async def send_button(self, interaction: Interaction, button: ui.Button):
        for item in self.children:
            item.disabled = True
        self.send_button.style = ButtonStyle.gray
        await interaction.response.edit_message(view=self)

        await self.fav_channel.send(embed=self.send_embed, view=self.send_url_view)
+       await interaction.delete_original_response()


@client.tree.context_menu()
async def message_forward(interaction: Interaction, message: Message):
    embed = Embed(title=None)
    if message.content:
        embed.description = message.content
    embed.timestamp = message.created_at

    embed.set_author(
        name=message.author.display_name, icon_url=message.author.display_avatar.url
    ).set_footer(
        text=f"forwarded from {interaction.user.display_name}",
        icon_url=interaction.user.display_avatar.url,
    )

    url_view = ui.View()
    url_view.add_item(
        ui.Button(label="Go to Message", style=ButtonStyle.url, url=message.jump_url)
    )

    send_channel_view = SendChannelView(embed=embed, url_view=url_view)

-   await interaction.response.send_message("where?", view=send_channel_view)
+   await interaction.response.send_message(
+       "where?", view=send_channel_view, ephemeral=True
+   )


client.run(os.getenv("TOKEN"))

image.png

自分だけ見えているやつですね.送信した後は消えちゃうので載せられません~~し,gifを載せるにしてもgifにするとき毎回謎画質になる3~~ので,ご自身で試してみてください.

以上となります.discord botでメッセージを転送することができました.
お疲れ様でした.

  1. 画像も転送したい場合は,メッセージのembedsからEmbedを複数取得します.その後取得したものと自身で作成した埋め込みをlistとして,送信の際にemvedsに指定するとできると思います.message_contentの権限が必要なので注意してください.

  2. TextInputはdisabledを持っていませんが,なんかViewでは使えなかったです.Modalだと使えるんですけどね.要検証

  3. 多分軽量化のためにいろいろしてくれてるんでしょうけど,そのせいで変になります.いい感じの動画の載せ方ありますかね...

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?