oauth2client для публикации сообщений в Blogger от бота Telegram на сервере Heroku

Я только что развернул своего телеграмм-бота с python-telegram-bot в Heroku.

Мой бот webhooks использует blogger для публикации определенных вещей. До сих пор я делал это с помощью слегка измененной пользовательской версии sample_tools из модуля google_apli_client.

my_tools:

"""
dependencies:
    pip3 install --upgrade google-api-python-client

This is a slightly modified implementation 
for substituting googleapiclient.sample_tools. It helps customizing some paths 
for my project files under different environments
"""
from __future__ import absolute_import

from environments import get_active_env

__all__ = ['init']

import argparse
import os

from googleapiclient import discovery
from googleapiclient.http import build_http
from oauth2client import tools, file, client


def init(argv, name, version, doc, scope=None, parents=[], 
         discovery_filename=None):
    """A common initialization routine for samples.

    Many of the sample applications do the same initialization, which has now
    been consolidated into this function. This function uses common idioms found
    in almost all the samples, i.e. for an API with name 'apiname', the
    credentials are stored in a file named apiname.dat, and the
    client_secrets.json file is stored in the same directory as the application
    main file.

    Args:
        argv: list of string, the command-line parameters of the application.
        name: string, name of the API.
        version: string, version of the API.
        doc: string, description of the application. Usually set to __doc__.
        file: string, filename of the application. Usually set to __file__.
        parents: list of argparse.ArgumentParser, additional command-line flags.
        scope: string, The OAuth scope used.
        discovery_filename: string, name of local discovery file (JSON). Use 
        when discovery doc not available via URL.

    Returns:
    A tuple of (service, flags), where service is the service object and flags
    is the parsed command-line flags.
    """
    if scope is None:
        scope = 'https://www.googleapis.com/auth/' + name

    # Parser command-line arguments.
    parent_parsers = [tools.argparser]
    parent_parsers.extend(parents)
    parser = argparse.ArgumentParser(
        description=doc,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        parents=parent_parsers)
    flags = parser.parse_args(argv[1:])

    # Name of a file containing the OAuth 2.0 information for this
    # application, including client_id and client_secret, which are found
    # on the API Access tab on the Google APIs
    # Console <http://code.google.com/apis/console>.
    client_secrets = os.path.join(os.path.dirname(__file__), get_active_env(),
                                 'client_secrets.json')

    # Set up a Flow object to be used if we need to authenticate.
    flow = client.flow_from_clientsecrets(client_secrets,
      scope=scope,
      message=tools.message_if_missing(client_secrets))

    # Prepare credentials, and authorize HTTP object with them.
    # If the credentials don't exist or are invalid, 
    # run through the native client flow.
    # The Storage object will ensure that if successful the good
    # credentials will get written back to a file in google_core directory.
    storage_file_path = os.path.join(os.path.dirname(__file__), name + '.dat')
    storage = file.Storage(storage_file_path)
    credentials = storage.get()
    if credentials is None or credentials.invalid:
        credentials = tools.run_flow(flow, storage, flags)
    http = credentials.authorize(http=build_http())

    if discovery_filename is None:
        # Construct a service object via the discovery service.
        service = discovery.build(name, 
                                  version, 
                                  http=http, 
                                  cache_discovery=False)
    else:
        # Construct a service object using a local discovery document file.
        with open(discovery_filename) as discovery_file:
            service = discovery.build_from_document(
                discovery_file.read(),
                base='https://www.googleapis.com/',
                http=http)
        service = discovery.build(name, 
                                  version, 
                                  http=http, 
                                  cache_discovery=False)
    return (service, flags)

Благодаря этому я мог выполнить аутентификацию, и браузер в ОС открылся бы и позволил мне (или конечному пользователю) разрешить приложению использовать моего (или пользователя) блоггера.

начальный фрагмент с использованием my_tools:

service, flags = my_tools.init(
    [], 'blogger', 'v3', __doc__,
    scope='https://www.googleapis.com/auth/blogger')

try:
    posts = service.posts()
    # This new_post is a custom object, but the important thing here
    # is getting the authorization, and then the service at the top
    insert = posts.insert(blogId=new_post.blog_id, body=new_post.body(), isDraft=new_post.is_draft)
    posts_doc = insert.execute()
    return posts_doc
except client.AccessTokenRefreshError:
    print('The credentials have been revoked or expired, please re-run the application to re-authorize')

Но теперь я не могу этого сделать, так как он находится в героку и в журналах появляется это сообщение:

app[web.1]: Your browser has been opened to visit:
app[web.1]: 
app[web.1]:     https://accounts.google.com/o/oauth2/auth?client_id=<client_id>&redirect_uri=http%3A%2F%2Flocalhost%3A8090%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fblogger&access_type=offline&response_type=code
app[web.1]: 
app[web.1]: If your browser is on a different machine then exit and re-run this
app[web.1]: application with the command-line parameter
app[web.1]: 
app[web.1]:   --noauth_local_webserver
app[web.1]:

Мне нужно автоматически авторизовать приложение heroku, учитывая, что к нему можно будет получить доступ только из бота Telegram, ограниченного некоторыми пользователями, ему не нужно перенаправлять пользователя в браузер и авторизоваться.

Мне нужен какой-то способ, чтобы боту было разрешено использовать blogger, а пользователь мог использовать бота с простым способом авторизации, когда это необходимо, или с некоторым файлом авторизации, уже хранящимся на сервере.

Я погуглил и посмотрел на эти ресурсы:

https://developers.google.com/api-client-library/python/auth/web-app https://github.com/burnash/gspread/wiki/How-to-get-OAuth-access-token-in-console%3F Django oauth2 Google не работает на сервере

но я совершенно не понимаю, что и как мне делать. Я чувствую, что мне нужно объяснение "объясни для чайников".

Отредактировано: меня указали на этот веб-сайт

https://developers.google.com/api-client-library/python/auth/service-accounts

поэтому я попробовал этот новый код.

новый фрагмент:

from oauth2client import service_account
    import googleapiclient.discovery
    import os
    from environments import get_active_env
    SERVICE_ACCOUNT_FILE = os.path.join(os.path.dirname(__file__), os.pardir, 'google_core', get_active_env(),
                                        'service_account.json')

    credentials = service_account.ServiceAccountCredentials.from_json_keyfile_name(
        SERVICE_ACCOUNT_FILE, scopes=['https://www.googleapis.com/auth/blogger'])
    service = googleapiclient.discovery.build('blogger', 'v3', credentials=credentials)

    try:
        posts = service.posts()
        insert = posts.insert(blogId=new_post.blog_id, body=new_post.body(), isDraft=new_post.is_draft)
        posts_doc = insert.execute()
        return posts_doc
    except client.AccessTokenRefreshError:
        print('The credentials have been revoked or expired, please re-run the application to re-authorize')

так что теперь я получаю это в журналах (я думаю, что здесь есть ошибка 403 HttpError, другие ошибки о том, что memcache или oauth2client.contrib.locked_file не импортируется, не имеют большого значения):

heroku[web.1]: Unidling
heroku[web.1]: State changed from down to starting
heroku[web.1]: Starting process with command `python my_bot.py`
heroku[web.1]: State changed from starting to up
heroku[router]: at=info method=POST path="/<bot_token>" host=telegram-bot-alfred.herokuapp.com request_id=<request_id> fwd="<ip>" dyno=web.1 connect=1ms service=2ms status=200 bytes=97 protocol=https
app[web.1]: INFO - Input: post_asin 
app[web.1]: INFO - Input ASIN: B079Z8THTF
app[web.1]: INFO - Printing offers for asin B079Z8THTF:
app[web.1]: INFO - EUR 36.98
app[web.1]: INFO - URL being requested: GET https://www.googleapis.com/discovery/v1/apis/blogger/v3/rest
app[web.1]: INFO - Attempting refresh to obtain initial access_token
app[web.1]: INFO - URL being requested: POST https://www.googleapis.com/blogger/v3/blogs/2270688467086771731/posts?isDraft=true&alt=json
app[web.1]: INFO - Refreshing access_token
app[web.1]: WARNING - Encountered 403 Forbidden with reason "forbidden"
app[web.1]: ERROR - Error with asin B079Z8THTF. We go to the next.
app[web.1]: Traceback (most recent call last):
app[web.1]:   File "my_bot.py", line 171, in process_asin_string
app[web.1]:     send_post_to_blogger(update.message, post)
app[web.1]:   File "/app/api_samples/blogger/blogger_insert.py", line 85, in send_post_to_blogger
app[web.1]:     response = post_at_blogger(post)
app[web.1]:   File "/app/api_samples/blogger/blogger_insert.py", line 72, in post_at_blogger
app[web.1]:     posts_doc = insert.execute()
app[web.1]:   File "/app/.heroku/python/lib/python3.6/site-packages/googleapiclient/http.py", line 844, in execute
app[web.1]:     raise HttpError(resp, content, uri=self.uri)
app[web.1]:   File "/app/.heroku/python/lib/python3.6/site-packages/oauth2client/_helpers.py", line 133, in positional_wrapper
app[web.1]:     return wrapped(*args, **kwargs)
app[web.1]: googleapiclient.errors.HttpError: <HttpError 403 when requesting https://www.googleapis.com/blogger/v3/blogs/2270688467086771731/posts?isDraft=true&alt=json returned "We're sorry, but you don't have permission to access this resource.">
app[web.1]: ERROR - Exception HttpError not handled
app[web.1]: Traceback (most recent call last):
app[web.1]:   File "my_bot.py", line 171, in process_asin_string
app[web.1]:     send_post_to_blogger(update.message, post)
app[web.1]:   File "/app/api_samples/blogger/blogger_insert.py", line 85, in send_post_to_blogger
app[web.1]:     response = post_at_blogger(post)
app[web.1]:   File "/app/api_samples/blogger/blogger_insert.py", line 72, in post_at_blogger
app[web.1]:     posts_doc = insert.execute()
app[web.1]:   File "/app/.heroku/python/lib/python3.6/site-packages/googleapiclient/http.py", line 844, in execute
app[web.1]:     raise HttpError(resp, content, uri=self.uri)
app[web.1]:   File "/app/.heroku/python/lib/python3.6/site-packages/oauth2client/_helpers.py", line 133, in positional_wrapper
app[web.1]:     return wrapped(*args, **kwargs)
app[web.1]: googleapiclient.errors.HttpError: <HttpError 403 when requesting https://www.googleapis.com/blogger/v3/blogs/2270688467086771731/posts?isDraft=true&alt=json returned "We're sorry, but you don't have permission to access this resource.">
app[web.1]: 
app[web.1]: During handling of the above exception, another exception occurred:
app[web.1]: 
app[web.1]: Traceback (most recent call last):
app[web.1]:   File "/app/exceptions/errors.py", line 47, in alfred
app[web.1]:     message.reply_text(rnd.choice(answers[type(exception)]))
app[web.1]: KeyError: <class 'googleapiclient.errors.HttpError'>
app[web.1]: WARNING - Error with asin B079Z8THTF. We go to the next

person madtyn    schedule 16.03.2018    source источник


Ответы (1)


Я нашел решение, просто указав такой параметр, как здесь:

service, flags = my_tools.init(
    ['', '--noauth_local_webserver'], 'blogger', 'v3', __doc__,
    scope='https://www.googleapis.com/auth/blogger')

Затем мне пришлось настроить некоторые методы из oauth2client.tools. Я сделал два метода и дополнительный код в my_tools. Каждый недостающий фрагмент легко импортируется или копируется из оригинальных инструментов Google:

# module scope
import argparse
from googleapiclient import discovery
from googleapiclient.http import build_http
from oauth2client import tools, file, client, _helpers
from oauth2client.tools import _CreateArgumentParser

_GO_TO_LINK_MESSAGE = """
Visit this link to get auth code

    {address}

"""

# argparser is an ArgumentParser that contains command-line options expected
# by tools.run(). Pass it in as part of the 'parents' argument to your own
# ArgumentParser.
argparser = _CreateArgumentParser()

_flow = None


# Methods
@_helpers.positional(3)
def run_flow(flow, flags=None):
    """
    Emulates the original method run_flow from oauth2client.tools getting the website to visit.

    The ``run()`` function is called from your application and runs
    through all the steps to obtain credentials. It takes a ``Flow``
    argument and attempts to open an authorization server page in the
    user's default web browser. The server asks the user to grant your
    application access to the user's data.  The user can then get an
    authentication code for inputing later

    :param flow: the google OAuth 2.0 Flow object with which the auth begun
    :param flags: the provided flags
    :return: the string with the website link where the user can authenticate and obtain a code
    """
    global _flow

    # I update the _flow object for using internally later
    _flow = flow

    # Really the flags aren't very used. In practice I copied the method as if noauth_local_webserver was provided
    if flags is None:
        flags = argparser.parse_args()
    logging.getLogger().setLevel(getattr(logging, flags.logging_level))

    oauth_callback = client.OOB_CALLBACK_URN
    _flow.redirect_uri = oauth_callback
    authorize_url = _flow.step1_get_authorize_url()

    return _GO_TO_LINK_MESSAGE.format(address=authorize_url)


def oauth_with(code, http=None):
    """
    If the code grants access,
    the function returns new credentials. The new credentials
    are also stored in the ``storage`` argument, which updates the file
    associated with the ``Storage`` object.

    :param code: the auth code
    :param http: the http transport object
    :return: the credentials if any
    """
    global _flow
    storage_file_path = get_credentials_path('blogger')
    storage = file.Storage(storage_file_path)
    try:
        # We now re-use the _flow stored earlier
        credential = _flow.step2_exchange(code, http=http)
    except client.FlowExchangeError as e:
        raise AlfredException(msg='Authentication has failed: {0}'.format(e))

    storage.put(credential)
    credential.set_store(storage)
    # We reset the flow
    _flow = None

    return credential
person madtyn    schedule 11.04.2018