HiveMinds / tw-install

Automatically installs Taskwarrior, Taskserver (TODO: and Timewarrior). This project aims to support automated installation of all Taskwarrior hook scripts, configuration flavours etc. with a single command.
GNU Affero General Public License v3.0
17 stars 7 forks source link

Automate asking google calendar sync permission #40

Open a-t-0 opened 3 years ago

a-t-0 commented 3 years ago
      <p>Currently the user must copy a url from a terminal, open a browser and copy the url and press enter before it can grant taskwarrior access to google calendar synchronisation. This can be automated by:</p>
  1. Create
    name: firstGcalSync.sh
    location: Taskwarrior-installation\AutoInstallTaskwarrior\src\main\resources\autoinstalltaskwarrior/
    how: Using java class CreateFiles.java of project AutoInstallTaskwarrior.
    When: during the installation/execution of AutoInstallTaskwarrior.jar
    Content:
#!/bin/bash
cd /home/testlinuxname/maintenance/gCal/
sudo /home/testlinuxname/maintenance/gCal/tw_gcal_sync --gcal-calendar "TW Reminders" --taskwarrior-tag remindme

where testlinuxname is replaced with the variable linux user name.

  1. then after having installed taskwarrior, install tw_gcal_sync.

  2. Then find:
    name:GCalSide.py
    location:/home//maintenance/gCal/taskw_gcal_sync/`

  3. Then find the line: creds = flow.run_local_server() (around line 188) and put the following code above it:

file = open("url.txt","w")
file.write(flow.authorization_url()[0])
file.close()
  1. Then copy:
    name:firstGCalSync.sh
    location: Taskwarrior-installation\AutoInstallTaskwarrior\src\main\resources\autoinstalltaskwarrior/ to the place dependending on protocol to make code run once at startup.

  2. Then, start a background loop script with powershell start process that
    Content:

First check if `url.txt` exists, if it does/is found:
copy the url that is in it,
Modify the url by putting the (hardcoded) http redirect url in in it 8080
then pass the url to powershell
In powershell find the favorite browser
open the url in the browser.

Then start checking whether the `url.txt` is deleted. If it takes longer than 2 minutes, restart the ask command, as the user might have accidentally closed the wsl popup window.  and restart checking for the `run.txt file`

TODO: Determine whether you can run the command inside powershell, automatically open the browser in the background, this way, you don't run the risk of the user accidentally closing the popup window. It is just the background process that is a limiting factor.

  1. Then run the vbs that opens an external shell that boots the wsl for the first time, and hence runs the firstGcalSync.sh.
    Name: tbd
    location: tbd
    Content:

Set WshShell = WScript.CreateObject("WScript.Shell")

WshShell.Run "wsl"

'Dim objIE
' Create an IE object
'Set objIE = CreateObject( "InternetExplorer.Application" )
'objIE.Navigate "about:blank"
' Wait till IE is ready

WScript.Sleep 6000

'WshShell.SendKeys "/home/testlinuxname/maintenance/gCal/./askSync.sh > /mnt/c/temp/output.txt"
'WshShell.SendKeys "/home/testlinuxname/maintenance/gCal/./askSync.sh >> /mnt/c/temp/output.txt"
WshShell.SendKeys "/home/testlinuxname/maintenance/gCal/./askSync.sh"

WshShell.SendKeys "{ENTER}"

' Create an IE object
'Set objIE = CreateObject( "InternetExplorer.Application" )
'objIE.Navigate "about:blank"
' Wait till IE is ready

WScript.Sleep 6000

'WshShell.SendKeys "^(c)"
'WshShell.SendKeys "exit"
'WshShell.SendKeys "{ENTER}"
'WshShell.SendKeys "exit"
'WshShell.SendKeys "{ENTER}"

Without the abruption, that waits, until the user has granted permission. Once the user has granted permission, delete the url.txt file

a-t-0 commented 3 years ago
      <p>Rethought workflow:</p>
  1. Install tw

  2. Install gcalsync

  3. Start a job in powershell
    Content:
    2.1 Scans existence of the of the url.txt file with infinite loop
    2.2 Once found url.txt,
    2.2.1 gets the url,
    2.2.2 puts the redirect hardcoded local host inbetween,
    2.2.3 Finds the preferred/default browser
    2.2.4 Opens the link in the default browser
    2.3 In the meantime, the normal installation shell also shows: please visit.. url.
    so that even if the user accidentally closes the browser, the user is still able to grant access to the taskwarrior google calendar sync.

  4. Run the command that shows the Please visit request, directly in the powershell installation script.

  5. Wait untill the request is granted, then proceed with the rest of the installation.

TODO: first install all non-user-dependent stuff like timewarrior etc. so that, if the user does not successfully activate this sync, the rest of the installation is still successful.

a-t-0 commented 3 years ago
      <p>Todo:</p>

Output the InjectCode.sh from java CreateLines
Output the askSync.sh from java CreateLines

Specify the location of the url.txt (Need the username for that which isn't in HardCoded).
Specify the location of InjectCode.sh
Specify the location of askSync.sh

Run the InjectCode.sh once before asking the permission with tw_gcal_sync.. command.

Then run the askSync.sh command

That's it, then it should automatically open the request for permission in the browser, while simultaneously typing the result in the powershell installer.

No need to put the url.txt file scanner in a job.

Perhaps roll back 1 commit to prevent error with "currentPath".

a-t-0 commented 3 years ago
      <p>The <code>run_local_server()</code>  internally creates a new (random) state when it is executed.  This means the url that is accepted is also randomly changed, as it contains that state.<br>

Method authorization_urL() also creates it's own new random state. That means there is a difference between the state in the url you output, and the state in the url that is listened to by the local server.

To solve this, instead of importing the default method run_local_server() from some library, import it from a custom made file named run_local_server.py (or even just from a class at the bottom (appended) of the code. And inject the 3 lines that output the url inside the modified run_local_server().

Source file:
https://google-auth-oauthlib.readthedocs.io/en/latest/_modules/google_auth_oauthlib/flow.html#InstalledAppFlow.run_local_server
contains:
a lot of lines.

a-t-0 commented 3 years ago
      <p>The modified file is called: <code>customLocalServer.py</code></p>

This file is placed in /home/<linux username>/maintenance/gCal/ since the importing looks from the directory where the command is given.

Source:https://github.com/googleapis/google-auth-library-python-oauthlib/blob/master/google_auth_oauthlib/flow.py

# Copyright 2016 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""OAuth 2.0 Authorization Flow
This module provides integration with `requests-oauthlib`_ for running the
`OAuth 2.0 Authorization Flow`_ and acquiring user credentials.
Here's an example of using :class:`Flow` with the installed application
authorization flow::
    from google_auth_oauthlib.flow import Flow
    # Create the flow using the client secrets file from the Google API
    # Console.
    flow = Flow.from_client_secrets_file(
        'path/to/client_secrets.json',
        scopes=['profile', 'email'],
        redirect_uri='urn:ietf:wg:oauth:2.0:oob')
    # Tell the user to go to the authorization URL.
    auth_url, _ = flow.authorization_url(prompt='consent')
    print('Please go to this URL: {}'.format(auth_url))
    # The user will get an authorization code. This code is used to get the
    # access token.
    code = input('Enter the authorization code: ')
    flow.fetch_token(code=code)
    # You can use flow.credentials, or you can just get a requests session
    # using flow.authorized_session.
    session = flow.authorized_session()
    print(session.get('https://www.googleapis.com/userinfo/v2/me').json())
This particular flow can be handled entirely by using
:class:`InstalledAppFlow`.
.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
.. _OAuth 2.0 Authorization Flow:
    https://tools.ietf.org/html/rfc6749#section-1.2
"""
from base64 import urlsafe_b64encode
import hashlib
import json
import logging
try:
    from secrets import SystemRandom
except ImportError:  # pragma: NO COVER
    from random import SystemRandom
from string import ascii_letters, digits
import webbrowser
import wsgiref.simple_server
import wsgiref.util

import google.auth.transport.requests
import google.oauth2.credentials
from six.moves import input

import google_auth_oauthlib.helpers

_LOGGER = logging.getLogger(__name__)

class InstalledAppFlow(Flow):
    """Authorization flow helper for installed applications.
    This :class:`Flow` subclass makes it easier to perform the
    `Installed Application Authorization Flow`_. This flow is useful for
    local development or applications that are installed on a desktop operating
    system.
    This flow has two strategies: The console strategy provided by
    :meth:`run_console` and the local server strategy provided by
    :meth:`run_local_server`.
    Example::
        from google_auth_oauthlib.flow import InstalledAppFlow
        flow = InstalledAppFlow.from_client_secrets_file(
            'client_secrets.json',
            scopes=['profile', 'email'])
        flow.run_local_server()
        session = flow.authorized_session()
        profile_info = session.get(
            'https://www.googleapis.com/userinfo/v2/me').json()
        print(profile_info)
        # {'name': '...',  'email': '...', ...}
    Note that these aren't the only two ways to accomplish the installed
    application flow, they are just the most common ways. You can use the
    :class:`Flow` class to perform the same flow with different methods of
    presenting the authorization URL to the user or obtaining the authorization
    response, such as using an embedded web view.
    .. _Installed Application Authorization Flow:
        https://developers.google.com/api-client-library/python/auth
        /installed-app
    """
    _OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'

    _DEFAULT_AUTH_PROMPT_MESSAGE = (
        'Please visit this URL to authorize this application: {url}')
    """str: The message to display when prompting the user for
    authorization."""
    _DEFAULT_AUTH_CODE_MESSAGE = (
        'Enter the authorization code: ')
    """str: The message to display when prompting the user for the
    authorization code. Used only by the console strategy."""

    _DEFAULT_WEB_SUCCESS_MESSAGE = (
        'The authentication flow has completed, you may close this window.')

    def run_console(
            self,
            authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
            authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE,
            **kwargs):
        """Run the flow using the console strategy.
        The console strategy instructs the user to open the authorization URL
        in their browser. Once the authorization is complete the authorization
        server will give the user a code. The user then must copy & paste this
        code into the application. The code is then exchanged for a token.
        Args:
            authorization_prompt_message (str): The message to display to tell
                the user to navigate to the authorization URL.
            authorization_code_message (str): The message to display when
                prompting the user for the authorization code.
            kwargs: Additional keyword arguments passed through to
                :meth:`authorization_url`.
        Returns:
            google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
                for the user.
        """
        kwargs.setdefault('prompt', 'consent')

        self.redirect_uri = self._OOB_REDIRECT_URI

        auth_url, _ = self.authorization_url(**kwargs)

        print(authorization_prompt_message.format(url=auth_url))

        code = input(authorization_code_message)

        self.fetch_token(code=code)

        return self.credentials

    def run_local_server(
            self, host='localhost', port=8080,
            authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
            success_message=_DEFAULT_WEB_SUCCESS_MESSAGE,
            open_browser=True,
            **kwargs):
        """Run the flow using the server strategy.
        The server strategy instructs the user to open the authorization URL in
        their browser and will attempt to automatically open the URL for them.
        It will start a local web server to listen for the authorization
        response. Once authorization is complete the authorization server will
        redirect the user's browser to the local web server. The web server
        will get the authorization code from the response and shutdown. The
        code is then exchanged for a token.
        Args:
            host (str): The hostname for the local redirect server. This will
                be served over http, not https.
            port (int): The port for the local redirect server.
            authorization_prompt_message (str): The message to display to tell
                the user to navigate to the authorization URL.
            success_message (str): The message to display in the web browser
                the authorization flow is complete.
            open_browser (bool): Whether or not to open the authorization URL
                in the user's browser.
            kwargs: Additional keyword arguments passed through to
                :meth:`authorization_url`.
        Returns:
            google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
                for the user.
        """
        wsgi_app = _RedirectWSGIApp(success_message)
        local_server = wsgiref.simple_server.make_server(
            host, port, wsgi_app, handler_class=_WSGIRequestHandler)

        self.redirect_uri = 'http://{}:{}/'.format(
            host, local_server.server_port)
        auth_url, _ = self.authorization_url(**kwargs)

        if open_browser:
            webbrowser.open(auth_url, new=1, autoraise=True)

        print(authorization_prompt_message.format(url=auth_url))

        local_server.handle_request()

        # Note: using https here because oauthlib is very picky that
        # OAuth 2.0 should only occur over https.
        authorization_response = wsgi_app.last_request_uri.replace(
            'http', 'https')
        self.fetch_token(authorization_response=authorization_response)

        return self.credentials

And inserted right below auth_url, _ = self.authorization_url(**kwargs):

        file=open("url.txt","w")
        file.write(auth_url)
        file.close()

Furthermore, insted of inserting the above 3 lines into GCalSide.py, the following line is modified:
From:

from google_auth_oauthlib.flow import InstalledAppFlow

To:

from customLocalServer import InstalledAppFlow
a-t-0 commented 3 years ago
      <p>Summary of code flow:</p>
  1. The askSync.sh script is exported from resources to /home/<linux username>/maintenance/ by the AutoInstallTaskwarrior.jar. (and made runnable with chmod)
  2. The injectCode.sh and customLocalHost.py are exported to /home/<linux username>/maintenance/ by the AutoInstallTaskwarrior.jar. (and made runnable with chmod)
  3. Taskwarrior is installed.
  4. tw_gcal_sync is installed (that creates the gCal folder that needs to be empty before the repository is cloned into it).
  5. The injectCode.sh and customLocalHost.py are MOVED from /home/<linux username>/maintenance/ to: /home/<linux username>/maintenance/gCal/taskw_gcal_sync by the setup.ps1
  6. The injectCode.sh is executed to modify the repo's /taskw_gcal_sync/GCalSide.py to import the custom google auth Flow object, (modification = exports auth url to txt.)
  7. Then a job is started with an infinite loop that checks whether the url.txt is created. (If it is found it copies it and opens a website in windows with that url.
  8. After the job is started and running in the background, the main setup.ps1 runs askSync.sh which executes the first taskwarrior google sync requests, which generates/outputs the authenthication url, that is then picked up by the background job.
  9. After authenthication, the main setup.ps1 continues with the next line, which stops and deletes the job.
  10. The url.txt files are deleted.
a-t-0 commented 3 years ago
      <p>Note the state is changed between the creation of the authorization by gcal, and the link opening by the wsl.</p>