Googleリマインダーを登録するAPIを構築する
What is this?
「Slack slashコマンドからGoogleリマインダーを登録できるようにしたい」の前編です。後編はこちらから。
Googleリマインダーを登録するAPIを構築します。え、そのAPIはGoogleが提供してないの?と思いますよね。どうやら公式APIは無いようなのです。
しかし、目標達成のためにはslashコマンドを受け取るためのAPIが必要なので、構築してみました。
※Slack slashコマンドを新規作成する方法は情報が豊富だと思いますので、本記事では割愛します
前提知識
本記事を読むために下記の知識を想定しています。
- 基礎的なAPIの理解
- python3 / pip
- Docker
Googleリマインダーとは
Googleカレンダーに付随するリマインダー機能です。他のGoogleサービス(Keepやタスク)でも共有されているようです。
2022/03/01現在、作成ボタンからは選択肢がないのですが、
作成ウィンドウから「リマインダー」を選択できます。
タスクとの違いは3つ
- 「毎日」や「毎週」のような繰り返し設定が可能
- 完了しないと、翌日に持ち越される
- タイトル以外の細かい説明は追加できない
公式APIはないが、非公式ラッパーが公開されている
Googleは多くのサービスのAPIを公開していると思っていたのですが、散々探した結果、Googleリマインダーに関しては公開されていないようです。2019年からあるサービスらしいのですが、そういうこともあるんですね。
そこで、google-reminder-api-wrapper
という非公式Pythonパッケージがあったので、今回はこちらを使用しました。
※2022/03/01時点での動作確認(新規リマインダー作成のみ)はできていますが、非公式ラッパーであるため今後の動作保証はできません
驚くべくことに、GoogleカレンダーのUIが飛ばしている情報をもとに開発したそうです。なお、今回無事に仕様できたので、最後にアップデートされている2019年からAPI仕様に変化はない模様。
pipでインストールし、クッキー情報を環境変数に設定することで利用可能です。
google-reminder-api-wrapperに必要なクッキー情報の探し方
クッキー情報はChromeデベロッパーツールを開いて、Networkタブを選択肢、クエストヘッダー情報内から必要な変数名を探してください。Networkタブを開いた状態で、UI上からリマインダー登録をした上で、reminder
で検索すると探しやすいです。
フォルダ構成
|-Dockerfile
|-requirements.txt
└src
└-main.py
Dockerファイルによる環境設定
後編で紹介するGoogle Cloud Runを使用するためにDockerで環境構築します。GCRのデフォルトポートが8080なので空けておきます。上記Googleカレンダーのクッキー情報やSlackアプリのキーはご自身のものを使用してください。
※本当はDockerファイルにベタ打ちしないほうがセキュリティ上よいのですが、今回はプライベートな利用であり、悪用されてもリマインダーがハックされるだけなので簡便さを重視しました。
FROM python:3
USER root
EXPOSE 8080:8080
# Envoirment variable for google reminder wrapper
ENV SID='xxxxxxxxxxxxxxxxxxx'
ENV HSID='xxxxxxxxxxxxxxxxxxx'
ENV SSID='xxxxxxxxxxxxxxxxxxx'
ENV APISID='xxxxxxxxxxxxxxxxxxx'
ENV SAPISID='xxxxxxxxxxxxxxxxxxx'
ENV authorization='xxxxxxxxxxxxxxxxxxx'
ENV key='xxxxxxxxxxxxxxxxxxx'
# Envoirment variable for Slack app
ENV SLACK_SIGNING_SECRET="xxxxxxxxxxxxxxxxxxx"
ENV SLACK_OAUTH_ACCESS_TOKEN="xxxxxxxxxxxxxxxxxxx"
RUN apt-get update
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
ADD requirements.txt /root/
RUN pip install -r /root/requirements.txt
ADD "/src/." /root/src/
WORKDIR "/root/src"
CMD ["python", "main.py"]
FlaskによるAPI実装
slashコマンドを実装する上で、既存情報が多いflaskを使用しました。
Flask
google-reminder-api-wrapper
認証部分
slashコマンドからの認証部分は下記記事を参考にしています。
Slack公式にも同様の例があるようです。
import os
import hashlib
import hmac
from flask import Flask, request
SLACK_SIGNING_SECRET = os.environ['SLACK_SIGNING_SECRET']
SLACK_OAUTH_ACCESS_TOKEN = os.environ['SLACK_OAUTH_ACCESS_TOKEN']
def verify(request):
try:
slack_secret = bytes(SLACK_SIGNING_SECRET, 'utf-8')
timestamp = request.headers['X-Slack-Request-Timestamp']
request_data = request.get_data().decode('utf-8')
base_string = f"v0:{timestamp}:{request_data}".encode('utf-8')
my_signature = 'v0=' + \
hmac.new(slack_secret, base_string, hashlib.sha256).hexdigest()
result = hmac.compare_digest(
my_signature, request.headers['X-Slack-Signature'])
except:
return False
return result
Googleリマインダーの呼び出し
google-reminder-api-wrapper
の利用は上記クッキー情報以外は特にトラップもなく、スムーズに実行できました。
日時の指定フォーマットはyyyy-MM-dd hh:mm
です。日付テキストの抽出がどうしようもないレベルの低さなのは許してください…まずはなるべく早く動かすこと重視だったので、そのうちちゃんとします。
from google_reminder_api_wrapper import ReminderApi
datetime_index = 16
def set_reminder(text: str, dt: str):
try:
api = ReminderApi()
new_reminder = api.create(text, dt)
except:
return False
return True
def define_request_texts(text):
title = text[:-datetime_index-1]
dt = text[-datetime_index:]
return title, dt
チュートリアル通りのflask実装
返り値がひどいのもゆるして、、、
app = Flask(__name__)
@app.route('/goremind', methods=['POST'])
def main():
if verify(request):
title, dt = define_request_texts(request.form['text'])
if set_reminder(title, dt):
return "done"
else:
return ('Google reminder query was not allowed', 405)
else:
return ('Request failed verification.', 401)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=os.environ.get('PORT', '8080'))
ソースコード全体
Dockerファイル以外のコードはGithubへ公開しています。
API部分はこれで完了!
終わってみるとすごくスッキリしていますが、ここまでくるのにものすごく紆余曲折ありました。もし、その悪戦苦闘の形跡に興味がある方がいればこちらへ。
後編では、このAPIをGoogle Cloud Runへデプロイします!
Discussion
追記
開発完了から約2月経過。google reminder wrapperが動かなくなる。デバッグしていくと、クッキー認証がうまくできていない。どうやら、一定期間経つとクッキー認証情報が変わってしまう模様。新しく値を取得して、変更すると修正できた。
デバッグ環境としてngrokを導入。下記を参考にした。