Метка: CRM

  • Google Contacts API и авторизация OAuth2

    Когда в адресной книге Google набирается несколько тысяч клиентов, становится тяжеловато ворочать этим объемным списком. Импорт/экспорт в странички Excel становится неудобным; кроме того появляются CRM-подобные приложения, в которых хотелось бы интегрировать самые разные базы данных, в том числе и контакты Google. Значит, пришло время доступиться к нашим контактам через консольные приложения, используя Google Contacts API.

    Получение списка контактов

    Используя API, мы можем не только получить список контактов, но также и видоизменять и дополнять его. В простейшем случае, первые 150 записей из адресной книги можно вытащить так, вбив это в адресную строку браузера:

    https://www.google.com/m8/feeds/contacts/default/full?access_token=ACCESS_TOKEN
    &alt=json&max-results=150&start-index=1

    Можно догадаться, что данные мы получим в формате json, что понятно, поскольку каждой записи в адресной книге соответствует куча полей, также понятно сколько записей будет выведено. И поскольку здесь нет ни нашего логина, ни пароля, можно предположить, что ACCESS_TOKEN — это та самая строка, которая обеспечивает авторизацию. Чтобы понять, как эта авторизация работает, мне в свое время пришлось выполнить несколько танцев с бубном. В этой статье делюсь результатами своих изысканий.

    Регистрируемся в Google

    Предполагаю, что у вас уже есть Google-аккаунт, или электронная почта. Логинимся на страничке Гугла и идем по адресу https://console.developers.google.com/cloud-resource-manager , где создаем новый проект. Даем ему произвольное название, например Beerware. Дальше переходим в другое место https://console.developers.google.com/apis/credentials. Жмем кнопочку «Создать учетные данные» и выбираем «Идентификатор клиента OAuth», и дальше отмечаем «Другие типы». Дадим название идентификатору — скажем Beerware Script.

    Не спрашивайте меня, что означает разнообразие в выборе различных опций. Погружаться в это можно долго, важно лишь что наш вариант — рабочий.

    Это все, что нужно было сделать в девелоперских панелях Гугла. Если вы нажмете на редактирование идентификатора, то увидите ID клиента и секрет клиента. Запомним эти названия, они понадобятся нам дальше. Чтобы не путаться с обозначениями при подстановке в другие строки, обозначим их CLIENT_ID и CLIENT_SECRET.

    Authorization Flow: как проходит авторизация OAuth2

    Конечной целью авторизации является получение ACCESS_TOKEN, который необходимо использовать каждый раз когда мы делаем запрос к адресной книге. Теперь нам нужно внимательно пройтись по всем этапам. Самая главная вещь, которая толком не описана в документации Google — какие этапы выполняются только один раз, а какие нужно выполнять периодически. Я буду это подчеркивать.

    Каждый этап — это обращение к серверу Google по HTTPS и получение ответа. Детали запроса я распишу позже, сейчас главное — уяснить общую схему.

    Получение кода авторизации

    Код авторизации сразу ставит в тупик. Зачем он нужен, если уже есть CLIENT_SECRET?  Разница в том, что код авторизации используется только один раз — для получения токенов. Замечу сразу, что если вы попытаетесь получить по одному и тому же коду авторизации токены еще раз, то получите ошибку. Таким образом, код авторизации действителен пока мы еще не получили токены. После их получения он сразу «протухает». Но об этом — дальше.

    CLIENT_SECRET, как и CLIENT_ID — постоянные строки, скрытые в нашем приложении вдали от посторонних глаз.

    Итак, посылаем на сервер CLIENT_ID, CLIENT_SECRET и получаем код авторизации. Назовем его AUTH_CODE.

    Получение токенов

    И снова заголовок наводит на размышления. Почему токены во множественном числе, когда вначале говорилось только об ACCESS_TOKEN? На самом деле, помимо этого токена, который используется при работе с адресной книгой, нам дают еще REFRESH_TOKEN. Его роль будет ясна ниже.

    Посылаем на сервер CLIENT_ID, CLIENT_SECRET, AUTH_CODE и получаем парочку ACCESS_TOKEN, REFRESH_TOKEN.

    Казалось бы, тут можно забыть о REFRESH_TOKEN, поскольку ACCESS_TOKEN  у нас в руках, и дальше еще забыть и про авторизацию и спокойно пользоваться ACCESS_TOKEN во всех запросах. Я так поначалу и делал, только обнаружил что в один прекрасный момент сервер перекрыл мне доступ. В чем дело? А причина была в том, что по задумке Google время жизни ACCESS_TOKEN — всего лишь час, после чего он протухает и работать с ним не получится.

    Что теперь, снова получать код авторизации и токены? Нет. Про эти два этапа «получение кода авторизации» и «получение токенов» можно полностью забыть и уже никогда к ним не возвращаться. Самое главное, что у нас есть — это REFRESH_TOKEN. Сохраните его в надежном месте, поскольку теперь кроме него нам ничего не нужно.

    Замечу, что эти пройденные этапы мы выполняем лично. Наше приложение Beerware Script о них ничего не знает: ему нужны только токены.

    Получение ACCESS_TOKEN по REFRESH_TOKEN

    Дальше все становится просто. Когда ACCESS_TOKEN протухает, то есть через час становится не действительным, или просто не дожидаясь этого момента, мы посылаем на сервер CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN и получаем обновленное значение ACCESS_TOKEN, которое используем для запросов к серверу. Важно знать, что при этом REFRESH_TOKEN не меняется. Поэтому для приложения критично запомнить его, например в файле, при получении в самый первый раз.

    Все этапы авторизации из командной строки

    Для различных языков есть соответствующие библиотеки OAuth2. Мы же, для ясности изложения, все будем делать ручками из командной строки, формируя запросы с помощью утилиты curl. В конце концов, все эти библиотеки делают тоже самое — формируют url запросы к серверу Google.

    Начнем с получения кода авторизации. Поскольку эта процедура выполняется только раз, не грех поработать вместо нашего приложения. Вбиваем в адресную строку броузера и наблюдаем:

    https://accounts.google.com/o/oauth2/auth?сlient_id=CLIENT_ID
    &redirect_uri=http://localhost:8080
    &scope=https://www.google.com/m8/feeds/&response_type=code

    Запросом response_type=code мы хотим увидеть код авторизации. Он будет переслан по адресу redirect_uri, а поскольку на URI=http://localhost:8080 ни у меня, ни думаю у вас — ничего нет, то естественно браузер даст ошибку. Но это неважно, поскольку искомый код появится в адресной строке браузера и мы возьмем его оттуда, копируя начиная с code= и продолжая до символа #, который присутствует в конце url. Так мы получаем AUTH_CODE.

    В середине процедуры вас может пробросить на страницу авторизации, где вы должны подтвердить доступ к адресной книге со стороны проекта Beerware.

    Теперь получаем токены. На запрос

    curl -X POST -d "code=AUTH_CODE&client_id=CLIENT_ID
    &client_secret=CLIENT_SECRET&redirect_uri=URI
    &grant_type=authorization_code" https://accounts.google.com/o/oauth2/token

    сервер выдаст нам долгожданную парочку токенов:

    {
    "access_token" : "XXXXXXXXXXXXXXXX",
    "expires_in" : 3600,
    "refresh_token" : "XXXXXXXXXXXXXXXX",
    "token_type" : "Bearer"
    }

    Если мы попробуем получить токены еще раз, с использованным кодом авторизации, нас ожидает ошибка:

    Authorization code redeemed

    Все, код авторизации протух и с ним уже сделать ничего нельзя!

    На этом ручная работа закончилась и скрипт Beerware Script может начинать работу. Отдаем ему ACCESS_TOKEN и REFRESH_TOKEN и забываем про авторизацию. Все, что нужно делать скрипту когда ACCESS_TOKEN становится недействительным — это получить его новое значение запросом

    curl -X POST -d "client_id=CLIENT_ID
    &client_secret=CLIENT_SECRET
    &refresh_token=REFRESH_TOKEN
    &grant_type=refresh_token" https://accounts.google.com/o/oauth2/token

    Само собой, это будет уже не утилита curl, а https запрос с соответствующей библиотеки нашего приложения Beerware Script.

    После запроса мы получим новый ACCESS_TOKEN:

    {
    "access_token" : "XXXXXXXXXXXXXXXX",
    "expires_in" : 3600,
    "token_type" : "Bearer"
    }

    Поработаем с адресной книгой

    Теперь самое время посмотреть, как будет выглядеть работа со списком адресов. Как обычно, на этом этапе нам ассистирует Python. Ниже — фрагменты скрипта Beerware Script, которые дают представление об общей идее работы с полученным списком.

    Вначале затребуем список:

    #!/usr/bin/python
    
    import json
    import requests
    
    access_token = 'XXXXXXXXXXXXXXXX'
    # область работы scope - с Google Contacts
    scope = 'https://www.google.com/m8/feeds'
    server = "https://www.google.com/m8/feeds/contacts/default/full?access_token=%s&scope=%s&alt=json"
    
    # получаем кусок адресной книги используя access_token
    r = requests.get(server % (access_token,scope))
    
    # это может пригодиться для отладки, если мы захотим увидеть ответ сервера при ошибке
    # print(r.content)
    
    # обычный результат запроса
    # 200 = Ok
    # 403 = ошибка авторизации, самое время обновить access_token!
    # но этот код вы напишете сами
    print(r.status_code)
    
    # получили адресную книгу в виде структуры dict
    data = json.loads(r.text)

    В комментах видно, в каком месте нужно ловить ошибку авторизации, когда ACCESS_TOKEN устареет. Дальше нужно что-то делать с полученным списком адресов, наверное самое очевидное — это получить номера телефонов.

    Делается это в цикле по всей структуре data:

    # все искомое расположено ниже уровней feed, entry
    # цикл по контактам
    for r in data['feed']['entry']:
        # распечатаем весь контакт: просто для наглядности и отладки
        print(json.dumps(r, ensure_ascii=False, sort_keys=True, indent=4, separators=(',', ': ')))
        # вытаскиваем имя контакта
        name = r['title']['$t'].encode('utf-8')
        # если контакт пустой, все пропускаем
        if name == '':
            continue
        print(name)
        # страхуемся: а вдруг у контакта нет телефона? тогда вылетим из скрипта с ошибкой
        if 'gd$phoneNumber' in r:
            # зачем цикл? а потому что телефонных номеров может быть больше одного
            for t in r['gd$phoneNumber']:
                phone = t['$t'].encode('utf-8')
                print("\t%s" % phone)

    В результате после работы скрипта получим список контактов, где под каждой фамилией будет список телефонов. Тема отдельного рассмотрения — как изменять существующие контакты и заводить новые. Это решается засылкой на сервер соответствующей xml записи. Подробно процедура описана здесь. Думаю, вы и сами отлично справитесь с этой задачей )