Edited at

Cloud Pub/Subを使ってgmailの通知を受け取るlinebot

この記事はCAMPHOR- advent calender 2018の23日目の記事です


動機

私の所属している東洋医学研究会(通称:東医研)には、専用のgmailアカウントがありますが、ほとんどの人がメールを見ていないので、今までは受信や送信メールがあれば、人力でグループLINEに投稿していました。これをbotでなんとかしたかった。


pub/subの設定

まずは、pub/subの設定をします。この設定をすることで、gmailに何らかの変更があった場合には、pub/subを通じてpostリクエストが届きます。

Gmail APIとPub/Subでリアルタイムメール受信 on ruby1の記事がとても参考になりました。この通りやったらうまくいきます

1. トピックを設定する

2. サブスクリプションを設定する

私の場合は、サブスクリプションのurlはhttps://プロジェクト名.appspot.com/pubsub/pushで受け取るようにしました


gmail apiの認証

公式のPython Quickstartを参考に進めます。上記のページに出てくるquickstart.pyを少し変えて、

store = file.Storage('token.json')

creds = store.get()
if not creds or creds.invalid:
flow = client.flow_from_clientsecrets('credentials.json', SCOPES)
creds = tools.run_flow(flow, store)
service = build('gmail', 'v1', http=creds.authorize(Http()))

#追加したコード
request = {
'labelIds': ["INBOX","SENT"],
'topicName': 'projects/******/topics/mail_for_toiken'
}
service.users().watch(userId='me', body=request).execute()

を実行すると、これでwatchメソッドが叩けたことになります。その際、crediential.jsonが作成されて、トークンがその中に保存されます。そのため、2回目以降はブラウザで認証する必要がありません


Pub/Subから送られてくるデータを加工する

ここまでの過程がうまくいくと、GAEで、pub/subから送られるjsonを受け取れます。公式のPush Notificationのページを参考にします


受け取るjsonの例

{

"message": {
"data": "eyJlbWFpbEFkZHJlc3MiOiAidXNlckBleGFtcGxlLmNvbSIsICJoaXN0b3J5SWQiOiAiMTIzNDU2Nzg5MCJ9",
"message_id": "1234567890"
},
"subscription": "projects/myproject/subscriptions/mysubscription"
}

"data"をbase64でデコードすると、以下のようになります

{"emailAddress": "user@example.com", "historyId": "1234567890"}

gmail apiで重要なのは、historyIdだけです。


GoogleAppEngineでgmail apiを使えるようにする。

実は、「gmail api の認証」の項で使ったコードはそのまま使用できません。GAEにアップロードしたファイルはread_only属性がつくので、認証の際にIOError(errno.EROFS, 'Read-only file system', filename)が返って来てしまいます。

oauth2client.client.GoogleCredentialsを使うとうまくいきます2


example

secret = json.load(open("client_secret_for_touiken.json"))

creds = GoogleCredentials(secret["access_token"], secret["client_id"], secret["client_secret"],
secret["refresh_token"], secret["token_expiry"], secret["token_uri"],
secret["user_agent"], secret["revoke_uri"])
service = build('gmail', 'v1', http=creds.authorize(Http()), cache_discovery=False)


historyIdからmessageを取得する

参考記事1にも書かれていますが、start_history_idをhistoryIdと同じにしてはいけません。私は、適当なclassを作って、前回のhistoryIdを保存するようにしました


historyIdからメッセージリストを取得

class PubSubReceiver:

def __init__(self, start_history_id=None, history_id=None):
self.start_history_id = start_history_id
self.historyId = history_id

def data(self, data):
history_id = int(json.loads(base64.b64decode(data))["historyId"])
if self.start_history_id is None:
self.start_history_id = history_id - 60
elif self.historyId < history_id:
self.start_history_id = self.historyId
else:
pass
self.historyId = history_id
return PubSubReceiver(start_history_id=self.start_history_id,
history_id=history_id)

def to_array(self):
logging.info("to_array")
try:
service = Service.create_service()
logging.debug("service_create")
history = (service.users().history().list(userId="me",
startHistoryId=self.start_history_id)
.execute())
logging.debug("get_history")
logging.debug(str(self.historyId) + ":" + str(history["historyId"]) + ":" + str(
self.historyId == int(history["historyId"])) + str("nextPageToken" in history))
history_list = history['history'] if 'history' in history else []

if self.historyId == int(history["historyId"]):
return history_list

while 'nextPageToken' in history:
print(history, history_list[0], history["historyId"])
page_token = history['nextPageToken']
history = (service.users().history().list(userId="me",
startHistoryId=self.start_history_id,
pageToken=page_token).execute())
history_list = history['history'] if 'history' in history else []
if self.historyId == int(history["historyId"]):
return history_list

except errors.HttpError as error:
logging.info('An error occurred: %s' % error)


history_idを解析すれば、messageのidを入手できます。idから本文や宛先などを取得するのは、マイナビニュースのページ34が詳しいです。


linebotを作る

developer trialならpush_messageが送れます。line-bot-sdk-pythonのREADMEを参考にしましょう


完成したもの

こんな感じ

スクリーンショット (39).png