Google Calendarの予定を操作するDiscord Botを作る
概要
Discord上で友人と話していたときにゲーム内の予定をチャットに流せたら便利だと言う話になったので、カレンダー情報を流すBotを作成しました。
言語
Python
使用ライブラリ
Pycord
schedule
google-api-python-client
google-auth-httplib2
google-auth-oauthlib
API
Google Calendar API
Discord Webhook
本篇
- Bot準備
- コマンド
- DiscordWebhookについて
- 予定の挿入
- 予定の確認
- 通知
プログラム中のsetterとgetterは一部省略しています。
Bot準備
公式ページのBotアカウント作成に従ってBotを作ります。ここで取得したBot用のトークンはconfigに保存しました。
続いて、以下のコードを実行します。これで最低限のBotが稼働します。
import discord
with open('config.json', 'r') as f:
config = json.load(f)
discordToken = config['discord_token']
client = discord.Client(ws = int(os.environ.get('PORT', 5000)))
@client.event
async def on_ready():
print('READY')
client.run(discordToken)
コマンド
Botを導入しているサーバー内で指定のコマンドとメッセージが送られたときにメッセージのコマンド部分を削除して処理に回しています。
import os
import json
import discord
from logic.calendarLogic import
with open('config.json', 'r') as f:
config = json.load(f)
discordToken = config['discord_token']
+ calendarText = config['calendar'].get('calendar_text')
+ listText = config['calendar'].get('list_text')
+ addEventText = config['calendar'].get('add_event_text')
+ addLongEventText = config['calendar'].get('add_long_event_text')
+ deleteEventText = config['calendar'].get('delete_text')
client = discord.Client(ws = int(os.environ.get('PORT', 5000)))
@client.event
async def on_ready():
print('READY')
+ @client.event
+ async def on_message(message):
+ content = message.content
+
+ if content == calendarText:
+ logic = CalendarLogic()
+ result = logic.calendarUrl()
+
+ if content == listText:
+ logic = CalendarLogic()
+ result = logic.get()
+
+ elif re.search(addEventText, content):
+ event = content.replace(addEventText, '')
+ logic = CalendarLogic()
+ logic.insert(event)
+
+ elif re.search(addLongEventText, content):
+ event = content.replace(addLongEventText, '')
+ logic = CalendarLogic()
+ logic.insertLongEvent(event)
+
+ elif re.search(deleteEventText, content):
+ eventId = content.replace(deleteEventText, '')
+ logic = CalendarLogic()
+ logic.delete(eventId)
client.run(discordToken)
同様に取得・削除のコマンドも追加します。
最終的に以下のようになりました。
DiscordWebhookについて
今回Botからのメッセージの送信はDiscord Webhookを使用します。
Webhookを使用することでメッセージごとのアバターの名前や送信者名の変更、embedsの整形が容易になります。
import json
class WebhookContent():
def __init__(self):
with open('config.json', 'r') as f:
config = json.load(f)
self.__config = config
self.__userName = config['webhook'].get('message').get('user_name')
self.__avatarUrl = config['webhook'].get('message').get('avatar_url')
self.__color = config['webhook'].get('message').get('color')
def createMessage(self, message):
body = {
"username": self.userName,
"avatar_url": self.avatarUrl,
"content" : message,
"embeds": []
}
return body
def createEmbeds(self, data):
embeds = {
"title": data.summary,
"description": f"{data.date} {data.time}",
"color": int(self.color, 16),
"footer": {
"icon_url": self.avatarUrl,
"text": data.eventId
},
}
if data.description:
fields = {
"fields": [
{
"name": "詳細",
"value": data.description,
}
]
}
embeds.update(fields)
return embeds
def createLongEventEmbeds(self, data):
embeds = {
"title": data.summary,
"description": f"{data.date} 〜 {data.endDate}",
"color": int(self.color, 16),
"footer": {
"icon_url": self.avatarUrl,
"text": data.eventId
},
}
if data.description:
fields = {
"fields": [
{
"name": "詳細",
"value": data.description,
}
]
}
embeds.update(fields)
return embeds
import json
import requests
class Webhook():
def __init__(self, body = None):
with open('config.json', 'r') as f:
config = json.load(f)
self.__url = config["webhook_url"]
@property
def body(self):
return self.__body
@body.setter
def body(self, val):
self.__body = val
@property
def url(self):
return self.__url
def send(self, body):
requests.post(self.url,json.dumps(body),headers={'Content-Type': 'application/json'})
カレンダーコマンド実行時にWebhookに必要な情報を乗せ、指定のURLにリクエストを行いメッセージを送信します。
予定の挿入
botを導入しているDiscordサーバーで所定のフォーマットのメッセージを受け取った時にカレンダーに予定を挿入します。
リクエスト操作については別記事にまとめましたので、ここでは実装部分を記載します。
受け取ったメッセージを各項目に分解してインスタンスを生成し、リクエストを行います。
import json
from api.calendarApi import CalendarApi
from data.calendarData import CalendarData
from data.calendarData import CalendarContent
class CalendarLogic():
def __init__(self):
with open('config.json', 'r') as f:
config = json.load(f)
self.__config = config
def insert(self, event):
try:
# 予定挿入
calendar = CalendarApi()
calData = CalendarData()
calContent = CalendarContent()
content = event.split(' ')
calData.date = content[0]
date = calData.date.split('/')
calData.year = int(date[0])
calData.month = int(date[1])
calData.day = int(date[2])
calData.time = content[1]
time = calData.time.split(':')
calData.hour = int(time[0])
calData.minute = int(time[1])
calData.summary = content[2]
if len(content) == 4:
calData.description = content[3]
else:
calData.description = ''
body = calContent.createInsertData(calData)
result = calendar.insert(body)
calData.eventId = result["id"]
# Webhookメッセージ送信
webhook = Webhook()
webhookData = WebhookData()
webhookContent = WebhookContent()
webhookData.summary = calData.summary
webhookData.description = calData.description
webhookData.eventId = calData.eventId
webhookData.date = calData.date
webhookData.time = calData.time
message = self.config['webhook'].get('message').get('insert_message')
body = webhookContent.createMessage(message)
embeds = webhookContent.createEmbeds(webhookData)
body["embeds"].append(embeds)
webhook.send(body)
except Exception as e:
print(e)
# エラーメッセージ送信
webhook = Webhook()
webhookContent = WebhookContent()
message = self.config['webhook'].get('message').get('insert_error_message')
body = webhookContent.createMessage(message)
webhook.send(body)
Botを導入しているサーバーに上で記したコマンドを送ることで予定を挿入します。
予定の取得
同様に予定の取得コマンドも実装します。
予定取得時にタイムゾーンとしてJSTを生成し指定しているところがポイントです。
# 一週間分の予定を返す
def get(self):
try:
result = []
calendar = CalendarApi()
result = calendar.get()
# メッセージ送信処理
webhook = Webhook()
webhookData = WebhookData()
webhookContent = WebhookContent()
if result == []:
message = self.config['webhook'].get('message')].get('event_none_message')
body = webhookContent.createMessage(message)
else:
message = self.config['webhook'].get('message').get('event_message')
body = webhookContent.createMessage(message)
for event in result:
webhookData.summary = event['summary']
webhookData.description = event.get('description', '')
webhookData.eventId = event['id']
# 終日予定と時間予定を区別しフォーマットを設定
start = event['start'].get('dateTime','').split('T')
if start != ['']:
webhookData.date = start[0].replace('-','/')
webhookData.time = start[1].replace(':00+09:00','')
embeds = webhookContent.createEmbeds(webhookData)
else:
webhookData.date = event["start"].get("date").replace('-','/')
webhookData.endDate = event["end"].get("date").replace('-','/')
embeds = webhookContent.createLongEventEmbeds(webhookData)
body["embeds"].append(embeds)
webhook.send(body)
except Exception as e:
print(e)
webhook = Webhook()
webhookData = WebhookData()
webhookContent = WebhookContent()
message = self.config['webhook'].get('message').get('event_error_message')
body = webhookContent.createMessage(message)
webhook.send(body)
コマンドを送ったときのプレビューです。
予定の削除
予定の削除コマンドも実装します。
予定のメッセージembedsのfooterに記載しているイベントIDを受け取り、リクエストを送ります。
def delete(self, event):
try:
calendar = CalendarApi()
calendar.delete(event)
webhook = Webhook()
webhookContent = WebhookContent()
message = self.config['webhook'].get('message').get('delete_message')
body = webhookContent.createMessage(message)
webhook.send(body)
except Exception as e:
print(e)
webhook = Webhook()
webhookContent = WebhookContent()
message = self.config['webhook'].get('message').get('delete_error_message')
body = webhookContent.createMessage(message)
webhook.send(body)
通知
個人で利用する時はGoogle Calendarに通知を送って貰えば良いとも思ったのですが、小規模のコミュニティで通知をしたい時は便利なのでDiscord上に予定の通知メッセージを送ります。
スケジューラにはscheduleライブラリを使用し、毎週月曜日と毎日の指定時刻に予定を取得して通知します。
import schedule
import time
from logic.schedulerLogic import SchedulerLogic
def EventSchedule():
logic = SchedulerLogic()
schedule.every().monday.at("08:45").do(logic.getWeeklyEvents)
schedule.every().day.at("09:00").do(logic.getDailyEvents)
EventSchedule()
while True:
schedule.run_pending()
time.sleep(1)
import json
import datetime
from google.auth import load_credentials_from_file
from googleapiclient.discovery import build
class CalendarApi():
def __init__(self):
with open('config.json', 'r') as f:
config = json.load(f)
self.__calendarId = config["calendar"].get('id')
SCOPES = [config["calendar"].get('calendar')]
gapi_creds = load_credentials_from_file('credentials.json', SCOPES)[0]
self.__service = build('calendar', 'v3', credentials=gapi_creds)
def get(self, day=0, routine=False):
now = datetime.datetime.now(datetime.timezone.utc)
period = now + datetime.timedelta(days=day)
if routine:
timeMax = period + datetime.timedelta(minutes=59)
timeMax = datetime.datetime(timeMax.year, timeMax.month, timeMax.day, timeMax.hour, timeMax.minutes, tzinfo=datetime.timezone.utc)
else:
timeMax = datetime.datetime(period.year, period.month, period.day, 23, 59, tzinfo=datetime.timezone.utc)
events_result = self.service.events().list(
calendarId=self.calendarId,
timeMin=now.isoformat(),
timeMax=timeMax.isoformat(),
maxResults=5,
singleEvents=True,
orderBy='startTime'
).execute()
events = events_result.get('items', [])
return events
引数days
で予定を取得したい日数を指定します。
import json
import datetime
from api.calendarApi import CalendarApi
from data.calendarData import CalendarData
from api.webhook import Webhook
from data.webhookData import WebhookData
from data.webhookData import WebhookContent
class SchedulerLogic():
def __init__(self):
with open('config.json', 'r') as f:
config = json.load(f)
self.__config = config
@property
def config(self):
return self.__config
def getWeeklyEvents(self):
try:
result = []
calendar = CalendarApi()
result = calendar.getEventsByDays(days=7)
webhook = Webhook()
webhookData = WebhookData()
webhookContent = WebhookContent()
if result != []:
message = self.config['webhook'].get('message').get('weekly_event_message')
body = webhookContent.create(message)
for event in result:
webhookData.summary = event['summary']
webhookData.description = event.get('description', '')
webhookData.eventId = event['id']
start = event['start'].get('dateTime','').split('T')
if start != ['']:
webhookData.date = start[0].replace('-','/')
webhookData.time = start[1].replace(':00+09:00','')
embeds = webhookContent.createEmbeds(webhookData)
else:
webhookData.date = event["start"].get("date").replace('-','/')
webhookData.endDate = event["end"].get("date").replace('-','/')
embeds = webhookContent.createLongEventEmbeds(webhookData)
body["embeds"].append(embeds)
webhook.send(body)
else:
message = self.config['webhook'].get('message').get('event_none_message')
body = webhookContent.create(message)
webhook.send(body)
except Exception as e:
print(e)
def getDailyEvents(self):
try:
result = []
calendar = CalendarApi()
result = calendar.getEventsByDays(days=0)
webhook = Webhook()
webhookData = WebhookData()
webhookContent = WebhookContent()
if result != []:
message = self.config['webhook'].get('message').get('daily_event_message')
body = webhookContent.create(message)
eventFlag = False
for event in result:
webhookData.summary = event['summary']
webhookData.description = event.get('description', '')
webhookData.eventId = event['id']
start = event['start'].get('dateTime','').split('T')
if start != ['']:
eventFlag = True
webhookData.date = start[0].replace('-','/')
webhookData.time = start[1].replace(':00+09:00','')
embeds = webhookContent.createEmbeds(webhookData)
body["embeds"].append(embeds)
if(eventFlag):
webhook.send(body)
except Exception as e:
print(e)
処理の多くはBotの予定確認と変わりません。
日毎の確認の場合、終日開催のイベント通知を省いています。
以上です。
おわりに
以前の記事ではBotからメッセージを送信していましたが、今回はWebhookを使用する方法に変更しました。Webhookの方が使用感がよく、見栄えも整えやすいので今後は優先的に使用したいと思います。
記事を書くことにも慣れていなく、コードのベタ貼りが多くなったと反省しています。簡潔に作ったものを伝えられるよう努力します。
Discussion