明日書く

インターネットやめろ

課題の神を創造した

なんでもう年末なんですかね、かげろんです。
10月末まで部長だったんですけど座を奪われました。
任期満了して今では老害です。
今回は部長になってもらったみやもっちゃんが12月にいつも部内でやっている発表会をやるとのことだったので、connpassを建てるだけ建てて高専という今までより大きな枠組みでやろう、そして運営してもらおうと推し進めました。
ここで発表した内容を書いておこうかと思います。

twitter.com
sanct.connpass.com

劣化コピー機完備

皆さんは課題を期日内に終わらせるように自力でやっていますか?
弊クラスでは〇〇塾という圧力団体が存在しており、課題やテストのログがアップロードされるため、朝学校に来てから課題を写す”劣化コピー機”がたくさん配備されています。
私たちとしてはとても楽ですが、教員からすれば面白いことではないでしょう。僕はクソ真面目な部分もあるのでまず課題を忘れたという言い訳ができないようにしようかと思います。

Bot動かせばええやん!(名推理)

今回作ったのはDiscordのBotです。あの塾のメンバーはDiscordグループに所属しているので、Botをぶち込めばいつかは見るかと思います。

環境構築

Pythonを全く触ったことがないので学習を兼ねてPythonの環境を構築したいと思います。大分前にAnacondaがいいと言われましたが私は反抗してWSLを使っていきます。
以下のサイトを参考にして導入しました。
qiita.com


最初VSCodeで開発しようとしていたので上手くできず、さらにpipのディレクトリが変更されており爆破が決定しました。Install Battle敗北です。

Discord Bot作成

Discord Botを作成するためには、Discord Developer PortalBotのアカウント作成とpip等でdiscord.pyを導入する必要があります。
以下のサイトを参考にしました。
qiita.com

カレンダーから予定を取得

今回はGoogleカレンダーから予定を取得します。
GoogleCalenderAPIを使えば予定の取得・追加ができるらしいのでぶち込みました。
qiita.com

プログラム完成

導入してサンプルを少し書き換えるとこうなりました。

from __future__ import print_function
import datetime
import pickle
import os.path
import discord
import asyncio
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from discord.ext import tasks

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

TOKEN = Botのトークン(文字列)
channelID = チャンネルID(数値)
client = discord.Client()

def GetEvents():
    """Shows basic usage of the Google Calendar API.
    Prints the start and name of the next 10 events on the user's calendar.
    """
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server()
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)

    service = build('calendar', 'v3', credentials=creds)

    # Call the Calendar API
    now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
    events_result = service.events().list(calendarId=カレンダーID(文字列),
                                        timeMin=now, maxResults=10, singleEvents=True,
                                        orderBy='startTime').execute()
    events = events_result.get('items', [])
    
    return events

# 起動時に動作する処理
@client.event
async def on_ready():
    # 起動したらループを開始する
    asyncio.ensure_future(time_check())

# 60秒に一回ループ
async def time_check():
    while True:
        # 予定の件数
        plans = 0
        # 現在の時刻
        now = (datetime.datetime.now() + datetime.timedelta(hours=9)).strftime('%H:%M')
        # 明日の日付
        tomorrow = (datetime.date.today() + datetime.timedelta(days=1)).strftime("%Y-%m-%d")

        if now == '21:00':
            channel = client.get_channel(channelID)

            date = str(tomorrow).split('-')
            messgae = date[0] + '年' + date[1] + '月' + date[2] + '日の課題/テストは'
            await channel.send(messgae)

            events = GetEvents()
            if not events:
                await channel.send('ありません.')
            else:
                for event in events:
                    start = event['start'].get('dateTime', event['start'].get('date'))
                    startDate = start.split('T')
                    if startDate[0] == tomorrow:
                        plans += 1
                        await channel.send(str(event['summary']))

                if plans == 0:
                    await channel.send('ありません.')
                else:
                    await channel.send('の' + str(plans) + '件です.')

        await asyncio.sleep(60)

# メッセージ受信時に動作する処理
@client.event
async def on_message(message):
    # メッセージ送信者がBotだった場合は無視する
    if message.author.bot:
        return
    # 「/plans」か「/予定」と発言したらGoogleカレンダーの予定が返る処理
    if message.content == '/plans' or message.content == '/予定':
        # 予定の件数
        plans = 0
        events = GetEvents()
        channel = client.get_channel(channelID)
        await channel.send('今後10件の予定は')
        if not events:
            await channel.send('ありません.')
        else:
            for event in events:
                start = event['start'].get('dateTime', event['start'].get('date'))
                
                # 得られた予定を文字列にして分解して編集
                sentences=str(start).split('T')
                words = sentences[0].split('-')
                eventMessage = words[0] + '年' + words[1] + '月' + words[2] + '日 に '

                # 時間指定もあれば追加
                if len(sentences) == 2:
                    sentences = sentences[1].split('+')
                    words.extend(sentences[0].split(':'))
                    eventMessage = eventMessage + words[3] + '時' + words[4] + '分 より '

                plans += 1
                eventMessage = eventMessage + str(event['summary'])
                await channel.send(eventMessage)

            if plans == 0:
                await channel.send('ありません.')
            else:
                await channel.send('の' + str(plans) + '件です.')


# Botの起動とDiscordサーバーへの接続
client.run(TOKEN)

DiscordのチャンネルIDは ユーザー設定>テーマ から開発者モードをオンにすると取得できます。
カレンダーIDは Googleカレンダー>マイカレンダーの設定>カレンダーの統合 からコピーしてください。

f:id:shade4827:20191217111350p:plain
12月16日に動作させた様子

大体望んだとおりの動作をしました。
では、これを常時起動していつでも予定を取得できるようにしよう、と言っても私は寮に入っているので朝の巡回時に電源を切られます。
そこでHerokuというPaaSを使うことにしました。

Herokuを使って常時稼働

Herokuを使うと常時実行できるらしいです。
signup.heroku.com
GitHubソースコード等々を上げ、デプロイします。
このとき、使用するライブラリを記載するrequirements.txtと言語のバージョンを記載するruntime.txt、実行時のコマンドを記載するProcfileも必要です。
今回はBotトークンやDiscordのチャンネルIDをソースコードにそのまま書いてあるためプライベートリポジトリとなっています。
Automatic deployを設定すると、コミットされても自動的にデプロイされます。世の中便利ですね。
f:id:shade4827:20191219154820p:plain
そして、Dynosをオンにすると動作が開始します。
f:id:shade4827:20191219155021p:plain
これらの手順についてはDiscord Bot作成で紹介した記事で詳しく説明されています。

完成

これで課題を忘れることがなくなったかと思います。
あとはGoogleカレンダーに予定を追加し忘れないかですね。
この機能についてもそのうち追加できたらなと思います。

実際に弊グループに導入したのですが、効果のほどはよく分かりません。
ただ、あまり情報系ではない人のほうが多いため「すごい」とか「bot有能」とか、褒められた気がします。
何かレスポンスがあるのがとてもいいですよね。
意見とか感想を貰えると開発意欲が湧くし、何よりやってよかったと思えます。
ぜひ皆さんも他人のためになる商品を作って金をぶんどってください。

おわりに

今回作ったBotは未完成です。
何か面白い機能の案があれば以下のフォームまでお願いします。私のタスクを増やしてください。
あとイスラム教の皆様におかれましては本当に申し訳ありませんでした。そのうち名前変えます。
docs.google.com

それでは、次回は怪文書を書けるよう精進してきます。