262 lines
9.3 KiB
Python
Raw Normal View History

2023-02-06 02:33:09 +00:00
import logging
import shutil
import sys
2023-02-06 02:33:09 +00:00
import tkinter.filedialog
from collections import defaultdict
2023-02-06 02:33:09 +00:00
from pathlib import Path
from typing import Optional
import click
from ruamel.yaml import YAML
from devine.core.config import Config, config
from devine.core.console import console
2023-02-06 02:33:09 +00:00
from devine.core.constants import context_settings
from devine.core.credential import Credential
@click.group(
short_help="Manage cookies and credentials for profiles of services.",
context_settings=context_settings)
@click.pass_context
def auth(ctx: click.Context) -> None:
"""Manage cookies and credentials for profiles of services."""
ctx.obj = logging.getLogger("auth")
@auth.command(
name="list",
short_help="List profiles and their state for a service or all services.",
context_settings=context_settings)
@click.argument("service", type=str, required=False)
def list_(service: Optional[str] = None) -> None:
2023-02-06 02:33:09 +00:00
"""
List profiles and their state for a service or all services.
\b
Profile and Service names are case-insensitive.
"""
service_f = service
auth_data: dict[str, dict[str, list]] = defaultdict(lambda: defaultdict(list))
if config.directories.cookies.exists():
for cookie_dir in config.directories.cookies.iterdir():
service = cookie_dir.name
for cookie in cookie_dir.glob("*.txt"):
if cookie.stem not in auth_data[service]:
auth_data[service][cookie.stem].append("Cookie")
2023-02-06 02:33:09 +00:00
for service, credentials in config.credentials.items():
for profile in credentials:
auth_data[service][profile].append("Credential")
for service, profiles in dict(sorted(auth_data.items())).items(): # type:ignore
2023-02-06 02:33:09 +00:00
if service_f and service != service_f.upper():
continue
console.log(service)
for profile, authorizations in dict(sorted(profiles.items())).items():
console.log(f' "{profile}": {", ".join(authorizations)}')
2023-02-06 02:33:09 +00:00
@auth.command(
short_help="View profile cookies and credentials for a service.",
context_settings=context_settings)
@click.argument("profile", type=str)
@click.argument("service", type=str)
@click.pass_context
def view(ctx: click.Context, profile: str, service: str) -> None:
"""
View profile cookies and credentials for a service.
\b
Profile and Service names are case-sensitive.
"""
log = ctx.obj
service_f = service
profile_f = profile
found = False
for cookie_dir in config.directories.cookies.iterdir():
if cookie_dir.name == service_f:
for cookie in cookie_dir.glob("*.txt"):
if cookie.stem == profile_f:
console.log(f"Cookie: {cookie}")
2023-02-06 02:33:09 +00:00
log.debug(cookie.read_text(encoding="utf8").strip())
found = True
break
for service, credentials in config.credentials.items():
if service == service_f:
for profile, credential in credentials.items():
if profile == profile_f:
console.log(f"Credential: {':'.join(list(credential))}")
2023-02-06 02:33:09 +00:00
found = True
break
if not found:
raise click.ClickException(
f"Could not find Profile '{profile_f}' for Service '{service_f}'."
f"\nThe profile and service values are case-sensitive."
)
@auth.command(
short_help="Check what profile is used by services.",
context_settings=context_settings)
@click.argument("service", type=str, required=False)
def status(service: Optional[str] = None) -> None:
2023-02-06 02:33:09 +00:00
"""
Check what profile is used by services.
\b
Service names are case-sensitive.
"""
found_profile = False
for service_, profile in config.profiles.items():
if not service or service_.upper() == service.upper():
console.log(f"{service_}: {profile or '--'}")
2023-02-06 02:33:09 +00:00
found_profile = True
if not found_profile:
console.log(f"No profile has been explicitly set for {service}")
2023-02-06 02:33:09 +00:00
default = config.profiles.get("default", "not set")
console.log(f"The default profile is {default}")
2023-02-06 02:33:09 +00:00
@auth.command(
short_help="Delete a profile and all of its authorization from a service.",
context_settings=context_settings)
@click.argument("profile", type=str)
@click.argument("service", type=str)
@click.option("--cookie", is_flag=True, default=False, help="Only delete the cookie.")
@click.option("--credential", is_flag=True, default=False, help="Only delete the credential.")
def delete(profile: str, service: str, cookie: bool, credential: bool):
2023-02-06 02:33:09 +00:00
"""
Delete a profile and all of its authorization from a service.
\b
By default this does remove both Cookies and Credentials.
You may remove only one of them with --cookie or --credential.
\b
Profile and Service names are case-sensitive.
Comments may be removed from config!
"""
service_f = service
profile_f = profile
found = False
if not credential:
for cookie_dir in config.directories.cookies.iterdir():
if cookie_dir.name == service_f:
for cookie_ in cookie_dir.glob("*.txt"):
if cookie_.stem == profile_f:
cookie_.unlink()
console.log(f"Deleted Cookie: {cookie_}")
2023-02-06 02:33:09 +00:00
found = True
break
if not cookie:
for key, credentials in config.credentials.items():
if key == service_f:
for profile, credential_ in credentials.items():
if profile == profile_f:
config_path = Config._Directories.user_configs / Config._Filenames.root_config
yaml, data = YAML(), None
yaml.default_flow_style = False
data = yaml.load(config_path)
del data["credentials"][key][profile_f]
yaml.dump(data, config_path)
console.log(f"Deleted Credential: {credential_}")
2023-02-06 02:33:09 +00:00
found = True
break
if not found:
raise click.ClickException(
f"Could not find Profile '{profile_f}' for Service '{service_f}'."
f"\nThe profile and service values are case-sensitive."
)
@auth.command(
short_help="Add a Credential and/or Cookies to an existing or new profile for a service.",
context_settings=context_settings)
@click.argument("profile", type=str)
@click.argument("service", type=str)
@click.option("--cookie", type=str, default=None, help="Direct path to Cookies to add.")
@click.option("--credential", type=str, default=None, help="Direct Credential string to add.")
@click.pass_context
def add(ctx: click.Context, profile: str, service: str, cookie: Optional[str] = None, credential: Optional[str] = None):
"""
Add a Credential and/or Cookies to an existing or new profile for a service.
\b
Cancel the Open File dialogue when presented if you do not wish to provide
cookies. The Credential should be in `Username:Password` form. The username
may be an email. If you do not wish to add a Credential, just hit enter.
\b
Profile and Service names are case-sensitive!
Comments may be removed from config!
"""
log = ctx.obj
service = service.upper()
profile = profile.lower()
if cookie:
cookie = Path(cookie)
2023-02-14 21:56:55 +01:00
if not cookie.is_file():
log.error(f"No such file or directory: {cookie}.")
sys.exit(1)
2023-02-06 02:33:09 +00:00
else:
print("Opening File Dialogue, select a Cookie file to import.")
cookie = tkinter.filedialog.askopenfilename(
title="Select a Cookie file (Cancel to skip)",
filetypes=[("Cookies", "*.txt"), ("All files", "*.*")]
)
if cookie:
cookie = Path(cookie)
else:
console.log("Skipped adding a Cookie...")
2023-02-06 02:33:09 +00:00
if credential:
try:
credential = Credential.loads(credential)
except ValueError as e:
raise click.ClickException(str(e))
else:
credential = input("Credential: ")
if credential:
try:
credential = Credential.loads(credential)
except ValueError as e:
raise click.ClickException(str(e))
else:
console.log("Skipped adding a Credential...")
2023-02-06 02:33:09 +00:00
if cookie:
final_path = (config.directories.cookies / service / profile).with_suffix(".txt")
final_path.parent.mkdir(parents=True, exist_ok=True)
if final_path.exists():
log.error(f"A Cookie file for the Profile {profile} on {service} already exists.")
sys.exit(1)
shutil.move(cookie, final_path)
console.log(f"Moved Cookie file to: {final_path}")
2023-02-06 02:33:09 +00:00
if credential:
config_path = Config._Directories.user_configs / Config._Filenames.root_config
yaml, data = YAML(), None
yaml.default_flow_style = False
data = yaml.load(config_path)
if not data:
data = {}
if "credentials" not in data:
data["credentials"] = {}
if service not in data["credentials"]:
data["credentials"][service] = {}
2023-02-06 02:33:09 +00:00
data["credentials"][service][profile] = credential.dumps()
yaml.dump(data, config_path)
console.log(f"Added Credential: {credential}")