diff --git a/docs/ubuntu.png b/docs/ubuntu.png old mode 100755 new mode 100644 index cd80444..67951a5 Binary files a/docs/ubuntu.png and b/docs/ubuntu.png differ diff --git a/docs/windows.png b/docs/windows.png old mode 100755 new mode 100644 index d4ed323..f13f4c0 Binary files a/docs/windows.png and b/docs/windows.png differ diff --git a/setup.py b/setup.py index fb80363..62c7fa4 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,9 @@ version = main.__version__ + '.0' if system() == 'Windows': - MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python', 'pydenticon'] + MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python', 'pydenticon', 'cv2'] else: - MODULES = [] + MODULES = ['pydenticon'] try: import pyaudio except ImportError: @@ -32,9 +32,9 @@ else: except ImportError: MODULES.append('opencv-python') try: - import pydenticon + import coloredlogs except ImportError: - MODULES.append('pydenticon') + MODULES.append('coloredlogs') def get_packages(): @@ -84,6 +84,9 @@ setup(name='Toxygen', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], entry_points={ 'console_scripts': ['toxygen=toxygen.main:main'] diff --git a/toxygen/app.py b/toxygen/app.py index a23816d..43ae756 100644 --- a/toxygen/app.py +++ b/toxygen/app.py @@ -1,12 +1,91 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os +import sys +import traceback +from random import shuffle +import threading +from time import sleep + +from gevent import monkey; monkey.patch_all(); del monkey # noqa +import gevent + +import tests.support_testing as ts +from user_data import settings + +IDLE_PERIOD = 0.10 + +try: + import coloredlogs + if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ: + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' + # https://pypi.org/project/coloredlogs/ +except ImportError as e: + coloredlogs = False + +global LOG +import logging +LOG = logging.getLogger('app') + +def setup_logging(oArgs): + global LOG + if coloredlogs: + aKw = dict(level=oArgs.loglevel, + logger=LOG, + fmt='%(name)s %(levelname)s %(message)s') + if oArgs.logfile: + oFd = open(oArgs.logfile, 'wt') + setattr(oArgs, 'log_oFd', oFd) + aKw['stream'] = oFd + coloredlogs.install(**aKw) + + else: + aKw = dict(level=oArgs.loglevel, + format='%(name)s %(levelname)-4s %(message)s') + if oArgs.logfile: + aKw['filename'] = oArgs.logfile + logging.basicConfig(**aKw) + + if oArgs.logfile: + oHandler = logging.StreamHandler(stream=sys.stdout) + LOG.addHandler(oHandler) + + logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S') + logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' + logging._defaultFormatter.default_msec_format = '' + + LOG.setLevel(oArgs.loglevel) + LOG.trace = lambda l: LOG.log(0, repr(l)) + LOG.info(f"Setting loglevel to {oArgs.loglevel!s}") + + if oArgs.loglevel < 20: + # opencv debug + sys.OpenCV_LOADER_DEBUG = True + +#? with ignoreStderr(): for png +# silence logging PyQt5.uic.uiparser +logging.getLogger('PyQt5.uic').setLevel(logging.ERROR) +logging.getLogger('PyQt5.uic.uiparser').setLevel(logging.ERROR) +logging.getLogger('PyQt5.uic.properties').setLevel(logging.ERROR) + +from PyQt5 import QtWidgets, QtGui, QtCore +from qtpy.QtCore import QTimer +from qtpy.QtWidgets import QApplication + +try: + import qdarkstylexxx +except ImportError: + qdarkstyle = None + from middleware import threads import middleware.callbacks as callbacks -from PyQt5 import QtWidgets, QtGui, QtCore import ui.password_screen as password_screen import updater.updater as updater -import os from middleware.tox_factory import tox_factory import wrapper.toxencryptsave as tox_encrypt_save import user_data.toxes +from user_data import settings +from user_data.settings import get_user_config_path, merge_args_into_settings + from user_data.settings import Settings from ui.login_screen import LoginScreen from user_data.profile_manager import ProfileManager @@ -37,26 +116,85 @@ from contacts.group_peer_factory import GroupPeerFactory from user_data.backup_service import BackupService import styles.style # TODO: dynamic loading +from tests.support_testing import lLOCAL, lGOOD, lNEW, lRELAYS, inodeinfo_test +from tests.bootstrap_node_info import iNodeInfo +from tests.tests_socks import main as oTOX_OPTIONS, iMain, ToxOptions +global iI +iI = 0 + +sSTYLE = """ +.QWidget {font-family Helvetica;} +.QCheckBox { font-family Helvetica;} +.QComboBox { font-family Helvetica;} +.QGroupBox { font-family Helvetica;} +.QLabel {font-family Helvetica;} +.QLineEdit { font-family Helvetica;} +.QListWidget { font-family Helvetica;} +.QListWidgetItem { font-family Helvetica;} +.QMainWindow {font-family Helvetica;} +.QMenu {font-family Helvetica;} +.QMenuBar {font-family Helvetica;} +.QPlainText {font-family Courier; weight: 75;} +.QPlainTextEdit {font-family Courier;} +.QPushButton {font-family Helvetica;} +.QRadioButton { font-family Helvetica; } +.QText {font-family Courier; weight: 75; } +.QTextBrowser {font-family Courier; weight: 75; } +.QTextSingleLine {font-family Courier; weight: 75; } +.QToolBar { font-weight: bold; } +""" +from copy import deepcopy class App: - def __init__(self, version, path_to_profile=None, uri=None): + def __init__(self, version, args): + global LOG + self._args = args + self._path = path_to_profile = args.profile + uri = args.uri + logfile = args.logfile + loglevel = args.loglevel + + setup_logging(args) + # sys.stderr.write( 'Command line args: ' +repr(oArgs) +'\n') + LOG.info("Command line: " +' '.join(sys.argv[1:])) + LOG.debug(f'oArgs = {args!r}') + LOG.info("Starting toxygen version " +version) + self._version = version - self._app = self._settings = self._profile_manager = self._plugin_loader = self._messenger = None + self._app = self._settings = self._profile_manager = None + self._plugin_loader = self._messenger = None self._tox = self._ms = self._init = self._main_loop = self._av_loop = None self._uri = self._toxes = self._tray = self._file_transfer_handler = self._contacts_provider = None - self._friend_factory = self._calls_manager = self._contacts_manager = self._smiley_loader = None + self._friend_factory = self._calls_manager = None + self._contacts_manager = self._smiley_loader = None self._group_peer_factory = self._tox_dns = self._backup_service = None self._group_factory = self._groups_service = self._profile = None if uri is not None and uri.startswith('tox:'): self._uri = uri[4:] - self._path = path_to_profile # ----------------------------------------------------------------------------------------------------------------- # Public methods # ----------------------------------------------------------------------------------------------------------------- - def main(self): + def set_trace(self): + LOG.debug('pdb.set_trace ') + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + import pdb; pdb.set_trace() + + def ten(self, i=0): + global iI + iI += 1 + if logging.getLogger('app').getEffectiveLevel() != 10: + sys.stderr.write('CHANGED '+str(logging.getLogger().level+'\n')) + LOG.setLevel(10) + LOG.root.setLevel(10) + logging.getLogger('app').setLevel(10) + #sys.stderr.write(f"ten '+str(iI)+' {i}"+' '+repr(LOG) +'\n') + #LOG.debug('ten '+str(iI)) + + def iMain(self): """ Main function of app. loads login screen if needed and starts main screen """ @@ -68,74 +206,154 @@ class App: self._load_base_style() - if not self._select_and_load_profile(): - return + encrypt_save = tox_encrypt_save.ToxEncryptSave() + self._toxes = user_data.toxes.ToxES(encrypt_save) + try: + # this throws everything as errors + if not self._select_and_load_profile(): + return 2 + if hasattr(self._args, 'update') and self._args.update: + if self._try_to_update(): return 3 - if self._try_to_update(): - return + self._load_app_styles() + if self._args.language != 'English': + # > /var/local/src/toxygen/toxygen/app.py(303)_load_app_translations()->None + # -> self._app.translator = translator + # (Pdb) Fatal Python error: Segmentation fault + self._load_app_translations() + self._create_dependencies() - self._load_app_styles() - self._load_app_translations() + self._start_threads(True) - self._create_dependencies() - self._start_threads() - - if self._uri is not None: - self._ms.add_contact(self._uri) + if self._uri is not None: + self._ms.add_contact(self._uri) + except Exception as e: + LOG.error(f"Error loading profile: {e!s}") + sys.stderr.write(' iMain(): ' +f"Error loading profile: {e!s}" \ + +'\n' + traceback.format_exc()+'\n') + util_ui.message_box(str(e), + util_ui.tr('Error loading profile')) + return 4 self._app.lastWindowClosed.connect(self._app.quit) + try: + self._execute_app() + self.quit() + retval = 0 + except KeyboardInterrupt: + retval = 0 + except Exception: + retval = 1 - self._execute_app() - - self._stop_app() + return retval # ----------------------------------------------------------------------------------------------------------------- # App executing # ----------------------------------------------------------------------------------------------------------------- def _execute_app(self): + LOG.debug("_execute_app") + while True: try: self._app.exec_() except Exception as ex: - util.log('Unhandled exception: ' + str(ex)) + LOG.error('Unhandled exception: ' + str(ex)) else: break + def quit(self, retval=0): + LOG.debug("quit") + oArgs = self._args + if hasattr(oArgs, 'log_oFd'): + oArgs.log_oFd.close() + delattr(oArgs, 'log_oFd') + + # failsafe: segfaults on exit + if hasattr(self, '_tox'): + if self._tox and hasattr(self._tox, 'kill'): + self._tox.kill() + del self._tox + + self._stop_app() + if hasattr(self, '_app'): + self._app.quit() + del self._app.quit + del self._app + raise SystemExit(retval) + def _stop_app(self): + LOG.debug("_stop_app") self._plugin_loader.stop() - self._stop_threads() - self._file_transfer_handler.stop() - self._tray.hide() + try: + self._stop_threads(is_app_closing=True) + except (Exception, RuntimeError): + # RuntimeError: cannot join current thread + pass + if hasattr(self, '_tray') and self._tray: + self._tray.hide() self._save_profile() self._settings.close() self._kill_toxav() self._kill_tox() + sys.stderr.write('_stop_app end' +'\n') # ----------------------------------------------------------------------------------------------------------------- # App loading # ----------------------------------------------------------------------------------------------------------------- def _load_base_style(self): - with open(util.join_path(util.get_styles_directory(), 'dark_style.qss')) as fl: - style = fl.read() + if self._args.theme in ['', 'default']: return + + if qdarkstyle: + LOG.debug("_load_base_style qdarkstyle " +self._args.theme) + # QDarkStyleSheet + if self._args.theme == 'light': + from qdarkstyle.light.palette import LightPalette + style = qdarkstyle.load_stylesheet(palette=LightPalette) + else: + from qdarkstyle.dark.palette import DarkPalette + style = qdarkstyle.load_stylesheet(palette=DarkPalette) + else: + LOG.debug("_load_base_style qss " +self._args.theme) + name = self._args.theme + '.qss' + with open(util.join_path(util.get_styles_directory(), name)) as fl: + style = fl.read() + style += '\n' +sSTYLE self._app.setStyleSheet(style) def _load_app_styles(self): + LOG.debug(f"_load_app_styles {list(settings.built_in_themes().keys())!r}") # application color scheme - if self._settings['theme'] == 'dark': - return - for theme in self._settings.built_in_themes().keys(): + if self._settings['theme'] in ['', 'default']: return + for theme in settings.built_in_themes().keys(): if self._settings['theme'] != theme: continue - theme_path = self._settings.built_in_themes()[theme] - file_path = util.join_path(util.get_styles_directory(), theme_path) - with open(file_path) as fl: - style = fl.read() + if qdarkstyle: + LOG.debug("_load_base_style qdarkstyle " +self._args.theme) + # QDarkStyleSheet + if self._args.theme == 'light': + from qdarkstyle.light.palette import LightPalette + style = qdarkstyle.load_stylesheet(palette=LightPalette) + else: + from qdarkstyle.dark.palette import DarkPalette + style = qdarkstyle.load_stylesheet(palette=DarkPalette) + else: + theme_path = settings.built_in_themes()[theme] + file_path = util.join_path(util.get_styles_directory(), theme_path) + if not os.path.isfile(file_path): + LOG.warn('_load_app_styles: no theme file ' + file_path) + continue + with open(file_path) as fl: + style = fl.read() + LOG.debug('_load_app_styles: loading theme file ' + file_path) + style += '\n' +sSTYLE self._app.setStyleSheet(style) + LOG.info('_load_app_styles: loaded theme ' +self._args.theme) break def _load_login_screen_translations(self): + LOG.debug("_load_login_screen_translations") current_language, supported_languages = self._get_languages() if current_language not in supported_languages: return @@ -146,47 +364,77 @@ class App: self._app.translator = translator def _load_icon(self): + LOG.debug("_load_icon") icon_file = os.path.join(util.get_images_directory(), 'icon.png') self._app.setWindowIcon(QtGui.QIcon(icon_file)) @staticmethod def _get_languages(): + LOG.debug("_get_languages") current_locale = QtCore.QLocale() curr_language = current_locale.languageToString(current_locale.language()) - supported_languages = Settings.supported_languages() + supported_languages = settings.supported_languages() return curr_language, supported_languages def _load_app_translations(self): - lang = Settings.supported_languages()[self._settings['language']] + LOG.debug("_load_app_translations") + lang = settings.supported_languages()[self._settings['language']] translator = QtCore.QTranslator() translator.load(os.path.join(util.get_translations_directory(), lang)) self._app.installTranslator(translator) self._app.translator = translator def _select_and_load_profile(self): - encrypt_save = tox_encrypt_save.ToxEncryptSave() - self._toxes = user_data.toxes.ToxES(encrypt_save) + LOG.debug("_select_and_load_profile: " +repr(self._path)) - if self._path is not None: # toxygen was started with path to profile - self._load_existing_profile(self._path) + if self._path is not None: + # toxygen was started with path to profile + try: + assert os.path.exists(self._path), self._path + self._load_existing_profile(self._path) + except Exception as e: + LOG.error('_load_existing_profile failed: ' + str(e)) + title = 'Loading the profile failed ' + if self._path: + title += os.path.basename(self._path) + text = 'Loading the profile failed - \n' +str(e) + if 'Dis' == 'Abled': + text += '\nLoading the profile failed - \n' \ + +str(e) +'\nContinue with a default profile?' + reply = util_ui.question(text, title) + if not reply: + LOG.debug('_load_existing_profile not continuing ') + raise + LOG.debug('_load_existing_profile continuing ') + # drop through + else: + util_ui.message_box(text, title) + raise else: auto_profile = Settings.get_auto_profile() if auto_profile is None: # no default profile + LOG.debug('_select_and_load_profile no default profile ') result = self._select_profile() if result is None: + LOG.debug('no selected profile ') return False if result.is_new_profile(): # create new profile if not self._create_new_profile(result.profile_path): + LOG.warn('no new profile ') return False + LOG.debug('created new profile ') else: # load existing profile self._load_existing_profile(result.profile_path) + # drop through self._path = result.profile_path else: # default profile + LOG.debug('loading default profile ') self._path = auto_profile self._load_existing_profile(auto_profile) - if Settings.is_active_profile(self._path): # profile is in use + if settings.is_active_profile(self._path): # profile is in use + LOG.warn(f"_select_and_load_profile active: {self._path}") profile_name = util.get_profile_name_from_path(self._path) title = util_ui.tr('Profile {}').format(profile_name) text = util_ui.tr( @@ -204,21 +452,31 @@ class App: # ----------------------------------------------------------------------------------------------------------------- def _start_threads(self, initial_start=True): + LOG.debug(f"_start_threads before: {threading.enumerate()!r}") # init thread - self._init = threads.InitThread(self._tox, self._plugin_loader, self._settings, initial_start) + self._init = threads.InitThread(self._tox, + self._plugin_loader, + self._settings, + self, + initial_start) self._init.start() + def te(): return [t.name for t in threading.enumerate()] + LOG.debug(f"_start_threads init: {te()!r}") # starting threads for tox iterate and toxav iterate self._main_loop = threads.ToxIterateThread(self._tox) self._main_loop.start() + self._av_loop = threads.ToxAVIterateThread(self._tox.AV) self._av_loop.start() if initial_start: threads.start_file_transfer_thread() + LOG.debug(f"_start_threads after: {[t.name for t in threading.enumerate()]!r}") def _stop_threads(self, is_app_closing=True): - self._init.stop_thread() + LOG.debug("_stop_threads") + self._init.stop_thread(1.0) self._av_loop.stop_thread() self._main_loop.stop_thread() @@ -226,34 +484,50 @@ class App: if is_app_closing: threads.stop_file_transfer_thread() + def iterate(self, n=100): + interval = self._tox.iteration_interval() + for i in range(n): + self._tox.iterate() + gevent.sleep(interval / 1000.0) + # ----------------------------------------------------------------------------------------------------------------- # Profiles # ----------------------------------------------------------------------------------------------------------------- def _select_profile(self): - self._load_login_screen_translations() + LOG.debug("_select_profile") + if self._args.language != 'English': + self._load_login_screen_translations() ls = LoginScreen() profiles = ProfileManager.find_profiles() ls.update_select(profiles) ls.show() self._app.exec_() - return ls.result def _load_existing_profile(self, profile_path): + LOG.info("_load_existing_profile " +repr(profile_path)) + assert os.path.exists(profile_path), profile_path self._profile_manager = ProfileManager(self._toxes, profile_path) data = self._profile_manager.open_profile() if self._toxes.is_data_encrypted(data): + LOG.debug("_entering password") data = self._enter_password(data) - self._settings = Settings(self._toxes, profile_path.replace('.tox', '.json')) - self._tox = self._create_tox(data) + LOG.debug("_entered password") + json_file = profile_path.replace('.tox', '.json') + assert os.path.exists(json_file), json_file + LOG.debug("creating _settings from: " +json_file) + self._settings = Settings(self._toxes, json_file, self) + self._tox = self._create_tox(data, self._settings) + LOG.debug("created _tox") def _create_new_profile(self, profile_name): + LOG.info("_create_new_profile " + profile_name) result = self._get_create_profile_screen_result() if result is None: return False if result.save_into_default_folder: - profile_path = util.join_path(Settings.get_default_path(), profile_name + '.tox') + profile_path = util.join_path(get_user_config_path(), profile_name + '.tox') else: profile_path = util.join_path(util.curr_directory(__file__), profile_name + '.tox') if os.path.isfile(profile_path): @@ -261,19 +535,24 @@ class App: util_ui.tr('Error')) return False name = profile_name or 'toxygen_user' - self._tox = tox_factory() - self._tox.self_set_name(name if name else 'Toxygen User') - self._tox.self_set_status_message('Toxing on Toxygen') + assert self._args self._path = profile_path if result.password: self._toxes.set_password(result.password) - self._settings = Settings(self._toxes, self._path.replace('.tox', '.json')) + self._settings = Settings(self._toxes, + self._path.replace('.tox', '.json'), + app=self) + self._tox = self._create_tox(None, + self._settings) + self._tox.self_set_name(name if name else 'Toxygen User') + self._tox.self_set_status_message('Toxing on Toxygen') + self._profile_manager = ProfileManager(self._toxes, profile_path) try: self._save_profile() except Exception as ex: - print(ex) - util.log('Profile creation exception: ' + str(ex)) + #? print(ex) + LOG.error('Profile creation exception: ' + str(ex)) text = util_ui.tr('Profile saving error! Does Toxygen have permission to write to this directory?') util_ui.message_box(text, util_ui.tr('Error')) @@ -286,6 +565,7 @@ class App: return True def _get_create_profile_screen_result(self): + LOG.debug("_get_create_profile_screen_result") cps = CreateProfileScreen() cps.show() self._app.exec_() @@ -293,6 +573,7 @@ class App: return cps.result def _save_profile(self, data=None): + LOG.debug("_save_profile") data = data or self._tox.get_savedata() self._profile_manager.save_profile(data) @@ -304,15 +585,18 @@ class App: """ Show password screen """ + LOG.debug("_enter_password") p = password_screen.PasswordScreen(self._toxes, data) p.show() self._app.lastWindowClosed.connect(self._app.quit) self._app.exec_() if p.result is not None: return p.result - self._force_exit() + self._force_exit(0) + return None def _reset(self): + LOG.debug("_reset") """ Create new tox instance (new network settings) :return: tox instance @@ -323,50 +607,99 @@ class App: self._save_profile(data) self._kill_toxav() self._kill_tox() - # create new tox instance - self._tox = self._create_tox(data) - self._start_threads(False) + try: + # create new tox instance + self._tox = self._create_tox(data, self._settings) + assert self._tox + self._start_threads(False) - tox_savers = [self._friend_factory, self._group_factory, self._plugin_loader, self._contacts_manager, - self._contacts_provider, self._messenger, self._file_transfer_handler, self._groups_service, - self._profile] - for tox_saver in tox_savers: - tox_saver.set_tox(self._tox) + tox_savers = [self._friend_factory, self._group_factory, + self._plugin_loader, self._contacts_manager, + self._contacts_provider, self._messenger, + self._file_transfer_handler, + self._groups_service, self._profile] + for tox_saver in tox_savers: + tox_saver.set_tox(self._tox) - self._calls_manager.set_toxav(self._tox.AV) - self._contacts_manager.update_friends_numbers() - self._contacts_manager.update_groups_lists() - self._contacts_manager.update_groups_numbers() + self._calls_manager.set_toxav(self._tox.AV) + self._contacts_manager.update_friends_numbers() + self._contacts_manager.update_groups_lists() + self._contacts_manager.update_groups_numbers() - self._init_callbacks() + self._init_callbacks() + except BaseException as e: + LOG.error(f"_reset : {e}") + LOG.debug('_reset: ' \ + +'\n' + traceback.format_exc()) + title = util_ui.tr('Reset Error') + text = util_ui.tr('Error:') + str(e) + util_ui.message_box(text, title) def _create_dependencies(self): - self._backup_service = BackupService(self._settings, self._profile_manager) + LOG.info(f"_create_dependencies toxygen version {self._version}") + if hasattr(self._args, 'update') and self._args.update: + self._backup_service = BackupService(self._settings, + self._profile_manager) self._smiley_loader = SmileyLoader(self._settings) self._tox_dns = ToxDns(self._settings) - self._ms = MainWindow(self._settings, self._tray) - db = Database(self._path.replace('.tox', '.db'), self._toxes) + self._ms = MainWindow(self._settings, self._tray, self) + + db_path = self._path.replace('.tox', '.db') + db = Database(db_path, self._toxes) + if os.path.exists(db_path) and hasattr(db, 'open'): + db.open() + + assert self._tox contact_items_factory = ContactItemsFactory(self._settings, self._ms) - self._friend_factory = FriendFactory(self._profile_manager, self._settings, - self._tox, db, contact_items_factory) - self._group_factory = GroupFactory(self._profile_manager, self._settings, self._tox, db, contact_items_factory) - self._group_peer_factory = GroupPeerFactory(self._tox, self._profile_manager, db, contact_items_factory) - self._contacts_provider = ContactProvider(self._tox, self._friend_factory, self._group_factory, + self._friend_factory = FriendFactory(self._profile_manager, + self._settings, + self._tox, + db, + contact_items_factory) + self._group_factory = GroupFactory(self._profile_manager, + self._settings, + self._tox, + db, + contact_items_factory) + self._group_peer_factory = GroupPeerFactory(self._tox, + self._profile_manager, + db, + contact_items_factory) + self._contacts_provider = ContactProvider(self._tox, + self._friend_factory, + self._group_factory, self._group_peer_factory) - self._profile = Profile(self._profile_manager, self._tox, self._ms, self._contacts_provider, self._reset) + self._profile = Profile(self._profile_manager, + self._tox, + self._ms, + self._contacts_provider, + self._reset) self._init_profile() self._plugin_loader = PluginLoader(self._settings, self) history = None - messages_items_factory = MessagesItemsFactory(self._settings, self._plugin_loader, self._smiley_loader, - self._ms, lambda m: history.delete_message(m)) - history = History(self._contacts_provider, db, self._settings, self._ms, messages_items_factory) - self._contacts_manager = ContactsManager(self._tox, self._settings, self._ms, self._profile_manager, - self._contacts_provider, history, self._tox_dns, + messages_items_factory = MessagesItemsFactory(self._settings, + self._plugin_loader, + self._smiley_loader, + self._ms, + lambda m: history.delete_message(m)) + history = History(self._contacts_provider, db, + self._settings, self._ms, messages_items_factory) + self._contacts_manager = ContactsManager(self._tox, + self._settings, + self._ms, + self._profile_manager, + self._contacts_provider, + history, self._tox_dns, messages_items_factory) history.set_contacts_manager(self._contacts_manager) - self._calls_manager = CallsManager(self._tox.AV, self._settings, self._ms, self._contacts_manager) - self._messenger = Messenger(self._tox, self._plugin_loader, self._ms, self._contacts_manager, + self._calls_manager = CallsManager(self._tox.AV, + self._settings, + self._ms, + self._contacts_manager, + self) + self._messenger = Messenger(self._tox, + self._plugin_loader, self._ms, self._contacts_manager, self._contacts_provider, messages_items_factory, self._profile, self._calls_manager) file_transfers_message_service = FileTransfersMessagesService(self._contacts_manager, messages_items_factory, @@ -376,49 +709,304 @@ class App: messages_items_factory.set_file_transfers_handler(self._file_transfer_handler) widgets_factory = None widgets_factory_provider = Provider(lambda: widgets_factory) - self._groups_service = GroupsService(self._tox, self._contacts_manager, self._contacts_provider, self._ms, + self._groups_service = GroupsService(self._tox, + self._contacts_manager, + self._contacts_provider, + self._ms, widgets_factory_provider) - widgets_factory = WidgetsFactory(self._settings, self._profile, self._profile_manager, self._contacts_manager, - self._file_transfer_handler, self._smiley_loader, self._plugin_loader, - self._toxes, self._version, self._groups_service, history, + widgets_factory = WidgetsFactory(self._settings, + self._profile, + self._profile_manager, + self._contacts_manager, + self._file_transfer_handler, + self._smiley_loader, + self._plugin_loader, + self._toxes, + self._version, + self._groups_service, + history, self._contacts_provider) - self._tray = tray.init_tray(self._profile, self._settings, self._ms, self._toxes) - self._ms.set_dependencies(widgets_factory, self._tray, self._contacts_manager, self._messenger, self._profile, - self._plugin_loader, self._file_transfer_handler, history, self._calls_manager, - self._groups_service, self._toxes) + if False: + self._tray = tray.init_tray(self._profile, + self._settings, + self._ms, self._toxes) + self._ms.set_dependencies(widgets_factory, + self._tray, + self._contacts_manager, + self._messenger, + self._profile, + self._plugin_loader, + self._file_transfer_handler, + history, + self._calls_manager, + self._groups_service, self._toxes, self) - self._tray.show() + if False: + # the tray icon does not die with the app + self._tray.show() self._ms.show() + # FixMe: + self._log = lambda line: LOG.log(self._args.loglevel, + self._ms.status(line)) + self._ms._log = self._log # used in callbacks.py + self.LOG = self._log # backwards + + if False: + self.status_handler = logging.Handler() + self.status_handler.setLevel(logging.INFO) # self._args.loglevel + self.status_handler.handle = self._ms.status + self._init_callbacks() + LOG.info("_create_dependencies toxygen version " +self._version) def _try_to_update(self): + LOG.debug("_try_to_update") updating = updater.start_update_if_needed(self._version, self._settings) if updating: + LOG.info("Updating toxygen version " +self._version) self._save_profile() self._settings.close() self._kill_toxav() self._kill_tox() return updating - def _create_tox(self, data): - return tox_factory(data, self._settings) + def _create_tox(self, data, settings_): + LOG.info("_create_tox calling tox_factory") + assert self._args + retval = tox_factory(data=data, settings=settings_, + args=self._args, app=self) + LOG.debug("_create_tox succeeded") + return retval - def _force_exit(self): - raise SystemExit() + def _force_exit(self, retval=0): + LOG.debug("_force_exit") + sys.exit(0) - def _init_callbacks(self): - callbacks.init_callbacks(self._tox, self._profile, self._settings, self._plugin_loader, self._contacts_manager, - self._calls_manager, self._file_transfer_handler, self._ms, self._tray, - self._messenger, self._groups_service, self._contacts_provider) + def _init_callbacks(self, ms=None): + LOG.debug("_init_callbacks") + callbacks.init_callbacks(self._tox, self._profile, self._settings, + self._plugin_loader, self._contacts_manager, + self._calls_manager, + self._file_transfer_handler, self._ms, + self._tray, + self._messenger, self._groups_service, + self._contacts_provider, self._ms) def _init_profile(self): + LOG.debug("_init_profile") if not self._profile.has_avatar(): self._profile.reset_avatar(self._settings['identicons']) def _kill_toxav(self): + LOG.debug("_kill_toxav") self._calls_manager.set_toxav(None) self._tox.AV.kill() def _kill_tox(self): + LOG.debug("_kill_tox") self._tox.kill() + + def _test_relays(self, lElts=None): + env = self._test_env() + if lElts is None: + lElts = env['lElts'] + # shuffle(env['lElts']) + LOG.debug(f"_test_relays {len(env['lElts'])}") + for host,port,key in env['lElts'][:10]: + try: + oRet = self._tox.add_tcp_relay(host, port, key) + LOG.debug('add_tcp_relay to ' +host +':' +str(port) \ + +' : ' +str(oRet)) + except Exception as e: + LOG.warn('tox_add_tcp_relay ' +host +' : ' +str(e)) + # LOG.error(traceback.format_exc()) + # LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) + + def _test_tox(self): + self.test_net() + self._ms.log_console() + + def test_net(self, lElts=None, oThread=None, iMax=4): + + LOG.debug("test_net " +self._args.network) + # bootstrap + LOG.debug('Calling generate_nodes: ') + lNodes = ts.generate_nodes(oArgs=self._args) + if lNodes: + self._settings['current_nodes'] = lNodes + else: + LOG.warn('empty generate_nodes: ') + + # if oThread and oThread._stop_thread: return + LOG.debug("test_net network=" +self._args.network +' iMax=' +str(iMax)) + if self._args.network not in ['local', 'localnew', 'newlocal']: + b = ts.bAreWeConnected() + if b is None: + i = os.system('ip route|grep ^def') + if i > 0: + b = False + else: + b = True + if not b: + LOG.warn("No default route for network " +self._args.network) + text = 'You have no default route - are you connected?' + reply = util_ui.question(text, "Are you connected?") + if not reply: return + iMax = 1 + else: + LOG.debug("Have default route for network " +self._args.network) + + LOG.debug(f"test_net {self._args.network} iMax= {iMax}") + i = 0 + while i < iMax: + # if oThread and oThread._stop_thread: return + i = i + 1 + LOG.debug(f"bootstrapping status # {i}") + self._test_bootstrap() + if hasattr(self._args, 'proxy_type') and self._args.proxy_type > 0: + LOG.debug(f"relaying status # {i}") + self._test_relays() + status = self._tox.self_get_connection_status() + LOG.debug(f"connecting status # {i}" +' : ' +repr(status)) + if status > 0: + LOG.info(f"Connected # {i}" +' : ' +repr(status)) + break + QtCore.QThread.msleep(3000) + # NO QtCore.QCoreApplication.processEvents() + LOG.trace(f"Connected status #{i}: {status!r}") + sleep(1) + + def _test_env(self): + _settings = self._settings + if 'proxy_type' not in _settings or _settings['proxy_type'] == 0 or \ + not _settings['proxy_host'] or not _settings['proxy_port']: + env = dict( prot = 'ipv4') + elif _settings['proxy_type'] == 2: + env = dict(prot = 'socks5', + https_proxy='', \ + socks_proxy='socks5://' \ + +_settings['proxy_host'] +':' \ + +str(_settings['proxy_port'])) + elif _settings['proxy_type'] == 1: + env = dict(prot = 'https', + socks_proxy='', \ + https_proxy='http://' \ + +_settings['proxy_host'] +':' \ + +str(_settings['proxy_port'])) + if 'current_nodes' in _settings and _settings['current_nodes']: + LOG.debug("Using current nodes "+' : ' +str(len(_settings['current_nodes']))) + lElts = _settings['current_nodes'] + elif _settings['network'] in ['local', 'newlocal']: + lElts = lLOCAL + elif _settings['network'] == 'old': + lElts = lGOOD + elif 'proxy_type' not in _settings or _settings['proxy_type'] == 0 or \ + not _settings['proxy_host'] or not _settings['proxy_port']: + lElts = lNEW + else: + lElts = lRELAYS + env['lElts'] = lElts + LOG.debug(f"test_env {len(env['lElts'])}") + return env + + def _test_bootstrap(self, lElts=None): + env = self._test_env() + if lElts is None: + lElts = env['lElts'] + #shuffle(env['lElts']) + LOG.debug(f"_test_bootstrap #Elts={len(lElts)}") + LOG.trace(f"_test_bootstrap lElts={lElts[:10]}") + for host,port,key in lElts[:10]: + try: + assert len(key) == 64, key + assert len(host) <= 16, host + if type(port) == str: + port = int(port) + oRet = self._tox.bootstrap(host, port, key) + LOG.debug('bootstrap to ' +host +':' +str(port) \ + +' : ' +repr(oRet)) + except Exception as e: + LOG.warn('self._tox.bootstrap host=' +host \ + +' port=' +str(port) \ + +' key=' +key \ + +' : ' +str(e)) + # LOG.error(traceback.format_exc()) + + LOG.debug("Connected status: " +repr(self._tox.self_get_connection_status())) + + def _test_socks(self, lElts=None): + LOG.debug("_test_socks") + if not self._tox: return + title = 'Extended Test Suite' + text = 'Run the Extended Test Suite?\nThe program may freeze for 1-10 minutes.' + i = os.system('ip route|grep ^def >/dev/null') + if i > 0: + text += '\nYou have no default route - are you connected?' + reply = util_ui.question(text, title) + if not reply: return + + env = self._test_env() + if lElts is None: + lElts = env['lElts'] + # shuffle(env['lElts']) + try: + inodeinfo_test(env['lElts'], env) + except Exception as e: + # json.decoder.JSONDecodeError + LOG.error(f"test_tox ' +' : {e}") + LOG.error('_test_tox(): ' \ + +'\n' + traceback.format_exc()) + title = 'Extended Test Suite Error' + text = 'Error:' + str(e) + util_ui.message_box(text, title) + + # LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) + self._ms.log_console() + + def _test_main(self): + from tests.tests_socks import main as tests_main + LOG.debug("_test_socks") + if not self._tox: return + title = 'Extended Test Suite' + text = 'Run the Extended Test Suite?\nThe program may freeze for 20-60 minutes.' + reply = util_ui.question(text, title) + if reply: + if hasattr(self._args, 'proxy_type') and self._args.proxy_type: + lArgs = ['--proxy_host', self._args.proxy_host, + '--proxy_port', str(self._args.proxy_port), + '--proxy_type', str(self._args.proxy_type), ] + else: + lArgs = list() + try: + tests_main(lArgs) + except Exception as e: + LOG.error(f"_test_socks(): {e}") + LOG.error('_test_socks(): ' \ + +'\n' + traceback.format_exc()) + title = 'Extended Test Suite Error' + text = 'Error:' + str(e) + util_ui.message_box(text, title) + self._ms.log_console() + +class GEventProcessing: + """Interoperability class between Qt/gevent that allows processing gevent + tasks during Qt idle periods.""" + def __init__(self, idle_period=IDLE_PERIOD): + # Limit the IDLE handler's frequency while still allow for gevent + # to trigger a microthread anytime + self._idle_period = idle_period + # IDLE timer: on_idle is called whenever no Qt events left for + # processing + self._timer = QTimer() + self._timer.timeout.connect(self.process_events) + self._timer.start(0) + def __enter__(self): + pass + def __exit__(self, *exc_info): + self._timer.stop() + def process_events(self, idle_period=None): + if idle_period is None: + idle_period = self._idle_period + # Cooperative yield, allow gevent to monitor file handles via libevent + gevent.sleep(idle_period) diff --git a/toxygen/av/calls.py b/toxygen/av/calls.py index d5f2fe7..b290798 100644 --- a/toxygen/av/calls.py +++ b/toxygen/av/calls.py @@ -1,14 +1,26 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import pyaudio import time import threading -from wrapper.toxav_enums import * -import cv2 import itertools -import numpy as np + +from wrapper.toxav_enums import * from av import screen_sharing from av.call import Call import common.tox_save +from utils import ui as util_ui +import tests.support_testing as ts +from middleware.threads import invoke_in_main_thread +from main import sleep +from middleware.threads import BaseThread + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + +TIMER_TIMEOUT = 30.0 +bSTREAM_CALLBACK = False class AV(common.tox_save.ToxAvSave): @@ -16,6 +28,13 @@ class AV(common.tox_save.ToxAvSave): super().__init__(toxav) self._settings = settings self._running = True + s = settings + if 'video' not in s: + LOG.warn("AV.__init__ 'video' not in s" ) + LOG.debug(f"AV.__init__ {s!r}" ) + elif 'device' not in s['video']: + LOG.warn("AV.__init__ 'device' not in s.video" ) + LOG.debug(f"AV.__init__ {s['video']!r}" ) self._calls = {} # dict: key - friend number, value - Call instance @@ -25,17 +44,27 @@ class AV(common.tox_save.ToxAvSave): self._audio_running = False self._out_stream = None - self._audio_rate = 8000 self._audio_channels = 1 self._audio_duration = 60 - self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000 + self._audio_rate_pa = 48000 + self._audio_rate_tox = 48000 + self._audio_rate_pa = 48000 + self._audio_krate_tox_audio = self._audio_rate_tox // 1000 + self._audio_krate_tox_video = 5000 + self._audio_sample_count_pa = self._audio_rate_pa * self._audio_channels * self._audio_duration // 1000 + self._audio_sample_count_tox = self._audio_rate_tox * self._audio_channels * self._audio_duration // 1000 self._video = None self._video_thread = None self._video_running = False - self._video_width = 640 - self._video_height = 480 + self._video_width = 320 + self._video_height = 240 + + iOutput = self._settings._args.audio['output'] + self.lPaSampleratesO = ts.lSdSamplerates(iOutput) + global oPYA + oPYA = self._audio = pyaudio.PyAudio() def stop(self): self._running = False @@ -51,27 +80,70 @@ class AV(common.tox_save.ToxAvSave): def __call__(self, friend_number, audio, video): """Call friend with specified number""" - self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0) + if friend_number in self._calls: + LOG.warn(f"__call__ already has {friend_number}") + return + if self._audio_krate_tox_audio not in ts.lToxSampleratesK: + LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") + + try: + self._toxav.call(friend_number, + self._audio_krate_tox_audio if audio else 0, + self._audio_krate_tox_video if video else 0) + except ArgumentError as e: + LOG.warn(f"_toxav.call already has {friend_number}") + return self._calls[friend_number] = Call(audio, video) - threading.Timer(30.0, lambda: self.finish_not_started_call(friend_number)).start() + threading.Timer(TIMER_TIMEOUT, + lambda: self.finish_not_started_call(friend_number)).start() def accept_call(self, friend_number, audio_enabled, video_enabled): + # obsolete + return call_accept_call(self, friend_number, audio_enabled, video_enabled) + + def call_accept_call(self, friend_number, audio_enabled, video_enabled): + LOG.debug(f"call_accept_call from {friend_number} {self._running}" + + f"{audio_enabled} {video_enabled}") + # import pdb; pdb.set_trace() - gets into q Qt exec_ problem + # ts.trepan_handler() + + if self._audio_krate_tox_audio not in ts.lToxSampleratesK: + LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") if self._running: self._calls[friend_number] = Call(audio_enabled, video_enabled) - self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0) + # audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. + # video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. + try: + self._toxav.answer(friend_number, + self._audio_krate_tox_audio if audio_enabled else 0, + self._audio_krate_tox_video if video_enabled else 0) + except ArgumentError as e: + LOG.debug(f"AV accept_call error from {friend_number} {self._running}" + + f"{e}") + raise if audio_enabled: + # may raise self.start_audio_thread() if video_enabled: + # may raise self.start_video_thread() def finish_call(self, friend_number, by_friend=False): + LOG.debug(f"finish_call {friend_number}") if not by_friend: self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL']) if friend_number in self._calls: del self._calls[friend_number] - if not len(list(filter(lambda c: c.out_audio, self._calls))): + try: + # AttributeError: 'int' object has no attribute 'out_audio' + if not len(list(filter(lambda c: c.out_audio, self._calls))): + self.stop_audio_thread() + if not len(list(filter(lambda c: c.out_video, self._calls))): + self.stop_video_thread() + except Exception as e: + LOG.error(f"finish_call FixMe: {e}") + # dunno self.stop_audio_thread() - if not len(list(filter(lambda c: c.out_video, self._calls))): self.stop_video_thread() def finish_not_started_call(self, friend_number): @@ -84,6 +156,7 @@ class AV(common.tox_save.ToxAvSave): """ New call state """ + LOG.debug(f"toxav_call_state_cb {friend_number}") call = self._calls[friend_number] call.is_active = True @@ -107,31 +180,80 @@ class AV(common.tox_save.ToxAvSave): """ Start audio sending """ + global oPYA + iInput = self._settings._args.audio['input'] if self._audio_thread is not None: + LOG.warn(f"start_audio_thread device={iInput}") return + iInput = self._settings._args.audio['input'] + LOG.debug(f"start_audio_thread device={iInput}") + lPaSamplerates = ts.lSdSamplerates(iInput) + if not(len(lPaSamplerates)): + e = f"No supported sample rates for device: audio[input]={iInput!r}" + LOG.error(f"No supported sample rates {e}") + raise RuntimeError(e) + if not self._audio_rate_pa in lPaSamplerates: + LOG.warn(f"{self._audio_rate_pa} not in {lPaSamplerates!r}") + if False: + self._audio_rate_pa = oPYA.get_device_info_by_index(iInput)['defaultSampleRate'] + else: + LOG.warn(f"Setting audio_rate to: {lPaSamplerates[0]}") + self._audio_rate_pa = lPaSamplerates[0] - self._audio_running = True + try: + LOG.debug( f"start_audio_thread framerate: {self._audio_rate_pa}" \ + +f" device: {iInput}" + +f" supported: {lPaSamplerates!r}") + if self._audio_rate_pa not in lPaSamplerates: + LOG.warn(f"PAudio sampling rate was {self._audio_rate_pa} changed to {lPaSamplerates[0]}") + self._audio_rate_pa = lPaSamplerates[0] - self._audio = pyaudio.PyAudio() - self._audio_stream = self._audio.open(format=pyaudio.paInt16, - rate=self._audio_rate, - channels=self._audio_channels, - input=True, - input_device_index=self._settings.audio['input'], - frames_per_buffer=self._audio_sample_count * 10) + if bSTREAM_CALLBACK: + self._audio_stream = oPYA.open(format=pyaudio.paInt16, + rate=self._audio_rate_pa, + channels=self._audio_channels, + input=True, + input_device_index=iInput, + frames_per_buffer=self._audio_sample_count_pa * 10, + stream_callback=self.send_audio_data) + self._audio_running = True + self._audio_stream.start_stream() + while self._audio_stream.is_active(): + sleep(0.1) + self._audio_stream.stop_stream() + self._audio_stream.close() - self._audio_thread = threading.Thread(target=self.send_audio) - self._audio_thread.start() + else: + self._audio_stream = oPYA.open(format=pyaudio.paInt16, + rate=self._audio_rate_pa, + channels=self._audio_channels, + input=True, + input_device_index=iInput, + frames_per_buffer=self._audio_sample_count_pa * 10) + self._audio_running = True + self._audio_thread = BaseThread(target=self.send_audio, + name='_audio_thread') + self._audio_thread.start() + + except Exception as e: + LOG.error(f"Starting self._audio.open {e}") + LOG.debug(repr(dict(format=pyaudio.paInt16, + rate=self._audio_rate_pa, + channels=self._audio_channels, + input=True, + input_device_index=iInput, + frames_per_buffer=self._audio_sample_count_pa * 10))) + # catcher in place in calls_manager + raise RuntimeError(e) + else: + LOG.debug(f"start_audio_thread {self._audio_stream!r}") def stop_audio_thread(self): if self._audio_thread is None: return - self._audio_running = False - self._audio_thread.join() - self._audio_thread = None self._audio_stream = None self._audio = None @@ -144,21 +266,39 @@ class AV(common.tox_save.ToxAvSave): def start_video_thread(self): if self._video_thread is not None: return + s = self._settings + if 'video' not in s: + LOG.warn("AV.__init__ 'video' not in s" ) + LOG.debug(f"start_video_thread {s!r}" ) + raise RuntimeError("start_video_thread not 'video' in s)" ) + elif 'device' not in s['video']: + LOG.error("start_video_thread not 'device' in s['video']" ) + LOG.debug(f"start_video_thread {s['video']!r}" ) + raise RuntimeError("start_video_thread not 'device' ins s['video']" ) + self._video_width = s['video']['width'] + self._video_height = s['video']['height'] + + LOG.info("start_video_thread " \ + +f" device: {s['video']['device']}" \ + +f" supported: {s['video']['width']} {s['video']['height']}") + + s['video']['device'] = -1 + if s['video']['device'] == -1: + self._video = screen_sharing.DesktopGrabber(s['video']['x'], + s['video']['y'], + s['video']['width'], + s['video']['height']) + else: + with ts.ignoreStdout(): + import cv2 + self._video = cv2.VideoCapture(s['video']['device']) + self._video.set(cv2.CAP_PROP_FPS, 25) + self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width) + self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height) self._video_running = True - self._video_width = s.video['width'] - self._video_height = s.video['height'] - - if s.video['device'] == -1: - self._video = screen_sharing.DesktopGrabber(self._settings.video['x'], self._settings.video['y'], - self._settings.video['width'], self._settings.video['height']) - else: - self._video = cv2.VideoCapture(self._settings.video['device']) - self._video.set(cv2.CAP_PROP_FPS, 25) - self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width) - self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height) - - self._video_thread = threading.Thread(target=self.send_video) + self._video_thread = BaseThread(target=self.send_video, + name='_video_thread') self._video_thread.start() def stop_video_thread(self): @@ -166,7 +306,17 @@ class AV(common.tox_save.ToxAvSave): return self._video_running = False - self._video_thread.join() + i = 0 + while i < ts.iTHREAD_JOINS: + self._video_thread.join(ts.iTHREAD_TIMEOUT) + try: + if not self._video_thread.is_alive(): break + except: + # AttributeError: 'NoneType' object has no attribute 'join' + break + i = i + 1 + else: + LOG.warn("self._video_thread.is_alive BLOCKED") self._video_thread = None self._video = None @@ -180,58 +330,109 @@ class AV(common.tox_save.ToxAvSave): """ if self._out_stream is None: - self._out_stream = self._audio.open(format=pyaudio.paInt16, - channels=channels_count, - rate=rate, - output_device_index=self._settings.audio['output'], - output=True) + iOutput = self._settings._args.audio['output'] + if not rate in self.lPaSampleratesO: + LOG.warn(f"{rate} not in {self.lPaSampleratesO!r}") + if False: + rate = oPYA.get_device_info_by_index(iOutput)['defaultSampleRate'] + LOG.warn(f"Setting audio_rate to: {self.lPaSampleratesO[0]}") + rate = self.lPaSampleratesO[0] + try: + with ts.ignoreStderr(): + self._out_stream = oPYA.open(format=pyaudio.paInt16, + channels=channels_count, + rate=rate, + output_device_index=iOutput, + output=True) + except Exception as e: + LOG.error(f"Error playing audio_chunk creating self._out_stream {e}") + LOG.debug(f"audio_chunk output_device_index={self._settings._args.audio['input']} rate={rate} channels={channels_count}") + invoke_in_main_thread(util_ui.message_box, + str(e), + util_ui.tr("Error Chunking audio")) + # dunno + self.stop() + return + self._out_stream.write(samples) # ----------------------------------------------------------------------------------------------------------------- # AV sending # ----------------------------------------------------------------------------------------------------------------- + def send_audio_data(self, data, count, *largs, **kwargs): + pcm = data + # :param sampling_rate: Audio sampling rate used in this frame. + if self._toxav is None: + raise RuntimeError("_toxav not initialized") + if self._audio_rate_tox not in ts.lToxSamplerates: + LOG.warn(f"ToxAudio sampling rate was {self._audio_rate_tox} changed to {ts.lToxSamplerates[0]}") + self._audio_rate_tox = ts.lToxSamplerates[0] + + for friend_num in self._calls: + if self._calls[friend_num].out_audio: + try: + # app.av.calls ERROR Error send_audio: One of the frame parameters was invalid. E.g. the resolution may be too small or too large, or the audio sampling rate may be unsupported + self._toxav.audio_send_frame(friend_num, + pcm, + count, + self._audio_channels, + self._audio_rate_tox) + except Exception as e: + LOG.error(f"Error send_audio audio_send_frame: {e}") + LOG.debug(f"send_audio self._audio_rate_tox={self._audio_rate_tox} self._audio_channels={self._audio_channels}") + invoke_in_main_thread(util_ui.message_box, + str(e), + util_ui.tr("Error send_audio audio_send_frame")) + pass + def send_audio(self): """ This method sends audio to friends """ - + i=0 + count = self._audio_sample_count_tox + LOG.debug(f"send_audio stream={self._audio_stream}") while self._audio_running: try: - pcm = self._audio_stream.read(self._audio_sample_count) - if pcm: - for friend_num in self._calls: - if self._calls[friend_num].out_audio: - try: - self._toxav.audio_send_frame(friend_num, pcm, self._audio_sample_count, - self._audio_channels, self._audio_rate) - except: - pass + pcm = self._audio_stream.read(count, exception_on_overflow=False) + if not pcm: + sleep(0.1) + else: + self.send_audio_data(pcm, count) except: pass - - time.sleep(0.01) + i += 1 + LOG.debug(f"send_audio {i}") + sleep(0.01) def send_video(self): """ This method sends video to friends """ + LOG.debug(f"send_video thread={threading.current_thread()}" + +f" self._video_running={self._video_running}" + +f" device: {self._settings['video']['device']}" ) while self._video_running: try: result, frame = self._video.read() if result: + LOG.warn(f"send_video video_send_frame _video.read") + else: height, width, channels = frame.shape for friend_num in self._calls: if self._calls[friend_num].out_video: try: y, u, v = self.convert_bgr_to_yuv(frame) self._toxav.video_send_frame(friend_num, width, height, y, u, v) - except: + except Exception as e: + LOG.debug(f"send_video video_send_frame ERROR {e}") pass + except: pass - time.sleep(0.01) + sleep(0.1) def convert_bgr_to_yuv(self, frame): """ @@ -264,11 +465,14 @@ class AV(common.tox_save.ToxAvSave): Y, U, V can be extracted using slices and joined in one list using itertools.chain.from_iterable() Function returns bytes(y), bytes(u), bytes(v), because it is required for ctypes """ + with ts.ignoreStdout(): + import cv2 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420) y = frame[:self._video_height, :] y = list(itertools.chain.from_iterable(y)) + import numpy as np u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int) u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2] u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:] diff --git a/toxygen/av/calls_manager.py b/toxygen/av/calls_manager.py index 5a48672..7e44cf5 100644 --- a/toxygen/av/calls_manager.py +++ b/toxygen/av/calls_manager.py @@ -1,22 +1,30 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import sys import threading -import cv2 + import av.calls from messenger.messages import * from ui import av_widgets import common.event as event +import utils.ui as util_ui +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) class CallsManager: - def __init__(self, toxav, settings, screen, contacts_manager): + def __init__(self, toxav, settings, main_screen, contacts_manager, app=None): self._call = av.calls.AV(toxav, settings) # object with data about calls self._call_widgets = {} # dict of incoming call widgets self._incoming_calls = set() self._settings = settings - self._screen = screen + self._main_screen = main_screen self._contacts_manager = contacts_manager self._call_started_event = event.Event() # friend_number, audio, video, is_outgoing self._call_finished_event = event.Event() # friend_number, is_declined + self._app = app def set_toxav(self, toxav): self._call.set_toxav(toxav) @@ -45,10 +53,10 @@ class CallsManager: if not self._contacts_manager.is_active_a_friend(): return if num not in self._call and self._contacts_manager.is_active_online(): # start call - if not self._settings.audio['enabled']: + if not self._settings['audio']['enabled']: return self._call(num, audio, video) - self._screen.active_call() + self._main_screen.active_call() self._call_started_event(num, audio, video, True) elif num in self._call: # finish or cancel call if you call with active friend self.stop_call(num, False) @@ -57,13 +65,13 @@ class CallsManager: """ Incoming call from friend. """ - if not self._settings.audio['enabled']: - return + LOG.debug(__name__ +f" incoming_call {friend_number}") + # if not self._settings['audio']['enabled']: return friend = self._contacts_manager.get_friend_by_number(friend_number) self._call_started_event(friend_number, audio, video, False) self._incoming_calls.add(friend_number) if friend_number == self._contacts_manager.get_active_number(): - self._screen.incoming_call() + self._main_screen.incoming_call() else: friend.actions = True text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") @@ -74,31 +82,73 @@ class CallsManager: def accept_call(self, friend_number, audio, video): """ Accept incoming call with audio or video + Called from a thread """ - self._call.accept_call(friend_number, audio, video) - self._screen.active_call() - if friend_number in self._incoming_calls: - self._incoming_calls.remove(friend_number) - del self._call_widgets[friend_number] + LOG.debug(f"CM accept_call from {friend_number} {audio} {video}") + sys.stdout.flush() + + try: + self._call.call_accept_call(friend_number, audio, video) + except Exception as e: + LOG.error(f"accept_call _call.accept_call ERROR for {friend_number} {e}") + self._main_screen.call_finished() + if hasattr(self._main_screen, '_settings') and \ + 'audio' in self._main_screen._settings and \ + 'input' in self._main_screen._settings['audio']: + iInput = self._settings['audio']['input'] + iOutput = self._settings['audio']['output'] + iVideo = self._settings['video']['device'] + LOG.debug(f"iInput={iInput} iOutput={iOutput} iVideo={iVideo}") + elif hasattr(self._main_screen, '_settings') and \ + hasattr(self._main_screen._settings, 'audio') and \ + 'input' not in self._main_screen._settings['audio']: + LOG.warn(f"'audio' not in {self._main_screen._settings!r}") + elif hasattr(self._main_screen, '_settings') and \ + hasattr(self._main_screen._settings, 'audio') and \ + 'input' not in self._main_screen._settings['audio']: + LOG.warn(f"'audio' not in {self._main_screen._settings!r}") + else: + LOG.warn(f"_settings not in self._main_screen") + util_ui.message_box(str(e), + util_ui.tr('ERROR Accepting call from {friend_number}')) + else: + self._main_screen.active_call() + + finally: + # does not terminate call - just the av_widget + if friend_number in self._incoming_calls: + self._incoming_calls.remove(friend_number) + try: + self._call_widgets[friend_number].close() + del self._call_widgets[friend_number] + except: + # RuntimeError: wrapped C/C++ object of type IncomingCallWidget has been deleted + + pass + LOG.debug(f" closed self._call_widgets[{friend_number}]") + def stop_call(self, friend_number, by_friend): """ Stop call with friend """ + LOG.debug(__name__+f" stop_call {friend_number}") if friend_number in self._incoming_calls: self._incoming_calls.remove(friend_number) is_declined = True else: is_declined = False - self._screen.call_finished() - is_video = self._call.is_video_call(friend_number) + self._main_screen.call_finished() self._call.finish_call(friend_number, by_friend) # finish or decline call if friend_number in self._call_widgets: self._call_widgets[friend_number].close() del self._call_widgets[friend_number] def destroy_window(): + #??? FixMed + is_video = self._call.is_video_call(friend_number) if is_video: + import cv2 cv2.destroyWindow(str(friend_number)) threading.Timer(2.0, destroy_window).start() diff --git a/toxygen/av/screen_sharing.py b/toxygen/av/screen_sharing.py index 265658c..3739f0c 100644 --- a/toxygen/av/screen_sharing.py +++ b/toxygen/av/screen_sharing.py @@ -1,4 +1,3 @@ -import numpy as np from PyQt5 import QtWidgets @@ -17,6 +16,7 @@ class DesktopGrabber: pixmap = self._screen.grabWindow(0, self._x, self._y, self._width, self._height) image = pixmap.toImage() s = image.bits().asstring(self._width * self._height * 4) + import numpy as np arr = np.fromstring(s, dtype=np.uint8).reshape((self._height, self._width, 4)) return True, arr diff --git a/toxygen/bootstrap/bootstrap.py b/toxygen/bootstrap/bootstrap.py index fad68c4..a8df3d4 100644 --- a/toxygen/bootstrap/bootstrap.py +++ b/toxygen/bootstrap/bootstrap.py @@ -1,83 +1,49 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import random import urllib.request from utils.util import * -from PyQt5 import QtNetwork, QtCore -import json +from PyQt5 import QtNetwork +from PyQt5 import QtCore +try: + import requests +except ImportError: + requests = None +try: + import pycurl + import certifi + from io import BytesIO +except ImportError: + pycurl = None +from user_data.settings import get_user_config_path +from tests.support_testing import download_url, _get_nodes_path -DEFAULT_NODES_COUNT = 4 +global LOG +import logging +LOG = logging.getLogger('app.'+'bootstrap') - -class Node: - - def __init__(self, node): - self._ip, self._port, self._tox_key = node['ipv4'], node['port'], node['public_key'] - self._priority = random.randint(1, 1000000) if node['status_tcp'] and node['status_udp'] else 0 - - def get_priority(self): - return self._priority - - priority = property(get_priority) - - def get_data(self): - return self._ip, self._port, self._tox_key - - -def generate_nodes(nodes_count=DEFAULT_NODES_COUNT): - with open(_get_nodes_path(), 'rt') as fl: - json_nodes = json.loads(fl.read())['nodes'] - nodes = map(lambda json_node: Node(json_node), json_nodes) - nodes = filter(lambda n: n.priority > 0, nodes) - sorted_nodes = sorted(nodes, key=lambda x: x.priority) - if nodes_count is not None: - sorted_nodes = sorted_nodes[-DEFAULT_NODES_COUNT:] - for node in sorted_nodes: - yield node.get_data() - - -def download_nodes_list(settings): - url = 'https://nodes.tox.chat/json' +def download_nodes_list(settings, oArgs): if not settings['download_nodes_list']: - return + return '' + url = settings['download_nodes_url'] + path = _get_nodes_path(oArgs=oArgs) + # dont download blindly so we can edit the file and not block on startup + if os.path.isfile(path): + with open(path, 'rt') as fl: + result = fl.read() + return result + LOG.debug("downloading list of nodes") + result = download_url(url, settings._app) + if not result: + LOG.warn("failed downloading list of nodes") + return '' + LOG.info("downloaded list of nodes") + _save_nodes(result, settings._app) + return result - if not settings['proxy_type']: # no proxy - try: - req = urllib.request.Request(url) - req.add_header('Content-Type', 'application/json') - response = urllib.request.urlopen(req) - result = response.read() - _save_nodes(result) - except Exception as ex: - log('TOX nodes loading error: ' + str(ex)) - else: # proxy - netman = QtNetwork.QNetworkAccessManager() - proxy = QtNetwork.QNetworkProxy() - proxy.setType( - QtNetwork.QNetworkProxy.Socks5Proxy if settings['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) - proxy.setHostName(settings['proxy_host']) - proxy.setPort(settings['proxy_port']) - netman.setProxy(proxy) - try: - request = QtNetwork.QNetworkRequest() - request.setUrl(QtCore.QUrl(url)) - reply = netman.get(request) - - while not reply.isFinished(): - QtCore.QThread.msleep(1) - QtCore.QCoreApplication.processEvents() - data = bytes(reply.readAll().data()) - _save_nodes(data) - except Exception as ex: - log('TOX nodes loading error: ' + str(ex)) - - -def _get_nodes_path(): - return join_path(curr_directory(__file__), 'nodes.json') - - -def _save_nodes(nodes): +def _save_nodes(nodes, app): if not nodes: return - print('Saving nodes...') - with open(_get_nodes_path(), 'wb') as fl: + with open(_get_nodes_path(oArgs=app._args), 'wb') as fl: + LOG.info("Saving nodes to " +_get_nodes_path()) fl.write(nodes) diff --git a/toxygen/contacts/basecontact.py b/toxygen/contacts/basecontact.py index 2058890..f1ae6bf 100644 --- a/toxygen/contacts/basecontact.py +++ b/toxygen/contacts/basecontact.py @@ -1,3 +1,4 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from user_data.settings import * from PyQt5 import QtCore, QtGui from wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE diff --git a/toxygen/contacts/contact.py b/toxygen/contacts/contact.py index e88acf2..b680c58 100644 --- a/toxygen/contacts/contact.py +++ b/toxygen/contacts/contact.py @@ -1,10 +1,17 @@ -from history.database import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from history.database import TIMEOUT, \ + SAVE_MESSAGES, MESSAGE_AUTHOR + from contacts import basecontact, common from messenger.messages import * from contacts.contact_menu import * from file_transfers import file_transfers as ft import re +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) class Contact(basecontact.BaseContact): """ @@ -139,7 +146,7 @@ class Contact(basecontact.BaseContact): and m.tox_message_id == tox_message_id, self._corr))[0] message.mark_as_sent() except Exception as ex: - util.log('Mark as sent ex: ' + str(ex)) + LOG.error(f"Mark as sent: {ex!s}") # ----------------------------------------------------------------------------------------------------------------- # Message deletion diff --git a/toxygen/contacts/contact_menu.py b/toxygen/contacts/contact_menu.py index 8178d31..0e4922e 100644 --- a/toxygen/contacts/contact_menu.py +++ b/toxygen/contacts/contact_menu.py @@ -1,6 +1,12 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from PyQt5 import QtWidgets -import utils.ui as util_ui +import utils.ui as util_ui +from wrapper.toxcore_enums_and_consts import * + +global LOG +import logging +LOG = logging.getLogger('app') # ----------------------------------------------------------------------------------------------------------------- # Builder @@ -99,8 +105,8 @@ class BaseContactMenuGenerator: (copy_menu_builder .with_name(util_ui.tr('Copy')) .with_action(util_ui.tr('Name'), lambda: main_screen.copy_text(self._contact.name)) - .with_action(util_ui.tr('Status message'), lambda: main_screen.copy_text(self._contact.status_message)) - .with_action(util_ui.tr('Public key'), lambda: main_screen.copy_text(self._contact.tox_id)) + .with_action(util_ui.tr("Status message"), lambda: main_screen.copy_text(self._contact.status_message)) + .with_action(util_ui.tr("Public key"), lambda: main_screen.copy_text(self._contact.tox_id)) ) return copy_menu_builder @@ -108,11 +114,11 @@ class BaseContactMenuGenerator: def _generate_history_menu_builder(self, history_loader, main_screen): history_menu_builder = ContactMenuBuilder() (history_menu_builder - .with_name(util_ui.tr('Chat history')) - .with_action(util_ui.tr('Clear history'), lambda: history_loader.clear_history(self._contact) + .with_name(util_ui.tr("Chat history")) + .with_action(util_ui.tr("Clear history"), lambda: history_loader.clear_history(self._contact) or main_screen.messages.clear()) - .with_action(util_ui.tr('Export as text'), lambda: history_loader.export_history(self._contact)) - .with_action(util_ui.tr('Export as HTML'), lambda: history_loader.export_history(self._contact, False)) + .with_action(util_ui.tr("Export as text"), lambda: history_loader.export_history(self._contact)) + .with_action(util_ui.tr("Export as HTML"), lambda: history_loader.export_history(self._contact, False)) ) return history_menu_builder @@ -127,16 +133,16 @@ class FriendMenuGenerator(BaseContactMenuGenerator): groups_menu_builder = self._generate_groups_menu(contacts_manager, groups_service) allowed = self._contact.tox_id in settings['auto_accept_from_friends'] - auto = util_ui.tr('Disallow auto accept') if allowed else util_ui.tr('Allow auto accept') + auto = util_ui.tr("Disallow auto accept") if allowed else util_ui.tr('Allow auto accept') builder = ContactMenuBuilder() menu = (builder - .with_action(util_ui.tr('Set alias'), lambda: main_screen.set_alias(number)) + .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) .with_submenu(history_menu_builder) .with_submenu(copy_menu_builder) .with_action(auto, lambda: main_screen.auto_accept(number, not allowed)) - .with_action(util_ui.tr('Remove friend'), lambda: main_screen.remove_friend(number)) - .with_action(util_ui.tr('Block friend'), lambda: main_screen.block_friend(number)) + .with_action(util_ui.tr("Remove friend"), lambda: main_screen.remove_friend(number)) + .with_action(util_ui.tr("Block friend"), lambda: main_screen.block_friend(number)) .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) .with_optional_submenu(plugins_menu_builder) .with_optional_submenu(groups_menu_builder) @@ -165,11 +171,13 @@ class FriendMenuGenerator(BaseContactMenuGenerator): def _generate_groups_menu(self, contacts_manager, groups_service): chats = contacts_manager.get_group_chats() + LOG.debug(f"_generate_groups_menu len(chats)={len(chats)} or self._contact.status={self._contact.status}") if not len(chats) or self._contact.status is None: - return None + #? return None + pass groups_menu_builder = ContactMenuBuilder() (groups_menu_builder - .with_name(util_ui.tr('Invite to group')) + .with_name(util_ui.tr("Invite to group")) .with_actions([(g.name, lambda: groups_service.invite_friend(self._contact.number, g.number)) for g in chats]) ) @@ -184,26 +192,26 @@ class GroupMenuGenerator(BaseContactMenuGenerator): builder = ContactMenuBuilder() menu = (builder - .with_action(util_ui.tr('Set alias'), lambda: main_screen.set_alias(number)) + .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) .with_submenu(copy_menu_builder) .with_submenu(history_menu_builder) - .with_optional_action(util_ui.tr('Manage group'), + .with_optional_action(util_ui.tr("Manage group"), lambda: groups_service.show_group_management_screen(self._contact), self._contact.is_self_founder()) - .with_optional_action(util_ui.tr('Group settings'), + .with_optional_action(util_ui.tr("Group settings"), lambda: groups_service.show_group_settings_screen(self._contact), not self._contact.is_self_founder()) - .with_optional_action(util_ui.tr('Set topic'), + .with_optional_action(util_ui.tr("Set topic"), lambda: groups_service.set_group_topic(self._contact), self._contact.is_self_moderator_or_founder()) - .with_action(util_ui.tr('Bans list'), - lambda: groups_service.show_bans_list(self._contact)) - .with_action(util_ui.tr('Reconnect to group'), +# .with_action(util_ui.tr("Bans list"), +# lambda: groups_service.show_bans_list(self._contact)) + .with_action(util_ui.tr("Reconnect to group"), lambda: groups_service.reconnect_to_group(self._contact.number)) - .with_optional_action(util_ui.tr('Disconnect from group'), + .with_optional_action(util_ui.tr("Disconnect from group"), lambda: groups_service.disconnect_from_group(self._contact.number), self._contact.status is not None) - .with_action(util_ui.tr('Leave group'), lambda: groups_service.leave_group(self._contact.number)) + .with_action(util_ui.tr("Leave group"), lambda: groups_service.leave_group(self._contact.number)) .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) ).build() @@ -218,10 +226,10 @@ class GroupPeerMenuGenerator(BaseContactMenuGenerator): builder = ContactMenuBuilder() menu = (builder - .with_action(util_ui.tr('Set alias'), lambda: main_screen.set_alias(number)) + .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) .with_submenu(copy_menu_builder) .with_submenu(history_menu_builder) - .with_action(util_ui.tr('Quit chat'), + .with_action(util_ui.tr("Quit chat"), lambda: contacts_manager.remove_group_peer(self._contact)) .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) ).build() diff --git a/toxygen/contacts/contact_provider.py b/toxygen/contacts/contact_provider.py index 76e8e79..8dda974 100644 --- a/toxygen/contacts/contact_provider.py +++ b/toxygen/contacts/contact_provider.py @@ -15,8 +15,10 @@ class ContactProvider(tox_save.ToxSave): # ----------------------------------------------------------------------------------------------------------------- def get_friend_by_number(self, friend_number): - public_key = self._tox.friend_get_public_key(friend_number) - + try: + public_key = self._tox.friend_get_public_key(friend_number) + except Exception as e: + return None return self.get_friend_by_public_key(public_key) def get_friend_by_public_key(self, public_key): @@ -29,7 +31,10 @@ class ContactProvider(tox_save.ToxSave): return friend def get_all_friends(self): - friend_numbers = self._tox.self_get_friend_list() + try: + friend_numbers = self._tox.self_get_friend_list() + except Exception as e: + return None friends = map(lambda n: self.get_friend_by_number(n), friend_numbers) return list(friends) @@ -39,13 +44,19 @@ class ContactProvider(tox_save.ToxSave): # ----------------------------------------------------------------------------------------------------------------- def get_all_groups(self): - group_numbers = range(self._tox.group_get_number_groups()) + try: + group_numbers = range(self._tox.group_get_number_groups()) + except Exception as e: + return None groups = map(lambda n: self.get_group_by_number(n), group_numbers) return list(groups) def get_group_by_number(self, group_number): - public_key = self._tox.group_get_chat_id(group_number) + try: + public_key = self._tox.group_get_chat_id(group_number) + except Exception as e: + return None return self.get_group_by_public_key(public_key) @@ -67,8 +78,8 @@ class ContactProvider(tox_save.ToxSave): def get_group_peer_by_id(self, group, peer_id): peer = group.get_peer_by_id(peer_id) - - return self._get_group_peer(group, peer) + if peer: + return self._get_group_peer(group, peer) def get_group_peer_by_public_key(self, group, public_key): peer = group.get_peer_by_public_key(public_key) diff --git a/toxygen/contacts/contacts_manager.py b/toxygen/contacts/contacts_manager.py index 87a61ff..c0ec665 100644 --- a/toxygen/contacts/contacts_manager.py +++ b/toxygen/contacts/contacts_manager.py @@ -1,9 +1,15 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from contacts.friend import Friend from contacts.group_chat import GroupChat from messenger.messages import * from common.tox_save import ToxSave from contacts.group_peer_contact import GroupPeerContact +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) class ContactsManager(ToxSave): """ @@ -129,8 +135,8 @@ class ContactsManager(ToxSave): self._set_current_contact_data(contact) self._active_contact_changed(contact) except Exception as ex: # no friend found. ignore - util.log('Friend value: ' + str(value)) - util.log('Error in set active: ' + str(ex)) + LOG.warn(f"no friend found. Friend value: {value!s}") + LOG.error('in set active: ' + str(ex)) raise active_contact = property(get_active, set_active) @@ -235,8 +241,9 @@ class ContactsManager(ToxSave): def get_or_create_group_peer_contact(self, group_number, peer_id): group = self.get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - if not self.check_if_contact_exists(peer.public_key): - self.add_group_peer(group, peer) + if peer: # broken + if not self.check_if_contact_exists(peer.public_key): + self.add_group_peer(group, peer) return self.get_contact_by_tox_id(peer.public_key) @@ -375,16 +382,18 @@ class ContactsManager(ToxSave): def remove_group_peer_by_id(self, group, peer_id): peer = group.get_peer_by_id(peer_id) - if not self.check_if_contact_exists(peer.public_key): - return - contact = self.get_contact_by_tox_id(peer.public_key) - self.remove_group_peer(contact) + if peer: # broken + if not self.check_if_contact_exists(peer.public_key): + return + contact = self.get_contact_by_tox_id(peer.public_key) + self.remove_group_peer(contact) def remove_group_peer(self, group_peer_contact): contact = self.get_contact_by_tox_id(group_peer_contact.tox_id) - self._cleanup_contact_data(contact) - num = self._contacts.index(contact) - self._delete_contact(num) + if contact: + self._cleanup_contact_data(contact) + num = self._contacts.index(contact) + self._delete_contact(num) def get_gc_peer_name(self, name): group = self.get_curr_contact() @@ -432,7 +441,7 @@ class ContactsManager(ToxSave): self.save_profile() return True except Exception as ex: # wrong data - util.log('Friend request failed with ' + str(ex)) + LOG.error('Friend request failed with ' + str(ex)) return str(ex) def process_friend_request(self, tox_id, message): @@ -451,7 +460,7 @@ class ContactsManager(ToxSave): data = self._tox.get_savedata() self._profile_manager.save_profile(data) except Exception as ex: # something is wrong - util.log('Accept friend request failed! ' + str(ex)) + LOG.error('Accept friend request failed! ' + str(ex)) def can_send_typing_notification(self): return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer() diff --git a/toxygen/contacts/group_chat.py b/toxygen/contacts/group_chat.py index 19ebc8e..a1ed948 100644 --- a/toxygen/contacts/group_chat.py +++ b/toxygen/contacts/group_chat.py @@ -1,3 +1,5 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + from contacts import contact from contacts.contact_menu import GroupMenuGenerator import utils.util as util @@ -6,6 +8,14 @@ from wrapper import toxcore_enums_and_consts as constants from common.tox_save import ToxSave from groups.group_ban import GroupBan +global LOG +import logging +LOG = logging.getLogger(__name__) +def LOG_ERROR(l): print('ERROR_: '+l) +def LOG_WARN(l): print('WARN_: '+l) +def LOG_INFO(l): print('INFO_: '+l) +def LOG_DEBUG(l): print('DEBUG_: '+l) +def LOG_TRACE(l): pass # print('TRACE+ '+l) class GroupChat(contact.Contact, ToxSave): @@ -73,6 +83,10 @@ class GroupChat(contact.Contact, ToxSave): return self.get_self_role() == constants.TOX_GROUP_ROLE['FOUNDER'] def add_peer(self, peer_id, is_current_user=False): + if peer_id > self._peers_limit: + LOG_WARN(f"add_peer id={peer_id} > {self._peers_limit}") + return + peer = GroupChatPeer(peer_id, self._tox.group_peer_get_name(self._number, peer_id), self._tox.group_peer_get_status(self._number, peer_id), @@ -86,25 +100,41 @@ class GroupChat(contact.Contact, ToxSave): self.remove_all_peers_except_self() else: peer = self.get_peer_by_id(peer_id) - self._peers.remove(peer) + if peer: # broken + self._peers.remove(peer) + else: + LOG_WARN(f"remove_peer empty peers for {peer_id}") def get_peer_by_id(self, peer_id): peers = list(filter(lambda p: p.id == peer_id, self._peers)) - - return peers[0] + if peers: + #? broken + return peers[0] + else: + LOG_WARN(f"get_peer_by_id empty peers for {peer_id}") + return [] def get_peer_by_public_key(self, public_key): peers = list(filter(lambda p: p.public_key == public_key, self._peers)) - - return peers[0] + # DEBUGc: group_moderation #0 mod_id=4294967295 event_type=3 + # WARN_: get_peer_by_id empty peers for 4294967295 + if peers: + return peers[0] + else: + LOG_WARN(f"get_peer_by_public_key empty peers for {public_key}") + return [] def remove_all_peers_except_self(self): self._peers = self._peers[:1] def get_peers_names(self): peers_names = map(lambda p: p.name, self._peers) - - return list(peers_names) + if peers_names: # broken + return list(peers_names) + else: + LOG_WARN(f"get_peers_names empty peers") + #? broken + return [] def get_peers(self): return self._peers[:] @@ -112,16 +142,17 @@ class GroupChat(contact.Contact, ToxSave): peers = property(get_peers) def get_bans(self): - ban_ids = self._tox.group_ban_get_list(self._number) - bans = [] - for ban_id in ban_ids: - ban = GroupBan(ban_id, - self._tox.group_ban_get_target(self._number, ban_id), - self._tox.group_ban_get_time_set(self._number, ban_id)) - bans.append(ban) - - return bans - + return [] +# ban_ids = self._tox.group_ban_get_list(self._number) +# bans = [] +# for ban_id in ban_ids: +# ban = GroupBan(ban_id, +# self._tox.group_ban_get_target(self._number, ban_id), +# self._tox.group_ban_get_time_set(self._number, ban_id)) +# bans.append(ban) +# +# return bans +# bans = property(get_bans) # ----------------------------------------------------------------------------------------------------------------- diff --git a/toxygen/contacts/profile.py b/toxygen/contacts/profile.py index 81220af..097c1fa 100644 --- a/toxygen/contacts/profile.py +++ b/toxygen/contacts/profile.py @@ -1,9 +1,13 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from contacts import basecontact import random import threading import common.tox_save as tox_save from middleware.threads import invoke_in_main_thread +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) class Profile(basecontact.BaseContact, tox_save.ToxSave): """ @@ -14,6 +18,7 @@ class Profile(basecontact.BaseContact, tox_save.ToxSave): :param tox: tox instance :param screen: ref to main screen """ + assert tox basecontact.BaseContact.__init__(self, profile_manager, tox.self_get_name(), diff --git a/toxygen/file_transfers/file_transfers_handler.py b/toxygen/file_transfers/file_transfers_handler.py index 114383b..0c137d9 100644 --- a/toxygen/file_transfers/file_transfers_handler.py +++ b/toxygen/file_transfers/file_transfers_handler.py @@ -1,11 +1,19 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from messenger.messages import * from ui.contact_items import * import utils.util as util from common.tox_save import ToxSave +from tests.support_testing import assert_main_thread +from copy import deepcopy +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) class FileTransfersHandler(ToxSave): - + lBlockAvatars = [] def __init__(self, tox, settings, contact_provider, file_transfers_message_service, profile): super().__init__(tox) self._settings = settings @@ -19,7 +27,8 @@ class FileTransfersHandler(ToxSave): # key = (friend number, file number), value - message id profile.avatar_changed_event.add_callback(self._send_avatar_to_contacts) - + self. lBlockAvatars = [] + def stop(self): self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {} self._settings.save() @@ -37,6 +46,7 @@ class FileTransfersHandler(ToxSave): :param file_name: file name without path """ friend = self._get_friend_by_number(friend_number) + if friend is None: return None auto = self._settings['allow_auto_accept'] and friend.tox_id in self._settings['auto_accept_from_friends'] inline = is_inline(file_name) and self._settings['allow_inline'] file_id = self._tox.file_get_file_id(friend_number, file_number) @@ -85,7 +95,9 @@ class FileTransfersHandler(ToxSave): self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) def cancel_not_started_transfer(self, friend_number, message_id): - self._get_friend_by_number(friend_number).delete_one_unsent_file(message_id) + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + friend.delete_one_unsent_file(message_id) def pause_transfer(self, friend_number, file_number, by_friend=False): """ @@ -115,6 +127,7 @@ class FileTransfersHandler(ToxSave): """ path = self._generate_valid_path(path, from_position) friend = self._get_friend_by_number(friend_number) + if friend is None: return None if not inline: rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number, from_position) else: @@ -145,6 +158,7 @@ class FileTransfersHandler(ToxSave): def send_inline(self, data, file_name, friend_number, is_resend=False): friend = self._get_friend_by_number(friend_number) + if friend is None: return None if friend.status is None and not is_resend: self._file_transfers_message_service.add_unsent_file_message(friend, file_name, data) return @@ -162,11 +176,12 @@ class FileTransfersHandler(ToxSave): :param file_id: file id of transfer """ friend = self._get_friend_by_number(friend_number) + if friend is None: return None if friend.status is None and not is_resend: self._file_transfers_message_service.add_unsent_file_message(friend, path, None) return elif friend.status is None and is_resend: - print('Error in sending') + LOG.error('Error in sending') return st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id) file_name = os.path.basename(path) @@ -186,23 +201,27 @@ class FileTransfersHandler(ToxSave): def transfer_finished(self, friend_number, file_number): transfer = self._file_transfers[(friend_number, file_number)] + friend = self._get_friend_by_number(friend_number) + if friend is None: return None t = type(transfer) if t is ReceiveAvatar: - self._get_friend_by_number(friend_number).load_avatar() + friend.load_avatar() elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings['allow_inline']): # inline image - print('inline') + LOG.debug('inline') inline = InlineImageMessage(transfer.data) message_id = self._insert_inline_before[(friend_number, file_number)] del self._insert_inline_before[(friend_number, file_number)] - index = self._get_friend_by_number(friend_number).insert_inline(message_id, inline) + if friend is None: return None + index = friend.insert_inline(message_id, inline) self._file_transfers_message_service.add_inline_message(transfer, index) del self._file_transfers[(friend_number, file_number)] def send_files(self, friend_number): - friend = self._get_friend_by_number(friend_number) - friend.remove_invalid_unsent_files() - files = friend.get_unsent_files() try: + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + friend.remove_invalid_unsent_files() + files = friend.get_unsent_files() for fl in files: data, path = fl.data, fl.path if data is not None: @@ -211,6 +230,7 @@ class FileTransfersHandler(ToxSave): self.send_file(path, friend_number, True) friend.clear_unsent_files() for key in self._paused_file_transfers.keys(): + # RuntimeError: dictionary changed size during iteration (path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[key] if not os.path.exists(path): del self._paused_file_transfers[key] @@ -218,12 +238,16 @@ class FileTransfersHandler(ToxSave): self.send_file(path, friend_number, True, key) del self._paused_file_transfers[key] except Exception as ex: - print('Exception in file sending: ' + str(ex)) + LOG.error('Exception in file sending: ' + str(ex)) def friend_exit(self, friend_number): - for friend_num, file_num in self._file_transfers.keys(): + # RuntimeError: dictionary changed size during iteration + lMayChangeDynamically = self._file_transfers.copy() + for friend_num, file_num in lMayChangeDynamically: if friend_num != friend_number: continue + if (friend_num, file_num) not in self._file_transfers: + continue ft = self._file_transfers[(friend_num, file_num)] if type(ft) is SendTransfer: self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, False, -1] @@ -240,8 +264,16 @@ class FileTransfersHandler(ToxSave): :param friend_number: number of friend who should get new avatar :param avatar_path: path to avatar or None if reset """ - sa = SendAvatar(avatar_path, self._tox, friend_number) - self._file_transfers[(friend_number, sa.file_number)] = sa + if (avatar_path, friend_number,) in self.lBlockAvatars: + return + + try: + sa = SendAvatar(avatar_path, self._tox, friend_number) + self._file_transfers[(friend_number, sa.file_number)] = sa + except Exception as e: + # ArgumentError('This client is currently not connected to the friend.') + LOG.error(f"send_avatar {e}") + self.lBlockAvatars.append( (avatar_path, friend_number,) ) def incoming_avatar(self, friend_number, file_number, size): """ @@ -251,6 +283,7 @@ class FileTransfersHandler(ToxSave): :param size: size of avatar or 0 (default avatar) """ friend = self._get_friend_by_number(friend_number) + if friend is None: return None ra = ReceiveAvatar(friend.get_contact_avatar_path(), self._tox, friend_number, size, file_number) if ra.state != FILE_TRANSFER_STATE['CANCELLED']: self._file_transfers[(friend_number, file_number)] = ra @@ -259,6 +292,7 @@ class FileTransfersHandler(ToxSave): friend.reset_avatar(self._settings['identicons']) def _send_avatar_to_contacts(self, _): + # from a callback friends = self._get_all_friends() for friend in filter(self._is_friend_online, friends): self.send_avatar(friend.number) @@ -269,6 +303,7 @@ class FileTransfersHandler(ToxSave): def _is_friend_online(self, friend_number): friend = self._get_friend_by_number(friend_number) + if friend is None: return None return friend.status is not None diff --git a/toxygen/groups/groups_service.py b/toxygen/groups/groups_service.py index b8fc7cc..258ab80 100644 --- a/toxygen/groups/groups_service.py +++ b/toxygen/groups/groups_service.py @@ -1,8 +1,11 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + import common.tox_save as tox_save import utils.ui as util_ui from groups.peers_list import PeersListGenerator from groups.group_invite import GroupInvite import wrapper.toxcore_enums_and_consts as constants +from wrapper.toxcore_enums_and_consts import * class GroupsService(tox_save.ToxSave): @@ -65,7 +68,17 @@ class GroupsService(tox_save.ToxSave): # ----------------------------------------------------------------------------------------------------------------- def invite_friend(self, friend_number, group_number): - self._tox.group_invite_friend(group_number, friend_number) + if self._tox.friend_get_connection_status(friend_number) == TOX_CONNECTION['NONE']: + title = f"Error in group_invite_friend {friend_number}" + e = f"Friend not connected friend_number={friend_number}" + util_ui.message_box(title +'\n' +str(e), title) + return + + try: + self._tox.group_invite_friend(group_number, friend_number) + except Exception as e: + title = f"Error in group_invite_friend {group_number} {friend_number}" + util_ui.message_box(title +'\n' +str(e), title) def process_group_invite(self, friend_number, group_name, invite_data): friend = self._get_friend_by_number(friend_number) @@ -188,6 +201,7 @@ class GroupsService(tox_save.ToxSave): # ----------------------------------------------------------------------------------------------------------------- def show_bans_list(self, group): + return widgets_factory = self._get_widgets_factory() self._screen = widgets_factory.create_groups_bans_screen(group) self._screen.show() diff --git a/toxygen/history/database.py b/toxygen/history/database.py index 751c74b..c17a968 100644 --- a/toxygen/history/database.py +++ b/toxygen/history/database.py @@ -1,7 +1,13 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from sqlite3 import connect import os.path import utils.util as util +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) TIMEOUT = 11 @@ -24,19 +30,30 @@ CONTACT_TYPE = { class Database: def __init__(self, path, toxes): - self._path, self._toxes = path, toxes + self._path = path + self._toxes = toxes self._name = os.path.basename(path) - if os.path.exists(path): - try: - with open(path, 'rb') as fin: - data = fin.read() - if toxes.is_data_encrypted(data): - data = toxes.pass_decrypt(data) - with open(path, 'wb') as fout: - fout.write(data) - except Exception as ex: - util.log('Db reading error: ' + str(ex)) - os.remove(path) + + def open(self): + path = self._path + toxes = self._toxes + if not os.path.exists(path): + LOG.warn('Db not found: ' +path) + return + try: + with open(path, 'rb') as fin: + data = fin.read() + except Exception as ex: + LOG.error('Db reading error: ' +path +' ' +str(ex)) + raise + try: + if toxes.is_data_encrypted(data): + data = toxes.pass_decrypt(data) + with open(path, 'wb') as fout: + fout.write(data) + except Exception as ex: + LOG.error('Db writing error: ' +path +' ' + str(ex)) + os.remove(path) # ----------------------------------------------------------------------------------------------------------------- # Public methods @@ -72,9 +89,11 @@ class Database: ' message_type INTEGER' ')') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG("ERROR: " +self._name +' Database exception! ' +str(e)) db.rollback() + return False finally: db.close() @@ -84,9 +103,11 @@ class Database: cursor = db.cursor() cursor.execute('DROP TABLE id' + tox_id + ';') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG("ERROR: " +self._name +' Database exception! ' +str(e)) db.rollback() + return False finally: db.close() @@ -98,9 +119,11 @@ class Database: '(message, author_name, author_type, unix_time, message_type) ' + 'VALUES (?, ?, ?, ?, ?, ?);', messages_iter) db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG("ERROR: " +self._name +' Database exception! ' +str(e)) db.rollback() + return False finally: db.close() @@ -111,9 +134,11 @@ class Database: cursor.execute('UPDATE id' + tox_id + ' SET author = 0 ' 'WHERE id = ' + str(message_id) + ' AND author = 2;') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG("ERROR: " +self._name +' Database exception! ' +str(e)) db.rollback() + return False finally: db.close() @@ -123,9 +148,11 @@ class Database: cursor = db.cursor() cursor.execute('DELETE FROM id' + tox_id + ' WHERE id = ' + str(unique_id) + ';') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG("ERROR: " +self._name +' Database exception! ' +str(e)) db.rollback() + return False finally: db.close() @@ -135,9 +162,11 @@ class Database: cursor = db.cursor() cursor.execute('DELETE FROM id' + tox_id + ';') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG("ERROR: " +self._name +' Database exception! ' +str(e)) db.rollback() + return False finally: db.close() diff --git a/toxygen/history/history.py b/toxygen/history/history.py index bd7e353..074322e 100644 --- a/toxygen/history/history.py +++ b/toxygen/history/history.py @@ -1,3 +1,4 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from history.history_logs_generators import * @@ -11,7 +12,7 @@ class History: self._messages_items_factory = messages_items_factory self._is_loading = False self._contacts_manager = None - + def __del__(self): del self._db @@ -26,7 +27,8 @@ class History: """ Save history to db """ - if self._settings['save_db']: + # me a mistake? was _db not _history + if self._settings['save_history'] or self._settings['save_db']: for friend in self._contact_provider.get_all_friends(): self._db.add_friend_to_db(friend.tox_id) if not self._settings['save_unsent_only']: diff --git a/toxygen/images/accept.png b/toxygen/images/accept.png old mode 100755 new mode 100644 index aaa1388..eedb818 Binary files a/toxygen/images/accept.png and b/toxygen/images/accept.png differ diff --git a/toxygen/images/accept_audio.png b/toxygen/images/accept_audio.png old mode 100755 new mode 100644 index 2fd2818..7969974 Binary files a/toxygen/images/accept_audio.png and b/toxygen/images/accept_audio.png differ diff --git a/toxygen/images/accept_video.png b/toxygen/images/accept_video.png old mode 100755 new mode 100644 index 2fdebe7..bac3af7 Binary files a/toxygen/images/accept_video.png and b/toxygen/images/accept_video.png differ diff --git a/toxygen/images/avatar.png b/toxygen/images/avatar.png old mode 100755 new mode 100644 index 83ac757..06255a1 Binary files a/toxygen/images/avatar.png and b/toxygen/images/avatar.png differ diff --git a/toxygen/images/busy.png b/toxygen/images/busy.png index 857b396..40b9bff 100644 Binary files a/toxygen/images/busy.png and b/toxygen/images/busy.png differ diff --git a/toxygen/images/busy_notification.png b/toxygen/images/busy_notification.png index a01eb3f..5f73464 100644 Binary files a/toxygen/images/busy_notification.png and b/toxygen/images/busy_notification.png differ diff --git a/toxygen/images/call.png b/toxygen/images/call.png old mode 100755 new mode 100644 index dc0d672..1820653 Binary files a/toxygen/images/call.png and b/toxygen/images/call.png differ diff --git a/toxygen/images/call_video.png b/toxygen/images/call_video.png old mode 100755 new mode 100644 index ef9fa86..ba153e9 Binary files a/toxygen/images/call_video.png and b/toxygen/images/call_video.png differ diff --git a/toxygen/images/decline.png b/toxygen/images/decline.png old mode 100755 new mode 100644 index 9bbc9d5..e6313fd Binary files a/toxygen/images/decline.png and b/toxygen/images/decline.png differ diff --git a/toxygen/images/decline_call.png b/toxygen/images/decline_call.png old mode 100755 new mode 100644 index 9f39789..3ac0b6d Binary files a/toxygen/images/decline_call.png and b/toxygen/images/decline_call.png differ diff --git a/toxygen/images/file.png b/toxygen/images/file.png old mode 100755 new mode 100644 index edbfad9..526fd10 Binary files a/toxygen/images/file.png and b/toxygen/images/file.png differ diff --git a/toxygen/images/finish_call.png b/toxygen/images/finish_call.png old mode 100755 new mode 100644 index a08361e..d8d85d7 Binary files a/toxygen/images/finish_call.png and b/toxygen/images/finish_call.png differ diff --git a/toxygen/images/finish_call_video.png b/toxygen/images/finish_call_video.png old mode 100755 new mode 100644 index 8465106..9e4f830 Binary files a/toxygen/images/finish_call_video.png and b/toxygen/images/finish_call_video.png differ diff --git a/toxygen/images/group.png b/toxygen/images/group.png index 22adab0..3ea6469 100644 Binary files a/toxygen/images/group.png and b/toxygen/images/group.png differ diff --git a/toxygen/images/icon.png b/toxygen/images/icon.png index a790ae1..6051ac7 100644 Binary files a/toxygen/images/icon.png and b/toxygen/images/icon.png differ diff --git a/toxygen/images/icon_new_messages.png b/toxygen/images/icon_new_messages.png old mode 100755 new mode 100644 index a3f1900..aa15890 Binary files a/toxygen/images/icon_new_messages.png and b/toxygen/images/icon_new_messages.png differ diff --git a/toxygen/images/idle.png b/toxygen/images/idle.png index 2550926..62fa74c 100644 Binary files a/toxygen/images/idle.png and b/toxygen/images/idle.png differ diff --git a/toxygen/images/idle_notification.png b/toxygen/images/idle_notification.png index 29f3b49..be372f9 100644 Binary files a/toxygen/images/idle_notification.png and b/toxygen/images/idle_notification.png differ diff --git a/toxygen/images/incoming_call.png b/toxygen/images/incoming_call.png old mode 100755 new mode 100644 index b83350a..6467b23 Binary files a/toxygen/images/incoming_call.png and b/toxygen/images/incoming_call.png differ diff --git a/toxygen/images/incoming_call_video.png b/toxygen/images/incoming_call_video.png old mode 100755 new mode 100644 index 4fe4c98..2301877 Binary files a/toxygen/images/incoming_call_video.png and b/toxygen/images/incoming_call_video.png differ diff --git a/toxygen/images/menu.png b/toxygen/images/menu.png old mode 100755 new mode 100644 index 4d72f03..72bd478 Binary files a/toxygen/images/menu.png and b/toxygen/images/menu.png differ diff --git a/toxygen/images/offline.png b/toxygen/images/offline.png index 70a863b..54f83b7 100644 Binary files a/toxygen/images/offline.png and b/toxygen/images/offline.png differ diff --git a/toxygen/images/offline_notification.png b/toxygen/images/offline_notification.png index 77006ed..98dc068 100644 Binary files a/toxygen/images/offline_notification.png and b/toxygen/images/offline_notification.png differ diff --git a/toxygen/images/online.png b/toxygen/images/online.png index 1e5f40a..2381304 100644 Binary files a/toxygen/images/online.png and b/toxygen/images/online.png differ diff --git a/toxygen/images/online_notification.png b/toxygen/images/online_notification.png index 6e85b15..72b988b 100644 Binary files a/toxygen/images/online_notification.png and b/toxygen/images/online_notification.png differ diff --git a/toxygen/images/pause.png b/toxygen/images/pause.png old mode 100755 new mode 100644 index 5c8ee4c..bbedc4a Binary files a/toxygen/images/pause.png and b/toxygen/images/pause.png differ diff --git a/toxygen/images/resume.png b/toxygen/images/resume.png old mode 100755 new mode 100644 index 22bb736..4ceca74 Binary files a/toxygen/images/resume.png and b/toxygen/images/resume.png differ diff --git a/toxygen/images/screenshot.png b/toxygen/images/screenshot.png old mode 100755 new mode 100644 index 5599da9..9c14c6f Binary files a/toxygen/images/screenshot.png and b/toxygen/images/screenshot.png differ diff --git a/toxygen/images/search.png b/toxygen/images/search.png index bf0dff6..8e4875b 100644 Binary files a/toxygen/images/search.png and b/toxygen/images/search.png differ diff --git a/toxygen/images/send.png b/toxygen/images/send.png old mode 100755 new mode 100644 index a2aeed8..ef17f60 Binary files a/toxygen/images/send.png and b/toxygen/images/send.png differ diff --git a/toxygen/images/smiley.png b/toxygen/images/smiley.png old mode 100755 new mode 100644 index 6b5c0f6..98787dc Binary files a/toxygen/images/smiley.png and b/toxygen/images/smiley.png differ diff --git a/toxygen/images/sticker.png b/toxygen/images/sticker.png old mode 100755 new mode 100644 index f82eae7..901de59 Binary files a/toxygen/images/sticker.png and b/toxygen/images/sticker.png differ diff --git a/toxygen/images/typing.png b/toxygen/images/typing.png old mode 100755 new mode 100644 index 26ad69b..405f80d Binary files a/toxygen/images/typing.png and b/toxygen/images/typing.png differ diff --git a/toxygen/main.py b/toxygen/main.py index eca3ac3..a9785c9 100644 --- a/toxygen/main.py +++ b/toxygen/main.py @@ -1,51 +1,424 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import sys +import os import app -from user_data.settings import * -import utils.util as util import argparse +import logging +import signal +import faulthandler +faulthandler.enable() + +import warnings +warnings.filterwarnings('ignore') + +import tests.support_testing as ts +try: + from trepan.interfaces import server as Mserver + from trepan.api import debug +except: + print('trepan3 TCP server NOT enabled.') +else: + import signal + try: + signal.signal(signal.SIGUSR1, ts.trepan_handler) + print('trepan3 TCP server enabled on port 6666.') + except: pass + +from user_data.settings import * +from user_data.settings import Settings +from user_data import settings +import utils.util as util +from tests import omain +with ts.ignoreStderr(): + import pyaudio __maintainer__ = 'Ingvar' -__version__ = '0.5.0' +__version__ = '0.5.0+' +from PyQt5 import QtCore +import gevent +if 'QtCore' in sys.modules: + def qt_sleep(fSec): + if fSec > .001: + QtCore.QThread.msleep(int(fSec*1000.0)) + QtCore.QCoreApplication.processEvents() + sleep = qt_sleep +elif 'gevent' in sys.modules: + sleep = gevent.sleep +else: + import time + sleep = time.sleep + +def reset(): + Settings.reset_auto_profile() def clean(): """Removes libs folder""" directory = util.get_libs_directory() util.remove(directory) - -def reset(): - Settings.reset_auto_profile() - - def print_toxygen_version(): - print('Toxygen v' + __version__) + print('Toxygen ' + __version__) +def setup_default_audio(): + # need: + audio = ts.get_audio() + # unfinished + global oPYA + oPYA = pyaudio.PyAudio() + audio['output_devices'] = dict() + i = oPYA.get_device_count() + while i > 0: + i -= 1 + if oPYA.get_device_info_by_index(i)['maxOutputChannels'] == 0: + continue + audio['output_devices'][i] = oPYA.get_device_info_by_index(i)['name'] + i = oPYA.get_device_count() + audio['input_devices'] = dict() + while i > 0: + i -= 1 + if oPYA.get_device_info_by_index(i)['maxInputChannels'] == 0: + continue + audio['input_devices'][i] = oPYA.get_device_info_by_index(i)['name'] + return audio -def main(): +def setup_video(oArgs): + video = setup_default_video() + if oArgs.video_input == '-1': + video['device'] = video['output_devices'][1] + else: + video['device'] = oArgs.video_input + return video + +def setup_audio(oArgs): + global oPYA + audio = setup_default_audio() + for k,v in audio['input_devices'].items(): + if v == 'default' and 'input' not in audio : + audio['input'] = k + if v == getattr(oArgs, 'audio_input'): + audio['input'] = k + LOG.debug(f"Setting audio['input'] {k} = {v} {k}") + break + for k,v in audio['output_devices'].items(): + if v == 'default' and 'output' not in audio: + audio['output'] = k + if v == getattr(oArgs, 'audio_output'): + audio['output'] = k + LOG.debug(f"Setting audio['output'] {k} = {v} " +str(k)) + break + + if hasattr(oArgs, 'mode') and getattr(oArgs, 'mode') > 1: + audio['enabled'] = True + audio['audio_enabled'] = True + audio['video_enabled'] = True + elif hasattr(oArgs, 'mode') and getattr(oArgs, 'mode') > 0: + audio['enabled'] = True + audio['audio_enabled'] = False + audio['video_enabled'] = True + else: + audio['enabled'] = False + audio['audio_enabled'] = False + audio['video_enabled'] = False + + return audio + + i = getattr(oArgs, 'audio_output') + if i >= 0: + try: + elt = oPYA.get_device_info_by_index(i) + if i >= 0 and ( 'maxOutputChannels' not in elt or \ + elt['maxOutputChannels'] == 0): + LOG.warn(f"Audio output device has no output channels: {i}") + oArgs.audio_output = -1 + except OSError as e: + LOG.warn("Audio output device error looking for maxOutputChannels: " \ + +str(i) +' ' +str(e)) + oArgs.audio_output = -1 + + if getattr(oArgs, 'audio_output') < 0: + LOG.info("Choose an output device:") + i = oPYA.get_device_count() + while i > 0: + i -= 1 + if oPYA.get_device_info_by_index(i)['maxOutputChannels'] == 0: + continue + LOG.info(str(i) \ + +' ' +oPYA.get_device_info_by_index(i)['name'] \ + +' ' +str(oPYA.get_device_info_by_index(i)['defaultSampleRate']) + ) + return 0 + + i = getattr(oArgs, 'audio_input') + if i >= 0: + try: + elt = oPYA.get_device_info_by_index(i) + if i >= 0 and ( 'maxInputChannels' not in elt or \ + elt['maxInputChannels'] == 0): + LOG.warn(f"Audio input device has no input channels: {i}") + setattr(oArgs, 'audio_input', -1) + except OSError as e: + LOG.warn("Audio input device error looking for maxInputChannels: " \ + +str(i) +' ' +str(e)) + setattr(oArgs, 'audio_input', -1) + if getattr(oArgs, 'audio_input') < 0: + LOG.info("Choose an input device:") + i = oPYA.get_device_count() + while i > 0: + i -= 1 + if oPYA.get_device_info_by_index(i)['maxInputChannels'] == 0: + continue + LOG.info(str(i) \ + +' ' +oPYA.get_device_info_by_index(i)['name'] + +' ' +str(oPYA.get_device_info_by_index(i)['defaultSampleRate']) + ) + return 0 + +def setup_default_video(): + default_video = ["-1"] + default_video.extend(ts.get_video_indexes()) + LOG.info(f"Video input choices: {default_video!r}") + video = {'device': -1, 'width': 320, 'height': 240, 'x': 0, 'y': 0} + video['output_devices'] = default_video + return video + +def main_parser(): + import cv2 + if not os.path.exists('/proc/sys/net/ipv6'): + bIpV6 = 'False' + else: + bIpV6 = 'True' + lIpV6Choices=[bIpV6, 'False'] + + audio = setup_default_audio() + default_video = setup_default_video() + + logfile = os.path.join(os.environ.get('TMPDIR', '/tmp'), 'toxygen.log') parser = argparse.ArgumentParser() parser.add_argument('--version', action='store_true', help='Prints Toxygen version') parser.add_argument('--clean', action='store_true', help='Delete toxcore libs from libs folder') parser.add_argument('--reset', action='store_true', help='Reset default profile') - parser.add_argument('--uri', help='Add specified Tox ID to friends') - parser.add_argument('profile', nargs='?', default=None, help='Path to Tox profile') - args = parser.parse_args() + parser.add_argument('--uri', type=str, default='', + help='Add specified Tox ID to friends') + parser.add_argument('--logfile', default=logfile, + help='Filename for logging') + parser.add_argument('--loglevel', type=int, default=logging.INFO, + help='Threshold for logging (lower is more) default: 20') + parser.add_argument('--proxy_host', '--proxy-host', type=str, + # oddball - we want to use '' as a setting + default='0.0.0.0', + help='proxy host') + parser.add_argument('--proxy_port', '--proxy-port', default=0, type=int, + help='proxy port') + parser.add_argument('--proxy_type', '--proxy-type', default=0, type=int, + choices=[0,1,2], + help='proxy type 1=https, 2=socks') + parser.add_argument('--tcp_port', '--tcp-port', default=0, type=int, + help='tcp port') + parser.add_argument('--auto_accept_path', '--auto-accept-path', type=str, + default=os.path.join(os.environ['HOME'], 'Downloads'), + help="auto_accept_path") + parser.add_argument('--mode', type=int, default=2, + help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0') + parser.add_argument('--font', type=str, default="Courier", + help='Message font') + parser.add_argument('--message_font_size', type=int, default=15, + help='Font size in pixels') + parser.add_argument('--local_discovery_enabled',type=str, + default='False', choices=['True','False'], + help='Look on the local lan') + parser.add_argument('--udp_enabled',type=str, + default='True', choices=['True','False'], + help='En/Disable udp') + parser.add_argument('--ipv6_enabled',type=str, + default=bIpV6, choices=lIpV6Choices, + help='En/Disable ipv6') + parser.add_argument('--compact_mode',type=str, + default='True', choices=['True','False'], + help='Compact mode') + parser.add_argument('--allow_inline',type=str, + default='False', choices=['True','False'], + help='Dis/Enable allow_inline') + parser.add_argument('--notifications',type=str, + default='True', choices=['True','False'], + help='Dis/Enable notifications') + parser.add_argument('--sound_notifications',type=str, + default='True', choices=['True','False'], + help='Enable sound notifications') + parser.add_argument('--calls_sound',type=str, + default='True', choices=['True','False'], + help='Enable calls_sound') + parser.add_argument('--core_logging',type=str, + default='False', choices=['True','False'], + help='Dis/Enable Toxcore notifications') + parser.add_argument('--hole_punching_enabled',type=str, + default='False', choices=['True','False'], + help='En/Enable hole punching') + parser.add_argument('--dht_announcements_enabled',type=str, + default='True', choices=['True','False'], + help='En/Disable DHT announcements') + parser.add_argument('--save_history',type=str, + default='True', choices=['True','False'], + help='En/Disable save history') + parser.add_argument('--update', type=int, default=0, + choices=[0,0], + help='Update program (broken)') + parser.add_argument('--download_nodes_list',type=str, + default='False', choices=['True','False'], + help='Download nodes list') + parser.add_argument('--nodes_json', type=str, + default='') + parser.add_argument('--download_nodes_url', type=str, + default='https://nodes.tox.chat/json') + parser.add_argument('--network', type=str, + choices=['main', 'new', 'local', 'newlocal'], + default='new') + parser.add_argument('--video_input', type=str, + default=-1, + choices=default_video['output_devices'], + help="Video input device number - /dev/video?") + parser.add_argument('--audio_input', type=str, + default=oPYA.get_default_input_device_info()['name'], + choices=audio['input_devices'].values(), + help="Audio input device name - aplay -L for help") + parser.add_argument('--audio_output', type=str, + default=oPYA.get_default_output_device_info()['index'], + choices=audio['output_devices'].values(), + help="Audio output device number - -1 for help") + parser.add_argument('--theme', type=str, default='default', + choices=['dark', 'default'], + help='Theme - style of UI') + parser.add_argument('--sleep', type=str, default='time', + # could expand this to tk, gtk, gevent... + choices=['qt','gevent','time'], + help='Sleep method - one of qt, gevent , time') + supported_languages = settings.supported_languages() + parser.add_argument('--language', type=str, default='English', + choices=supported_languages, + help='Languages') + parser.add_argument('profile', type=str, nargs='?', default=None, + help='Path to Tox profile') + return parser - if args.version: +# clean out the unchanged settings so these can override the profile +lKEEP_SETTINGS = ['uri', + 'profile', + 'loglevel', + 'logfile', + 'mode', + 'audio', + 'video', + 'ipv6_enabled', + 'udp_enabled', + 'local_discovery_enabled', + 'theme', + 'network', + 'message_font_size', + 'font', + 'save_history', + 'language', + 'update', + 'proxy_host', + 'proxy_type', + 'proxy_port', + 'core_logging', + 'audio', + 'video' + ] # , 'nodes_json' +lBOOLEANS = [ + 'local_discovery_enabled', + 'udp_enabled', + 'ipv6_enabled', + 'compact_mode', + 'allow_inline', + 'notifications', + 'sound_notifications', + 'hole_punching_enabled', + 'dht_announcements_enabled', + 'save_history', + 'download_nodes_list' + 'core_logging', + ] + +class A(): pass + +global oAPP +oAPP = None +def main(lArgs): + global oPYA + from argparse import Namespace + parser = main_parser() + default_ns = parser.parse_args([]) + oArgs = parser.parse_args(lArgs) + + if oArgs.version: print_toxygen_version() - return + return 0 - if args.clean: + if oArgs.clean: clean() - return + return 0 - if args.reset: + if oArgs.reset: reset() - return + return 0 - toxygen = app.App(__version__, args.profile, args.uri) - toxygen.main() + # if getattr(oArgs, 'network') in ['newlocal', 'localnew']: oArgs.network = 'new' + # clean out the unchanged settings so these can override the profile + for key in default_ns.__dict__.keys(): + if key in lKEEP_SETTINGS: continue + if not hasattr(oArgs, key): continue + if getattr(default_ns, key) == getattr(oArgs, key): + delattr(oArgs, key) + + for key in lBOOLEANS: + if not hasattr(oArgs, key): continue + val = getattr(oArgs, key) + if type(val) == bool: continue + if val in ['False', 'false', '0']: + setattr(oArgs, key, False) + else: + setattr(oArgs, key, True) + + aArgs = A() + for key in oArgs.__dict__.keys(): + setattr(aArgs, key, getattr(oArgs, key)) + setattr(aArgs, 'video', setup_video(oArgs)) + aArgs.video = setup_video(oArgs) + assert 'video' in aArgs.__dict__ + + setattr(aArgs, 'audio', setup_audio(oArgs)) + aArgs.audio = setup_audio(oArgs) + assert 'audio' in aArgs.__dict__ + + oArgs = aArgs + toxygen = app.App(__version__, oArgs) + global oAPP + oAPP = toxygen + i = toxygen.iMain() + return i if __name__ == '__main__': - main() + iRet = 0 + try: + iRet = main(sys.argv[1:]) + except KeyboardInterrupt: + iRet = 0 + except SystemExit as e: + iRet = e + except Exception as e: + import traceback + sys.stderr.write(f"Exception from main {e}" \ + +'\n' + traceback.format_exc() +'\n' ) + iRet = 1 + + # Exception ignored in: + # File "/usr/lib/python3.9/threading.py", line 1428, in _shutdown + # lock.acquire() + # gevent.exceptions.LoopExit as e: + # This operation would block forever + sys.stderr.write('Calling sys.exit' +'\n') + with ts.ignoreStdout(): + sys.exit(iRet) diff --git a/toxygen/messenger/messages.py b/toxygen/messenger/messages.py index e777c4b..f59978d 100644 --- a/toxygen/messenger/messages.py +++ b/toxygen/messenger/messages.py @@ -38,8 +38,8 @@ class Message: MESSAGE_ID = 0 - def __init__(self, message_type, author, time): - self._time = time + def __init__(self, message_type, author, iTime): + self._time = iTime self._type = message_type self._author = author self._widget = None @@ -66,6 +66,7 @@ class Message: message_id = property(get_message_id) def get_widget(self, *args): + # FixMe self._widget = self._create_widget(*args) return self._widget @@ -81,6 +82,7 @@ class Message: self._widget.mark_as_sent() def _create_widget(self, *args): + # overridden pass @staticmethod @@ -95,8 +97,8 @@ class TextMessage(Message): Plain text or action message """ - def __init__(self, message, owner, time, message_type, message_id=0): - super().__init__(message_type, owner, time) + def __init__(self, message, owner, iTime, message_type, message_id=0): + super().__init__(message_type, owner, iTime) self._message = message self._id = message_id @@ -119,8 +121,8 @@ class TextMessage(Message): class OutgoingTextMessage(TextMessage): - def __init__(self, message, owner, time, message_type, tox_message_id=0): - super().__init__(message, owner, time, message_type) + def __init__(self, message, owner, iTime, message_type, tox_message_id=0): + super().__init__(message, owner, iTime, message_type) self._tox_message_id = tox_message_id def get_tox_message_id(self): @@ -134,8 +136,8 @@ class OutgoingTextMessage(TextMessage): class GroupChatMessage(TextMessage): - def __init__(self, id, message, owner, time, message_type, name): - super().__init__(id, message, owner, time, message_type) + def __init__(self, id, message, owner, iTime, message_type, name): + super().__init__(id, message, owner, iTime, message_type) self._user_name = name @@ -144,8 +146,8 @@ class TransferMessage(Message): Message with info about file transfer """ - def __init__(self, author, time, state, size, file_name, friend_number, file_number): - super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], author, time) + def __init__(self, author, iTime, state, size, file_name, friend_number, file_number): + super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], author, iTime) self._state = state self._size = size self._file_name = file_name @@ -185,10 +187,10 @@ class TransferMessage(Message): file_name = property(get_file_name) - def transfer_updated(self, state, percentage, time): + def transfer_updated(self, state, percentage, iTime): self._state = state if self._widget is not None: - self._widget.update_transfer_state(state, percentage, time) + self._widget.update_transfer_state(state, percentage, iTime) def _create_widget(self, *args): return FileTransferItem(self, *args) @@ -196,9 +198,9 @@ class TransferMessage(Message): class UnsentFileMessage(TransferMessage): - def __init__(self, path, data, time, author, size, friend_number): + def __init__(self, path, data, iTime, author, size, friend_number): file_name = os.path.basename(path) - super().__init__(author, time, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) + super().__init__(author, iTime, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) self._data, self._path = data, path def get_data(self): @@ -235,5 +237,5 @@ class InlineImageMessage(Message): class InfoMessage(TextMessage): - def __init__(self, message, time): - super().__init__(message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) + def __init__(self, message, iTime): + super().__init__(message, None, iTime, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py index e859135..25b3c87 100644 --- a/toxygen/messenger/messenger.py +++ b/toxygen/messenger/messenger.py @@ -1,6 +1,12 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import common.tox_save as tox_save from messenger.messages import * +from tests.support_testing import assert_main_thread +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) class Messenger(tox_save.ToxSave): @@ -76,6 +82,7 @@ class Messenger(tox_save.ToxSave): if not text or friend_number < 0: return + assert_main_thread() friend = self._get_friend_by_number(friend_number) messages = self._split_message(text.encode('utf-8')) @@ -106,7 +113,7 @@ class Messenger(tox_save.ToxSave): message_id = self._tox.friend_send_message(friend_number, message.type, message.text.encode('utf-8')) message.tox_message_id = message_id except Exception as ex: - util.log('Sending pending messages failed with ' + str(ex)) + LOG.warn('Sending pending messages failed with ' + str(ex)) # ----------------------------------------------------------------------------------------------------------------- # Messaging - groups @@ -142,6 +149,9 @@ class Messenger(tox_save.ToxSave): t = util.get_unix_time() group = self._get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) + if not peer: + LOG.warn('FixMe new_group_message group.get_peer_by_id ' + str(peer_id)) + return text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), t, message_type) self._add_message(text_message, group) @@ -158,6 +168,7 @@ class Messenger(tox_save.ToxSave): if not text or group_number < 0 or peer_id < 0: return + assert_main_thread() group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) group = self._get_group_by_number(group_number) @@ -182,6 +193,9 @@ class Messenger(tox_save.ToxSave): t = util.get_unix_time() group = self._get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) + if not peer: + LOG.warn('FixMe new_group_private_message group.get_peer_by_id ' + str(peer_id)) + return text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), t, message_type) group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) @@ -291,10 +305,12 @@ class Messenger(tox_save.ToxSave): self._create_info_message_item(message) def _create_info_message_item(self, message): + assert_main_thread() self._items_factory.create_message_item(message) self._screen.messages.scrollToBottom() def _add_message(self, text_message, contact): + assert_main_thread() if self._contacts_manager.is_contact_active(contact): # add message to list self._create_message_item(text_message) self._screen.messages.scrollToBottom() diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py index b9a4099..9bc9d9d 100644 --- a/toxygen/middleware/callbacks.py +++ b/toxygen/middleware/callbacks.py @@ -1,15 +1,40 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os +import threading from PyQt5 import QtGui from wrapper.toxcore_enums_and_consts import * from wrapper.toxav_enums import * from wrapper.tox import bin_to_string import utils.ui as util_ui import utils.util as util -import cv2 -import numpy as np from middleware.threads import invoke_in_main_thread, execute from notifications.tray import tray_notification from notifications.sound import * -import threading +from datetime import datetime + +iMAX_INT32 = 4294967295 +def LOG_ERROR(l): print('ERRORc: '+l) +def LOG_WARN(l): print('WARNc: '+l) +def LOG_INFO(l): print('INFOc: '+l) +def LOG_DEBUG(l): print('DEBUGc: '+l) +def LOG_TRACE(l): pass # print('TRACE+ '+l) + +global aTIMES +aTIMES=dict() +def bTooSoon(key, sSlot, fSec=10.0): + # rate limiting + global aTIMES + if sSlot not in aTIMES: + aTIMES[sSlot] = dict() + OTIME = aTIMES[sSlot] + now = datetime.now() + if key not in OTIME: + OTIME[key] = now + return False + delta = now - OTIME[key] + OTIME[key] = now + if delta.total_seconds() < fSec: return True + return False # TODO: refactoring. Use contact provider instead of manager @@ -17,15 +42,48 @@ import threading # Callbacks - current user # ----------------------------------------------------------------------------------------------------------------- +global iBYTES +iBYTES=0 +def sProcBytes(sFile=None): + global iBYTES + if sFile is None: + pid = os.getpid() + sFile = f"/proc/{pid}/net/softnet_stat" + if os.path.exists(sFile): + total = 0 + with open(sFile, 'r') as iFd: + for elt in iFd.readlines(): + i = elt.find(' ') + p = int(elt[:i], 16) + total = total + p + if iBYTES == 0: + iBYTES = total + return '' + diff = total - iBYTES + s = f' {diff // 1024} Kbytes' + else: + s = '' + return s def self_connection_status(tox, profile): """ Current user changed connection status (offline, TCP, UDP) """ + pid = os.getpid() + sFile = '/proc/'+str(pid) +'/net/softnet_stat' + sSlot = 'self connection status' def wrapped(tox_link, connection, user_data): - print('Connection status: ', str(connection)) - status = tox.self_get_status() if connection != TOX_CONNECTION['NONE'] else None - invoke_in_main_thread(profile.set_status, status) + key = f"connection {connection}" + if bTooSoon(key, sSlot, 10): return + s = sProcBytes(sFile) + try: + status = tox.self_get_status() if connection != TOX_CONNECTION['NONE'] else None + if status: + LOG_DEBUG(f"self_connection_status: connection={connection} status={status}" +' '+s) + invoke_in_main_thread(profile.set_status, status) + except Exception as e: + LOG_ERROR(f"self_connection_status: {e}") + pass return wrapped @@ -36,13 +94,17 @@ def self_connection_status(tox, profile): def friend_status(contacts_manager, file_transfer_handler, profile, settings): + sSlot = 'friend status' def wrapped(tox, friend_number, new_status, user_data): """ Check friend's status (none, busy, away) """ - print("Friend's #{} status changed!".format(friend_number)) + LOG_DEBUG(f"Friend's #{friend_number} status changed") + key = f"friend_number {friend_number}" + if bTooSoon(key, sSlot, 10): return friend = contacts_manager.get_friend_by_number(friend_number) - if friend.status is None and settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: + if friend.status is None and settings['sound_notifications'] and \ + profile.status != TOX_USER_STATUS['BUSY']: sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) invoke_in_main_thread(friend.set_status, new_status) @@ -61,7 +123,7 @@ def friend_connection_status(contacts_manager, profile, settings, plugin_loader, """ Check friend's connection status (offline, udp, tcp) """ - print("Friend #{} connection status: {}".format(friend_number, new_status)) + LOG_DEBUG(f"Friend #{friend_number} connection status: {new_status}") friend = contacts_manager.get_friend_by_number(friend_number) if new_status == TOX_CONNECTION['NONE']: invoke_in_main_thread(friend.set_status, None) @@ -79,11 +141,14 @@ def friend_connection_status(contacts_manager, profile, settings, plugin_loader, def friend_name(contacts_provider, messenger): + sSlot = 'friend_name' def wrapped(tox, friend_number, name, size, user_data): """ Friend changed his name """ - print('New name friend #' + str(friend_number)) + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 60): return + LOG_DEBUG(f'New name friend #' + str(friend_number)) friend = contacts_provider.get_friend_by_number(friend_number) old_name = friend.name new_name = str(name, 'utf-8') @@ -92,16 +157,19 @@ def friend_name(contacts_provider, messenger): return wrapped - def friend_status_message(contacts_manager, messenger): + sSlot = 'status_message' def wrapped(tox, friend_number, status_message, size, user_data): """ :return: function for callback friend_status_message. It updates friend's status message and calls window repaint """ friend = contacts_manager.get_friend_by_number(friend_number) + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 10): return + invoke_in_main_thread(friend.set_status_message, str(status_message, 'utf-8')) - print('User #{} has new status message'.format(friend_number)) + LOG_DEBUG(f'User #{friend_number} has new status message') invoke_in_main_thread(messenger.send_messages, friend_number) return wrapped @@ -112,6 +180,7 @@ def friend_message(messenger, contacts_manager, profile, settings, window, tray) """ New message from friend """ + LOG_DEBUG(f"friend_message #{friend_number}") message = str(message, 'utf-8') invoke_in_main_thread(messenger.new_message, friend_number, message_type, message) if not window.isActiveWindow(): @@ -121,7 +190,8 @@ def friend_message(messenger, contacts_manager, profile, settings, window, tray) if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: sound_notification(SOUND_NOTIFICATION['MESSAGE']) icon = os.path.join(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + if tray: + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped @@ -131,7 +201,7 @@ def friend_request(contacts_manager): """ Called when user get new friend request """ - print('Friend request') + LOG_DEBUG(f'Friend request') key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE) invoke_in_main_thread(contacts_manager.process_friend_request, tox_id, str(message, 'utf-8')) @@ -140,9 +210,12 @@ def friend_request(contacts_manager): def friend_typing(messenger): + sSlot = "friend_typing" def wrapped(tox, friend_number, typing, user_data): + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 10): return + LOG_DEBUG(f"friend_typing #{friend_number}") invoke_in_main_thread(messenger.friend_typing, friend_number, typing) - return wrapped @@ -164,7 +237,7 @@ def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager """ def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data): if file_type == TOX_FILE_KIND['DATA']: - print('File') + LOG_DEBUG(f'file_transfer_handler File') try: file_name = str(file_name[:file_name_size], 'utf-8') except: @@ -184,7 +257,7 @@ def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) else: # avatar - print('Avatar') + LOG_DEBUG(f'file_transfer_handler Avatar') invoke_in_main_thread(file_transfer_handler.incoming_avatar, friend_number, file_number, @@ -259,15 +332,17 @@ def lossy_packet(plugin_loader): # ----------------------------------------------------------------------------------------------------------------- def call_state(calls_manager): - def wrapped(toxav, friend_number, mask, user_data): + def wrapped(iToxav, friend_number, mask, user_data): """ New call state """ - print(friend_number, mask) + LOG_DEBUG(f"call_state #{friend_number}") if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: invoke_in_main_thread(calls_manager.stop_call, friend_number, True) else: - calls_manager.toxav_call_state_cb(friend_number, mask) + # guessing was calls_manager. + #? incoming_call + calls_manager._call.toxav_call_state_cb(friend_number, mask) return wrapped @@ -277,7 +352,7 @@ def call(calls_manager): """ Incoming call from friend """ - print(friend_number, audio, video) + LOG_DEBUG(f"Incoming call from {friend_number} {audio} {video}") invoke_in_main_thread(calls_manager.incoming_call, audio, video, friend_number) return wrapped @@ -288,7 +363,9 @@ def callback_audio(calls_manager): """ New audio chunk """ - calls_manager.call.audio_chunk( + LOG_DEBUG(f"callback_audio #{friend_number}") + # guessing was .call + calls_manager._call.audio_chunk( bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), audio_channels_count, rate) @@ -324,6 +401,9 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u It can be created from initial y, u, v using slices """ + LOG_DEBUG(f"video_receive_frame from {friend_number}") + import cv2 + import numpy as np try: y_size = abs(max(width, abs(ystride))) u_size = abs(max(width // 2, abs(ustride))) @@ -349,7 +429,8 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u invoke_in_main_thread(cv2.imshow, str(friend_number), frame) except Exception as ex: - print(ex) + LOG_ERROR(f"video_receive_frame {ex} #{friend_number}") + pass # ----------------------------------------------------------------------------------------------------------------- # Callbacks - groups @@ -361,18 +442,23 @@ def group_message(window, tray, tox, messenger, settings, profile): New message in group chat """ def wrapped(tox_link, group_number, peer_id, message_type, message, length, user_data): + LOG_DEBUG(f"group_message #{group_number}") message = str(message[:length], 'utf-8') invoke_in_main_thread(messenger.new_group_message, group_number, message_type, message, peer_id) if window.isActiveWindow(): return bl = settings['notify_all_gc'] or profile.name in message name = tox.group_peer_get_name(group_number, peer_id) - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and (not settings.locked) and bl: - invoke_in_main_thread(tray_notification, name, message, tray, window) - if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']: + if settings['sound_notifications'] and bl and \ + profile.status != TOX_USER_STATUS['BUSY']: sound_notification(SOUND_NOTIFICATION['MESSAGE']) - icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + if False and settings['tray_icon']: + if settings['notifications'] and \ + profile.status != TOX_USER_STATUS['BUSY'] and \ + (not settings.locked) and bl: + invoke_in_main_thread(tray_notification, name, message, tray, window) + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped @@ -382,6 +468,7 @@ def group_private_message(window, tray, tox, messenger, settings, profile): New private message in group chat """ def wrapped(tox_link, group_number, peer_id, message_type, message, length, user_data): + LOG_DEBUG(f"group_private_message #{group_number}") message = str(message[:length], 'utf-8') invoke_in_main_thread(messenger.new_group_private_message, group_number, message_type, message, peer_id) if window.isActiveWindow(): @@ -400,13 +487,15 @@ def group_private_message(window, tray, tox, messenger, settings, profile): def group_invite(window, settings, tray, profile, groups_service, contacts_provider): def wrapped(tox, friend_number, invite_data, length, group_name, group_name_length, user_data): + LOG_DEBUG(f"group_invite friend_number={friend_number}") group_name = str(bytes(group_name[:group_name_length]), 'utf-8') invoke_in_main_thread(groups_service.process_group_invite, friend_number, group_name, bytes(invite_data[:length])) if window.isActiveWindow(): return - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: + if settings['notifications'] and \ + profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: friend = contacts_provider.get_friend_by_number(friend_number) title = util_ui.tr('New invite to group chat') text = util_ui.tr('{} invites you to group "{}"').format(friend.name, group_name) @@ -419,6 +508,7 @@ def group_invite(window, settings, tray, profile, groups_service, contacts_provi def group_self_join(contacts_provider, contacts_manager, groups_service): def wrapped(tox, group_number, user_data): + LOG_DEBUG(f"group_self_join #{group_number}") group = contacts_provider.get_group_by_number(group_number) invoke_in_main_thread(group.set_status, TOX_USER_STATUS['NONE']) invoke_in_main_thread(groups_service.update_group_info, group) @@ -428,8 +518,15 @@ def group_self_join(contacts_provider, contacts_manager, groups_service): def group_peer_join(contacts_provider, groups_service): + sSlot = "group_peer_join" def wrapped(tox, group_number, peer_id, user_data): + key = f"group_peer_join #{group_number} peer_id={peer_id}" + if bTooSoon(key, sSlot, 20): return group = contacts_provider.get_group_by_number(group_number) + if peer_id > group._peers_limit: + LOG_ERROR(key +f" {peer_id} > {group._peers_limit}") + return + LOG_DEBUG(key) group.add_peer(peer_id) invoke_in_main_thread(groups_service.generate_peers_list) invoke_in_main_thread(groups_service.update_group_info, group) @@ -439,28 +536,41 @@ def group_peer_join(contacts_provider, groups_service): def group_peer_exit(contacts_provider, groups_service, contacts_manager): def wrapped(tox, group_number, peer_id, message, length, user_data): + LOG_DEBUG(f"group_peer_exit #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) group.remove_peer(peer_id) invoke_in_main_thread(groups_service.generate_peers_list) return wrapped - def group_peer_name(contacts_provider, groups_service): def wrapped(tox, group_number, peer_id, name, length, user_data): + LOG_DEBUG(f"group_peer_name #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - peer.name = str(name[:length], 'utf-8') - invoke_in_main_thread(groups_service.generate_peers_list) + if peer: + peer.name = str(name[:length], 'utf-8') + invoke_in_main_thread(groups_service.generate_peers_list) + else: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group!r} has no peer_id={peer_id} in _peers!r") + return return wrapped def group_peer_status(contacts_provider, groups_service): def wrapped(tox, group_number, peer_id, peer_status, user_data): + LOG_DEBUG(f"group_peer_status #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - peer.status = peer_status + if peer: + peer.status = peer_status + else: + # _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group!r} has no peer_id={peer_id} in _peers!r") + # TODO: add info message invoke_in_main_thread(groups_service.generate_peers_list) return wrapped @@ -468,32 +578,62 @@ def group_peer_status(contacts_provider, groups_service): def group_topic(contacts_provider): def wrapped(tox, group_number, peer_id, topic, length, user_data): + LOG_DEBUG(f"group_topic #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) - topic = str(topic[:length], 'utf-8') - invoke_in_main_thread(group.set_status_message, topic) + if group: + topic = str(topic[:length], 'utf-8') + invoke_in_main_thread(group.set_status_message, topic) + else: + _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_WARN(f"group_topic {group!r} has no peer_id={peer_id} in {_peers!r}") + # TODO: add info message return wrapped - def group_moderation(groups_service, contacts_provider, contacts_manager, messenger): - def update_peer_role(group, mod_peer_id, peer_id, new_role): peer = group.get_peer_by_id(peer_id) - peer.role = new_role + if peer: + peer.role = new_role + # TODO: add info message + else: + # FixMe: known signal to revalidate roles... + # _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"update_peer_role group {group!r} has no peer_id={peer_id} in _peers!r") # TODO: add info message def remove_peer(group, mod_peer_id, peer_id, is_ban): - contacts_manager.remove_group_peer_by_id(group, peer_id) - group.remove_peer(peer_id) + peer = group.get_peer_by_id(peer_id) + if peer: + contacts_manager.remove_group_peer_by_id(group, peer_id) + group.remove_peer(peer_id) + else: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group!r} has no peer_id={peer_id} in _peers!r") # TODO: add info message + # source_peer_number, target_peer_number, def wrapped(tox, group_number, mod_peer_id, peer_id, event_type, user_data): + if mod_peer_id == iMAX_INT32 or peer_id == iMAX_INT32: + # FixMe: known signal to revalidate roles... + return + LOG_DEBUG(f"group_moderation #{group_number} mod_id={mod_peer_id} peer_id={peer_id} event_type={event_type}") group = contacts_provider.get_group_by_number(group_number) + mod_peer = group.get_peer_by_id(mod_peer_id) + if not mod_peer: + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group!r} has no mod_peer_id={mod_peer_id} in _peers!r") + return + peer = group.get_peer_by_id(peer_id) + if not peer: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group!r} has no peer_id={peer_id} in _peers!r") + return if event_type == TOX_GROUP_MOD_EVENT['KICK']: remove_peer(group, mod_peer_id, peer_id, False) - elif event_type == TOX_GROUP_MOD_EVENT['BAN']: - remove_peer(group, mod_peer_id, peer_id, True) elif event_type == TOX_GROUP_MOD_EVENT['OBSERVER']: update_peer_role(group, mod_peer_id, peer_id, TOX_GROUP_ROLE['OBSERVER']) elif event_type == TOX_GROUP_MOD_EVENT['USER']: @@ -509,6 +649,7 @@ def group_moderation(groups_service, contacts_provider, contacts_manager, messen def group_password(contacts_provider): def wrapped(tox_link, group_number, password, length, user_data): + LOG_DEBUG(f"group_password #{group_number}") password = str(password[:length], 'utf-8') group = contacts_provider.get_group_by_number(group_number) group.password = password @@ -519,6 +660,7 @@ def group_password(contacts_provider): def group_peer_limit(contacts_provider): def wrapped(tox_link, group_number, peer_limit, user_data): + LOG_DEBUG(f"group_peer_limit #{group_number}") group = contacts_provider.get_group_by_number(group_number) group.peer_limit = peer_limit @@ -528,6 +670,7 @@ def group_peer_limit(contacts_provider): def group_privacy_state(contacts_provider): def wrapped(tox_link, group_number, privacy_state, user_data): + LOG_DEBUG(f"group_privacy_state #{group_number}") group = contacts_provider.get_group_by_number(group_number) group.is_private = privacy_state == TOX_GROUP_PRIVACY_STATE['PRIVATE'] @@ -540,7 +683,7 @@ def group_privacy_state(contacts_provider): def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, calls_manager, file_transfer_handler, main_window, tray, messenger, groups_service, - contacts_provider): + contacts_provider, ms=None): """ Initialization of all callbacks. :param tox: Tox instance @@ -557,6 +700,10 @@ def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, :param groups_service: GroupsService instance :param contacts_provider: ContactsProvider instance """ + global LOG + import logging + LOG = logging.getLogger('app.'+__name__) + # self callbacks tox.callback_self_connection_status(self_connection_status(tox, profile)) diff --git a/toxygen/middleware/threads.py b/toxygen/middleware/threads.py index 5f9404b..faffbb3 100644 --- a/toxygen/middleware/threads.py +++ b/toxygen/middleware/threads.py @@ -1,10 +1,40 @@ -from bootstrap.bootstrap import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import sys import threading import queue -from utils import util -import time from PyQt5 import QtCore +from bootstrap.bootstrap import * +from bootstrap.bootstrap import download_nodes_list +import tests.support_testing as ts +from utils import util + +if 'QtCore' in sys.modules: + def qt_sleep(fSec): + if fSec > .001: + QtCore.QThread.msleep(int(fSec*1000.0)) + QtCore.QCoreApplication.processEvents() + sleep = qt_sleep +elif 'gevent' in sys.modules: + import gevent + sleep = gevent.sleep +else: + import time + sleep = time.sleep +import time +sleep = time.sleep + +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+'threads') +# log = lambda x: LOG.info(x) + +def LOG_ERROR(l): print('ERRORt: '+l) +def LOG_WARN(l): print('WARNt: '+l) +def LOG_INFO(l): print('INFOt: '+l) +def LOG_DEBUG(l): print('DEBUGt: '+l) +def LOG_TRACE(l): pass # print('TRACE+ '+l) # ----------------------------------------------------------------------------------------------------------------- # Base threads @@ -12,25 +42,45 @@ from PyQt5 import QtCore class BaseThread(threading.Thread): - def __init__(self): - super().__init__() + def __init__(self, name=None, target=None): self._stop_thread = False + if name: + super().__init__(name=name, target=target) + else: + super().__init__(target=target) - def stop_thread(self): + def stop_thread(self, timeout=-1): self._stop_thread = True - self.join() - + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.join(timeout) + if not self.is_alive(): break + i = i + 1 + else: + LOG_WARN(f"BaseThread {self.name} BLOCKED") class BaseQThread(QtCore.QThread): - def __init__(self): + def __init__(self, name=None): + # NO name=name super().__init__() self._stop_thread = False + self.name = str(id(self)) - def stop_thread(self): + def stop_thread(self, timeout=-1): self._stop_thread = True - self.wait() - + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.wait(timeout) + if not self.isRunning(): break + i = i + 1 + sleep(ts.iTHREAD_TIMEOUT) + else: + LOG_WARN(f"BaseQThread {self.name} BLOCKED") # ----------------------------------------------------------------------------------------------------------------- # Toxcore threads @@ -38,45 +88,51 @@ class BaseQThread(QtCore.QThread): class InitThread(BaseThread): - def __init__(self, tox, plugin_loader, settings, is_first_start): - super().__init__() - self._tox, self._plugin_loader, self._settings = tox, plugin_loader, settings + def __init__(self, tox, plugin_loader, settings, app, is_first_start): + super().__init__(name='InitThread') + self._tox = tox + self._plugin_loader = plugin_loader + self._settings = settings + self._app = app self._is_first_start = is_first_start def run(self): - if self._is_first_start: - # download list of nodes if needed - download_nodes_list(self._settings) - # start plugins - self._plugin_loader.load() - - # bootstrap + LOG_DEBUG('InitThread run: ') try: - for data in generate_nodes(): - if self._stop_thread: - return - self._tox.bootstrap(*data) - self._tox.add_tcp_relay(*data) - except: - pass + if self._is_first_start: + if self._settings['download_nodes_list']: + LOG_INFO('downloading list of nodes') + download_nodes_list(self._settings, oArgs=self._app._args) - for _ in range(10): - if self._stop_thread: - return - time.sleep(1) - - while not self._tox.self_get_connection_status(): - try: - for data in generate_nodes(None): + if False: + lNodes = ts.generate_nodes() + LOG_INFO(f"bootstrapping {len(lNodes)!s} nodes") + for data in lNodes: if self._stop_thread: return self._tox.bootstrap(*data) self._tox.add_tcp_relay(*data) - except: - pass - finally: - time.sleep(5) + else: + LOG_INFO(f"calling test_net nodes") + threading.Timer(1.0, + self._app.test_net, + args=list(), + kwargs=dict(lElts=None, oThread=self, iMax=2) + ).start() + + if self._is_first_start: + LOG_INFO('starting plugins') + self._plugin_loader.load() + + except Exception as e: + LOG_DEBUG(f"InitThread run: ERROR {e}") + pass + for _ in range(ts.iTHREAD_JOINS): + if self._stop_thread: + return + sleep(ts.iTHREAD_SLEEP) + return class ToxIterateThread(BaseQThread): @@ -85,21 +141,27 @@ class ToxIterateThread(BaseQThread): self._tox = tox def run(self): + LOG_DEBUG('ToxIterateThread run: ') while not self._stop_thread: - self._tox.iterate() - time.sleep(self._tox.iteration_interval() / 1000) + try: + iMsec = self._tox.iteration_interval() + self._tox.iterate() + except Exception as e: + # Fatal Python error: Segmentation fault + LOG_ERROR('ToxIterateThread run: {e}') + sleep(iMsec / 1000) class ToxAVIterateThread(BaseQThread): - def __init__(self, toxav): super().__init__() self._toxav = toxav - + def run(self): + LOG_DEBUG('ToxAVIterateThread run: ') while not self._stop_thread: self._toxav.iterate() - time.sleep(self._toxav.iteration_interval() / 1000) + sleep(self._toxav.iteration_interval() / 1000) # ----------------------------------------------------------------------------------------------------------------- @@ -109,7 +171,7 @@ class ToxAVIterateThread(BaseQThread): class FileTransfersThread(BaseQThread): def __init__(self): - super().__init__() + super().__init__('FileTransfers') self._queue = queue.Queue() self._timeout = 0.01 @@ -124,14 +186,12 @@ class FileTransfersThread(BaseQThread): except queue.Empty: pass except queue.Full: - util.log('Queue is full in _thread') + LOG_WARN('Queue is full in _thread') except Exception as ex: - util.log('Exception in _thread: ' + str(ex)) + LOG_ERROR('in _thread: ' + str(ex)) _thread = FileTransfersThread() - - def start_file_transfer_thread(): _thread.start() diff --git a/toxygen/middleware/tox_factory.py b/toxygen/middleware/tox_factory.py index 9ee5c01..8addad4 100644 --- a/toxygen/middleware/tox_factory.py +++ b/toxygen/middleware/tox_factory.py @@ -1,34 +1,102 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import user_data.settings import wrapper.tox import wrapper.toxcore_enums_and_consts as enums import ctypes +import traceback +import os +global LOG +import logging +LOG = logging.getLogger('app.'+'tox_factory') +def LOG_DEBUG(l): print('DEBUGf: '+l) +def LOG_LOG(l): print('TRACf: '+l) -def tox_factory(data=None, settings=None): +from ctypes import * +from utils import util +from utils import ui as util_ui + +def tox_log_cb(iTox, level, file, line, func, message, *args): + """ + * @param level The severity of the log message. + * @param file The source file from which the message originated. + * @param line The source line from which the message originated. + * @param func The function from which the message originated. + * @param message The log message. + * @param user_data The user data pointer passed to tox_new in options. + """ + file = str(file, 'UTF-8') + func = str(func, 'UTF-8') + message = str(message, 'UTF-8') + if file == 'network.c' and line == 660: return + # root WARNING 3network.c#944:b'send_packet'attempted to send message with network family 10 (probably IPv6) on IPv4 socket + if file == 'network.c' and line == 944: return + message = f"{file}#{line}:{func} {message}" + LOG_LOG(# 'TRAC: ' + + message) + +def tox_factory(data=None, settings=None, args=None, app=None): """ :param data: user data from .tox file. None = no saved data, create new profile :param settings: current profile settings. None = default settings will be used :return: new tox instance """ - if settings is None: + if not settings: + LOG.warn("tox_factory using get_default_settings") settings = user_data.settings.Settings.get_default_settings() + else: + user_data.settings.clean_settings(settings) - tox_options = wrapper.tox.Tox.options_new() - tox_options.contents.udp_enabled = settings['udp_enabled'] - tox_options.contents.proxy_type = settings['proxy_type'] - tox_options.contents.proxy_host = bytes(settings['proxy_host'], 'UTF-8') - tox_options.contents.proxy_port = settings['proxy_port'] - tox_options.contents.start_port = settings['start_port'] - tox_options.contents.end_port = settings['end_port'] - tox_options.contents.tcp_port = settings['tcp_port'] - tox_options.contents.local_discovery_enabled = settings['lan_discovery'] - if data: # load existing profile - tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] - tox_options.contents.savedata_data = ctypes.c_char_p(data) - tox_options.contents.savedata_length = len(data) - else: # create new profile - tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] - tox_options.contents.savedata_data = None - tox_options.contents.savedata_length = 0 + try: + tox_options = wrapper.tox.Tox.options_new() + tox_options.contents.ipv6_enabled = settings['ipv6_enabled'] + tox_options.contents.udp_enabled = settings['udp_enabled'] + tox_options.contents.proxy_type = int(settings['proxy_type']) + if type(settings['proxy_host']) == str: + tox_options.contents.proxy_host = bytes(settings['proxy_host'],'UTF-8') + elif type(settings['proxy_host']) == bytes: + tox_options.contents.proxy_host = settings['proxy_host'] + else: + tox_options.contents.proxy_host = b'' + tox_options.contents.proxy_port = int(settings['proxy_port']) + tox_options.contents.start_port = settings['start_port'] + tox_options.contents.end_port = settings['end_port'] + tox_options.contents.tcp_port = settings['tcp_port'] + tox_options.contents.local_discovery_enabled = settings['local_discovery_enabled'] + tox_options.contents.dht_announcements_enabled = settings['dht_announcements_enabled'] + tox_options.contents.hole_punching_enabled = settings['hole_punching_enabled'] + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = ctypes.c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 - return wrapper.tox.Tox(tox_options) + # overrides + tox_options.contents.local_discovery_enabled = False + tox_options.contents.ipv6_enabled = False + tox_options.contents.hole_punching_enabled = False + + LOG.debug("wrapper.tox.Tox settings: " +repr(settings)) + + if tox_options._options_pointer: + c_callback = CFUNCTYPE(None, c_void_p, c_int, c_char_p, c_int, c_char_p, c_char_p, c_void_p) + tox_options.self_logger_cb = c_callback(tox_log_cb) + wrapper.tox.Tox.libtoxcore.tox_options_set_log_callback( + tox_options._options_pointer, + tox_options.self_logger_cb) + else: + logging.warn("No tox_options._options_pointer to add self_logger_cb" ) + + retval = wrapper.tox.Tox(tox_options) + except Exception as e: + if app and hasattr(app, '_log'): + app._log(f"ERROR: wrapper.tox.Tox failed: {e}") + LOG.warn(traceback.format_exc()) + raise + + if app and hasattr(app, '_log'): + app._log("DEBUG: wrapper.tox.Tox succeeded") + return retval diff --git a/toxygen/network/tox_dns.py b/toxygen/network/tox_dns.py index 02e97f5..2de374a 100644 --- a/toxygen/network/tox_dns.py +++ b/toxygen/network/tox_dns.py @@ -1,20 +1,40 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import json import urllib.request import utils.util as util from PyQt5 import QtNetwork, QtCore +try: + import requests +except ImportError: + requests = None +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) class ToxDns: - def __init__(self, settings): + def __init__(self, settings, log=None): self._settings = settings + self._log = log @staticmethod def _send_request(url, data): - req = urllib.request.Request(url) - req.add_header('Content-Type', 'application/json') - response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8')) - res = json.loads(str(response.read(), 'utf-8')) + if requests: + LOG.info('send_request loading with requests: ' + str(url)) + headers = dict() + headers['Content-Type'] = 'application/json' + req = requests.get(url, headers=headers) + if req.status_code < 300: + retval = req.content + else: + raise LookupError(str(req.status_code)) + else: + req = urllib.request.Request(url) + req.add_header('Content-Type', 'application/json') + response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8')) + retval = response.read() + res = json.loads(str(retval, 'utf-8')) if not res['c']: return res['tox_id'] else: @@ -29,12 +49,25 @@ class ToxDns: site = email.split('@')[1] data = {"action": 3, "name": "{}".format(email)} urls = ('https://{}/api'.format(site), 'http://{}/api'.format(site)) - if not self._settings['proxy_type']: # no proxy + if requests: + for url in urls: + LOG.info('TOX nodes loading with requests: ' + str(url)) + try: + headers = dict() + headers['Content-Type'] = 'application/json' + req = requests.get(url, headers=headers) + if req.status_code < 300: + result = req.content + return result + except Exception as ex: + LOG.error('ERROR: TOX DNS loading error with requests: ' + str(ex)) + + elif not self._settings['proxy_type']: # no proxy for url in urls: try: return self._send_request(url, data) except Exception as ex: - util.log('TOX DNS ERROR: ' + str(ex)) + LOG.error('ERROR: TOX DNS ' + str(ex)) else: # proxy netman = QtNetwork.QNetworkAccessManager() proxy = QtNetwork.QNetworkProxy() @@ -60,6 +93,6 @@ class ToxDns: if not result['c']: return result['tox_id'] except Exception as ex: - util.log('TOX DNS ERROR: ' + str(ex)) + LOG.error('ERROR: TOX DNS ' + str(ex)) return None # error diff --git a/toxygen/notifications/sound.py b/toxygen/notifications/sound.py index 361cd05..a106e80 100644 --- a/toxygen/notifications/sound.py +++ b/toxygen/notifications/sound.py @@ -3,6 +3,9 @@ import wave import pyaudio import os.path +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) SOUND_NOTIFICATION = { 'MESSAGE': 0, @@ -25,9 +28,19 @@ class AudioFile: def play(self): data = self.wf.readframes(self.chunk) - while data: - self.stream.write(data) - data = self.wf.readframes(self.chunk) + try: + while data: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + except Exception as e: + LOG.error(f"Error during AudioFile play {e!s}") + LOG.debug("Error during AudioFile play " \ + +' rate=' +str(self.wf.getframerate()) \ + + 'format=' +str(self.p.get_format_from_width(self.wf.getsampwidth())) \ + +' channels=' +str(self.wf.getnchannels()) \ + ) + + raise def close(self): self.stream.close() diff --git a/toxygen/plugin_support/plugin_support.py b/toxygen/plugin_support/plugin_support.py index ed45910..2bb7f40 100644 --- a/toxygen/plugin_support/plugin_support.py +++ b/toxygen/plugin_support/plugin_support.py @@ -1,3 +1,4 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import utils.util as util import os import importlib @@ -5,6 +6,14 @@ import inspect import plugins.plugin_super_class as pl import sys +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('plugin_support') +def trace(msg, *args, **kwargs): LOG._log(0, msg, []) +LOG.trace = trace + +log = lambda x: LOG.info(x) class Plugin: @@ -46,38 +55,49 @@ class PluginLoader: """ path = util.get_plugins_directory() if not os.path.exists(path): - util.log('Plugin dir not found') + self._app._LOG('WARN: Plugin directory not found: ' + path) return - else: - sys.path.append(path) + + sys.path.append(path) files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] for fl in files: if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'): continue - name = fl[:-3] # module name without .py + base_name = fl[:-3] # module name without .py try: - module = importlib.import_module(name) # import plugin - except ImportError: - util.log('Import error in module ' + name) + module = importlib.import_module(base_name) # import plugin + LOG.trace('Imported module: ' +base_name +' file: ' +fl) + except ImportError as e: + LOG.warn(f"Import error: {e}" +' file: ' +fl) continue except Exception as ex: - util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + LOG.error('importing ' + base_name + ' Exception: ' + str(ex)) continue for elem in dir(module): obj = getattr(module, elem) # looking for plugin class in module if not inspect.isclass(obj) or not hasattr(obj, 'is_plugin') or not obj.is_plugin: continue - print('Plugin', elem) try: # create instance of plugin class - instance = obj(self._app) - is_active = instance.get_short_name() in self._settings['plugins'] + instance = obj(self._app) # name, short_name, app + # needed by bday... + instance._profile=self._app._ms._profile + instance._settings=self._settings + short_name = instance.get_short_name() + is_active = short_name in self._settings['plugins'] if is_active: - instance.start() + try: + instance.start() + self._app.LOG('INFO: Started Plugin ' +short_name) + except Exception as e: + self._app.LOG.error(f"Starting Plugin ' +short_name +' {e}") + # else: LOG.info('Defined Plugin ' +short_name) except Exception as ex: - util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + LOG.error('in module ' + short_name + ' Exception: ' + str(ex)) continue - self._plugins[instance.get_short_name()] = Plugin(instance, is_active) + short_name = instance.get_short_name() + self._plugins[short_name] = Plugin(instance, is_active) + LOG.info('Added plugin: ' +short_name +' from file: ' +fl) break def callback_lossless(self, friend_number, data): @@ -114,7 +134,7 @@ class PluginLoader: for plugin in self._plugins.values(): try: result.append([plugin.instance.get_name(), # plugin full name - plugin.is_active, # is enabled + plugin.is_active, # is enabled plugin.instance.get_description(), # plugin description plugin.instance.get_short_name()]) # key - short unique name except: @@ -126,7 +146,13 @@ class PluginLoader: """ Return window or None for specified plugin """ - return self._plugins[key].instance.get_window() + try: + if key in self._plugins and hasattr(self._plugins[key], 'instance'): + return self._plugins[key].instance.get_window() + except Exception as e: + self._app.LOG('WARN: ' +key +' _plugins no slot instance: ' +str(e)) + + return None def toggle_plugin(self, key): """ @@ -162,6 +188,7 @@ class PluginLoader: for plugin in self._plugins.values(): if not plugin.is_active: continue + try: result.extend(plugin.instance.get_menu(num)) except: @@ -173,6 +200,10 @@ class PluginLoader: for plugin in self._plugins.values(): if not plugin.is_active: continue + if not hasattr(plugin.instance, 'get_message_menu'): + name = plugin.instance.get_short_name() + self._app.LOG('WARN: get_message_menu not found: ' + name) + continue try: result.extend(plugin.instance.get_message_menu(menu, selected_text)) except: @@ -189,6 +220,11 @@ class PluginLoader: del self._plugins[key] def reload(self): - print('Reloading plugins') + path = util.get_plugins_directory() + if not os.path.exists(path): + self._app.LOG('WARN: Plugin directory not found: ' + path) + return + self.stop() + self._app.LOG('INFO: Reloading plugins from ' +path) self.load() diff --git a/toxygen/smileys/smileys.py b/toxygen/smileys/smileys.py index 0391856..43ae3fd 100644 --- a/toxygen/smileys/smileys.py +++ b/toxygen/smileys/smileys.py @@ -4,6 +4,11 @@ import os from collections import OrderedDict from PyQt5 import QtCore +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) class SmileyLoader: """ @@ -31,7 +36,7 @@ class SmileyLoader: self._smileys = json.loads(fl.read()) fl.seek(0) tmp = json.loads(fl.read(), object_pairs_hook=OrderedDict) - print('Smiley pack {} loaded'.format(pack_name)) + LOG.info('Smiley pack {} loaded'.format(pack_name)) keys, values, self._list = [], [], [] for key, value in tmp.items(): value = util.join_path(self.get_smileys_path(), value) @@ -42,7 +47,7 @@ class SmileyLoader: except Exception as ex: self._smileys = {} self._list = [] - print('Smiley pack {} was not loaded. Error: {}'.format(pack_name, ex)) + LOG.error('Smiley pack {} was not loaded. Error: {}'.format(pack_name, str(ex))) def get_smileys_path(self): return util.join_path(util.get_smileys_directory(), self._curr_pack) if self._curr_pack is not None else None diff --git a/toxygen/stickers/tox/black.png b/toxygen/stickers/tox/black.png old mode 100755 new mode 100644 index 5d1e0eb..3a38a70 Binary files a/toxygen/stickers/tox/black.png and b/toxygen/stickers/tox/black.png differ diff --git a/toxygen/stickers/tox/red.png b/toxygen/stickers/tox/red.png old mode 100755 new mode 100644 index 3185319..cf7fb77 Binary files a/toxygen/stickers/tox/red.png and b/toxygen/stickers/tox/red.png differ diff --git a/toxygen/stickers/tox/tox_logo.png b/toxygen/stickers/tox/tox_logo.png old mode 100755 new mode 100644 index 977c5fc..afb2d2d Binary files a/toxygen/stickers/tox/tox_logo.png and b/toxygen/stickers/tox/tox_logo.png differ diff --git a/toxygen/stickers/tox/tox_logo_1.png b/toxygen/stickers/tox/tox_logo_1.png old mode 100755 new mode 100644 index cf1932c..038d833 Binary files a/toxygen/stickers/tox/tox_logo_1.png and b/toxygen/stickers/tox/tox_logo_1.png differ diff --git a/toxygen/stickers/tox/white.png b/toxygen/stickers/tox/white.png old mode 100755 new mode 100644 index 745b597..bee4a90 Binary files a/toxygen/stickers/tox/white.png and b/toxygen/stickers/tox/white.png differ diff --git a/toxygen/styles/rc/Hmovetoolbar.png b/toxygen/styles/rc/Hmovetoolbar.png old mode 100755 new mode 100644 index cead99e..4b55192 Binary files a/toxygen/styles/rc/Hmovetoolbar.png and b/toxygen/styles/rc/Hmovetoolbar.png differ diff --git a/toxygen/styles/rc/Hsepartoolbar.png b/toxygen/styles/rc/Hsepartoolbar.png old mode 100755 new mode 100644 index 7f183c8..58840be Binary files a/toxygen/styles/rc/Hsepartoolbar.png and b/toxygen/styles/rc/Hsepartoolbar.png differ diff --git a/toxygen/styles/rc/Vmovetoolbar.png b/toxygen/styles/rc/Vmovetoolbar.png old mode 100755 new mode 100644 index 512edce..c3b4762 Binary files a/toxygen/styles/rc/Vmovetoolbar.png and b/toxygen/styles/rc/Vmovetoolbar.png differ diff --git a/toxygen/styles/rc/Vsepartoolbar.png b/toxygen/styles/rc/Vsepartoolbar.png old mode 100755 new mode 100644 index d9dc156..5de9a34 Binary files a/toxygen/styles/rc/Vsepartoolbar.png and b/toxygen/styles/rc/Vsepartoolbar.png differ diff --git a/toxygen/styles/rc/branch_closed-on.png b/toxygen/styles/rc/branch_closed-on.png old mode 100755 new mode 100644 index d081e9b..9020fe7 Binary files a/toxygen/styles/rc/branch_closed-on.png and b/toxygen/styles/rc/branch_closed-on.png differ diff --git a/toxygen/styles/rc/branch_closed.png b/toxygen/styles/rc/branch_closed.png old mode 100755 new mode 100644 index d652159..7c20500 Binary files a/toxygen/styles/rc/branch_closed.png and b/toxygen/styles/rc/branch_closed.png differ diff --git a/toxygen/styles/rc/branch_open-on.png b/toxygen/styles/rc/branch_open-on.png old mode 100755 new mode 100644 index ec372b2..f41f80c Binary files a/toxygen/styles/rc/branch_open-on.png and b/toxygen/styles/rc/branch_open-on.png differ diff --git a/toxygen/styles/rc/branch_open.png b/toxygen/styles/rc/branch_open.png old mode 100755 new mode 100644 index 66f8e1a..efb6068 Binary files a/toxygen/styles/rc/branch_open.png and b/toxygen/styles/rc/branch_open.png differ diff --git a/toxygen/styles/rc/checkbox_checked.png b/toxygen/styles/rc/checkbox_checked.png old mode 100755 new mode 100644 index e09ce02..1539bc9 Binary files a/toxygen/styles/rc/checkbox_checked.png and b/toxygen/styles/rc/checkbox_checked.png differ diff --git a/toxygen/styles/rc/checkbox_checked_disabled.png b/toxygen/styles/rc/checkbox_checked_disabled.png old mode 100755 new mode 100644 index e09ce02..1539bc9 Binary files a/toxygen/styles/rc/checkbox_checked_disabled.png and b/toxygen/styles/rc/checkbox_checked_disabled.png differ diff --git a/toxygen/styles/rc/checkbox_checked_focus.png b/toxygen/styles/rc/checkbox_checked_focus.png old mode 100755 new mode 100644 index e09ce02..1539bc9 Binary files a/toxygen/styles/rc/checkbox_checked_focus.png and b/toxygen/styles/rc/checkbox_checked_focus.png differ diff --git a/toxygen/styles/rc/checkbox_indeterminate.png b/toxygen/styles/rc/checkbox_indeterminate.png old mode 100755 new mode 100644 index 41024f7..15e221b Binary files a/toxygen/styles/rc/checkbox_indeterminate.png and b/toxygen/styles/rc/checkbox_indeterminate.png differ diff --git a/toxygen/styles/rc/checkbox_indeterminate_disabled.png b/toxygen/styles/rc/checkbox_indeterminate_disabled.png old mode 100755 new mode 100644 index abdc01d..bc26933 Binary files a/toxygen/styles/rc/checkbox_indeterminate_disabled.png and b/toxygen/styles/rc/checkbox_indeterminate_disabled.png differ diff --git a/toxygen/styles/rc/checkbox_indeterminate_focus.png b/toxygen/styles/rc/checkbox_indeterminate_focus.png old mode 100755 new mode 100644 index a9a16f7..7c00620 Binary files a/toxygen/styles/rc/checkbox_indeterminate_focus.png and b/toxygen/styles/rc/checkbox_indeterminate_focus.png differ diff --git a/toxygen/styles/rc/checkbox_unchecked.png b/toxygen/styles/rc/checkbox_unchecked.png old mode 100755 new mode 100644 index 30deeb5..30631ba Binary files a/toxygen/styles/rc/checkbox_unchecked.png and b/toxygen/styles/rc/checkbox_unchecked.png differ diff --git a/toxygen/styles/rc/checkbox_unchecked_disabled.png b/toxygen/styles/rc/checkbox_unchecked_disabled.png old mode 100755 new mode 100644 index 30deeb5..30631ba Binary files a/toxygen/styles/rc/checkbox_unchecked_disabled.png and b/toxygen/styles/rc/checkbox_unchecked_disabled.png differ diff --git a/toxygen/styles/rc/checkbox_unchecked_focus.png b/toxygen/styles/rc/checkbox_unchecked_focus.png old mode 100755 new mode 100644 index 30deeb5..30631ba Binary files a/toxygen/styles/rc/checkbox_unchecked_focus.png and b/toxygen/styles/rc/checkbox_unchecked_focus.png differ diff --git a/toxygen/styles/rc/close-hover.png b/toxygen/styles/rc/close-hover.png old mode 100755 new mode 100644 index 657943a..f8fbb31 Binary files a/toxygen/styles/rc/close-hover.png and b/toxygen/styles/rc/close-hover.png differ diff --git a/toxygen/styles/rc/close-pressed.png b/toxygen/styles/rc/close-pressed.png old mode 100755 new mode 100644 index 937d005..7c644b6 Binary files a/toxygen/styles/rc/close-pressed.png and b/toxygen/styles/rc/close-pressed.png differ diff --git a/toxygen/styles/rc/close.png b/toxygen/styles/rc/close.png old mode 100755 new mode 100644 index bc0f576..b3e51a0 Binary files a/toxygen/styles/rc/close.png and b/toxygen/styles/rc/close.png differ diff --git a/toxygen/styles/rc/down_arrow.png b/toxygen/styles/rc/down_arrow.png old mode 100755 new mode 100644 index e271f7f..ff4a62b Binary files a/toxygen/styles/rc/down_arrow.png and b/toxygen/styles/rc/down_arrow.png differ diff --git a/toxygen/styles/rc/down_arrow_disabled.png b/toxygen/styles/rc/down_arrow_disabled.png old mode 100755 new mode 100644 index 5805d98..388339c Binary files a/toxygen/styles/rc/down_arrow_disabled.png and b/toxygen/styles/rc/down_arrow_disabled.png differ diff --git a/toxygen/styles/rc/left_arrow.png b/toxygen/styles/rc/left_arrow.png old mode 100755 new mode 100644 index f808d2d..f0c00ea Binary files a/toxygen/styles/rc/left_arrow.png and b/toxygen/styles/rc/left_arrow.png differ diff --git a/toxygen/styles/rc/left_arrow_disabled.png b/toxygen/styles/rc/left_arrow_disabled.png old mode 100755 new mode 100644 index f5b9af8..570a940 Binary files a/toxygen/styles/rc/left_arrow_disabled.png and b/toxygen/styles/rc/left_arrow_disabled.png differ diff --git a/toxygen/styles/rc/radio_checked.png b/toxygen/styles/rc/radio_checked.png old mode 100755 new mode 100644 index 14b1cb1..c6aacda Binary files a/toxygen/styles/rc/radio_checked.png and b/toxygen/styles/rc/radio_checked.png differ diff --git a/toxygen/styles/rc/radio_checked_disabled.png b/toxygen/styles/rc/radio_checked_disabled.png old mode 100755 new mode 100644 index 14b1cb1..c6aacda Binary files a/toxygen/styles/rc/radio_checked_disabled.png and b/toxygen/styles/rc/radio_checked_disabled.png differ diff --git a/toxygen/styles/rc/radio_checked_focus.png b/toxygen/styles/rc/radio_checked_focus.png old mode 100755 new mode 100644 index 14b1cb1..c6aacda Binary files a/toxygen/styles/rc/radio_checked_focus.png and b/toxygen/styles/rc/radio_checked_focus.png differ diff --git a/toxygen/styles/rc/radio_unchecked.png b/toxygen/styles/rc/radio_unchecked.png old mode 100755 new mode 100644 index 27af811..c0565b5 Binary files a/toxygen/styles/rc/radio_unchecked.png and b/toxygen/styles/rc/radio_unchecked.png differ diff --git a/toxygen/styles/rc/radio_unchecked_disabled.png b/toxygen/styles/rc/radio_unchecked_disabled.png old mode 100755 new mode 100644 index 27af811..c0565b5 Binary files a/toxygen/styles/rc/radio_unchecked_disabled.png and b/toxygen/styles/rc/radio_unchecked_disabled.png differ diff --git a/toxygen/styles/rc/radio_unchecked_focus.png b/toxygen/styles/rc/radio_unchecked_focus.png old mode 100755 new mode 100644 index 27af811..c0565b5 Binary files a/toxygen/styles/rc/radio_unchecked_focus.png and b/toxygen/styles/rc/radio_unchecked_focus.png differ diff --git a/toxygen/styles/rc/right_arrow.png b/toxygen/styles/rc/right_arrow.png old mode 100755 new mode 100644 index 9b0a4e6..75e5b5a Binary files a/toxygen/styles/rc/right_arrow.png and b/toxygen/styles/rc/right_arrow.png differ diff --git a/toxygen/styles/rc/right_arrow_disabled.png b/toxygen/styles/rc/right_arrow_disabled.png old mode 100755 new mode 100644 index 5c0bee4..31f4831 Binary files a/toxygen/styles/rc/right_arrow_disabled.png and b/toxygen/styles/rc/right_arrow_disabled.png differ diff --git a/toxygen/styles/rc/sizegrip.png b/toxygen/styles/rc/sizegrip.png old mode 100755 new mode 100644 index 350583a..09473be Binary files a/toxygen/styles/rc/sizegrip.png and b/toxygen/styles/rc/sizegrip.png differ diff --git a/toxygen/styles/rc/stylesheet-branch-end.png b/toxygen/styles/rc/stylesheet-branch-end.png old mode 100755 new mode 100644 index cb5d3b5..5569ee6 Binary files a/toxygen/styles/rc/stylesheet-branch-end.png and b/toxygen/styles/rc/stylesheet-branch-end.png differ diff --git a/toxygen/styles/rc/stylesheet-branch-more.png b/toxygen/styles/rc/stylesheet-branch-more.png old mode 100755 new mode 100644 index 6271140..57fe30d Binary files a/toxygen/styles/rc/stylesheet-branch-more.png and b/toxygen/styles/rc/stylesheet-branch-more.png differ diff --git a/toxygen/styles/rc/stylesheet-vline.png b/toxygen/styles/rc/stylesheet-vline.png old mode 100755 new mode 100644 index 87536cc..253cacb Binary files a/toxygen/styles/rc/stylesheet-vline.png and b/toxygen/styles/rc/stylesheet-vline.png differ diff --git a/toxygen/styles/rc/transparent.png b/toxygen/styles/rc/transparent.png old mode 100755 new mode 100644 index 483df25..cf1c4f6 Binary files a/toxygen/styles/rc/transparent.png and b/toxygen/styles/rc/transparent.png differ diff --git a/toxygen/styles/rc/undock.png b/toxygen/styles/rc/undock.png old mode 100755 new mode 100644 index 88691d7..4a7b0c8 Binary files a/toxygen/styles/rc/undock.png and b/toxygen/styles/rc/undock.png differ diff --git a/toxygen/styles/rc/up_arrow.png b/toxygen/styles/rc/up_arrow.png old mode 100755 new mode 100644 index abcc724..0cc7d6d Binary files a/toxygen/styles/rc/up_arrow.png and b/toxygen/styles/rc/up_arrow.png differ diff --git a/toxygen/styles/rc/up_arrow_disabled.png b/toxygen/styles/rc/up_arrow_disabled.png old mode 100755 new mode 100644 index b9c8e3b..99c6b67 Binary files a/toxygen/styles/rc/up_arrow_disabled.png and b/toxygen/styles/rc/up_arrow_disabled.png differ diff --git a/toxygen/ui/av_widgets.py b/toxygen/ui/av_widgets.py index e5773a8..24e4a38 100644 --- a/toxygen/ui/av_widgets.py +++ b/toxygen/ui/av_widgets.py @@ -1,9 +1,16 @@ +import threading + from PyQt5 import QtCore, QtGui, QtWidgets -from ui import widgets -import utils.util as util import pyaudio import wave +from ui import widgets +import utils.util as util +import tests.support_testing as ts + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) class IncomingCallWidget(widgets.CenteredWidget): @@ -11,7 +18,7 @@ class IncomingCallWidget(widgets.CenteredWidget): super().__init__() self._settings = settings self._calls_manager = calls_manager - self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint) + self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint) # | QtCore.Qt.WindowStaysOnTopHint self.resize(QtCore.QSize(500, 270)) self.avatar_label = QtWidgets.QLabel(self) self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64)) @@ -45,9 +52,9 @@ class IncomingCallWidget(widgets.CenteredWidget): self.accept_audio.setIconSize(QtCore.QSize(150, 150)) self.accept_video.setIconSize(QtCore.QSize(140, 140)) self.decline.setIconSize(QtCore.QSize(140, 140)) - self.accept_audio.setStyleSheet("QPushButton { border: none }") - self.accept_video.setStyleSheet("QPushButton { border: none }") - self.decline.setStyleSheet("QPushButton { border: none }") + #self.accept_audio.setStyleSheet("QPushButton { border: none }") + #self.accept_video.setStyleSheet("QPushButton { border: none }") + #self.decline.setStyleSheet("QPushButton { border: none }") self.setWindowTitle(text) self.name.setText(name) self.call_type.setText(text) @@ -56,75 +63,113 @@ class IncomingCallWidget(widgets.CenteredWidget): self.accept_video.clicked.connect(self.accept_call_with_video) self.decline.clicked.connect(self.decline_call) - class SoundPlay(QtCore.QThread): + output_device_index = self._settings._args.audio['output'] - def __init__(self): - QtCore.QThread.__init__(self) - self.a = None + if False and self._settings['calls_sound']: + class SoundPlay(QtCore.QThread): - def run(self): - class AudioFile: - chunk = 1024 + def __init__(self): + QtCore.QThread.__init__(self) + self.a = None - def __init__(self, fl): - self.stop = False - self.fl = fl - self.wf = wave.open(self.fl, 'rb') - self.p = pyaudio.PyAudio() - self.stream = self.p.open( - format=self.p.get_format_from_width(self.wf.getsampwidth()), - channels=self.wf.getnchannels(), - rate=self.wf.getframerate(), - output=True) + def run(self): + class AudioFile: + chunk = 1024 - def play(self): - while not self.stop: - data = self.wf.readframes(self.chunk) - while data and not self.stop: - self.stream.write(data) - data = self.wf.readframes(self.chunk) + def __init__(self, fl): + self.stop = False + self.fl = fl self.wf = wave.open(self.fl, 'rb') + self.p = pyaudio.PyAudio() + self.stream = self.p.open( + format=self.p.get_format_from_width(self.wf.getsampwidth()), + channels=self.wf.getnchannels(), + rate=self.wf.getframerate(), + # why no device? + output_device_index=output_device_index, + output=True) - def close(self): - self.stream.close() - self.p.terminate() + def play(self): + while not self.stop: + data = self.wf.readframes(self.chunk) + # dunno + if not data: break + while data and not self.stop: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + self.wf = wave.open(self.fl, 'rb') - self.a = AudioFile(util.join_path(util.get_sounds_directory(), 'call.wav')) - self.a.play() - self.a.close() + def close(self): + try: + self.stream.close() + self.p.terminate() + except Exception as e: + # malloc_consolidate(): unaligned fastbin chunk detected + LOG.warn("SoundPlay close exception {e}") + + self.a = AudioFile(util.join_path(util.get_sounds_directory(), 'call.wav')) + self.a.play() + self.a.close() - if self._settings['calls_sound']: self.thread = SoundPlay() self.thread.start() else: self.thread = None def stop(self): + if self._processing: + self.close() if self.thread is not None: self.thread.a.stop = True - self.thread.wait() - self.close() + i = 0 + while i < ts.iTHREAD_JOINS: + self.thread.wait(ts.iTHREAD_TIMEOUT) + if not self.thread.isRunning(): break + i = i + 1 + else: + LOG.warn(f"SoundPlay {self.thread.a} BLOCKED") + # Fatal Python error: Segmentation fault + self.thread.a.stream.close() + self.thread.a.p.terminate() + self.thread.a.close() + # dunno -failsafe + self.thread.terminate() + #? dunno + self._processing = False def accept_call_with_audio(self): if self._processing: + LOG.warn(f" accept_call_with_audio from {self._friend_number}") return + LOG.debug(f" accept_call_with_audio from {self._friend_number}") self._processing = True - self._calls_manager.accept_call(self._friend_number, True, False) - self.stop() + try: + self._calls_manager.accept_call(self._friend_number, True, False) + finally: + self.stop() def accept_call_with_video(self): + # ts.trepan_handler() + if self._processing: + LOG.warn(__name__+f" accept_call_with_video from {self._friend_number}") return + self.setWindowTitle('Answering video call') self._processing = True - self._calls_manager.accept_call(self._friend_number, True, True) - self.stop() + LOG.debug(f" accept_call_with_video from {self._friend_number}") + try: + self._calls_manager.accept_call(self._friend_number, True, True) + finally: + self.stop() def decline_call(self): if self._processing: return self._processing = True - self._calls_manager.stop_call(self._friend_number, False) - self.stop() + try: + self._calls_manager.stop_call(self._friend_number, False) + finally: + self.stop() def set_pixmap(self, pixmap): self.avatar_label.setPixmap(pixmap) diff --git a/toxygen/ui/group_bans_widgets.py b/toxygen/ui/group_bans_widgets.py index b2758c7..77fedaa 100644 --- a/toxygen/ui/group_bans_widgets.py +++ b/toxygen/ui/group_bans_widgets.py @@ -48,8 +48,9 @@ class GroupBansScreen(CenteredWidget): self._refresh_bans_list() def _retranslate_ui(self): - self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) - +# self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) + pass + def _refresh_bans_list(self): self.bansListWidget.clear() can_cancel_ban = self._group.is_self_moderator_or_founder() diff --git a/toxygen/ui/group_peers_list.py b/toxygen/ui/group_peers_list.py index 9d2632d..16a6cfc 100644 --- a/toxygen/ui/group_peers_list.py +++ b/toxygen/ui/group_peers_list.py @@ -11,12 +11,15 @@ class PeerItem(QtWidgets.QWidget): self.nameLabel.setGeometry(5, 0, width - 5, 34) name = peer.name if peer.is_current_user: - name += util_ui.tr(' (You)') + name += util_ui.tr(' *') self.nameLabel.setText(name) if peer.status == TOX_USER_STATUS['NONE']: - style = 'QLabel {color: green}' + if peer.is_current_user: + style = 'QLabel {color: green;}' + else: + style = 'QLabel {color: green}' elif peer.status == TOX_USER_STATUS['AWAY']: - style = 'QLabel {color: yellow}' + style = 'QLabel {color: blue}' else: style = 'QLabel {color: red}' self.nameLabel.setStyleSheet(style) diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py index 5a510a5..d1dcfc6 100644 --- a/toxygen/ui/main_screen.py +++ b/toxygen/ui/main_screen.py @@ -1,18 +1,81 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os +import logging from ui.contact_items import * from ui.widgets import MultilineEdit from ui.main_screen_widgets import * import utils.util as util import utils.ui as util_ui from PyQt5 import uic +from PyQt5 import QtWidgets, QtGui +from user_data.settings import Settings +iMAX = 70 +global LOG +LOG = logging.getLogger('app.'+__name__) + +class QTextEditLogger(logging.Handler): + def __init__(self, parent, app): + super().__init__() + self.widget = QtWidgets.QPlainTextEdit(parent) + self.widget.setReadOnly(True) + + if app and app._settings: + size = app._settings['message_font_size'] + font_name = app._settings['font'] + else: + size = 12 + font_name = "Courier New" + font = QtGui.QFont(font_name, size, QtGui.QFont.Bold) + self.widget.setFont(font) + + def emit(self, record): + msg = self.format(record) + self.widget.appendPlainText(msg) + + +class LogDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit): + + def __init__(self, parent=None, app=None): + global iMAX + super().__init__(parent) + + logTextBox = QTextEditLogger(self, app) + # You can format what is printed to text box - %(levelname)s + logTextBox.setFormatter(logging.Formatter('%(name)s %(asctime)-4s - %(message)s')) + logTextBox.setLevel(app._args.loglevel) + logging.getLogger().addHandler(logTextBox) + + self._button = QtWidgets.QPushButton(self) + self._button.setText('Copy All') + self._logTextBox = logTextBox + + layout = QtWidgets.QVBoxLayout() + # Add the new logging box widget to the layout + layout.addWidget(logTextBox.widget) + layout.addWidget(self._button) + self.setLayout(layout) + settings = Settings.get_default_settings(app._args) + #self.setBaseSize( + self.resize(min(iMAX * settings['message_font_size'], parent.width()), 350) + + # Connect signal to slot + self._button.clicked.connect(self.test) + + def test(self): + # FixMe: 65:8: E1101: Instance of 'QTextEditLogger' has no 'selectAll' member (no-member) + # :66:8: E1101: Instance of 'QTextEditLogger' has no 'copy' member (no-member) + self._logTextBox.selectAll() + self._logTextBox.copy() class MainWindow(QtWidgets.QMainWindow): - def __init__(self, settings, tray): + def __init__(self, settings, tray, app): super().__init__() self._settings = settings self._contacts_manager = None self._tray = tray + self._app = app self._widget_factory = None self._modal_window = None self._plugins_loader = None @@ -23,9 +86,14 @@ class MainWindow(QtWidgets.QMainWindow): self._file_transfer_handler = self._history_loader = self._groups_service = self._calls_manager = None self._should_show_group_peers_list = False self.initUI() + global iMAX + if iMAX == 100: + # take a rough guess of 2/3 the default width at the default font + iMAX = settings['width'] * 2/3 / settings['message_font_size'] + self._me = LogDialog(self, app) def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader, - file_transfer_handler, history_loader, calls_manager, groups_service, toxes): + file_transfer_handler, history_loader, calls_manager, groups_service, toxes, app): self._widget_factory = widget_factory self._tray = tray self._contacts_manager = contacts_manager @@ -36,6 +104,7 @@ class MainWindow(QtWidgets.QMainWindow): self._calls_manager = calls_manager self._groups_service = groups_service self._toxes = toxes + self._app = app self._messenger = messenger self._contacts_manager.active_contact_changed.add_callback(self._new_contact_selected) self.messageEdit.set_dependencies(messenger, contacts_manager, file_transfer_handler) @@ -51,11 +120,17 @@ class MainWindow(QtWidgets.QMainWindow): def setup_menu(self, window): self.menubar = QtWidgets.QMenuBar(window) self.menubar.setObjectName("menubar") - self.menubar.setNativeMenuBar(False) - self.menubar.setMinimumSize(self.width(), 25) - self.menubar.setMaximumSize(self.width(), 25) - self.menubar.setBaseSize(self.width(), 25) - self.menuProfile = QtWidgets.QMenu(self.menubar) + self.menubar.setNativeMenuBar(True) # was False + self.menubar.setMinimumSize(self.width(), 250) + self.menubar.setMaximumSize(self.width(), 32) + self.menubar.setBaseSize(self.width(), 250) + + self.actionTest_tox = QtWidgets.QAction(window) + self.actionTest_tox.setObjectName("actionTest_tox") + self.actionTest_socks = QtWidgets.QAction(window) + self.actionTest_socks.setObjectName("actionTest_socks") + self.actionQuit_program = QtWidgets.QAction(window) + self.actionQuit_program.setObjectName("actionQuit_program") self.menuProfile = QtWidgets.QMenu(self.menubar) self.menuProfile.setObjectName("menuProfile") @@ -64,7 +139,7 @@ class MainWindow(QtWidgets.QMainWindow): self.menuSettings.setObjectName("menuSettings") self.menuPlugins = QtWidgets.QMenu(self.menubar) self.menuPlugins.setObjectName("menuPlugins") - self.menuAbout = QtWidgets.QMenu(self.menubar) + self.menuAbout = QtWidgets.QMenu(self.menubar) # alignment=QtCore.Qt.AlignRight self.menuAbout.setObjectName("menuAbout") self.actionAdd_friend = QtWidgets.QAction(window) @@ -81,6 +156,9 @@ class MainWindow(QtWidgets.QMainWindow): self.actionNetwork.setObjectName("actionNetwork") self.actionAbout_program = QtWidgets.QAction(window) self.actionAbout_program.setObjectName("actionAbout_program") + + self.actionLog_console = QtWidgets.QAction(window) + self.actionLog_console.setObjectName("actionLog_console") self.updateSettings = QtWidgets.QAction(window) self.actionSettings = QtWidgets.QAction(window) self.actionSettings.setObjectName("actionSettings") @@ -89,6 +167,8 @@ class MainWindow(QtWidgets.QMainWindow): self.pluginData = QtWidgets.QAction(window) self.importPlugin = QtWidgets.QAction(window) self.reloadPlugins = QtWidgets.QAction(window) + self.reloadToxchat = QtWidgets.QAction(window) + self.lockApp = QtWidgets.QAction(window) self.createGC = QtWidgets.QAction(window) self.joinGC = QtWidgets.QAction(window) @@ -97,6 +177,10 @@ class MainWindow(QtWidgets.QMainWindow): self.menuProfile.addAction(self.actionAdd_friend) self.menuProfile.addAction(self.actionSettings) self.menuProfile.addAction(self.lockApp) + self.menuProfile.addAction(self.actionTest_tox) + self.menuProfile.addAction(self.actionTest_socks) + self.menuProfile.addAction(self.actionQuit_program) + self.menuGC.addAction(self.createGC) self.menuGC.addAction(self.joinGC) self.menuGC.addAction(self.gc_invites) @@ -110,6 +194,9 @@ class MainWindow(QtWidgets.QMainWindow): self.menuPlugins.addAction(self.pluginData) self.menuPlugins.addAction(self.importPlugin) self.menuPlugins.addAction(self.reloadPlugins) + self.menuPlugins.addAction(self.reloadToxchat) + self.menuPlugins.addAction(self.actionLog_console) + self.menuAbout.addAction(self.actionAbout_program) self.menubar.addAction(self.menuProfile.menuAction()) @@ -118,7 +205,12 @@ class MainWindow(QtWidgets.QMainWindow): self.menubar.addAction(self.menuPlugins.menuAction()) self.menubar.addAction(self.menuAbout.menuAction()) + self.actionTest_socks.triggered.connect(self.test_socks) + self.actionTest_tox.triggered.connect(self.test_tox) + + self.actionQuit_program.triggered.connect(self.quit_program) self.actionAbout_program.triggered.connect(self.about_program) + self.actionLog_console.triggered.connect(self.log_console) self.actionNetwork.triggered.connect(self.network_settings) self.actionAdd_friend.triggered.connect(self.add_contact_triggered) self.createGC.triggered.connect(self.create_gc) @@ -134,6 +226,7 @@ class MainWindow(QtWidgets.QMainWindow): self.lockApp.triggered.connect(self.lock_app) self.importPlugin.triggered.connect(self.import_plugin) self.reloadPlugins.triggered.connect(self.reload_plugins) + self.reloadToxchat.triggered.connect(self.reload_toxchat) self.gc_invites.triggered.connect(self._open_gc_invites_list) def languageChange(self, *args, **kwargs): @@ -141,10 +234,17 @@ class MainWindow(QtWidgets.QMainWindow): def event(self, event): if event.type() == QtCore.QEvent.WindowActivate: - self._tray.setIcon(QtGui.QIcon(util.join_path(util.get_images_directory(), 'icon.png'))) + if hasattr(self, '_tray') and self._tray: + self._tray.setIcon(QtGui.QIcon(util.join_path(util.get_images_directory(), 'icon.png'))) self.messages.repaint() return super().event(event) + def status(self, line): + """For now, this uses the unused space on the menubar line + It could be a status line at the bottom, or a statusline with history.""" + self.menuAbout.setTitle(line[:iMAX]) + return line + def retranslateUi(self): self.lockApp.setText(util_ui.tr("Lock")) self.menuPlugins.setTitle(util_ui.tr("Plugins")) @@ -163,12 +263,17 @@ class MainWindow(QtWidgets.QMainWindow): self.actionNotifications.setText(util_ui.tr("Notifications")) self.actionNetwork.setText(util_ui.tr("Network")) self.actionAbout_program.setText(util_ui.tr("About program")) + self.actionLog_console.setText(util_ui.tr("Console Log")) + self.actionTest_tox.setText(util_ui.tr("Bootstrap")) + self.actionTest_socks.setText(util_ui.tr("Test program")) + self.actionQuit_program.setText(util_ui.tr("Quit program")) self.actionSettings.setText(util_ui.tr("Settings")) self.audioSettings.setText(util_ui.tr("Audio")) self.videoSettings.setText(util_ui.tr("Video")) self.updateSettings.setText(util_ui.tr("Updates")) self.importPlugin.setText(util_ui.tr("Import plugin")) self.reloadPlugins.setText(util_ui.tr("Reload plugins")) + self.reloadToxchat.setText(util_ui.tr("Reload tox.chat")) self.searchLineEdit.setPlaceholderText(util_ui.tr("Search")) self.sendMessageButton.setToolTip(util_ui.tr("Send message")) @@ -409,6 +514,7 @@ class MainWindow(QtWidgets.QMainWindow): self.peers_list.setGeometry(width * 3 // 4, 0, width - width * 3 // 4, self.height() - 155) invites_button_visible = self.groupInvitesPushButton.isVisible() + LOG.debug(f"invites_button_visible={invites_button_visible}") self.friends_list.setGeometry(0, 125 if invites_button_visible else 100, 270, self.height() - 150 if invites_button_visible else self.height() - 125) @@ -445,9 +551,12 @@ class MainWindow(QtWidgets.QMainWindow): # Functions which called when user click in menu # ----------------------------------------------------------------------------------------------------------------- + def log_console(self): + self._me.show() + def about_program(self): # TODO: replace with window - text = util_ui.tr('Toxygen is Tox client written on Python.\nVersion: ') + text = util_ui.tr('Toxygen is Tox client written in Python.\nVersion: ') text += '' + '\nGitHub: https://github.com/toxygen-project/toxygen/' title = util_ui.tr('About') util_ui.message_box(text, title) @@ -504,13 +613,16 @@ class MainWindow(QtWidgets.QMainWindow): self._modal_window.show() def reload_plugins(self): - if self._plugin_loader is not None: + if hasattr(self, '_plugin_loader') and self._plugin_loader is not None: self._plugin_loader.reload() + def reload_toxchat(self): + pass + @staticmethod def import_plugin(): - directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugin')) - if directory: + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugins')) + if directory and os.path.isdir(directory): src = directory + '/' dest = util.get_plugins_directory() util.copy(src, dest) @@ -523,6 +635,22 @@ class MainWindow(QtWidgets.QMainWindow): else: util_ui.message_box(util_ui.tr('Error. Profile password is not set.'), util_ui.tr("Cannot lock app")) + def test_tox(self): + self._app._test_tox() + + def test_socks(self): + self._app._test_socks() + + def quit_program(self): + try: + self.close_window() + self._app._stop_app() + except KeyboardInterrupt: + pass + sys.stderr.write('sys.exit' +'\n') + # unreached? + sys.exit(0) + def show_menu(self): if not hasattr(self, 'menu'): self.menu = DropdownMenu(self) @@ -712,7 +840,8 @@ class MainWindow(QtWidgets.QMainWindow): def update_gc_invites_button_state(self): invites_count = self._groups_service.group_invites_count - self.groupInvitesPushButton.setVisible(invites_count > 0) + LOG.debug(f"invites_count={invites_count}") + self.groupInvitesPushButton.setVisible(True) # invites_count > 0 text = util_ui.tr('{} new invites to group chats').format(invites_count) self.groupInvitesPushButton.setText(text) self.resizeEvent() diff --git a/toxygen/ui/menu.py b/toxygen/ui/menu.py index 8aec578..4516acd 100644 --- a/toxygen/ui/menu.py +++ b/toxygen/ui/menu.py @@ -1,18 +1,27 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from PyQt5 import QtCore, QtGui, QtWidgets, uic +import pyaudio + from user_data.settings import * from utils.util import * from ui.widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow -import pyaudio import updater.updater as updater import utils.ui as util_ui -import cv2 +import tests.support_testing as ts +from user_data import settings +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +global oPYA +oPYA = pyaudio.PyAudio() class AddContact(CenteredWidget): """Add contact form""" def __init__(self, settings, contacts_manager, tox_id=''): super().__init__() + self._app = QtWidgets.QApplication.instance() self._settings = settings self._contacts_manager = contacts_manager uic.loadUi(get_views_path('add_contact_screen'), self) @@ -56,6 +65,7 @@ class NetworkSettings(CenteredWidget): """Network settings form: UDP, Ipv6 and proxy""" def __init__(self, settings, reset): super().__init__() + self._app = QtWidgets.QApplication.instance() self._settings = settings self._reset = reset uic.loadUi(get_views_path('network_settings_screen'), self) @@ -68,16 +78,20 @@ class NetworkSettings(CenteredWidget): self.portLineEdit = LineEdit(self) self.portLineEdit.setGeometry(100, 325, 270, 30) + self.urlLineEdit = LineEdit(self) + self.urlLineEdit.setGeometry(100, 370, 270, 30) + self.restartCorePushButton.clicked.connect(self._restart_core) self.ipv6CheckBox.setChecked(self._settings['ipv6_enabled']) self.udpCheckBox.setChecked(self._settings['udp_enabled']) self.proxyCheckBox.setChecked(self._settings['proxy_type']) self.ipLineEdit.setText(self._settings['proxy_host']) self.portLineEdit.setText(str(self._settings['proxy_port'])) + self.urlLineEdit.setText(str(self._settings['download_nodes_url'])) self.httpProxyRadioButton.setChecked(self._settings['proxy_type'] == 1) self.socksProxyRadioButton.setChecked(self._settings['proxy_type'] != 1) self.downloadNodesCheckBox.setChecked(self._settings['download_nodes_list']) - self.lanCheckBox.setChecked(self._settings['lan_discovery']) + self.lanCheckBox.setChecked(self._settings['local_discovery_enabled']) self._retranslate_ui() self.proxyCheckBox.stateChanged.connect(lambda x: self._activate_proxy()) self._activate_proxy() @@ -90,11 +104,13 @@ class NetworkSettings(CenteredWidget): self.proxyCheckBox.setText(util_ui.tr("Proxy")) self.ipLabel.setText(util_ui.tr("IP:")) self.portLabel.setText(util_ui.tr("Port:")) + self.urlLabel.setText(util_ui.tr("ChatUrl:")) self.restartCorePushButton.setText(util_ui.tr("Restart TOX core")) self.httpProxyRadioButton.setText(util_ui.tr("HTTP")) self.socksProxyRadioButton.setText(util_ui.tr("Socks 5")) self.downloadNodesCheckBox.setText(util_ui.tr("Download nodes list from tox.chat")) - self.warningLabel.setText(util_ui.tr("WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) +# self.warningLabel.setText(util_ui.tr("WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) + self.warningLabel.setText(util_ui.tr("Changing settings require 'Restart TOX core'")) def _activate_proxy(self): bl = self.proxyCheckBox.isChecked() @@ -113,14 +129,15 @@ class NetworkSettings(CenteredWidget): self._settings['proxy_type'] = 2 - int(self.httpProxyRadioButton.isChecked()) if proxy_enabled else 0 self._settings['proxy_host'] = str(self.ipLineEdit.text()) self._settings['proxy_port'] = int(self.portLineEdit.text()) + self._settings['download_nodes_url'] = str(self.urlLineEdit.text()) self._settings['download_nodes_list'] = self.downloadNodesCheckBox.isChecked() - self._settings['lan_discovery'] = self.lanCheckBox.isChecked() + self._settings['local_discovery_enabled'] = self.lanCheckBox.isChecked() self._settings.save() # recreate tox instance self._reset() self.close() except Exception as ex: - log('Exception in restart: ' + str(ex)) + LOG.error('ERROR: Exception in restart: ' + str(ex)) class PrivacySettings(CenteredWidget): @@ -131,6 +148,7 @@ class PrivacySettings(CenteredWidget): :type contacts_manager: ContactsManager """ super().__init__() + self._app = QtWidgets.QApplication.instance() self._contacts_manager = contacts_manager self._settings = settings self.initUI() @@ -249,6 +267,7 @@ class NotificationsSettings(CenteredWidget): def __init__(self, setttings): super().__init__() + self._app = QtWidgets.QApplication.instance() self._settings = setttings uic.loadUi(get_views_path('notifications_settings_screen'), self) self._update_ui() @@ -281,6 +300,7 @@ class InterfaceSettings(CenteredWidget): def __init__(self, settings, smiley_loader): super().__init__() + self._app = QtWidgets.QApplication.instance() self._settings = settings self._smiley_loader = smiley_loader @@ -289,10 +309,10 @@ class InterfaceSettings(CenteredWidget): self.center() def _update_ui(self): - themes = list(self._settings.built_in_themes().keys()) + themes = list(settings.built_in_themes().keys()) self.themeComboBox.addItems(themes) theme = self._settings['theme'] - if theme in self._settings.built_in_themes().keys(): + if theme in settings.built_in_themes().keys(): index = themes.index(theme) else: index = 0 @@ -312,10 +332,10 @@ class InterfaceSettings(CenteredWidget): index = smiley_packs.index('default') self.smileysPackComboBox.setCurrentIndex(index) - app_closing_setting = self._settings['close_app'] - self.closeRadioButton.setChecked(app_closing_setting == 0) - self.hideRadioButton.setChecked(app_closing_setting == 1) - self.closeToTrayRadioButton.setChecked(app_closing_setting == 2) + self._app_closing_setting = self._settings['close_app'] + self.closeRadioButton.setChecked(self._app_closing_setting == 0) + self.hideRadioButton.setChecked(self._app_closing_setting == 1) + self.closeToTrayRadioButton.setChecked(self._app_closing_setting == 2) self.compactModeCheckBox.setChecked(self._settings['compact_mode']) self.showAvatarsCheckBox.setChecked(self._settings['show_avatars']) @@ -337,7 +357,7 @@ class InterfaceSettings(CenteredWidget): self.closeRadioButton.setText(util_ui.tr("Close app")) self.hideRadioButton.setText(util_ui.tr("Hide app")) self.closeToTrayRadioButton.setText(util_ui.tr("Close to tray")) - self.mirrorModeCheckBox.setText(util_ui.tr("Mirror mode")) +# self.mirrorModeCheckBox.setText(util_ui.tr("Mirror mode")) self.compactModeCheckBox.setText(util_ui.tr("Compact contact list")) self.importSmileysPushButton.setText(util_ui.tr("Import smiley pack")) self.importStickersPushButton.setText(util_ui.tr("Import sticker pack")) @@ -360,24 +380,23 @@ class InterfaceSettings(CenteredWidget): copy(src, dest) def closeEvent(self, event): - app = QtWidgets.QApplication.instance() self._settings['theme'] = str(self.themeComboBox.currentText()) try: theme = self._settings['theme'] - styles_path = join_path(get_styles_directory(), self._settings.built_in_themes()[theme]) + styles_path = join_path(get_styles_directory(), settings.built_in_themes()[theme]) with open(styles_path) as fl: style = fl.read() - app.setStyleSheet(style) + self._app.setStyleSheet(style) except IsADirectoryError: pass self._settings['smileys'] = self.smileysCheckBox.isChecked() restart = False - if self._settings['mirror_mode'] != self.mirrorModeCheckBox.isChecked(): - self._settings['mirror_mode'] = self.mirrorModeCheckBox.isChecked() - restart = True +# if self._settings['mirror_mode'] != self.mirrorModeCheckBox.isChecked(): +# self._settings['mirror_mode'] = self.mirrorModeCheckBox.isChecked() +# restart = True if self._settings['compact_mode'] != self.compactModeCheckBox.isChecked(): self._settings['compact_mode'] = self.compactModeCheckBox.isChecked() @@ -394,9 +413,9 @@ class InterfaceSettings(CenteredWidget): if self._settings['language'] != language: self._settings['language'] = language path = Settings.supported_languages()[language] - app.removeTranslator(app.translator) - app.translator.load(join_path(get_translations_directory(), path)) - app.installTranslator(app.translator) + self._app.removeTranslator(self._app.translator) + self._app.translator.load(join_path(get_translations_directory(), path)) + self._app.installTranslator(self._app.translator) app_closing_setting = 0 if self.hideRadioButton.isChecked(): @@ -417,19 +436,26 @@ class AudioSettings(CenteredWidget): def __init__(self, settings): super().__init__() + self._app = QtWidgets.QApplication.instance() self._settings = settings self._in_indexes = self._out_indexes = None uic.loadUi(get_views_path('audio_settings_screen'), self) self._update_ui() self.center() - + def closeEvent(self, event): - self._settings.audio['input'] = self._in_indexes[self.inputDeviceComboBox.currentIndex()] - self._settings.audio['output'] = self._out_indexes[self.outputDeviceComboBox.currentIndex()] - self._settings.save() + if 'audio' not in self._settings: + ex = f"self._settings=id(self._settings) {self._settings!r}" + LOG.warn('AudioSettings.closeEvent settings error: ' + str(ex)) + else: + self._settings['audio']['input'] = \ + self._in_indexes[self.inputDeviceComboBox.currentIndex()] + self._settings['audio']['output'] = \ + self._out_indexes[self.outputDeviceComboBox.currentIndex()] + self._settings.save() def _update_ui(self): - p = pyaudio.PyAudio() + p = oPYA self._in_indexes, self._out_indexes = [], [] for i in range(p.get_device_count()): device = p.get_device_info_by_index(i) @@ -439,8 +465,11 @@ class AudioSettings(CenteredWidget): if device["maxOutputChannels"]: self.outputDeviceComboBox.addItem(str(device["name"])) self._out_indexes.append(i) - self.inputDeviceComboBox.setCurrentIndex(self._in_indexes.index(self._settings.audio['input'])) - self.outputDeviceComboBox.setCurrentIndex(self._out_indexes.index(self._settings.audio['output'])) + try: + self.inputDeviceComboBox.setCurrentIndex(self._in_indexes.index(self._settings['audio']['input'])) + self.outputDeviceComboBox.setCurrentIndex(self._out_indexes.index(self._settings['audio']['output'])) + except: pass + self._retranslate_ui() def _retranslate_ui(self): @@ -463,11 +492,12 @@ class DesktopAreaSelectionWindow(RubberBandWindow): class VideoSettings(CenteredWidget): """ - Audio calls settings form + Video calls settings form """ def __init__(self, settings): super().__init__() + self._app = QtWidgets.QApplication.instance() self._settings = settings uic.loadUi(get_views_path('video_settings_screen'), self) self._devices = self._frame_max_sizes = None @@ -479,24 +509,35 @@ class VideoSettings(CenteredWidget): if self.deviceComboBox.currentIndex() == 0: return try: - self._settings.video['device'] = self.devices[self.input.currentIndex()] + # AttributeError: 'VideoSettings' object has no attribute 'devices' + # ERROR: Saving video settings error: 'VideoSettings' object has no attribute 'input' + index = self.deviceComboBox.currentIndex() + if index in self._devices: + self._settings['video']['device'] = self._devices[index] + else: + LOG.warn(f"{index} not in deviceComboBox self._devices {self._devices!r}") text = self.resolutionComboBox.currentText() - self._settings.video['width'] = int(text.split(' ')[0]) - self._settings.video['height'] = int(text.split(' ')[-1]) + if len(text.split(' ')[0]) > 1: + self._settings['video']['width'] = int(text.split(' ')[0]) + self._settings['video']['height'] = int(text.split(' ')[-1]) self._settings.save() except Exception as ex: - print('Saving video settings error: ' + str(ex)) + LOG.error('ERROR: Saving video settings error: ' + str(ex)) def save(self, x, y, width, height): self.desktopAreaSelection = None - self._settings.video['device'] = -1 - self._settings.video['width'] = width - self._settings.video['height'] = height - self._settings.video['x'] = x - self._settings.video['y'] = y + self._settings['video']['device'] = -1 + self._settings['video']['width'] = width + self._settings['video']['height'] = height + self._settings['video']['x'] = x + self._settings['video']['y'] = y self._settings.save() def _update_ui(self): + try: + import cv2 + except ImportError: + cv2 = None self.deviceComboBox.currentIndexChanged.connect(self._device_changed) self.selectRegionPushButton.clicked.connect(self._button_clicked) self._devices = [-1] @@ -505,23 +546,34 @@ class VideoSettings(CenteredWidget): self._frame_max_sizes = [(size.width(), size.height())] desktop = util_ui.tr("Desktop") self.deviceComboBox.addItem(desktop) - for i in range(10): - v = cv2.VideoCapture(i) - if v.isOpened(): - v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000) - v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000) + with ts.ignoreStdout(): + # was range(10) + for i in map(int, ts.get_video_indexes()): + v = cv2.VideoCapture(i) + if v.isOpened(): + v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000) + v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000) - width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT)) - del v - self._devices.append(i) - self._frame_max_sizes.append((width, height)) - self.deviceComboBox.addItem(util_ui.tr('Device #') + str(i)) + width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT)) + del v + self._devices.append(i) + self._frame_max_sizes.append((width, height)) + self.deviceComboBox.addItem(util_ui.tr('Device #') + str(i)) + + if 'device' not in self._settings['video']: + LOG.warn("'device' not in self._settings['video']: {self._settings!r}") + self._settings['video']['device'] = self._devices[-1] + iIndex = self._settings['video']['device'] try: - index = self._devices.index(self._settings.video['device']) + index = self._devices.index(iIndex) self.deviceComboBox.setCurrentIndex(index) - except: - print('Video devices error!') + except Exception as e: + # off by one - what's Desktop? + se = f"ERROR: Video devices index error: index={iIndex} {e}" + LOG.warn(se) + # util_ui.message_box(se, util_ui.tr(f"ERROR: Video devices error")) + self._retranslate_ui() def _retranslate_ui(self): @@ -535,7 +587,7 @@ class VideoSettings(CenteredWidget): def _device_changed(self): index = self.deviceComboBox.currentIndex() self.selectRegionPushButton.setVisible(index == 0) - self.resolutionComboBox.setVisible(index != 0) + self.resolutionComboBox.setVisible(True) # index != 0 width, height = self._frame_max_sizes[index] self.resolutionComboBox.clear() dims = [ @@ -559,6 +611,7 @@ class PluginsSettings(CenteredWidget): def __init__(self, plugin_loader): super().__init__() + self._app = QtWidgets.QApplication.instance() self._plugin_loader = plugin_loader self._window = None self.initUI() @@ -589,14 +642,24 @@ class PluginsSettings(CenteredWidget): self.open.setText(util_ui.tr("Open selected plugin")) def open_plugin(self): + ind = self.comboBox.currentIndex() - plugin = self.data[ind] - window = self.pl_loader.plugin_window(plugin[-1]) - if window is not None: - self._window = window - self._window.show() - else: - util_ui.message_box(util_ui.tr('No GUI found for this plugin'), util_ui.tr('Error')) + plugin = self.data[ind] # ['SearchPlugin', True, 'Description', 'srch'] + # key in self._plugins and hasattr(self._plugins[key], 'instance'): + window = self._plugin_loader.plugin_window(plugin[-1]) + if window is not None and not hasattr(window, 'show'): + LOG.error(util_ui.tr('ERROR: No show for the plugin: ' +repr(window) +' ' +repr(window))) + util_ui.message_box(util_ui.tr('ERROR: No show for the plugin ' +repr(window)), util_ui.tr('Error')) + elif window is not None: + try: + self._window = window + self._window.show() + except Exception as e: + LOG.error(util_ui.tr('ERROR: Error for the plugin: ' +repr(window) +' ' +str(e))) + util_ui.message_box(util_ui.tr('ERROR: Error for the plugin: ' +repr(window)), util_ui.tr('Error')) + elif window is None: + LOG.warn(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window')) + util_ui.message_box(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window'), util_ui.tr('Error')) def update_list(self): self.comboBox.clear() @@ -637,6 +700,7 @@ class UpdateSettings(CenteredWidget): def __init__(self, settings, version): super().__init__() + self._app = QtWidgets.QApplication.instance() self._settings = settings self._version = version uic.loadUi(get_views_path('update_settings_screen'), self) diff --git a/toxygen/ui/messages_widgets.py b/toxygen/ui/messages_widgets.py index 8a46fd0..bf4da13 100644 --- a/toxygen/ui/messages_widgets.py +++ b/toxygen/ui/messages_widgets.py @@ -81,7 +81,8 @@ class MessageBrowser(QtWidgets.QTextBrowser): movie = QtGui.QMovie(self) movie.setFileName(file_name) self.urls[movie] = url - movie.frameChanged[int].connect(lambda x: self.animate(movie)) + # Value 'movie.frameChanged' is unsubscriptable + movie.frameChanged().connect(lambda x: self.animate(movie)) movie.start() def animate(self, movie): diff --git a/toxygen/ui/password_screen.py b/toxygen/ui/password_screen.py index bbae7ff..1e38964 100644 --- a/toxygen/ui/password_screen.py +++ b/toxygen/ui/password_screen.py @@ -2,6 +2,9 @@ from ui.widgets import CenteredWidget, LineEdit, DialogWithResult from PyQt5 import QtCore, QtWidgets import utils.ui as util_ui +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) class PasswordArea(LineEdit): @@ -78,6 +81,7 @@ class PasswordScreen(PasswordScreenBase): new_data = self._encrypt.pass_decrypt(self._data) except Exception as ex: self.warning.setVisible(True) + LOG.error(f"Decryption error: {ex}") print('Decryption error:', ex) else: self.close_with_result(new_data) diff --git a/toxygen/ui/peer_screen.py b/toxygen/ui/peer_screen.py index 8f2d5ba..bd390d1 100644 --- a/toxygen/ui/peer_screen.py +++ b/toxygen/ui/peer_screen.py @@ -40,7 +40,7 @@ class PeerScreen(CenteredWidget): self.rolesComboBox.setVisible(can_change_role_or_ban) self.roleNameLabel.setVisible(not can_change_role_or_ban) self.banGroupBox.setEnabled(can_change_role_or_ban) - self.banPushButton.clicked.connect(self._ban_peer) +# self.banPushButton.clicked.connect(self._ban_peer) self.kickPushButton.clicked.connect(self._kick_peer) self._retranslate_ui() @@ -53,7 +53,7 @@ class PeerScreen(CenteredWidget): self.roleLabel.setText(util_ui.tr('Role:')) self.copyPublicKeyPushButton.setText(util_ui.tr('Copy public key')) self.sendPrivateMessagePushButton.setText(util_ui.tr('Send private message')) - self.banPushButton.setText(util_ui.tr('Ban peer')) +# self.banPushButton.setText(util_ui.tr('Ban peer')) self.kickPushButton.setText(util_ui.tr('Kick peer')) self.banGroupBox.setTitle(util_ui.tr('Ban peer')) self.ipBanRadioButton.setText(util_ui.tr('IP')) diff --git a/toxygen/ui/profile_settings_screen.py b/toxygen/ui/profile_settings_screen.py index 2e55d3d..8e20211 100644 --- a/toxygen/ui/profile_settings_screen.py +++ b/toxygen/ui/profile_settings_screen.py @@ -29,7 +29,7 @@ class ProfileSettings(CenteredWidget): self._auto = Settings.get_auto_profile() == self._profile_manager.get_path() self.toxIdLabel.setText(self._profile.tox_id) self.nameLineEdit.setText(self._profile.name) - self.statusMessageLineEdit.setText(self._profile.status_message) + self.statusMessageLineEdit.setText(str(self._profile.status_message)) self.defaultProfilePushButton.clicked.connect(self._toggle_auto_profile) self.copyToxIdPushButton.clicked.connect(self._copy_tox_id) self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) @@ -69,7 +69,7 @@ class ProfileSettings(CenteredWidget): self.statusComboBox.addItem(util_ui.tr("Online")) self.statusComboBox.addItem(util_ui.tr("Away")) self.statusComboBox.addItem(util_ui.tr("Busy")) - self.copyPublicKeyPushButton.setText(util_ui.tr("Copy public key")) + self.copyPublicKeyPushButton.setText(util_ui.tr("Copy public key" +' (64)')) self._set_default_profile_button_text() @@ -144,7 +144,7 @@ class ProfileSettings(CenteredWidget): reply = util_ui.question(util_ui.tr('Do you want to move your profile to this location?'), util_ui.tr('Use new path')) - + self._settings.export(directory) self._profile.export_db(directory) self._profile_manager.export_profile(self._settings, directory, reply) diff --git a/toxygen/ui/widgets.py b/toxygen/ui/widgets.py index e7fe623..32ebfe4 100644 --- a/toxygen/ui/widgets.py +++ b/toxygen/ui/widgets.py @@ -1,15 +1,21 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from PyQt5 import QtCore, QtGui, QtWidgets import utils.ui as util_ui - +import logging class DataLabel(QtWidgets.QLabel): """ Label with elided text """ def setText(self, text): - text = ''.join('\u25AF' if len(bytes(c, 'utf-8')) >= 4 else c for c in text) + try: + text = ''.join('\u25AF' if len(bytes(str(c), 'utf-8')) >= 4 else c for c in str(text)) + except Exception as e: + logging.error(f"DataLabel::setText: {e}") + return + metrics = QtGui.QFontMetrics(self.font()) - text = metrics.elidedText(text, QtCore.Qt.ElideRight, self.width()) + text = metrics.elidedText(str(text), QtCore.Qt.ElideRight, self.width()) super().setText(text) diff --git a/toxygen/ui/widgets_factory.py b/toxygen/ui/widgets_factory.py index 128e85e..0c8fd08 100644 --- a/toxygen/ui/widgets_factory.py +++ b/toxygen/ui/widgets_factory.py @@ -1,3 +1,4 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from ui.main_screen_widgets import * from ui.menu import * from ui.groups_widgets import * @@ -8,7 +9,6 @@ from ui.group_settings_widgets import * from ui.group_bans_widgets import * from ui.profile_settings_screen import ProfileSettings - class WidgetsFactory: def __init__(self, settings, profile, profile_manager, contacts_manager, file_transfer_handler, smiley_loader, @@ -42,6 +42,11 @@ class WidgetsFactory: return AudioSettings(self._settings) def create_video_settings_window(self): + try: + import cv2 + except ImportError: + cv2 = None + if cv2 is None: return None return VideoSettings(self._settings) def create_update_settings_window(self): diff --git a/toxygen/updater/updater.py b/toxygen/updater/updater.py index 329353c..2311282 100644 --- a/toxygen/updater/updater.py +++ b/toxygen/updater/updater.py @@ -1,3 +1,4 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import utils.util as util import utils.ui as util_ui import os @@ -6,15 +7,20 @@ import urllib from PyQt5 import QtNetwork, QtCore import subprocess +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) + +TIMEOUT=10 def connection_available(): try: - urllib.request.urlopen('http://216.58.192.142', timeout=1) # google.com + urllib.request.urlopen('http://216.58.192.142', timeout=TIMEOUT) # google.com return True except: return False - def updater_available(): if is_from_sources(): return os.path.exists(util.curr_directory() + '/toxygen_updater.py') @@ -70,12 +76,11 @@ def download(version): os.chdir(util.curr_directory()) url = get_url(version) params = get_params(url, version) - print('Updating Toxygen') - util.log('Updating Toxygen') + LOG.info('Updating Toxygen') try: subprocess.Popen(params) except Exception as ex: - util.log('Exception: running updater failed with ' + str(ex)) + LOG.error('running updater failed with ' + str(ex)) def send_request(version, settings): @@ -97,7 +102,7 @@ def send_request(version, settings): attr = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute) return attr is not None and 200 <= attr < 300 except Exception as ex: - util.log('TOXYGEN UPDATER ERROR: ' + str(ex)) + LOG.error('TOXYGEN UPDATER ' + str(ex)) return False diff --git a/toxygen/user_data/profile_manager.py b/toxygen/user_data/profile_manager.py index 05e2f2d..7fd430a 100644 --- a/toxygen/user_data/profile_manager.py +++ b/toxygen/user_data/profile_manager.py @@ -1,16 +1,31 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import utils.util as util import os + from user_data.settings import Settings from common.event import Event +from user_data.settings import get_user_config_path +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + + +def LOG_ERROR(l): print('ERROR_: '+l) +def LOG_WARN(l): print('WARN_: '+l) +def LOG_INFO(l): print('INFO_: '+l) +def LOG_DEBUG(l): print('DEBUG_: '+l) +def LOG_TRACE(l): pass # print('TRACE+ '+l) class ProfileManager: """ Class with methods for search, load and save profiles """ def __init__(self, toxes, path): + assert path self._toxes = toxes self._path = path + assert path self._directory = os.path.dirname(path) self._profile_saved_event = Event() # create /avatars if not exists: @@ -50,20 +65,21 @@ class ProfileManager: data = self._toxes.pass_encrypt(data) with open(self._path, 'wb') as fl: fl.write(data) - print('Profile saved successfully') + LOG_INFO('Profile saved successfully to' +self._path) self._profile_saved_event(data) def export_profile(self, settings, new_path, use_new_path): - path = new_path + os.path.basename(self._path) with open(self._path, 'rb') as fin: data = fin.read() + path = new_path + os.path.basename(self._path) with open(path, 'wb') as fout: fout.write(data) - print('Profile exported successfully') - util.copy(self._directory + 'avatars', new_path + 'avatars') + LOG.info('Profile exported successfully to ' +path) + util.copy(os.path.join(self._directory, 'avatars'), + os.path.join(new_path, 'avatars')) if use_new_path: - self._path = new_path + os.path.basename(self._path) + self._path = os.path.join(new_path, os.path.basename(self._path)) self._directory = new_path settings.update_path(new_path) @@ -72,7 +88,7 @@ class ProfileManager: """ Find available tox profiles """ - path = Settings.get_default_path() + path = get_user_config_path() result = [] # check default path if not os.path.exists(path): diff --git a/toxygen/user_data/settings.py b/toxygen/user_data/settings.py index 71422c2..66fb93f 100644 --- a/toxygen/user_data/settings.py +++ b/toxygen/user_data/settings.py @@ -1,49 +1,195 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from platform import system import json -from utils.util import * -import pyaudio -from common.event import Event +import os +from pprint import pprint +from utils.util import * +from utils.util import log, join_path +from common.event import Event +import utils.ui as util_ui +import utils.util as util_utils +import user_data +import tests.support_testing as ts + +global LOG +import logging +LOG = logging.getLogger('settings') + +def merge_args_into_settings(args, settings): + if args: + print(repr(args.__dict__.keys())) + if not hasattr(args, 'audio'): + LOG.warn('No audio ' +repr(args)) + settings['audio'] = getattr(args, 'audio') + if not hasattr(args, 'video'): + LOG.warn('No video ' +repr(args)) + settings['video'] = getattr(args, 'video') + for key in settings.keys(): + # proxy_type proxy_port proxy_host + not_key = 'not_' +key + if hasattr(args, key): + val = getattr(args, key) + if type(val) == bytes: + # proxy_host - ascii? + # filenames - ascii? + val = str(val, 'UTF-8') + settings[key] = val + elif hasattr(args, not_key): + val = not getattr(args, not_key) + settings[key] = val + clean_settings(settings) + return + +def clean_settings(self): + # failsafe to ensure C tox is bytes and Py settings is str + + # overrides + self['mirror_mode'] = False + # REQUIRED!! + if self['ipv6_enabled'] and not os.path.exists('/proc/sys/net/ipv6'): + LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist') + self['ipv6_enabled'] = False + + if 'proxy_type' in self and self['proxy_type'] == 0: + self['proxy_host'] = '' + self['proxy_port'] = 0 + + if 'proxy_type' in self and self['proxy_type'] != 0 and \ + 'proxy_host' in self and self['proxy_host'] != '' and \ + 'proxy_port' in self and self['proxy_port'] != 0: + if 'udp_enabled' in self and self['udp_enabled']: + # We don't currently support UDP over proxy. + LOG.info("UDP enabled and proxy set: disabling UDP") + self['udp_enabled'] = False + if 'local_discovery_enabled' in self and self['local_discovery_enabled']: + LOG.info("local_discovery_enabled enabled and proxy set: disabling local_discovery_enabled") + self['local_discovery_enabled'] = False + if 'dht_announcements_enabled' in self and self['dht_announcements_enabled']: + LOG.info("dht_announcements_enabled enabled and proxy set: disabling dht_announcements_enabled") + self['dht_announcements_enabled'] = False + + if 'auto_accept_path' in self and \ + type(self['auto_accept_path']) == bytes: + self['auto_accept_path'] = str(self['auto_accept_path'], 'UTF-8') + + for key in Settings.get_default_settings(): + if key not in self: continue + if type(self[key]) == bytes: + LOG.warn('bytes setting in: ' +key \ + +' ' + repr(self[key])) + # ascii? + # self[key] = str(self[key], 'utf-8') + LOG.debug("Cleaned settings") + +def get_user_config_path(): + system = util_utils.get_platform() + if system == 'Windows': + return os.path.join(os.getenv('APPDATA'), 'Tox/') + elif system == 'Darwin': + return os.path.join(os.getenv('HOME'), 'Library/Application Support/Tox/') + else: + return os.path.join(os.getenv('HOME'), '.config/tox/') + +def supported_languages(): + return { + 'English': 'en_EN', + 'French': 'fr_FR', + 'Russian': 'ru_RU', + 'Ukrainian': 'uk_UA' + } + +def built_in_themes(): + return { + 'dark': 'dark_style.qss', + 'default': 'style.qss' + } + +def get_global_settings_path(): + return os.path.join(get_base_directory(), 'toxygen.json') + +def is_active_profile(profile_path): + sFile = profile_path + '.lock' + if not os.path.isfile(sFile): + return False + try: + import psutil + except Exception as e: + return True + with open(sFile, 'rb') as iFd: + sPid = iFd.read() + if sPid and int(sPid.strip()) in psutil.pids(): + return True + LOG.debug('Unlinking stale lock file ' +sFile) + try: + os.unlink(sFile) + except: + pass + return False class Settings(dict): """ Settings of current profile + global app settings """ - def __init__(self, toxes, path): + def __init__(self, toxes, path, app): self._path = path self._profile_path = path.replace('.json', '.tox') self._toxes = toxes + self._app = app + self._args = app._args + self.LOG = lambda l: LOG.log(self._args.loglevel, l) + self._log = self.LOG + self._settings_saved_event = Event() - if os.path.isfile(path): - with open(path, 'rb') as fl: - data = fl.read() + if path and os.path.isfile(path): try: + with open(path, 'rb') as fl: + data = fl.read() if toxes.is_data_encrypted(data): data = toxes.pass_decrypt(data) info = json.loads(str(data, 'utf-8')) + LOG.debug('Parsed settings from: ' + str(path)) except Exception as ex: - info = Settings.get_default_settings() - log('Parsing settings error: ' + str(ex)) - super().__init__(info) - self._upgrade() + title = 'Error opening/parsing settings file: ' + text = title + path + LOG.error(title +str(ex)) + util_ui.message_box(text, title) + info = Settings.get_default_settings(app._args) + user_data.settings.clean_settings(info) else: - super().__init__(Settings.get_default_settings()) + LOG.debug('get_default_settings for: ' + repr(path)) + info = Settings.get_default_settings(app._args) + + if not os.path.exists(path): + merge_args_into_settings(app._args, info) + else: + aC = self._changed(app._args, info) + if aC: + title = 'Override profile with commandline - ' + if path: + title += os.path.basename(path) + text = 'Override profile with command-line settings? \n' + # text += '\n'.join([str(key) +'=' +str(val) for + # key,val in self._changed(app._args).items()]) + text += repr(aC) + reply = util_ui.question(text, title) + if reply: + merge_args_into_settings(app._args, info) + info['audio'] = getattr(app._args, 'audio') + info['video'] = getattr(app._args, 'video') + super().__init__(info) + self._upgrade() + + LOG.info('Parsed settings from: ' + str(path)) + ex = f"self=id(self) {self!r}" + LOG.debug(ex) + self.save() self.locked = False self.closing = False self.unlockScreen = False - p = pyaudio.PyAudio() - input_devices = output_devices = 0 - for i in range(p.get_device_count()): - device = p.get_device_info_by_index(i) - if device["maxInputChannels"]: - input_devices += 1 - if device["maxOutputChannels"]: - output_devices += 1 - self.audio = {'input': p.get_default_input_device_info()['index'] if input_devices else -1, - 'output': p.get_default_output_device_info()['index'] if output_devices else -1, - 'enabled': input_devices and output_devices} - self.video = {'device': -1, 'width': 640, 'height': 480, 'x': 0, 'y': 0} + # ----------------------------------------------------------------------------------------------------------------- # Properties @@ -79,8 +225,16 @@ class Settings(dict): Mark current profile as active """ path = self._profile_path + '.lock' - with open(path, 'w') as fl: - fl.write('active') + try: + import shutil + except: + pass + else: + shutil.copy2(self._profile_path, path) + # need to open this with the same perms as _profile_path + # copy profile_path and then write? + with open(path, 'wb') as fl: + fl.write(bytes(str(os.getpid()), 'ascii')) def export(self, path): text = json.dumps(self) @@ -98,7 +252,7 @@ class Settings(dict): @staticmethod def get_auto_profile(): - p = Settings.get_global_settings_path() + p = get_global_settings_path() if not os.path.isfile(p): return None with open(p) as fl: @@ -106,7 +260,7 @@ class Settings(dict): try: auto = json.loads(data) except Exception as ex: - log(str(ex)) + LOG.warn(f"json.loads {data}: {ex!s}") auto = {} if 'profile_path' in auto: path = str(auto['profile_path']) @@ -115,9 +269,14 @@ class Settings(dict): if os.path.isfile(path): return path + @staticmethod + def supported_languages(): + # backwards + return supported_languages() + @staticmethod def set_auto_profile(path): - p = Settings.get_global_settings_path() + p = get_global_settings_path() if os.path.isfile(p): with open(p) as fl: data = fl.read() @@ -130,7 +289,7 @@ class Settings(dict): @staticmethod def reset_auto_profile(): - p = Settings.get_global_settings_path() + p = get_global_settings_path() if os.path.isfile(p): with open(p) as fl: data = fl.read() @@ -143,28 +302,36 @@ class Settings(dict): fl.write(json.dumps(data)) @staticmethod - def is_active_profile(profile_path): - return os.path.isfile(profile_path + '.lock') - - @staticmethod - def get_default_settings(): + def get_default_settings(args=None): """ Default profile settings """ - return { - 'theme': 'dark', - 'ipv6_enabled': False, + retval = { + # FixMe: match? /var/local/src/c-toxcore/toxcore/tox.h + 'ipv6_enabled': True, 'udp_enabled': True, + 'local_discovery_enabled': True, + 'dht_announcements_enabled': True, 'proxy_type': 0, - 'proxy_host': '127.0.0.1', - 'proxy_port': 9050, + 'proxy_host': '', + 'proxy_port': 0, 'start_port': 0, 'end_port': 0, 'tcp_port': 0, - 'notifications': True, + 'local_discovery_enabled': True, + 'hole_punching_enabled': False, + # tox_log_cb *log_callback; + 'experimental_thread_safety': False, + # operating_system + + 'theme': 'default', + 'notifications': False, 'sound_notifications': False, 'language': 'English', - 'save_history': False, + 'calls_sound': False, # was True + + 'save_history': True, + 'save_unsent_only': False, 'allow_inline': True, 'allow_auto_accept': True, 'auto_accept_path': None, @@ -175,7 +342,6 @@ class Settings(dict): 'friends_aliases': [], 'show_avatars': False, 'typing_notifications': False, - 'calls_sound': True, 'blocked': [], 'plugins': [], 'notes': {}, @@ -194,43 +360,26 @@ class Settings(dict): 'show_welcome_screen': True, 'close_app': 0, 'font': 'Times New Roman', - 'update': 1, + 'update': 0, 'group_notifications': True, - 'download_nodes_list': False, + 'download_nodes_list': False, # + 'download_nodes_url': 'https://nodes.tox.chat/json', 'notify_all_gc': False, - 'lan_discovery': True, - 'backup_directory': None + 'backup_directory': None, + + 'audio': {'input': -1, + 'output': -1, + 'enabled': True}, + 'video': {'device': -1, + 'width': 320, + 'height': 240, + 'x': 0, + 'y': 0}, + 'current_nodes': None, + 'network': 'new', + 'tray_icon': False, } - - @staticmethod - def supported_languages(): - return { - 'English': 'en_EN', - 'French': 'fr_FR', - 'Russian': 'ru_RU', - 'Ukrainian': 'uk_UA' - } - - @staticmethod - def built_in_themes(): - return { - 'dark': 'dark_style.qss', - 'default': 'style.qss' - } - - @staticmethod - def get_global_settings_path(): - return os.path.join(get_base_directory(), 'toxygen.json') - - @staticmethod - def get_default_path(): - system = get_platform() - if system == 'Windows': - return os.getenv('APPDATA') + '/Tox/' - elif system == 'Darwin': - return os.getenv('HOME') + '/Library/Application Support/Tox/' - else: - return os.getenv('HOME') + '/.config/tox/' + return retval # ----------------------------------------------------------------------------------------------------------------- # Private methods @@ -242,3 +391,16 @@ class Settings(dict): if key not in self: print(key) self[key] = default[key] + + def _changed(self, aArgs, info): + aRet = dict() + default = Settings.get_default_settings() + for key in default: + if key in ['audio', 'video']: continue + if key not in aArgs.__dict__: continue + val = aArgs.__dict__[key] + if val in ['0.0.0.0']: continue + if key in aArgs.__dict__ and info[key] != val: + aRet[key] = val + return aRet + diff --git a/toxygen/utils/util.py b/toxygen/utils/util.py index 5bd5c3a..7a98174 100644 --- a/toxygen/utils/util.py +++ b/toxygen/utils/util.py @@ -1,3 +1,4 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import os import time import shutil @@ -19,14 +20,26 @@ def cached(func): return wrapped_func - -def log(data): +oFD=None +def log(data=None): + global oFD + if not oFD: + if 'TMPDIR' in os.environ: + logdir = os.environ['TMPDIR'] + else: + logdir = '/tmp' + try: + oFD = open(join_path(logdir, 'toxygen.log'), 'a') + except Exception as ex: + oFD = None + print(f"ERROR: opening toxygen.log: {ex}") + return + if data is None: return oFD try: - with open(join_path(curr_directory(), 'logs.log'), 'a') as fl: - fl.write(str(data) + '\n') + oFD.write(str(data) +'\n') except Exception as ex: - print(ex) - + print(f"ERROR: writing to toxygen.log: {ex}") + return data def curr_directory(current_file=None): return os.path.dirname(os.path.realpath(current_file or __file__)) @@ -144,7 +157,6 @@ def time_offset(): result = hours * 60 + minutes - h * 60 - m return result - def unix_time_to_long_str(unix_time): date_time = datetime.datetime.utcfromtimestamp(unix_time) @@ -168,3 +180,11 @@ def is_re_valid(regex): @cached def get_platform(): return platform.system() + +def get_user_config_path(): + if get_platform() == 'Windows': + return os.getenv('APPDATA') + '/Tox/' + elif get_platform() == 'Darwin': + return os.getenv('HOME') + '/Library/Application Support/Tox/' + else: + return os.getenv('HOME') + '/.config/tox/'