mirror of
https://github.com/devine-dl/devine.git
synced 2025-05-04 19:49:44 +00:00
I've moved log.info calls to console.log calls to reduce conflicts of logs at the same time as console refreshes, but also to reduce unnecessary log level text being printed to the console. We don't need to know if a log is an `info` level log, but I've kept log.error's and such as we would want to know if a log is an error log and such.
262 lines
9.3 KiB
Python
262 lines
9.3 KiB
Python
import logging
|
|
import shutil
|
|
import sys
|
|
import tkinter.filedialog
|
|
from collections import defaultdict
|
|
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
|
|
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:
|
|
"""
|
|
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")
|
|
|
|
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
|
|
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)}')
|
|
|
|
|
|
@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}")
|
|
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))}")
|
|
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:
|
|
"""
|
|
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 '--'}")
|
|
found_profile = True
|
|
|
|
if not found_profile:
|
|
console.log(f"No profile has been explicitly set for {service}")
|
|
|
|
default = config.profiles.get("default", "not set")
|
|
console.log(f"The default profile is {default}")
|
|
|
|
|
|
@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):
|
|
"""
|
|
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_}")
|
|
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_}")
|
|
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)
|
|
if not cookie.is_file():
|
|
log.error(f"No such file or directory: {cookie}.")
|
|
sys.exit(1)
|
|
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...")
|
|
|
|
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...")
|
|
|
|
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}")
|
|
|
|
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] = {}
|
|
data["credentials"][service][profile] = credential.dumps()
|
|
yaml.dump(data, config_path)
|
|
console.log(f"Added Credential: {credential}")
|