Skip to content

Commit

Permalink
Add rough workaround for the need to login with real user&pass to dow…
Browse files Browse the repository at this point in the history
…nload attachments.
  • Loading branch information
joanbm committed Nov 2, 2018
1 parent 5e53515 commit 3620105
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 22 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,10 @@ Create a backup from Todoist's servers, including attachments:

``python3 __main__.py download --with-attachments --token 0123456789abcdef``

Download a specific backup from Todoist's servers, including attachments, and with tracing/progress info:
Create a backup from Todoist's servers, without including attachments, and with tracing/progress info:

``python3 __main__.py --verbose download --token 0123456789abcdef``

List available backups:

``python3 __main__.py list --token 0123456789abcdef``

Print full help:

``python3 __main__.py -h``
Expand Down
2 changes: 1 addition & 1 deletion full_offline_backup_for_todoist/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ControllerDependencyInjector(object, metaclass=ABCMeta):
""" Rudimentary dependency injection container for the controller """

@abstractmethod
def __init__(self, token, verbose):
def __init__(self, auth, verbose):
""" Initializes the dependencies according to the user configuration """

@property
Expand Down
24 changes: 14 additions & 10 deletions full_offline_backup_for_todoist/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ def __init__(self, controller_factory, controller_dependencies_factory):
self.__controller = None

@staticmethod
def __add_token_group(parser):
def __add_authorization_group(parser):
token_group = parser.add_mutually_exclusive_group(required=True)
token_group.add_argument("--token", type=str,
help="todoist API token (see Settings --> Integration)")
token_group.add_argument("--token-file", type=str,
help="file containing the todoist API token")
parser.add_argument("--email", type=str,
help="todoist email address for authorization")
parser.add_argument("--password", type=str,
help="todoist email address for authorization")

def __parse_command_line_args(self, prog, arguments):
example1_str = "Example: {} download --token 0123456789abcdef".format(prog)
example1_str = "Example: {} download --token 0123456789abcdef --email [email protected] --password P4ssW0rD".format(prog)
parser = argparse.ArgumentParser(prog=prog, formatter_class=argparse.RawTextHelpFormatter,
epilog=example1_str)
parser.add_argument("--verbose", action="store_true", help="print details to console")
Expand All @@ -35,7 +39,7 @@ def __parse_command_line_args(self, prog, arguments):
help="download attachments and attach to the backup file")
parser_download.add_argument("--output-file", type=str,
help="name of the file that will store the backup")
self.__add_token_group(parser_download)
self.__add_authorization_group(parser_download)

return parser.parse_args(arguments)

Expand All @@ -45,18 +49,18 @@ def run(self, prog, arguments):
args.func(args)

@staticmethod
def __get_token(args):
if args.token_file:
return Path(args.token_file).read_text()

return args.token
def __get_auth(args):
token = Path(args.token_file).read_text() if args.token_file else args.token
email = args.email
password = args.password
return {"token": token, "email": email, "password": password}

def handle_download(self, args):
""" Handles the download subparser with the specified command line arguments """

# Configure controller
token = self.__get_token(args)
dependencies = self.__controller_dependencies_factory(token, args.verbose)
auth = self.__get_auth(args)
dependencies = self.__controller_dependencies_factory(auth, args.verbose)
controller = self.__controller_factory(dependencies)

# Setup zip virtual fs
Expand Down
15 changes: 10 additions & 5 deletions full_offline_backup_for_todoist/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
from .backup_downloader import TodoistBackupDownloader
from .backup_attachments_downloader import TodoistBackupAttachmentsDownloader
from .tracer import ConsoleTracer, NullTracer
from .url_downloader import URLLibURLDownloader
from .url_downloader import URLLibURLDownloader, TodoistAuthURLDownloader

class RuntimeControllerDependencyInjector(ControllerDependencyInjector):
""" Implementation of the dependency injection container for the actual runtime objects """

def __init__(self, token, verbose):
super(RuntimeControllerDependencyInjector, self).__init__(token, verbose)
def __init__(self, auth, verbose):
super(RuntimeControllerDependencyInjector, self).__init__(auth, verbose)
self.__tracer = ConsoleTracer() if verbose else NullTracer()
urldownloader = URLLibURLDownloader()
todoist_api = TodoistApi(token, self.__tracer, urldownloader)
if ("email" in auth and auth["email"] is not None and
"password" in auth and auth["password"] is not None):
urldownloader = TodoistAuthURLDownloader(self.__tracer, auth["email"], auth["password"])
else:
self.__tracer.trace("NOTE: No email & password given, falling back to no-auth downloader")
urldownloader = URLLibURLDownloader()
todoist_api = TodoistApi(auth["token"], self.__tracer, urldownloader)
self.__backup_downloader = TodoistBackupDownloader(self.__tracer, todoist_api)
self.__backup_attachments_downloader = TodoistBackupAttachmentsDownloader(
self.__tracer, urldownloader)
Expand Down
2 changes: 1 addition & 1 deletion full_offline_backup_for_todoist/tests/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_runtime_dependency_injector_caches_values(self):
# Arrange

# Act
runtimedi = RuntimeControllerDependencyInjector("1234", True)
runtimedi = RuntimeControllerDependencyInjector({"token":"1234"}, False)
tracer1 = runtimedi.tracer
tracer2 = runtimedi.tracer
backup_downloader1 = runtimedi.backup_downloader
Expand Down
58 changes: 58 additions & 0 deletions full_offline_backup_for_todoist/url_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from abc import ABCMeta, abstractmethod
import urllib.request
import urllib.parse
import http.cookiejar

class URLDownloader(object, metaclass=ABCMeta):
""" Implementation of a class to download the contents of an URL """
Expand All @@ -22,3 +23,60 @@ def get(self, url, data=None):

with urllib.request.urlopen(real_url) as url_handle:
return url_handle.read()

class TodoistAuthURLDownloader(URLDownloader):
""" Implementation of a class to download the contents of an URL through URLLib,
authenticating before with Todoist's servers using a username/password """

URL_SHOWLOGIN='https://todoist.com/Users/showLogin'
URL_POSTLOGIN='https://todoist.com/Users/login'
URL_FILE_TO_DOWNLOAD="https://files.todoist.com/VxxTiO9w6TCd_gHiHxpovh3oLL71OllvLxXFgv70jhxJkMSuu6veoX7fVnasbZpc/by/15956439/as/IMG_3285.JPG"

LOGIN_PARAM_CSRF="csrf"
LOGIN_PARAM_EMAIL="email"
LOGIN_PARAM_PASSWORD="password"

def __init__(self, tracer, email, password):
self.__tracer = tracer
self.__email = email
self.__password = password
self.__opener = None

def get(self, url, data=None):
if self.__opener is None:
# Set up a cookie jar, to gather the login's cookies
cookiejar = http.cookiejar.CookieJar()
cookie_process = urllib.request.HTTPCookieProcessor(cookiejar)
self.__opener = urllib.request.build_opener(cookie_process)

self.__tracer.trace("Auth Step 1: Get CSRF token")

# Ping the login page, in order to get a CSRF token as a cookie
with self.__opener.open(TodoistAuthURLDownloader.URL_SHOWLOGIN) as get_csrf_response:
pass

self.__tracer.trace("Auth Step 2: Building login request params")

# Build the parameters (CSRF, email and password) for the login POST request
csrf_value = next(c.value for c in cookiejar if c.name == TodoistAuthURLDownloader.LOGIN_PARAM_CSRF)
params = {
TodoistAuthURLDownloader.LOGIN_PARAM_CSRF: csrf_value,
TodoistAuthURLDownloader.LOGIN_PARAM_EMAIL: self.__email,
TodoistAuthURLDownloader.LOGIN_PARAM_PASSWORD: self.__password}
params_str = urllib.parse.urlencode(params).encode('utf-8')

self.__tracer.trace("Auth Step 3: Send login request")

# Send the login POST request, which will give us our identifier cookie
with self.__opener.open(TodoistAuthURLDownloader.URL_POSTLOGIN, params_str) as login_response:
pass

self.__tracer.trace("Auth completed")

real_url = url
if data is not None:
real_url += "?" + urllib.parse.urlencode(data)

self.__tracer.trace("Downloading URL: {}".format(real_url))
with self.__opener.open(real_url) as url_handle:
return url_handle.read()

0 comments on commit 3620105

Please sign in to comment.