diff --git a/dashboard.py b/dashboard.py index 3c1c7e5f..1f95a943 100644 --- a/dashboard.py +++ b/dashboard.py @@ -11,6 +11,7 @@ import json import sys import time import tkinter as tk +from calendar import timegm from os.path import getsize, isdir, isfile, join from typing import Any, cast from watchdog.observers.api import BaseObserver @@ -182,7 +183,7 @@ class Dashboard(FileSystemEventHandler): if data: # Can be empty if polling while the file is being re-written entry = json.loads(data) # Status file is shared between beta and live. Filter out status not in this game session. - entry_timestamp = time.mktime(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) + entry_timestamp = timegm(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) if entry_timestamp >= self.session_start and self.status != entry: self.status = entry self.root.event_generate('<>', when="tail") diff --git a/loadout.py b/loadout.py index 2e2061a2..11e0f895 100644 --- a/loadout.py +++ b/loadout.py @@ -1,13 +1,17 @@ -"""Export ship loadout in Companion API json format.""" +""" +loadout.py - Export ship loadout in Companion API json format. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" +from __future__ import annotations import json -import os -import pathlib import re import time +from os import listdir from os.path import join -from typing import Optional - import companion import util_ships from config import config @@ -16,7 +20,7 @@ from EDMCLogging import get_main_logger logger = get_main_logger() -def export(data: companion.CAPIData, requested_filename: Optional[str] = None) -> None: +def export(data: companion.CAPIData, requested_filename: str | None = None) -> None: """ Write Ship Loadout in Companion API JSON format. @@ -32,15 +36,14 @@ def export(data: companion.CAPIData, requested_filename: Optional[str] = None) - with open(requested_filename, 'wt') as h: h.write(string) return - - elif not requested_filename: + if not requested_filename: logger.error(f"{requested_filename=} is not valid") return # Look for last ship of this type ship = util_ships.ship_file_name(data['ship'].get('shipName'), data['ship']['name']) - regexp = re.compile(re.escape(ship) + r'\.\d\d\d\d\-\d\d\-\d\dT\d\d\.\d\d\.\d\d\.txt') - oldfiles = sorted([x for x in os.listdir(config.get_str('outdir')) if regexp.match(x)]) + regexp = re.compile(re.escape(ship) + r'\.\d\d\d\d-\d\d-\d\dT\d\d\.\d\d\.\d\d\.txt') + oldfiles = sorted([x for x in listdir(config.get_str('outdir')) if regexp.match(x)]) if oldfiles: with open(join(config.get_str('outdir'), oldfiles[-1]), 'rU') as h: if h.read() == string: @@ -50,10 +53,9 @@ def export(data: companion.CAPIData, requested_filename: Optional[str] = None) - # Write - with open( - pathlib.Path(config.get_str('outdir')) / pathlib.Path( - ship + '.' + time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time)) + '.txt' - ), - 'wt' - ) as h: + output_directory = config.get_str('outdir') + ship_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time)) + file_path = join(output_directory, f"{ship}.{ship_time}.txt") + + with open(file_path, 'wt') as h: h.write(string) diff --git a/protocol.py b/protocol.py index cf3a9f75..f3b956d2 100644 --- a/protocol.py +++ b/protocol.py @@ -1,12 +1,17 @@ -"""protocol handler for cAPI authorisation.""" -# spell-checker: words ntdll GURL alloc wfile instantiatable pyright +""" +protocol.py - Protocol Handler for cAPI Auth. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" +from __future__ import annotations + import os import sys import threading -import urllib.error -import urllib.parse -import urllib.request -from typing import TYPE_CHECKING, Optional, Type +from urllib import parse +from typing import TYPE_CHECKING, Type from config import config from constants import appname, protocolhandler_redirect @@ -34,7 +39,7 @@ class GenericProtocolHandler: def __init__(self) -> None: self.redirect = protocolhandler_redirect # Base redirection URL self.master: 'tkinter.Tk' = None # type: ignore - self.lastpayload: Optional[str] = None + self.lastpayload: str | None = None def start(self, master: 'tkinter.Tk') -> None: """Start Protocol Handler.""" @@ -42,7 +47,6 @@ class GenericProtocolHandler: def close(self) -> None: """Stop / Close Protocol Handler.""" - pass def event(self, url: str) -> None: """Generate an auth event.""" @@ -75,7 +79,7 @@ if sys.platform == 'darwin' and getattr(sys, 'frozen', False): # noqa: C901 # i def start(self, master: 'tkinter.Tk') -> None: """Start Protocol Handler.""" GenericProtocolHandler.start(self, master) - self.lasturl: Optional[str] = None + self.lasturl: str | None = None self.eventhandler = EventHandler.alloc().init() def poll(self) -> None: @@ -106,12 +110,11 @@ if sys.platform == 'darwin' and getattr(sys, 'frozen', False): # noqa: C901 # i def handleEvent_withReplyEvent_(self, event, replyEvent) -> None: # noqa: N802 N803 # Required to override """Actual event handling from NSAppleEventManager.""" - protocolhandler.lasturl = urllib.parse.unquote( # noqa: F821: type: ignore # It's going to be a DPH in - # this code + protocolhandler.lasturl = parse.unquote( event.paramDescriptorForKeyword_(keyDirectObject).stringValue() ).strip() - protocolhandler.master.after(DarwinProtocolHandler.POLL, protocolhandler.poll) # noqa: F821 # type: ignore + protocolhandler.master.after(DarwinProtocolHandler.POLL, protocolhandler.poll) elif (config.auth_force_edmc_protocol @@ -124,9 +127,8 @@ elif (config.auth_force_edmc_protocol # This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces assert sys.platform == 'win32' # spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL - from ctypes import windll # type: ignore from ctypes import ( # type: ignore - POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at + windll, POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at ) from ctypes.wintypes import ( ATOM, BOOL, DWORD, HBRUSH, HGLOBAL, HICON, HINSTANCE, HMENU, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR, @@ -271,7 +273,7 @@ elif (config.auth_force_edmc_protocol def __init__(self) -> None: super().__init__() - self.thread: Optional[threading.Thread] = None + self.thread: threading.Thread | None = None def start(self, master: 'tkinter.Tk') -> None: """Start the DDE thread.""" @@ -342,7 +344,7 @@ elif (config.auth_force_edmc_protocol if args.lower().startswith('open("') and args.endswith('")'): logger.trace_if('frontier-auth.windows', f'args are: {args}') - url = urllib.parse.unquote(args[6:-2]).strip() + url = parse.unquote(args[6:-2]).strip() if url.startswith(self.redirect): logger.debug(f'Message starts with {self.redirect}') self.event(url) @@ -381,7 +383,7 @@ else: # Linux / Run from source if not os.getenv("EDMC_NO_UI"): logger.info(f'Web server listening on {self.redirect}') - self.thread: Optional[threading.Thread] = None + self.thread: threading.Thread | None = None def start(self, master: 'tkinter.Tk') -> None: """Start the HTTP server thread.""" @@ -420,15 +422,14 @@ else: # Linux / Run from source def parse(self) -> bool: """Parse a request.""" logger.trace_if('frontier-auth.http', f'Got message on path: {self.path}') - url = urllib.parse.unquote(self.path) + url = parse.unquote(self.path) if url.startswith('/auth'): logger.debug('Request starts with /auth, sending to protocolhandler.event()') - protocolhandler.event(url) # noqa: F821 + protocolhandler.event(url) self.send_response(200) return True - else: - self.send_response(404) # Not found - return False + self.send_response(404) # Not found + return False def do_HEAD(self) -> None: # noqa: N802 # Required to override """Handle HEAD Request.""" @@ -440,14 +441,37 @@ else: # Linux / Run from source if self.parse(): self.send_header('Content-Type', 'text/html') self.end_headers() - self.wfile.write('Authentication successful'.encode('utf-8')) - self.wfile.write('

Authentication successful

'.encode('utf-8')) + self.wfile.write(self._generate_auth_response().encode('utf-8')) else: + self.send_response(404) self.end_headers() def log_request(self, code: int | str = '-', size: int | str = '-') -> None: """Override to prevent logging.""" - pass + + def _generate_auth_response(self) -> str: + """ + Generate the authentication response HTML. + + :return: The HTML content of the authentication response. + """ + return ( + '' + '' + 'Authentication successful - Elite: Dangerous' + '' + '' + '' + '

Authentication successful

' + '

Thank you for authenticating.

' + '

Please close this browser tab now.

' + '' + '' + ) def get_handler_impl() -> Type[GenericProtocolHandler]: @@ -459,14 +483,13 @@ def get_handler_impl() -> Type[GenericProtocolHandler]: if sys.platform == 'darwin' and getattr(sys, 'frozen', False): return DarwinProtocolHandler # pyright: reportUnboundVariable=false - elif ( + if ( (sys.platform == 'win32' and config.auth_force_edmc_protocol) or (getattr(sys, 'frozen', False) and not is_wine and not config.auth_force_localserver) ): return WindowsProtocolHandler - else: - return LinuxProtocolHandler + return LinuxProtocolHandler # *late init* singleton diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 1d9749ac..24470a93 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -1,5 +1,9 @@ """ -A clickable ttk label for HTTP links. +ttkHyperlinkLabel.py - Clickable ttk labels. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. In addition to standard ttk.Label arguments, takes the following arguments: url: The URL as a string that the user will be sent to on clicking on @@ -14,6 +18,8 @@ In addition to standard ttk.Label arguments, takes the following arguments: May be imported by plugins """ +from __future__ import annotations + import sys import tkinter as tk import webbrowser @@ -26,14 +32,22 @@ if TYPE_CHECKING: # FIXME: Split this into multi-file module to separate the platforms -class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object): # type: ignore +class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # type: ignore """Clickable label for HTTP links.""" def __init__(self, master: tk.Frame | None = None, **kw: Any) -> None: - self.url = 'url' in kw and kw.pop('url') or None + """ + Initialize the HyperlinkLabel. + + :param master: The master widget. + :param kw: Additional keyword arguments. + """ + self.font_u: tk_font.Font + self.font_n = None + self.url = kw.pop('url', None) self.popup_copy = kw.pop('popup_copy', False) self.underline = kw.pop('underline', None) # override ttk.Label's underline - self.foreground = kw.get('foreground') or 'blue' + self.foreground = kw.get('foreground', 'blue') self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup( 'TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option @@ -44,11 +58,11 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) tk.Label.__init__(self, master, **kw) else: - ttk.Label.__init__(self, master, **kw) # type: ignore + ttk.Label.__init__(self, master, **kw) self.bind('', self._click) - self.menu = tk.Menu(None, tearoff=tk.FALSE) + self.menu = tk.Menu(tearoff=tk.FALSE) # LANG: Label for 'Copy' as in 'Copy and Paste' self.menu.add_command(label=_('Copy'), command=self.copy) # As in Copy and Paste self.bind(sys.platform == 'darwin' and '' or '', self._contextmenu) @@ -74,29 +88,30 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) setattr(self, thing, kw[thing]) # Emulate disabledforeground option for ttk.Label - if kw.get('state') == tk.DISABLED: - if 'foreground' not in kw: + if 'state' in kw: + state = kw['state'] + if state == tk.DISABLED and 'foreground' not in kw: kw['foreground'] = self.disabledforeground - elif 'state' in kw: - if 'foreground' not in kw: + elif state != tk.DISABLED and 'foreground' not in kw: kw['foreground'] = self.foreground if 'font' in kw: self.font_n = kw['font'] self.font_u = tk_font.Font(font=self.font_n) self.font_u.configure(underline=True) - kw['font'] = self.underline is True and self.font_u or self.font_n + kw['font'] = self.font_u if self.underline is True else self.font_n if 'cursor' not in kw: - if (kw['state'] if 'state' in kw else str(self['state'])) == tk.DISABLED: + state = kw.get('state', str(self['state'])) + if state == tk.DISABLED: kw['cursor'] = 'arrow' # System default elif self.url and (kw['text'] if 'text' in kw else self['text']): - kw['cursor'] = sys.platform == 'darwin' and 'pointinghand' or 'hand2' + kw['cursor'] = 'pointinghand' if sys.platform == 'darwin' else 'hand2' else: - kw['cursor'] = (sys.platform == 'darwin' and 'notallowed') or ( - sys.platform == 'win32' and 'no') or 'circle' + kw['cursor'] = 'notallowed' if sys.platform == 'darwin' else ( + 'no' if sys.platform == 'win32' else 'circle') - return super(HyperlinkLabel, self).configure(cnf, **kw) + return super().configure(cnf, **kw) def __setitem__(self, key: str, value: Any) -> None: """ @@ -105,15 +120,15 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) :param key: option name :param value: option value """ - self.configure(None, **{key: value}) + self.configure(**{key: value}) def _enter(self, event: tk.Event) -> None: if self.url and self.underline is not False and str(self['state']) != tk.DISABLED: - super(HyperlinkLabel, self).configure(font=self.font_u) + super().configure(font=self.font_u) def _leave(self, event: tk.Event) -> None: if not self.underline: - super(HyperlinkLabel, self).configure(font=self.font_n) + super().configure(font=self.font_n) def _click(self, event: tk.Event) -> None: if self.url and self['text'] and str(self['state']) != tk.DISABLED: