涼しい風

Discord.pyでdiscordbotを作っていた人のブログです。開発のお役に立てれば幸いです。2019/11/10を持って更新は終了しました。

「Twihook」で簡単にDiscordへのTwitter投稿通知

Twitterの投稿情報をDiscordで通知させるには、Webhookという仕組みを使います。 多くの場合、これには「IFTTT」(https://ifttt.com/) というサービスが使用されています。

しかし、IFTTTは投稿から通知まで最大15分のラグが生じる上、始めたての人にとっては複雑だと思います。 これを10分以内に抑え、更にTwitter通知に特化したサービスがTwihookです。

この記事は非公式です、問題があれば削除します・・・)

導入

1.Botをサーバーへ招待する

discordapp.com

2.Webhookを作成

Webhookによって、DiscordとTwitterを繋げます。

この操作は現在PC版限定です。 もしスマートフォン or タブレットで操作するなら、ブラウザ版でPC版サイトに切り替えましょう。

サーバー設定から、「Webhook」タブをクリック。

f:id:coolwind0202:20190916230531p:plain
webhook画面の例

右上に大きくある「ウェブフックを作成」を押します。

f:id:coolwind0202:20190916230930p:plain
Webhookの作成画面

このような画面が出現します。 お名前は触らなくても良いです。 チャンネルに、Twitter投稿を通知したいチャンネルを指定します

アイコンは、通知時のアイコンになります(これも自由です)。 そうしたら、ウェブフックURLの「Copy」を押します。次にすぐ使います。

3.チャンネルで利用登録し、Webhookと連携

まずは、サーバーのテキストチャンネルで /register と送信します。 するとTwihookからDMが届くので、それに添付されたURLでTwitter認証します。

7桁の数字が表示されましたか? 数字をTwihookとのDMにそのまま送信してください。

f:id:coolwind0202:20190916231742p:plain
送信例

そうしたら、先ほどCopyしたウェブフックURLを用意してください。 サーバーのテキストチャンネルに/webhook new ウェブフックURLと送信します。

ここまで正しく手順を踏んでいれば以下のように送信されます。 f:id:coolwind0202:20190920194744p:plain

4.通知させるアカウントを設定

これでようやく、Discord側に通知させるTwitterアカウントを設定できます!

先ほど送信されたwebhook manage ID部分を、そのままコピペし、/に繋げて送信します。 (/webhook manage IDというように

f:id:coolwind0202:20190926224343p:plain 画像では既にユーザーが登録されていますが、ここでは0️⃣絵文字をクリックしてください。

f:id:coolwind0202:20190920195305p:plain

このように表示されるので、あなたがサーバーに通知させたいTwitterのユーザー名を送信します。

f:id:coolwind0202:20190920195554p:plain

もし成功すると画像のようにメッセージが編集されます、これでアカウントの設定は終わりです。

5.テキストを設定する

最後に、通知時にどのようなテキストとともに更新を通知するかを設定しなければなりません。 これを行わないと「テキストが設定されていないため、表示することができませんでした。管理人は設定をお願いします。 」と延々に送信される事になります(小声)

f:id:coolwind0202:20190926224135p:plain 画像のように表示されますから、また0️⃣絵文字をクリックします。

f:id:coolwind0202:20190921003837p:plain このように表示されましたでしょうか?(人によって少し動作が異なりますが・・・)

数字の0️⃣、1️⃣をクリックするとそれぞれのテンプレート通りに送信されます!

完了 ー 適切に変更されました。 と表示されたら全ての設定が完了です。お疲れ様でした。

補足

無料版であれば10分ごとに確認が行われます。(より高速な有料版はPixiv Fan boxで受け付け予定のこと)

最後に、Twihookの公式サーバーのリンクを掲載します。

discord.gg

データをGoogleスプレッドシートで読み書き

こんにちは、この記事では、Discord公式APIのラッパー、Discord.py と、Googleスプレッドシートを使ってデータを保存したり、読み込んだりしてみます。

この記事ではPythonGoogleスプレッドシートの編集を行えるgspread_asyncioを使用します。 GoogleスプレッドシートAPIを直接操作する方法は以下の記事で解説しています coolwind0202.hatenadiary.jp

qiita.com

導入まではこちらの記事が参考になります、こちらの記事のスプレッドシート共有の部分までの説明は省略します。

共有まで終わったら、

pip install gspread_asyncio
pip install oauth2client

でそれぞれのパッケージをinstallできます。

コード例の準備のため、スプレッドシートを画像のように事前に編集しています。 f:id:coolwind0202:20190919215834p:plain

実装

import discord
import gspread-asyncio
from oauth2client.service_account import ServiceAccountCredentials

client = discord.Client()
token = ""

@client.event
async def on_ready():
    client.agcm = gspread_asyncio.AsyncioGspreadClientManager(spread_auth)

@client.event
async def on_message(message):

    if message.content .startsswith("/log"):
        agc = await client.agcm.authorize()
        tmp = await agc.open('作成したGoogleスプレッドシートのファイル名')
        wks = await tmp.get_worksheet(0) # 開きたいシートの番号(0始まり)

        await wks.append_row([str(message.author.id),message.author.name,message.guild.name,                  message.channel.name,message.content,message.jump_url]) # 現在時点で、表の最後尾の行に情報を書き込む

        await message.channel..send("Succeeded💪")

def spread_auth():
    
    return ServiceAccountCredentials.from_json_keyfile_name(
        r"リンク記事内でダウンロードしたjsonファイルのパス",
        ['https://spreadsheets.google.com/feeds', 
        'https://www.googleapis.com/auth/drive',
        'https://www.googleapis.com/auth/spreadsheets'])

client.run(token)

bot commands framework使用版)

import discord
from discord.ext import commands

bot = commands.Bot(command_prefix='!')

@bot.event
async def on_ready():
    bot.agcm = gspread_asyncio.AsyncioGspreadClientManager(spread_auth)

@bot.command()
async def spread_test(ctx):

    agc = await bot.agcm.authorize()
    tmp = await agc.open('gspread_test')
    wks = await tmp.get_worksheet(0)

    await wks.append_row([str(ctx.author.id),ctx.author.name,ctx.guild.name,
                          ctx.message.channel.name,ctx.message.content,ctx.message.jump_url])

    await ctx.send("Succeeded💪")

def spread_auth():
    
    return ServiceAccountCredentials.from_json_keyfile_name(
        r"リンク記事内でダウンロードしたjsonファイルのパス",
        ['https://spreadsheets.google.com/feeds', 
        'https://www.googleapis.com/auth/drive',
        'https://www.googleapis.com/auth/spreadsheets'])

bot.run(token)

コード例には以下の記事を参考にしています。 qiita.com なおパスを直接表に出すのもあれなので、その場合は環境変数を使うと良いです。

読み込む

やっぱり一方的に書き込むだけじゃ物足りないので、今度はこれを読みだしてみましょう。

(楽なのでbot commands frameworkを使っています、反映する場合はbotをclientに置き換えたり色々お願いします・・・)

@bot.command()
async def spread_read(ctx,row:int):

    agc = await bot.agcm.authorize()
    tmp = await agc.open('gspread_test')
    wks = await tmp.get_worksheet(0)

    head = await wks.row_values(1)
    values = await wks.row_values(row)

    result = []

    for i in range(len(head)):
        result.append(head[i]+" / "+values[i])

    await ctx.send("\n".join(result))

f:id:coolwind0202:20190922151302p:plain
実行例

だいたいこんな感じです。

Discord.pyでカテゴリ内にチャンネルを作成するサンプルコード

Discord公式APIのラッパー、Discord.py を使ってDiscordサーバーの特定カテゴリ内のチャンネル一覧を取得したり、新規チャンネルを作成する方法を紹介します。


実はカテゴリも普通のテキストチャンネルと同じく、client.get_channel( チャンネルID ) 等で取得できます。 なおカテゴリはCategoryChannelクラスのオブジェクトとなります。

import discord

client = discord.Client()
token = ""

@client.event
async def on_message(message):
    print( client.get_channel(message.channel.category_id).channels ) #メッセージが送信されたチャンネルのカテゴリのチャンネル一覧を表示
    
    if message.content.startswith("/cateinfo"):
        tmp = message.content[10:]
        category_channel = client.get_channel( int(tmp) )

        await message.channel.send( [i.name for i in category_channel.channels] )

    elif message.content.startswith("/create"):
        category_channel = client.get_channel(message.channel.category_id)

        await category_channel.create_text_channel( message.author.name )

client.run(token)

このコードでは、/cateinfoは、/cateinfo IDと送信することで、そのIDのカテゴリのチャンネル一覧を送信します。

/createと送信すると、メッセージが送信されたカテゴリーにチャンネルを作成します。 ポイントはawait CategoryChannel.create_text_channel( チャンネル名 )です。

あくまで例なので、色々例外処理が無いのに注意です。

Discord.pyでURLのQRコードを自動生成するbotを作る

こんにちは、この記事ではDiscord公式APIのラッパー、Discord.py と、QRコードを生成してくれるAPI「QRcode API」を使って、DiscordbotにQRコードを自動生成させてみます。

お世話になるQRcode APIの公式webページは以下になります。

goqr.me

導入

もしコマンドラインなど触ったことが無ければ を読んでください(丸投げ)

aiohttpを使う

pythonのwebリクエストでよく使われるのは、サードパーティー製のrequestsや標準ライブラリのurllibだと思います。 これらのライブラリはいわゆる同期処理で、上から順番に、1つずつ処理を行っていきます。

しかしdiscord.pyでは、非同期で通信が可能なライブラリaiohttpの使用を推奨しています。 これは、同期処理だと、仮に処理が重かった場合に、本来は常に通信すべきdiscord.py側とbotの接続が途切れてしまう可能性があるからです。

幸いなことにdiscord.pyの公式ドキュメントにサンプルコードが載っているのでそのまま使わせて頂きましょう。

discordpy.readthedocs.io

コード

import discord
import aiohttp

client = discord.Client()
token = ""

@client.event
async def on_message(message):
    if message.content.startswith("!qrcode"):
        tmp = message.content.split(" ")

        if len(tmp) > 1:
            url = tmp[1]
        else:
            await message.channel.send("URLが指定されていません。")
            return

        payload = {"data":url,"size":"200x200"} # sizeは自由に変更可

        async with aiohttp.ClientSession() as session:
            async with session.get("https://api.qrserver.com/v1/create-qr-code/",params=payload) as resp:
                if resp.status != 200:
                    return await message.channel.send('ファイルをダウンロードできませんでした。')

                data = io.BytesIO(await resp.read())

        await message.channel.send(file=discord.File(data, url+'.png'))

client.run(token)

f:id:coolwind0202:20190919165734p:plain
実行例

こんな感じでQRコードが生成されます!

終わりに

QR code APIを使って生成したQRコードをDiscord.pyで送信するまでの方法を紹介しました。 今後これを作る人が現れるのか不明ですが、お役に立てば幸いです

Discord.pyで、メンバーを別のボイスチャンネルへ移動させる/蹴る

Discord公式APIのラッパー、Discord.py でメンバーを別のボイスチャンネルへ移動させる方法を紹介します。

使い方

メンバーを移動させるには、await member.move_to(channel)を使います。

import discord

client = discord.Client()
token = ""

@client.event
async def on_message(message):
    if message.content.startswith("!move"):
        if message.author.voice == None:
            return

        vc = message.guild.get_channel(移動先のvcのID)
        await message.author.move_to(vc)

    if message.content == "!kick":
        await message.author.move_to(None)

移動先をNoneにすることで、VCからのキックを表現できます(注:サーバーからのキックではありません。)


もし移動させるメンバーをメッセージ送信者以外にするなら、

member = client.get_member(メンバーのID)
await member.move_to(vc)

とすればOKです。

Discord.py - Messageのメモ

discord.py のドキュメントも全てが訳されている訳ではないのでメモです。 個人的なものなので書き方が曖昧かもしれません、えらい人教えてください・・・


discord.Messageは、サーバーやDMに投稿された全てのメッセージに当てはまるクラスです。

あくまでmessageが持つクラスであり、また自分でこれのインスタンスを作成することはありません。

全てのメッセージは、以下の値を持ちます。(Noneの場合もある) 値は以下のように参照します。

@client.event
async def on_message(message):
    print(message.tts) # ttsメッセージであるかが表示されます
    
    mes = message.channel.fetch_messages( メッセージのID )
    print(mes.content) # メッセージの内容が表示されます

tts

メッセージがttsメッセージであるかを表します。 ttsメッセージの時はTrue、そうでないときはFalseになります。

ttsって何?という方はDiscordの公式ヘルプを参照してください。 support.discordapp.com

type

これはメッセージのタイプを表します。 個人的には正直使い所がよくわかりません。

MessageTypeクラスの値を返します。ほぼ訳されているのでリンクと補足のみとします。

discordpy.readthedocs.io

ここではchannel_follow_addのみ訳されていません。

Discordには、パートナー、検証済みサーバー、開発者サーバーにおいてのみ使用できる「Announcement Channel」という特殊なチャンネルが存在するのですが、これらのサーバーは、ユーザーが「Follow」ボタンを押すことでユーザー自らの管理するサーバーにもそのチャンネルの投稿を通知させる機能を持っています。

channel_follow_addは、その通知への接続時に表示される特殊メッセージを表す値です。

author - 送信者

これは、メッセージの投稿者を表します。 もしそのメッセージが通常のサーバーで投稿されたなら、それはMemberオブジェクトを返します。 もしそのメッセージがDMに投稿されたなら、それはUserオブジェクトを返します。

content - 内容

メッセージの内容です。 文字列型で返します。

nonce

公式ドキュメントによれば、メッセージが正常に送信されたかを確認する値なのですが、どんな値なのかわかりませんでした。 こちらの環境では整数型を返しました。

embeds - 埋め込みの一覧

これはメッセージの埋め込みを表します。 もし埋め込みが存在するならEmbedオブジェクトのリストを、無ければ空のリストを返します。

channel - チャンネル

メッセージが送信されたチャンネルです。 もしテキストチャンネルから送信されたならTextChannelオブジェクト、DMチャンネルならDMChannelオブジェクトあるいはGroupChannelオブジェクトを返します。

mention_everyone - everyoneメンションの有無

これは、@everyone などでサーバーの全員に対するメンションが行われているメッセージかを表します。 これは真偽値を返します。

mentions - メンションのリスト

メッセージに含まれるメンションの対象になっているメンバーのリストを返します。 もし誰にもメンションしていないなら、これは空のリストになります。

channel_mentions - チャンネルメンションのリスト

mentionsと同様に、メンションの対象になっているチャンネルのリストを返します。(#チャンネル名のように) もしどのチャンネルにもメンションしていないなら、これは空のリストになります。

role_mentions - 役職メンションのリスト

mentionsと同様に、メンションの対象になっている役職のリストを返します。 もしどの役職にもメンションしていないなら、これは空のリストになります。

id

メッセージのIDを表します。 これは整数型を返します。

webhook_id

もしそのメッセージがwebhookによって送信されたなら、そのWebhookのIDを返します。 整数型になります。

attachment - 添付ファイルリスト

これは添付ファイルを表すAttachmentオブジェクトのリストを返します。 もし1つもメッセージにファイルが添付されていないのなら、これは空のリストになります。

pinned - ピン留めの有無

これはメッセージが取得時点でピン留めされているかを表します。 真偽値が返されます。

reactions - リアクション

もしメッセージにリアクションが付けられているのなら、これはReactionオブジェクトのリストを返します。

guild - サーバー

メッセージが投稿されたサーバー。 Guildオブジェクトを返します。

raw_mentions/raw_channel_mentions/raw_role_mentions

それぞれ<@ID>(メンバー)、<#ID>(チャンネル)、<&ID>(役職)の文字列がメッセージ内容に含まれているとき、そのIDのリストを返します。

clean_content - 整形済みメッセージ

これは、メッセージ内容をユーザーの表示と同じように変換したものを返します。 メンションの<@ID>@名前に変換される、といった具合です

created_at - 投稿時間

メッセージの投稿時間を返します。 datetime.datetimeオブジェクトで表されます。

jump_url - URL

このメッセージのURLを返します。 文字列で表され、リンクを作成するときなどに使えそうです。

is_system() - システムメッセージか

メッセージがシステムメッセージであるかを返します。 message.typeのdefaultか、そうでないか、とだいたい同じだと思います。

system_content

もし、メッセージがシステムメッセージであるなら、その内容の英語版のメッセージを返します。 普通のメッセージなら、そのまま内容を返します。

activity/call/application

把握できませんでした。 もし分かったら追記します。

コルーチン・メソッド

メッセージに対して以下の処理を行うことが可能です。 ただし、manage_messages(メッセージ管理)権限が必要です。

await delete() - 削除

メッセージを削除します。

@client.event
async def on_message(message):
    await message.delete()

引数にはdelay=秒数を指定できるようです。これはメッセージを削除する前にbotを待機させる秒数です。

await edit() -編集

メッセージを編集します。ただし、他人のメッセージは編集できない仕様です。

@client.event
async def on_message(message):
    my_message = await message.channel.send("こんにちは")
    await my_message.edit(content="こんばんは、60秒後に削除されます。" , delete_after=60.0 )
  • content メッセージの編集後の内容。文字列で指定します。
  • embed メッセージの新しい埋め込み。Embedオブジェクトを指定しますが、もしNoneを明示すれば削除されます。
  • subpress メッセージの埋め込みの抑制を変更します。Trueにすれば埋め込みを抑制、Falseなら抑制を解除。
  • delete_after メッセージを編集後に一定時間経ったら削除させるようにできます。浮動小数点型を指定します。

await channel.send()は、送信に成功したmessageを返すので、それを保存しています。)

await pin() / await unpin() - ピン留め変更

それぞれ、メッセージをピン留め、ピン留め解除します。

@client.event
async def on_message(message):

    if message.content == "pin_change":

        if message.pinned == True:
            await message.unpin()
            await message.channel.send("メッセージのピン留めを解除しました。")

        elif message.pinned == False:
            await message.pin()
            await message.channel.send("メッセージをピン留めしました。")

await add_reaction()/await remove_reaction()/await clean_reactions() - リアクション変更

リアクションを追加または削除、もしくは全て削除します。

@client.event
async def on_message(message):
    
    if message.content == "Reaction":
        await message.add_reaction("😀")

    elif message.content == "Remove":
        await message.remove_reaction("😀",message.guild.me)

    elif message.content.startswith("clear"):
        mes_ID = message.content.split(" ")[1]
        mes = message.channel.fetch_messages(mes_ID)
        await mes.clear_reactions()

await remove_reaction()の第二引数にはMemberオブジェクト(厳密にはMemberを表すSnowFlake)を指定します。 なお、guild.meは、サーバーにおけるbot自身のMemberオブジェクトを取得します。

await ack()

把握できませんでした。 もし分かったら追記します。


以上、Messageの値や行える処理をまとめました。

Discord.pyのコマンドについて

Discord公式APIのラッパー、Discord.py 及び Bot Commands Framework の、コマンドの引数についての概説です。

discord.pyには、discord.ext.commandsというフレームワークが含まれています。

これは、botのコマンド追加を簡易にするためのものです。

最初は戸惑うかもしれませんが、一度覚えてしまえば怖くありません。

基本

import discord
from discord.ext import commands # フレームワークをimport

token = ""
bot = commands.Bot( command_prefix = '!' ) # command_prefixに指定したものが接頭辞

@bot.command()
async def cmd_1(ctx):
    await ctx.send( "テストです" )

bot.run(token)

まず、botdiscord.ext.commandsに含まれるBotクラスを継承します。

コマンドは、非同期関数として定義しますが、必ず「ctx」という引数を受け取らなければなりません

このctx(contextの略、文脈の意)には、コマンドが実行された時の様々な情報が含まれています。

ここでは、await ctx.send()としていますが、これはコマンドが実行されたチャンネルにメッセージを送信するメソッドです。


await ctx.send()await ctx.channel.send()のショートカットです。 したがってctxからchannelを取得することが可能です。

引数を受け取る

ctxの他にも引数を設定することが可能です。 引数には以下の3つが存在します。

  • 位置引数
  • 可変長引数
  • キーワード引数

ただし、ctxのみの時と違って、ユーザーは引数を 空白区切り or 改行区切り で入力する必要があります。

1つずつ見ていきます。

位置引数

これは、位置、数が完全に固定された引数です。

@bot.command()
async def cmd_2(ctx,arg1):
    await ctx.send(ctx.author.name + " said, '" + arg1 + "'.")

このように指定します。 ユーザーは、arg1を空白区切りで1つのみ入力します。

入力させたい情報が1つならこれで問題無いです。

なお、discord.pyには、入力した引数を色々変換してくれる「コンバータ」という機能が存在します。

coolwind0202.hatenadiary.jp

ctxには、authorというコマンド実行者の情報も含まれています。nameは、そのmemberの名前を表します。

可変長引数

これは、数が決まっていない引数です。

実引数はリストとして受け取ります。

@bot.command
async def cmd_3(ctx,*args):
    await ctx.send( "入力された引数の個数は " + str( len(args) ) + "個です" )

注意点として、可変長引数はいくらでも増やせますし、0個でも通ってしまいます。

もし個数を制限したいなら、

if len(args) > 5:
    await ctx.send("引数が多すぎます。")
    return

のようにすると良いです。

キーワード引数

3つめはキーワード引数です。

これは、送信された引数を見境なく全て受け取ろうとします。 空白や改行もそのまま受け取ります。

長文を入力させるときなどに使うと良いかもしれません。

@bot.command()
async def cmd_3(ctx,*,arg):
    await ctx.send(arg)

この記事ではコマンドの引数について解説しました。