Skip to content
This repository has been archived by the owner on Jun 13, 2024. It is now read-only.

Commit

Permalink
Add init functionality to generate automatically a config file to u…
Browse files Browse the repository at this point in the history
…se with `run` command (#52)

Closes #39
  • Loading branch information
Crissal1995 committed Sep 14, 2023
1 parent 0d3735d commit 1524bfe
Show file tree
Hide file tree
Showing 7 changed files with 752 additions and 24 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ repos:
- "email-validator==2.0.0.post2"
- "email-validator==2.0.0.post2"
- "types-markdown==3.4.2.10"
- "questionary==2.0.1"
127 changes: 112 additions & 15 deletions automsr/browser/profile.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import json
import logging
import os
import sqlite3
import sys
from enum import Enum, auto
from pathlib import Path
from typing import Dict, List, Optional
from typing import ClassVar, Dict, List, Optional, Set, Tuple

from attr import field
from attrs import define

from automsr.config import validate_email

logger = logging.getLogger(__name__)

ENV_HOME = os.environ.get("HOME")
Expand All @@ -20,6 +24,7 @@ class ChromeVariant(Enum):
"""

CHROME = auto()
CHROME_BETA = auto()
CHROME_CANARY = auto()
CHROMIUM = auto()

Expand Down Expand Up @@ -85,27 +90,120 @@ def from_directory(cls, path: Path) -> Optional["ChromeProfile"]:
displayed_name = preferences_dict.get("profile", {}).get("name", "")
return cls(displayed_name=displayed_name, path=path)

def get_email(self) -> Optional[str]:
"""
Try to get the Outlook email address from the profile data.
"""

allowed_domains = {"outlook", "live", "hotmail", "msn"}

@define(order=True, frozen=True)
class Record:
email: str = field(order=False)
timestamp: int

@classmethod
def from_row(cls, row: Tuple[str, str]) -> Optional["Record"]:
"""
Parse a row obtained from the Login Data database of Chrome.
If the row is not compatible with our criteria, returns None.
"""

assert len(row) == 2
email_value: str = row[0]
timestamp_value: int = int(row[1])

if not validate_email(email_value, raise_on_error=False):
return None

domain = email_value.split("@")[1].split(".")[0]
if domain not in allowed_domains:
return None

return cls(email=email_value, timestamp=timestamp_value)

# TODO check if this path is valid for every OS
login_database: Path = self.path / "Login Data"
if not login_database.is_file():
logger.debug("No login database found: %s", login_database)
return None

with sqlite3.connect(login_database) as conn:
cur = conn.execute(
"""\
select t.username_value, t.date_last_used
from main.logins t
where t.username_value <> ''
and t.origin_url like '%live.com%';"""
)
all_rows: List[Tuple[str, str]] = cur.fetchall()

valid_records: List[Optional[Record]] = [
Record.from_row(row=row) for row in all_rows
]
valid_non_null_records: List[Record] = [
record for record in valid_records if record is not None
]
unique_emails: Set[str] = {record.email for record in valid_non_null_records}
logger.debug("Outlook emails found: %s", unique_emails)

if not unique_emails:
logger.debug("No Outlook email found!")
return None
elif len(unique_emails) > 1:
logger.debug(
"More than one Outlook email found! Will return the latest email used."
)
latest_record: Record = max(valid_non_null_records)
return latest_record.email
else:
logger.debug("Found only one Outlook email.")
return unique_emails.pop()


@define
class ProfilesExecutor:
chrome_variant: ChromeVariant = ChromeVariant.CHROME
profiles_root_path: Optional[Path] = None

CHROME_PROFILES_LOCATIONS = {
# source: https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#default-location
CHROME_DEFAULT_PROFILES_LOCATIONS: ClassVar[
Dict[str, Dict[ChromeVariant, Path]]
] = {
"macOS": {
ChromeVariant.CHROME: f"{ENV_HOME}/Library/Application Support/Google/Chrome",
ChromeVariant.CHROME_CANARY: f"{ENV_HOME}/Library/Application Support/Google/Chrome",
ChromeVariant.CHROMIUM: f"{ENV_HOME}/Library/Application Support/Google/Chrome",
ChromeVariant.CHROME: Path(
f"{ENV_HOME}/Library/Application Support/Google/Chrome"
),
ChromeVariant.CHROME_BETA: Path(
f"{ENV_HOME}/Library/Application Support/Google/Chrome Beta"
),
ChromeVariant.CHROME_CANARY: Path(
f"{ENV_HOME}/Library/Application Support/Google/Chrome Canary"
),
ChromeVariant.CHROMIUM: Path(
f"{ENV_HOME}/Library/Application Support/Chromium"
),
},
"windows": {
ChromeVariant.CHROME: f"{ENV_LOCALAPPDATA}\\Google\\Chrome\\User Data",
ChromeVariant.CHROME_CANARY: f"{ENV_LOCALAPPDATA}\\Google\\Chrome SxS\\User Data",
ChromeVariant.CHROMIUM: f"{ENV_HOME}/Library/Application Support/Chromium",
ChromeVariant.CHROME: Path(
f"{ENV_LOCALAPPDATA}\\Google\\Chrome\\User Data"
),
ChromeVariant.CHROME_BETA: Path(
f"{ENV_LOCALAPPDATA}\\Google\\Chrome Beta\\User Data"
),
ChromeVariant.CHROME_CANARY: Path(
f"{ENV_LOCALAPPDATA}\\Google\\Chrome SxS\\User Data"
),
ChromeVariant.CHROMIUM: Path(f"{ENV_LOCALAPPDATA}\\Chromium\\User Data"),
},
"linux": {
ChromeVariant.CHROME: f"{ENV_HOME}/.config/google-chrome",
ChromeVariant.CHROME_CANARY: f"{ENV_HOME}/.config/google-chrome-beta",
ChromeVariant.CHROMIUM: f"{ENV_HOME}/.config/chromium",
ChromeVariant.CHROME: Path(f"{ENV_HOME}/.config/google-chrome"),
ChromeVariant.CHROME_BETA: Path(f"{ENV_HOME}/.config/google-chrome-beta"),
ChromeVariant.CHROME_CANARY: Path(
f"{ENV_HOME}/.config/google-chrome-unstable"
),
ChromeVariant.CHROMIUM: Path(f"{ENV_HOME}/.config/chromium"),
},
}

Expand All @@ -116,20 +214,19 @@ def get_profiles_root_path(self) -> Path:

if self.profiles_root_path is not None:
logger.info(
"Profiles root path manually set to: %s", self.profiles_root_path
"Profiles root path was manually set to: %s", self.profiles_root_path
)
return self.profiles_root_path

platform = get_platform()
logger.debug("Platform to use: %s", platform)
logger.debug("Chrome variant to use: %s", self.chrome_variant)

profiles_root_path_str = self.CHROME_PROFILES_LOCATIONS[platform][
profiles_root_path = self.CHROME_DEFAULT_PROFILES_LOCATIONS[platform][
self.chrome_variant
]
profiles_root_path = Path(profiles_root_path_str)
logger.info(
"Profiles root path automatically found to be: %s", profiles_root_path
"Profiles root path was automatically found to be: %s", profiles_root_path
)
return profiles_root_path

Expand Down
44 changes: 44 additions & 0 deletions automsr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from automsr.browser.profile import OutputFormat, ProfilesExecutor
from automsr.config import Config
from automsr.executor import MultipleTargetsExecutor
from automsr.init import InitExecutor
from automsr.mail import EmailExecutor

logger = logging.getLogger(__name__)
Expand All @@ -30,10 +31,14 @@ class Args:
verbose: bool = False

# Run args
# ...

# Profiles args
format: OutputFormat = field(default=OutputFormat.LIST, converter=OutputFormat)

# Init args
enable_logs: bool = False


def run(args: Args) -> None:
"""
Expand Down Expand Up @@ -73,6 +78,21 @@ def email(args: Args) -> None:
sys.exit(1)


def init(args: Args) -> None:
"""
Method to invoke when `init` is executed.
"""

if not args.enable_logs:
_logger = logging.getLogger()
while _logger.hasHandlers():
_logger.removeHandler(_logger.handlers[0])
_logger.addHandler(logging.NullHandler())

executor = InitExecutor()
executor.execute()


def add_common_flags(parser: ArgumentParser) -> None:
"""
Add common flags to a generic parser.
Expand Down Expand Up @@ -132,6 +152,22 @@ def add_email_flags(parser: ArgumentParser) -> None:
parser.set_defaults(func=email)


def add_init_flags(parser: ArgumentParser) -> None:
"""
Add `init` flags to a generic parser.
Flags provided:
* --enable-logs
"""

parser.add_argument(
"--enable-logs",
action="store_true",
help="Enable logging to stderr. Defaults to False to avoid output pollution.",
)
parser.set_defaults(func=init)


def cli() -> None:
# Construct the base parser
parser = ArgumentParser()
Expand Down Expand Up @@ -161,6 +197,14 @@ def cli() -> None:
add_common_flags(parser=email_parser)
add_email_flags(parser=email_parser)

# Construct the `init` parser
init_parser = subparsers.add_parser(
name="init",
help="Create the config file in a step-by-step procedure.",
)
add_common_flags(parser=init_parser)
add_init_flags(parser=init_parser)

# Parse arguments
try:
raw_args = vars(parser.parse_args())
Expand Down
Loading

0 comments on commit 1524bfe

Please sign in to comment.