def test_localization(self): english_lines = localization.access_localization_data()['English'] num_lines_total = len(english_lines) - 4 incorrect_hashes = [] test_text = "This text isn't in the localization files" meta_keys = ('name_localized', 'code', 'credits', 'notes') for key in english_lines: test_key = localization.hash_text( localization.access_localization_data()['English'][key]) if key != test_key and key not in meta_keys: incorrect_hashes.append( (key, test_key, localization.access_localization_data()['English'][key])) self.assertEqual(incorrect_hashes, []) for language in localization.langs: localizer = localization.Localizer(language=language, persist_missing=False) self.assertEqual( repr(localizer), f'localization.Localizer ({language}, appending=False, 0 missing lines)' ) num_equal_lines = 0 for key_english in english_lines: if key_english in meta_keys: continue line_english = localization.access_localization_data( )['English'][key_english] line_localized = localizer.text(line_english) try: self.assertNotEqual(line_localized, "") self.assertEqual(line_localized.count('{0}'), line_english.count('{0}')) self.assertEqual(line_localized.count('{1}'), line_english.count('{1}')) except AssertionError as error: raise AssertionError( f"{error}\n{line_english}\n{line_localized}") if line_localized == line_english: num_equal_lines += 1 if language == 'English': self.assertEqual(num_equal_lines, num_lines_total) else: self.assertLess(num_equal_lines, num_lines_total / 4) self.assertEqual(localizer.text(test_text), test_text) db = utils.access_db() self.assertTrue(test_text in db['missing_localization']) db['missing_localization'] = [] utils.access_db(write=db)
def receive_update_check( self, raise_exceptions: bool = False) -> Optional[Tuple[str, str, str]]: import requests self.checked_response = True try: result = self.api_future.result() except (requests.Timeout, requests.exceptions.ReadTimeout): self.log.error(f"Update check timed out", reportable=False) if raise_exceptions: raise except (requests.RequestException, requests.ConnectionError): self.log.error( f"Connection error in updater: {traceback.format_exc()}", reportable=False) if raise_exceptions: raise except Exception: self.log.error( f"Non-connection based update error: {traceback.format_exc()}") else: self.log.debug( f"Update check took {round(result.elapsed.microseconds / 1000000, 3)} seconds" ) response: dict = result.json() self.api_future = None try: newest_version: str = response['tag_name'] downloads_url: str = response['html_url'] changelog: str = response['body'] except KeyError: if 'message' in response and 'API rate limit exceeded' in response[ 'message']: rate_limit_message: str = f"Github {response['message'].split('(')[0][:-1]}" raise RateLimitError(rate_limit_message) else: raise changelog_formatted: str = format_changelog(changelog) if launcher.VERSION == newest_version: self.log.debug(f"Up to date ({launcher.VERSION})") else: # out of date self.log.error( f"Out of date, newest version is {newest_version} (this is {launcher.VERSION})", reportable=False) # save available version for the launcher db: Dict[str, Union[bool, list, str]] = utils.access_db() db['available_version'] = newest_version utils.access_db(db) return newest_version, downloads_url, changelog_formatted
def exc_already_reported(tb: str) -> bool: try: tb_hash: str = str(zlib.crc32(tb.encode('UTF8', errors='replace'))) # technically not a hash but w/e db: dict = utils.access_db() if tb_hash in db['tb_hashes']: return True else: db['tb_hashes'].append(tb_hash) utils.access_db(db) return False except Exception: return False
def error(self, message_in: str, reportable: bool = True): if self.log_level_allowed('Error'): self.write_log('ERROR', message_in, use_errors_file=reportable) if reportable and settings.get('sentry_level') == 'All errors': db: Dict[str, Union[bool, list, str]] = utils.access_db() message_hash: int = zlib.adler32(message_in.encode('UTF8')) if message_hash not in db[ 'error_hashes'] and message_hash not in self.local_error_hashes: self.local_error_hashes.append(message_hash) sentry_sdk.capture_message(message_in[-512:]) db['error_hashes'].append(message_hash) utils.access_db(write=db) else: self.debug( "Not reporting the error (has already been reported)")
def out_of_date_warning() -> str: try: available_version: str = utils.access_db()['available_version'] if available_version: return f"\n\nBTW an newer version for TF2 Rich Presence is available ({available_version}), which may have fixed this crash." else: return "" except Exception: return ""
def close_window(self): try: db: Dict[str, Union[bool, list, str]] = utils.access_db() # save position regardless of preserve_window_pos setting save_pos = get_window_center(self.master) db['gui_position'] = list(save_pos) utils.access_db(db) self.log.info( f"Closing main window and exiting program (saving pos as {save_pos}, loop body time stats are {min(self.main_loop_body_times)} min, " f"{statistics.median(self.main_loop_body_times)} median, {max(self.main_loop_body_times)} max)" ) self.master.destroy() except Exception: pass # we really do need the program to close now if self.main_controlled: self.alive = False # this makes main raise SystemExit ASAP else: del self.log raise SystemExit
def __init__(self, log: Optional[logger.Log] = None, language: Optional[str] = None, appending: bool = False, persist_missing: bool = True): self.log: Optional[logger.Log] = log self.language: str = language if language else settings.get('language') self.appending: bool = appending # if extending localization files self.text.cache_clear() self.missing_lines: List[str] = utils.access_db( )['missing_localization'] if persist_missing else []
def detect_system_language(log: logger.Log): db: Dict[str, Union[bool, list, str]] = utils.access_db() if not db['has_asked_language']: language_codes: dict = { read_localization_files()[lang]['code']: lang for lang in langs[1:] } system_locale: str = locale.windows_locale[ ctypes.windll.kernel32.GetUserDefaultUILanguage()] system_language_code: str = system_locale.split('_')[0] is_brazilian_port: bool = system_locale == 'pt_BR' if system_language_code in language_codes or is_brazilian_port: system_language: str = language_codes[system_language_code] can_localize: bool = True else: if system_language_code != 'en': log.error(f"System locale {system_locale} is not localizable") can_localize = False if can_localize and settings.get('language') != system_language: log.info( f"System language ({system_locale}, {system_language}) != settings language ({settings.get('language')}), asking to change" ) db['has_asked_language'] = True utils.access_db(db) system_language_display: str = 'Português Brasileiro' if is_brazilian_port else system_language # this is intentionally not localized changed_language: str = messagebox.askquestion( f"TF2 Rich Presence {launcher.VERSION}", f"Change language to your system's default ({system_language_display})?" ) log.debug(f"Changed language: {changed_language}") if changed_language == 'yes': settings.change('language', system_language)
def text(self, english_text: str) -> str: # TODO: use manual language keys instead of text hashes (maybe) english_text_adler32: str = hash_text(english_text) if self.appending: # only used for development access_localization_data(append=(english_text_adler32, english_text)) return english_text if english_text_adler32 not in access_localization_data()[ self.language]: if english_text not in self.missing_lines: self.missing_lines.append(english_text) db: Dict[str, Union[bool, list, str]] = utils.access_db() db['missing_localization'].append(english_text) utils.access_db(db) if self.log: self.log.error( f"\"{english_text}\" not in {self.language} localization", reportable=False) # no available translation, so must default to the English text return english_text if self.language == 'English': # returns what was passed to this function, NOT what's in English.json # this means that that file is only used for helping with translating return english_text else: try: return access_localization_data()[ self.language][english_text_adler32] except KeyError as error: raise KeyError( f"{error}, ({english_text_adler32}, {self.language})")
def __init__(self, log: logger.Log, main_controlled: bool = False): self.main_controlled: bool = main_controlled self.log: logger.Log = log self.log.info("Initializing main GUI") self.loc: localization.Localizer = localization.Localizer(self.log) self.master: tk.Tk = tk.Tk() tk.Frame.__init__(self, self.master) self.pack(fill=tk.BOTH, expand=1, padx=0, pady=0) self.alive: bool = True self.scale: float = settings.get('gui_scale') / 100 self.size: Tuple[int, int] = (round(500 * self.scale), round(250 * self.scale)) self.master.geometry(f'{self.size[0]}x{self.size[1] + 20}' ) # the +20 is for the menu bar self.master.title( self.loc.text("TF2 Rich Presence").format(launcher.VERSION)) set_window_icon(self.log, self.master, False) self.master.resizable(False, False) # disables resizing self.master.protocol('WM_DELETE_WINDOW', self.close_window) self.log.debug("Set up main window") # misc stuff that doesn't go anywhere else default_bg: ImageTk.PhotoImage = ImageTk.PhotoImage( Image.new('RGB', self.size)) font_size: int = round(12 * self.scale) self.blank_image: ImageTk.PhotoImage = ImageTk.PhotoImage( Image.new('RGBA', (1, 1), color=(0, 0, 0, 0))) self.paused_image: ImageTk.PhotoImage = ImageTk.PhotoImage( Image.new('RGBA', self.size, (0, 0, 0, 128))) self.vignette: Image = self.load_image('vignette').resize(self.size) self.clean_console_log: bool = False self.text_state: Tuple[str, ...] = ('', ) self.bg_state: Tuple[str, int, int] = ('', 0, 0) self.fg_state: str = '' self.class_state: str = '' self.available_update_data: Tuple[str, str, str] = ('', '', '') self.update_window_open: bool = False self.bottom_text_state: Dict[str, bool] = { 'discord': False, 'kataiser': False, 'queued': False, 'holiday': False } self.bottom_text_queue_state: str = "" self.holiday_text: str = "" self.launched_tf2_with_button: bool = False self.tf2_launch_cmd: Optional[Tuple[str, str]] = None self.main_loop_body_times: List[float] = [] self.update_checker: updater.UpdateChecker = updater.UpdateChecker( self.log) self.console_log_path: Optional[str] = None menu_bar: tk.Menu = tk.Menu(self.master) self.file_menu = tk.Menu(menu_bar, tearoff=0) help_menu = tk.Menu(menu_bar, tearoff=0) menu_bar.add_cascade(label=self.loc.text("File"), menu=self.file_menu) menu_bar.add_cascade(label=self.loc.text("Help"), menu=help_menu) self.file_menu.add_command(label=self.loc.text("Change settings"), command=self.menu_open_settings, accelerator="Ctrl+S") self.file_menu.add_command( label=self.loc.text("Restore default settings"), command=self.menu_restore_defaults) self.file_menu.add_command(label=self.loc.text("Open console.log"), command=self.menu_open_console_log, state=tk.DISABLED) self.file_menu.add_command( label=self.loc.text("Trim/clean console.log"), command=self.menu_clean_console_log, state=tk.DISABLED) self.file_menu.add_command(label=self.loc.text("Open save directory"), command=self.menu_open_save_directory) self.file_menu.add_command(label=self.loc.text("Exit"), command=self.menu_exit, accelerator="Ctrl+Q") self.console_log_command_indices: Tuple[int, int] = (2, 3) help_menu.add_command(label=self.loc.text("Open Github page"), command=self.menu_open_github) help_menu.add_command(label=self.loc.text("Open readme"), command=self.menu_open_readme) help_menu.add_command(label=self.loc.text("Open changelog"), command=self.menu_open_changelog) help_menu.add_command(label=self.loc.text("Open license"), command=self.menu_open_license) help_menu.add_separator() help_menu.add_command(label=self.loc.text("Check for updates"), command=self.menu_check_updates) help_menu.add_command(label=self.loc.text("Report bug/issue"), command=self.menu_report_issue) help_menu.add_command(label=self.loc.text("About"), command=self.menu_about, accelerator="Ctrl+A") self.master.config(menu=menu_bar) self.bind_all('<Control-s>', self.menu_open_settings) self.bind_all('<Control-q>', self.menu_exit) self.bind_all('<Control-a>', self.menu_about) self.log.debug("Created menu bar") previous_gui_position: List[int] = utils.access_db()['gui_position'] if previous_gui_position == [ 0, 0 ] or not settings.get('preserve_window_pos'): # center the window on the screen window_x: int = round(self.winfo_screenwidth() / 2) window_y: int = round(self.winfo_screenheight() / 2) - 40 pos_window_by_center(self.master, window_x, window_y) self.log.debug( f"Window position: {(window_x, window_y)} (centered)") else: # remember previous position window_x, window_y = previous_gui_position pos_window_by_center(self.master, window_x - 8, window_y - 41) self.log.debug( f"Window position: {(window_x, window_y)} (remembered)") # create the main drawing canvas and its elements self.canvas: tk.Canvas = tk.Canvas(width=self.size[0], height=self.size[1], borderwidth=0, highlightthickness=0) self.bg_image: int = self.canvas.create_image(0, 0, anchor=tk.NW, image=default_bg) self.bg_rect: int = self.canvas.create_image(0, 0, anchor=tk.NW) self.fg_shadow: int = self.canvas.create_image(65 * self.scale, 45 * self.scale, anchor=tk.NW) self.fg_image: int = self.canvas.create_image(85 * self.scale, 65 * self.scale, anchor=tk.NW) self.class_image: int = self.canvas.create_image(90 * self.scale, 180 * self.scale, anchor=tk.CENTER) self.text_1: int = self.canvas.create_text(358 * self.scale, 110 * self.scale, font=('TkDefaultFont', font_size), fill='white', anchor=tk.CENTER) self.text_3_0: int = self.canvas.create_text(220 * self.scale, 105 * self.scale, font=('TkDefaultFont', font_size), fill='white', anchor=tk.W) self.text_3_1: int = self.canvas.create_text(220 * self.scale, 125 * self.scale, font=('TkDefaultFont', font_size), fill='white', anchor=tk.W) self.text_3_2: int = self.canvas.create_text(220 * self.scale, 145 * self.scale, font=('TkDefaultFont', font_size), fill='gray', anchor=tk.W) self.text_4_0: int = self.canvas.create_text(220 * self.scale, 95 * self.scale, font=('TkDefaultFont', font_size), fill='white', anchor=tk.W) self.text_4_1: int = self.canvas.create_text(220 * self.scale, 115 * self.scale, font=('TkDefaultFont', font_size), fill='white', anchor=tk.W) self.text_4_2: int = self.canvas.create_text(220 * self.scale, 135 * self.scale, font=('TkDefaultFont', font_size), fill='white', anchor=tk.W) self.text_4_3: int = self.canvas.create_text(220 * self.scale, 155 * self.scale, font=('TkDefaultFont', font_size), fill='gray', anchor=tk.W) self.update_text: int = self.canvas.create_text(467 * self.scale, 20 * self.scale, font=('TkDefaultFont', font_size), fill='light gray', anchor=tk.E) self.update_icon: int = self.canvas.create_image(492 * self.scale, 8 * self.scale, anchor=tk.NE) self.paused_overlay: int = self.canvas.create_image(0, 0, anchor=tk.NW) self.paused_text: int = self.canvas.create_text( 5 * self.scale, 5 * self.scale, font=('TkDefaultFont', round(14 * self.scale)), fill='white', anchor=tk.NW) self.bottom_text: int = self.canvas.create_text( 2 * self.scale, 248 * self.scale, font=('TkDefaultFont', round(10 * self.scale)), fill='light gray', anchor=tk.SW) self.canvas.tag_bind(self.update_text, '<Button-1>', self.show_update_menu) self.canvas.tag_bind(self.update_icon, '<Button-1>', self.show_update_menu) self.canvas.place(x=0, y=0) self.log.debug("Created canvas elements") self.launch_tf2_button: tk.Button = tk.Button( self.master, text=self.loc.text("Launch TF2"), font=('TkDefaultFont', round(9 * self.scale)), command=self.launch_tf2) self.safe_update() self.window_dimensions = self.master.winfo_width( ), self.master.winfo_height() self.log.debug( f"Window size: {self.window_dimensions} (scale {self.scale})") # move window to the top (but don't keep it there) self.master.lift() self.master.attributes('-topmost', True) self.master.after_idle(self.master.attributes, '-topmost', False) self.log.debug("Finished creating GUI")
def __init__(self, path: Optional[str] = None): # make sure there's actually somewhere to put the log if os.path.isdir('logs'): self.logs_path: str = 'logs' created_logs_dir: bool = False else: self.logs_path = os.path.join(os.getenv('APPDATA'), 'TF2 Rich Presence', 'logs') if not os.path.isdir(self.logs_path): os.mkdir( os.path.join(os.getenv('APPDATA'), 'TF2 Rich Presence')) os.mkdir(self.logs_path) time.sleep(0.1) # ensure it gets created created_logs_dir = True else: created_logs_dir = False # find user's pc and account name user_pc_name: str = socket.gethostname() try: user_identifier: str = getpass.getuser() except ModuleNotFoundError: user_identifier = user_pc_name if path: self.filename: str = path else: existing_logs: List[str] = sorted(os.listdir(self.logs_path)) log_index: int = 0 while True: filename: str = f'TF2RP_{user_pc_name}_{user_identifier}_{launcher.VERSION}_{log_index}.log' log_index += 1 if filename not in existing_logs and f'{filename}.gz' not in existing_logs: break self.filename = os.path.join(self.logs_path, filename) # setup self.filename_errors: str = os.path.join( self.logs_path, f'TF2RP_{user_pc_name}_{user_identifier}_{launcher.VERSION}.errors.log' ) self.last_log_time: float = time.perf_counter() self.to_stderr: bool = launcher.DEBUG self.force_disabled: bool = False self.log_levels: List[str] = [ 'Debug', 'Info', 'Error', 'Critical', 'Off' ] self.local_error_hashes: List[int] = [] # just in case DB.json breaks if self.enabled(): self.log_file: TextIO = open(self.filename, 'a', encoding='UTF8') if created_logs_dir: self.debug( f"Created logs folder at {os.path.abspath(self.logs_path)}" ) try: utils.access_db(write=utils.access_db(), pass_permission_error=False) except PermissionError: self.error( "DB.json can't be written to, due to permissions. This could cause crashes" ) self.debug(f"Created {repr(self)}")