はじめに
この記事は、discord.pyとgoogle apps scriptがあらかたわかっている人向けに書かれている。もしわかっていない人がいるならPythonで実用Discord Bot(discordpy解説)などを参考にしてほしい。
やったこと
とある研究会を運営するにあたって、discordを連絡手段に利用した。
google formを利用し研究会への参加者を募った。この際に参加者の名前とメールアドレスを収集した。
収集したメールアドレス宛にgoogle apps script を利用することでdiscordへの参加リンクを送信してdiscordサーバーに参加してもらった。
参加者にはdiscord内でのニックネームを名前 (所属)に変更してもらった。
このニックネームをdiscord bot で読むことで、名前とdiscrod_user_idを.csvファイルとして出力し、それをgoogle driveにアップロードし、 apps scriptを用いて処理することで、google formで集めた参加希望者とdiscrodに参加しニックネームを変更した参加者を紐づけた。
最後に、紐づけることができなかった参加者にgoogle app scriptを利用することで参加リマインドメールを送信した。
プログラムの解説
処理の流れ
- google formを用いて参加者を収集 google spread sheetに情報を保存 discord招待メールを送信
- discord.pyを用いてニックネームとdiscord_user_idを取得。一覧を.csvファイルで保存
- .csvファイルをgoogle driveにアップロード
- google Apps Scriptを用いてgoogle spread sheet上で参加者とidを紐づけ 紐づいていない参加者にリマインドメールを送信
1, 参加者収集~情報保存
参加者から収集した情報は
- 名前
- メールアドレス
- そのほか1
である。
この結果をgoogle spread sheetに収集し、以下のようなGoogle Apps Scriptを実行してdiscord招待メールを送信した。
function sendEmails() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シートの名前");
const data = sheet.getDataRange().getValues();
name_index = ; //名前が入力されているインデックス。A列が0の整数値
mail_index = ; //メールアドレスが入力されているインデックス
for (let i = 1; i < data.length; i++) { //多くの場合は for (let (i = 1~ だと思う。一人目の情報が格納されたインデックスを指定すること 0 スタート)
const name = data[i][name_index]; // 名前
const email = data[i][mail_index]; // B列:メールアドレス
const subject = "discord 参加のお願い"; // 件名
const body = `${name} 様
この度は参加申込みをいただき、ありがとうございます。
~~~メール本文~~
何卒よろしくお願いいたします。
`;
MailApp.sendEmail(email, subject, body);
Logger.log(`${name}様に送信完了`);
}
}
2,disrod.pyを用いた.csvファイル作成
以下のプログラムを実行し、.csvという名前のファイルを作成した。
import discord
from discord.ext import commands
import re
import csv
import os
TOKEN = "YOUR_BOT_TOKEN" # ここにボットのトークンを貼り付け
# 監査対象のサーバーIDを設定
TARGET_GUILD_ID = YOUR_GUILD_ID # ここに監査したいサーバーのIDを貼り付け
# ニックネームのパターン
# フォーマット: 名前 + [半角/全角スペース] + ( + 任意の文字列 + )
# または、名前 + [半角/全角スペース] + ( + 任意の文字列 + )
# 半角スペースと全角スペースを許容し、半角括弧と全角括弧を許容
# 括弧内は空文字でも任意の文字の並びでもOK
# この正規表現は、(または(で始まり、)または)で終わるパターンを検出します。
NICKNAME_FORMAT_PATTERN = re.compile(
r".+\s[\((].*[\))]"
)
intents = discord.Intents.default()
intents.members = True # メンバーインテントを有効にする
intents.guilds = True # ギルドインテントを有効にする
bot = commands.Bot(command_prefix='!', intents=intents)
@bot.event
async def on_ready():
print(f'{bot.user} としてログインしました!')
print('ニックネームの設定状況とフォーマットを確認します...')
guild = bot.get_guild(TARGET_GUILD_ID)
if guild is None:
print(f"エラー: 指定されたサーバーID {TARGET_GUILD_ID} が見つかりません。")
await bot.close()
return
print(f"サーバー '{guild.name}' ({guild.id}) のメンバーニックネームを確認中...")
# 結果を格納するリスト
nickname_not_set_members = []
nickname_set_members = []
formatted_nickname_members = []
not_formatted_nickname_members = []
for member in guild.members:
# Bot自身はスキップ
if member.bot:
continue
nickname = member.nick # ニックネームを取得
if nickname is None:
# ニックネームが設定されていない場合
nickname_not_set_members.append({'user_id': member.id, 'user_name': member.name, 'current_nickname': 'なし'})
else:
# ニックネームが設定されている場合
nickname_set_members.append({'user_id': member.id, 'user_name': member.name, 'nickname': nickname})
# 設定されているニックネームが指定のフォーマットに合致するかチェック
if NICKNAME_FORMAT_PATTERN.search(nickname):
formatted_nickname_members.append({'user_id': member.id, 'user_name': member.name, 'nickname': nickname})
else:
not_formatted_nickname_members.append({'user_id': member.id, 'user_name': member.name, 'nickname': nickname, 'reason': f'指定フォーマットに合致しません: "{nickname}"'})
# CSV ファイルに保存するための出力ディレクトリ
output_dir = "MENBERS"
os.makedirs(output_dir, exist_ok=True)
# 1. ニックネームが設定されていない(変更されていない)人のリスト
not_set_csv_path = os.path.join(output_dir, "nickname_not_set_members.csv")
with open(not_set_csv_path, 'w', newline='', encoding='utf-8') as f:
fieldnames = ['user_id', 'user_name', 'current_nickname']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(nickname_not_set_members)
print(f"ニックネームが設定されていない(変更されていない)メンバーのリストを '{not_set_csv_path}' に保存しました。")
# 2. ニックネームが指定フォーマットに合致する人のリスト
formatted_csv_path = os.path.join(output_dir, "formatted_nickname_members.csv")
with open(formatted_csv_path, 'w', newline='', encoding='utf-8') as f:
fieldnames = ['user_id', 'user_name', 'nickname']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(formatted_nickname_members)
print(f"指定フォーマットに合致するメンバーのリストを '{formatted_csv_path}' に保存しました。")
# 3. ニックネームが指定フォーマットに合致しない人のリスト
not_formatted_csv_path = os.path.join(output_dir, "not_formatted_nickname_members.csv")
with open(not_formatted_csv_path, 'w', newline='', encoding='utf-8') as f:
fieldnames = ['user_id', 'user_name', 'nickname', 'reason']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(not_formatted_nickname_members)
print(f"指定フォーマットに合致しないメンバーのリストを '{not_formatted_csv_path}' に保存しました。")
print('ニックネームの確認が完了しました。Botをシャットダウンします。')
await bot.close()
# Botを実行
bot.run(TOKEN)
3,GASを用いたgoogle spread sheetとの紐づけ
先ほど作ったcsvファイルformatted_csv_path.csvとnickname_not_set_members.csvをgoogle driveにアップロードし、csvファイルのidを得る。その後、以下のgsファイルを実行する
vfunction myFunction() {
const csvFileId = 'csvファイルid';
const spreadsheetId = 'google spreadshet id';
const sheetName = 'シート名';
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
// --- ここからメール送信機能の追加 ---
// メールアドレスの列インデックス (例: G列がメールアドレスの場合、インデックスは6)
const emailColIndex = ;
// discordに参加した場合にチェックする列のインデックス
const ajColIndex = ;
// discord idを格納する列のインデックス
const apColIndex = ;
// メール本文
const text =`参加者の皆様
お世話になっております。
先日ご案内いたしましたdiscord公式サーバーですが、現時点での参加が確認できておりません。
~~~メール本文~~~
`; // 必要に応じてメール本文を編集してください
// スプレッドシートの最新のデータ範囲を取得し直す
const updatedSheetValues = sheet.getDataRange().getValues();
// メール送信対象者のリスト
const recipients = [];
for (let k = 0; k < updatedSheetValues.length; k++) {
const row = updatedSheetValues[k];
const apValue = row[apColIndex];
const ajValue = row[ajColIndex];//
const recipientEmail = row[emailColIndex];
// ヘッダー行をスキップ (必要であれば、k > 0 でスキップ)
// 最初の行がヘッダーであれば、k > 0 とします。
// 今回のケースではk > 0 を追加してヘッダー行をスキップします。
if (k === 0) {
continue;
}
// 条件チェック: AP列が 'y' でない かつ AJ列がチェック(TRUE)ではない
// AP列が空欄の場合も'y'ではないと判断します
// AJ列がチェックボックスの場合、チェックされているとTRUE、空欄だとFALSEになります
if (apValue !== 'y' && ajValue !== true && recipientEmail) {
// recipientEmailが存在し、有効な形式であることを確認することも推奨されます
recipients.push({
email: recipientEmail,
name: row[3] // D列の名前(メールの宛名などに使えるように)
});
Logger.log(`メール送信対象者: ${row[3]} (${recipientEmail}) - AP: "${apValue}", AJ: "${ajValue}"`);
}
}
// 対象者へのメール送信
if (recipients.length > 0) {
Logger.log(`${recipients.length} 人にメールを送信します。`);
recipients.forEach(recipient => {
try {
GmailApp.sendEmail(
recipient.email, // 宛先
'【重要】discoed参加のお願い', // 件名
text // 本文
);
Logger.log(`メールを ${recipient.name} (${recipient.email}) に送信しました。`);
Logger.log(text)
} catch (e) {
Logger.log(`エラー: ${recipient.name} (${recipient.email}) へのメール送信に失敗しました: ${e.message}`);
}
});
Logger.log('すべてのメール送信処理が完了しました。');
} else {
Logger.log('メール送信対象者はいませんでした。');
}
}
これで、設定した列に紐づけされたdiscord.user.idが格納されるはずです。
終わりに
これを用いれば、アンケート結果に応じたdiscrod.roleの付与などに応用が利くと思っています。
皆様の参考になれば幸いです
-
発表希望やアレルギーの有無などを収集した。詳しくは述べないが、ここで収集した情報によってdiscrodのroleを付与した。かなり使いやすかった。 ↩