Python
GoogleCalendar
DirectoryAPI
ハングアウト
GSuite

G Suite 組織内ユーザーの予定を名前から取得してみる

はじめに

ハングアウトBotを作ってみるからの続き。

ハングアウトBotのプラグインとしてG Suite組織内ユーザーの名前を問い合わせると、
その問い合わせた人の予定を返す機能を作成したのその時のメモ。
主にGoogleのAdminSDKのDirectoryAPIとCalendarAPIの使い方。

Googleカレンダーの予定を取得するには

Googleカレンダーの予定を取得するには、Google Calendar APIのevents.listを使用します。
Google APIs Explorer Calendar API v3 calendar.events.list

Googleカレンダーに登録されている予定(event)を取得したい場合は、calendarIdが必須となります。
calendarIdは通常Googleアカウントのメールアドレスになっています。
※カレンダーを追加していたりする場合はその限りではありませんが、calendarId=メールアドレスで進みます。

calendarIdを取得するには

Googleアカウントのメールアドレスなので、メールアドレスを知っている場合はいいのですが、
G Suiteを利用している場合、全員のメールアドレスなんて知らないですw
そんな人にも朗報。calendarIdだったりメールアドレスを調べるAPIも用意されています。

Google Calendar API calendrList.list

calendarIdはGoogle Calendar APIのcalendarList.listを使用することで取得出来ます。
Google APIs Explorer Calendar API v3 calendar.calendarList.list

このAPIの特徴としては

  • APIのパラメータにクエリが無いため、参照出来るカレンダーのリストが全て抽出される。
  • G Suiteを利用している場合、組織内ユーザー全て抽出される。
  • カレンダー名が名前かと思いきや、ユーザー自身が自由に設定出来るため必ずしも名前じゃない。

となっています。
使えない…。

Google People API people.people.connections.list

メールアドレスでいいってことは自分自身の連絡先を見ればいいのでは?
Gmailの連絡先からは組織内ユーザー見えているし。

自分自身の連絡先はGoogle People APIのpeople.people.connections.listを使用することで取得出来ます。
Google APIs Explorer Google People API v1 people.people.connections.list

このAPIの特徴としては

  • APIのパラメータにクエリが無いため、登録している連絡先が全て抽出される。
  • ユーザー自身が登録した連絡先のみが抽出される。
  • 組織内ユーザーの連絡先なんて抽出されない

となっています。
組織内ユーザーの連絡先は抽出されないようです

使えない…。

Admin SDK Directory API

よくよく調べてみると、G Suiteの組織内ユーザーはAdmin SDKのDirectory APIのdirectory.users.listを使用することで取得出来るみたいです。
Google APIs Explorer Admin Directory API directory_v1 directory.users.list

このAPIの特徴としては

  • APIのパラメータにクエリがあるので、条件で絞り込んで抽出出来る。
  • 組織内のユーザーの連絡先がもちろん抽出出来る。
  • 組織内ユーザーのみなので自分自身の連絡先はPeople APIを使用する必要がある。

となっています。
求めていたものはこれでした。

ひとまずAPI Explorerで実行してみます。
image.png

domainテキストボックスに自組織のドメインを入力して実行してみます。
想定通りだと組織内ユーザーの情報が取得出来るはずです。

APIのレスポンス
{
 "error": {
  "errors": [
   {
    "domain": "global",
    "reason": "forbidden",
    "message": "Not Authorized to access this resource/api"
   }
  ],
  "code": 403,
  "message": "Not Authorized to access this resource/api"
 }
}

このリソース/APIにアクセスする権限が無いとエラーとなります。

利用しようとしているAdmin SDKはG Suiteを管理する機能も有しているため、
管理者権限が無いユーザーでは検索もままならないのでしょうか。

image.png
何か方法はないものかとパラメータを見てるとviewTypeというパラメータを発見。
説明を読んでみるとADMIN_VIEWDOMAIN_PUBLICどちらで検索する?的なことを書いていたので、
viewTypeにDOMAIN_PUBLICを指定して再度実行すると、見事組織内のユーザーの情報が検索されました。

APIのレスポンス
{
 "kind": "admin#directory#users",
 "etag": "{etagの値}",
 "users": [
  {
   "kind": "admin#directory#user",
   "id": "{idの値}",
   "etag": "{etagの値}",
   "primaryEmail": "{メールアドレス}",
   "name": {
    "givenName": "{名前}",
    "familyName": "{名字}",
    "fullName": "{フルネーム}"
   },
   "emails": [
    {
     "address": "{メールアドレス}",
     "primary": true
    }
   ],
   "thumbnailPhotoUrl": "{プロフィール写真のURL}",
   "thumbnailPhotoEtag": "{プロフィール写真のetagの値}"
  }
 ]
}

primaryEmailにメールアドレスが設定されているので、
その値をCalendar APIのevents.listのcalendarIdに設定し、検索すると見事に予定を取得出来ます。

まとめ

  • Googleカレンダーの予定はGoogle Calendar APIのevents.listを使用して取得出来る。
  • 予定を検索するためにはcalendarId(メールアドレス)が必要となる。
  • calendarId(メールアドレス)を取得するAPIは複数あるが、組織内ユーザーのcalendarIdを取得するにはAdmin SDK Directory API以外は使えない。
  • G Suiteの管理者で無い場合は、viewTypeの設定が必須。

ハングアウトBotで使えるプラグインのソース

/bot calendar 名前
/bot 何してる 名前
でユーザーの予定を返してくれるBotのプラグインのソースです。
このソースそのままで動くはずです。
GoogleのOAuth2認証用にクライアントIDが必要ですが、色々と記事があるのでその部分は割愛。
私はGoogle Developers Calendar API Python Quickstartを参考にしました。

動作イメージは↓のような感じ。
image.png

  • 予定が存在したら"終日の予定"、"直近1分の予定"を返答します。
  • 非公開の予定については、予定があることのみを返答します。
  • そもそも存在しない人の場合は、指定した名前の人がいないことを返答します。
  • 予定が無い人の場合は、予定が無いことを返答します。
calendar.py
from __future__ import print_function
import plugins
import httplib2
import os
import json
import dateutil.parser
import datetime

from apiclient import discovery
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage

try:
    import argparse
    flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
except ImportError:
    flags = None

SCOPES = ["https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/admin.directory.user.readonly"]
CLIENT_SECRET_FILE = "client_secret.json"
APPLICATION_NAME = "HangoutsBot"

# 起動時に実行
def _initialise(bot):
    plugins.register_user_command(["calendar", "何してる"])

# 「calendar」が送られてきた時の処理
def calendar(bot, event, message):
    bot_text = calendarBot(message)
    yield from bot.coro_send_message(event.conv, bot_text)
    return

def 何してる(bot, event, message):
    bot_text = calendarBot(message)
    yield from bot.coro_send_message(event.conv, bot_text)
    return

def calendarBot(message):

    credentials = get_credentials()
    http = credentials.authorize(httplib2.Http())
    calendarAPI = discovery.build("calendar", "v3", http=http)
    directoryAPI = discovery.build("admin", "directory_v1", http=http)

    usersResult = directoryAPI.users().list(domain="{ドメイン}", maxResults=100, viewType="domain_public", query="name:" + message).execute()
    users = usersResult.get("users", [])

    if not users:
        return "お探しの方はいないようです"
    else:
        if len(users) > 1 and len(users) <= 10:
            user_text = "該当する方が複数いるようです<br>"
            for var in range(0, len(users)):
                user_text = user_text + users[var]["name"]["fullName"] + "<br>"
            return user_text
        elif len(users) > 10:
            return "該当する方が10人より多いようです。入力する名前を変えてみてください。"

    # 予定の取得範囲を現在時刻から1分後までに設定
    now = datetime.datetime.utcnow()
    after1h = now + datetime.timedelta(minutes = 1)
    nowformat = now.isoformat() + "Z"
    after1hformat = after1h.isoformat() + "Z"

    eventsResult = calendarAPI.events().list(
        calendarId=users[0]["primaryEmail"], timeMin=nowformat, timeMax=after1hformat, maxResults=10, singleEvents=True,
        orderBy="startTime").execute()
    events = eventsResult.get("items", [])

    event_Cnt = 0
    bot_text = message + "さんは、今、<br>"
    event1_text = ""
    event2_text = ""

    if not events:
        bot_text = bot_text + "特に予定はありません"
    else:
        for event in events:
            location_text = ""
            time_text = ""
            summary_text = ""
            canceledEvent = 0

            if not event["status"] == "cancelled":
                if event.get("attendees"):
                    for attend in event["attendees"]:
                        if attend["email"] == users[0]["primaryEmail"] and attend["responseStatus"] == "declined":
                            canceledEvent = 1
                            break

                if canceledEvent == 0:
                    if event["start"].get("dateTime"):
                        event_Cnt = event_Cnt + 1

                        # 場所が登録されている場合
                        if event.get("location") and len(event["location"]) > 0:
                            location_text = event.get("location") + " で "

                        # 終了時間
                        enddate = dateutil.parser.parse(event["end"]["dateTime"])
                        time_text = time_text + str(enddate.hour) + "時" + str(enddate.minute) + "分まで "

                        # 予定
                        if event.get("summary") and len(event["summary"]) > 0:
                            summary_text = event.get("summary") + " の予定です<br>"
                        else:
                            summary_text = "予定があるようです<br>"

                        event1_text = event1_text + location_text + time_text + summary_text
                    else:
                        event_Cnt = event_Cnt + 1

                        # 場所が登録されている場合
                        if event.get("location") and len(event["location"]) > 0:
                           location_text = event.get("location") + " で "

                        time_text = "終日 "

                        # 予定
                        if event.get("summary") and len(event["summary"]) > 0:
                            summary_text = event.get("summary") + " の予定です<br>"
                        else:
                            summary_text = "予定があるようです<br>"

                        event2_text = event2_text + location_text + time_text + summary_text

        if event_Cnt == 0:
            bot_text = bot_text + "特に予定はありません"
        else:
            bot_text = bot_text + event2_text + event1_text

    return bot_text

def get_credentials():
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credentials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir, 'calendar-python.json')

    store = Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        if flags:
            credentials = tools.run_flow(flow, store, flags)
        else: # Needed only for compatibility with Python 2.6
            credentials = tools.run(flow, store)
    return credentials