diff --git a/.gitignore b/.gitignore index 78c26f5..0a8182a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ *.pyc *.pyo -*.ui toxygen/toxcore tests/tests tests/libs @@ -25,3 +24,5 @@ html Toxygen.egg-info *.tox .cache +*.db + diff --git a/.travis.yml b/.travis.yml index cfabadd..a4011e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ install: - pip install pyqt5 - pip install pyaudio - pip install opencv-python + - pip install pydenticon before_script: # Opus - wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz @@ -37,15 +38,16 @@ before_script: - sudo ldconfig - cd .. # Toxcore - - git clone https://github.com/irungentoo/toxcore.git + - git clone https://github.com/ingvar1995/toxcore.git --branch=ngc_rebase - cd toxcore - - autoreconf -if - - ./configure + - mkdir _build && cd _build + - cmake .. - make -j$(nproc) - sudo make install - echo '/usr/local/lib/' | sudo tee -a /etc/ld.so.conf.d/locallib.conf - sudo ldconfig - cd .. + - cd .. script: - py.test tests/travis.py - py.test tests/tests.py diff --git a/MANIFEST.in b/MANIFEST.in index 6629fb6..89e57c6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,4 +16,4 @@ include toxygen/styles/*.qss include toxygen/translations/*.qm include toxygen/libs/libtox.dll include toxygen/libs/libsodium.a -include toxygen/nodes.json +include toxygen/bootstrap/nodes.json diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..0b45358 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:16.04 + +RUN apt-get update && \ +apt-get install build-essential libtool autotools-dev automake checkinstall cmake check git yasm libsodium-dev libopus-dev libvpx-dev pkg-config -y && \ +git clone https://github.com/ingvar1995/toxcore.git --branch=ngc_rebase && \ +cd toxcore && mkdir _build && cd _build && \ +cmake .. && make && make install + +RUN apt-get install portaudio19-dev python3-pyqt5 python3-pyaudio python3-pip -y && \ +pip3 install numpy pydenticon opencv-python pyinstaller + +RUN useradd -ms /bin/bash toxygen +USER toxygen diff --git a/build/build.sh b/build/build.sh new file mode 100644 index 0000000..fb6c4b2 --- /dev/null +++ b/build/build.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +cd ~ +git clone https://github.com/toxygen-project/toxygen.git --branch=next_gen +cd toxygen/toxygen + +pyinstaller --windowed --icon=images/icon.ico main.py + +cp -r styles dist/main/ +find . -type f ! -name '*.qss' -delete +cp -r plugins dist/main/ +mkdir -p dist/main/ui/views +cp -r ui/views dist/main/ui/ +cp -r sounds dist/main/ +cp -r smileys dist/main/ +cp -r stickers dist/main/ +cp -r bootstrap dist/main/ +find . -type f ! -name '*.json' -delete +cp -r images dist/main/ +cp -r translations dist/main/ +find . -name "*.ts" -type f -delete + +cd dist +mv main toxygen +cd toxygen +mv main toxygen +wget -O updater https://github.com/toxygen-project/toxygen_updater/releases/download/v0.1/toxygen_updater_linux_64 +echo "[Paths]" >> qt.conf +echo "Prefix = PyQt5/Qt" >> qt.conf +cd .. + +tar -zcvf toxygen_linux_64.tar.gz toxygen > /dev/null +rm -rf toxygen diff --git a/docs/compile.md b/docs/compile.md index 995dc35..b4f6810 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -2,10 +2,18 @@ You can compile Toxygen using [PyInstaller](http://www.pyinstaller.org/) -Install PyInstaller: -``pip3 install pyinstaller`` +Use Dockerfile and build script from `build` directory: -Compile Toxygen: -``pyinstaller --windowed --icon images/icon.ico main.py`` +1. Build image: +``` +docker build -t toxygen . +``` -Don't forget to copy /images/, /sounds/, /translations/, /styles/, /smileys/, /stickers/, /plugins/ (and /libs/libtox.dll, /libs/libsodium.a on Windows) to /dist/main/ +2. Run container: +``` +docker run -it toxygen bash +``` + +3. Execute `build.sh` script: + +```./build.sh``` diff --git a/docs/contact.md b/docs/contact.md index c66da1c..9f80595 100644 --- a/docs/contact.md +++ b/docs/contact.md @@ -2,4 +2,4 @@ 1) Using GitHub - open issue -2) Use Toxygen Tox Group - add bot kalina@toxme.io (or 12EDB939AA529641CE53830B518D6EB30241868EE0E5023C46A372363CAEC91C2C948AEFE4EB) +2) Use Toxygen Tox Group (NGC) - ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA diff --git a/setup.py b/setup.py index 746163e..fb80363 100644 --- a/setup.py +++ b/setup.py @@ -2,15 +2,17 @@ from setuptools import setup from setuptools.command.install import install from platform import system from subprocess import call -from toxygen.util import program_version +import main import sys +import os +from utils.util import curr_directory, join_path -version = program_version + '.0' +version = main.__version__ + '.0' if system() == 'Windows': - MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python'] + MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python', 'pydenticon'] else: MODULES = [] try: @@ -29,6 +31,19 @@ else: import cv2 except ImportError: MODULES.append('opencv-python') + try: + import pydenticon + except ImportError: + MODULES.append('pydenticon') + + +def get_packages(): + directory = join_path(curr_directory(__file__), 'toxygen') + for root, dirs, files in os.walk(directory): + packages = map(lambda d: 'toxygen.' + d, dirs) + packages = ['toxygen'] + list(packages) + + return packages class InstallScript(install): @@ -62,7 +77,7 @@ setup(name='Toxygen', author='Ingvar', maintainer='Ingvar', license='GPL3', - packages=['toxygen', 'toxygen.plugins', 'toxygen.styles'], + packages=get_packages(), install_requires=MODULES, include_package_data=True, classifiers=[ @@ -71,8 +86,8 @@ setup(name='Toxygen', 'Programming Language :: Python :: 3.6', ], entry_points={ - 'console_scripts': ['toxygen=toxygen.main:main'], + 'console_scripts': ['toxygen=toxygen.main:main'] }, cmdclass={ - 'install': InstallScript, + 'install': InstallScript }) diff --git a/tests/tests.py b/tests/tests.py index 27618af..e3c9b6b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,177 +1,18 @@ -from toxygen.profile import * -from toxygen.tox_dns import tox_dns -from toxygen.history import History -from toxygen.smileys import SmileyLoader -from toxygen.messages import * -import toxygen.toxes as encr -import toxygen.util as util -import time +from toxygen.middleware.tox_factory import * +# TODO: add new tests + class TestTox: def test_creation(self): - name = b'Toxygen User' - status_message = b'Toxing on Toxygen' + name = 'Toxygen User' + status_message = 'Toxing on Toxygen' tox = tox_factory() tox.self_set_name(name) tox.self_set_status_message(status_message) data = tox.get_savedata() del tox tox = tox_factory(data) - assert tox.self_get_name() == str(name, 'utf-8') - assert tox.self_get_status_message() == str(status_message, 'utf-8') - - -class TestProfileHelper: - - def test_creation(self): - file_name, path = 'test.tox', os.path.dirname(os.path.realpath(__file__)) + '/' - data = b'test' - with open(path + file_name, 'wb') as fl: - fl.write(data) - ph = ProfileHelper(path, file_name[:4]) - assert ProfileHelper.get_path() == path - assert ph.open_profile() == data - assert os.path.exists(path + 'avatars/') - - -class TestDNS: - - def test_dns(self): - Settings._instance = Settings.get_default_settings() - bot_id = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5' - tox_id = tox_dns('groupbot@toxme.io') - assert tox_id == bot_id - - def test_dns2(self): - Settings._instance = Settings.get_default_settings() - bot_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6' - tox_id = tox_dns('echobot@toxme.io') - assert tox_id == bot_id - - -class TestEncryption: - - def test_encr_decr(self): - tox = tox_factory() - data = tox.get_savedata() - lib = encr.ToxES() - for password in ('easypassword', 'njvnFjfn7vaGGV6', 'toxygen'): - lib.set_password(password) - copy_data = data[:] - new_data = lib.pass_encrypt(data) - assert lib.is_data_encrypted(new_data) - new_data = lib.pass_decrypt(new_data) - assert copy_data == new_data - - -class TestSmileys: - - def test_loading(self): - settings = {'smiley_pack': 'default', 'smileys': True} - sm = SmileyLoader(settings) - assert sm.get_smileys_path() is not None - l = sm.get_packs_list() - assert len(l) == 4 - - -def create_singletons(): - folder = util.curr_directory() + '/abc' - Settings._instance = Settings.get_default_settings() - if not os.path.exists(folder): - os.makedirs(folder) - ProfileHelper(folder, 'test') - - -def create_friend(name, status_message, number, tox_id): - friend = Friend(None, number, name, status_message, None, tox_id) - return friend - - -def create_random_friend(): - name, status_message, number = 'Friend', 'I am friend!', 0 - tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6' - friend = create_friend(name, status_message, number, tox_id) - return friend - - -class TestFriend: - - def test_friend_creation(self): - create_singletons() - name, status_message, number = 'Friend', 'I am friend!', 0 - tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6' - friend = create_friend(name, status_message, number, tox_id) - assert friend.name == name - assert friend.tox_id == tox_id - assert friend.status_message == status_message - assert friend.number == number - - def test_friend_corr(self): - create_singletons() - friend = create_random_friend() - t = time.time() - friend.append_message(InfoMessage('Info message', t)) - friend.append_message(TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t + 0.001, 0)) - friend.append_message(TextMessage('Hello!', MESSAGE_OWNER['FRIEND'], t + 0.002, 0)) - assert friend.get_last_message_text() == 'Hello! It is test!' - assert len(friend.get_corr()) == 3 - assert len(friend.get_corr_for_saving()) == 2 - friend.append_message(TextMessage('Not sent', MESSAGE_OWNER['NOT_SENT'], t + 0.002, 0)) - arr = friend.get_unsent_messages_for_saving() - assert len(arr) == 1 - assert arr[0][0] == 'Not sent' - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['RUNNING'], - 100, 'file_name', friend.number, 0) - friend.append_message(tm) - friend.clear_corr() - assert len(friend.get_corr()) == 1 - assert len(friend.get_corr_for_saving()) == 0 - friend.append_message(TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t, 0)) - assert len(friend.get_corr()) == 2 - assert len(friend.get_corr_for_saving()) == 1 - - def test_history_search(self): - create_singletons() - friend = create_random_friend() - message = 'Hello! It is test!' - friend.append_message(TextMessage(message, MESSAGE_OWNER['ME'], time.time(), 0)) - last_message = friend.get_last_message_text() - assert last_message == message - result = friend.search_string('e[m|s]') - assert result is not None - result = friend.search_string('tox') - assert result is None - - -class TestHistory: - - def test_history(self): - create_singletons() - db_name = 'my_name' - name, status_message, number = 'Friend', 'I am friend!', 0 - tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6' - friend = create_friend(name, status_message, number, tox_id) - history = History(db_name) - history.add_friend_to_db(friend.tox_id) - assert history.friend_exists_in_db(friend.tox_id) - text_message = 'Test!' - t = time.time() - friend.append_message(TextMessage(text_message, MESSAGE_OWNER['ME'], t, 0)) - messages = friend.get_corr_for_saving() - history.save_messages_to_db(friend.tox_id, messages) - getter = history.messages_getter(friend.tox_id) - messages = getter.get_all() - assert len(messages) == 1 - assert messages[0][0] == text_message - assert messages[0][1] == MESSAGE_OWNER['ME'] - assert messages[0][-1] == 0 - history.delete_message(friend.tox_id, t) - getter = history.messages_getter(friend.tox_id) - messages = getter.get_all() - assert len(messages) == 0 - history.delete_friend_from_db(friend.tox_id) - assert not history.friend_exists_in_db(friend.tox_id) + assert tox.self_get_name() == name + assert tox.self_get_status_message() == status_message diff --git a/toxygen/app.py b/toxygen/app.py new file mode 100644 index 0000000..a23816d --- /dev/null +++ b/toxygen/app.py @@ -0,0 +1,424 @@ +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.settings import Settings +from ui.login_screen import LoginScreen +from user_data.profile_manager import ProfileManager +from plugin_support.plugin_support import PluginLoader +from ui.main_screen import MainWindow +from ui import tray +import utils.ui as util_ui +import utils.util as util +from contacts.profile import Profile +from file_transfers.file_transfers_handler import FileTransfersHandler +from contacts.contact_provider import ContactProvider +from contacts.friend_factory import FriendFactory +from contacts.group_factory import GroupFactory +from contacts.contacts_manager import ContactsManager +from av.calls_manager import CallsManager +from history.database import Database +from ui.widgets_factory import WidgetsFactory +from smileys.smileys import SmileyLoader +from ui.items_factories import MessagesItemsFactory, ContactItemsFactory +from messenger.messenger import Messenger +from network.tox_dns import ToxDns +from history.history import History +from file_transfers.file_transfers_messages_service import FileTransfersMessagesService +from groups.groups_service import GroupsService +from ui.create_profile_screen import CreateProfileScreen +from common.provider import Provider +from contacts.group_peer_factory import GroupPeerFactory +from user_data.backup_service import BackupService +import styles.style # TODO: dynamic loading + + +class App: + + def __init__(self, version, path_to_profile=None, uri=None): + self._version = version + self._app = self._settings = self._profile_manager = 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._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): + """ + Main function of app. loads login screen if needed and starts main screen + """ + self._app = QtWidgets.QApplication([]) + self._load_icon() + + if util.get_platform() == 'Linux': + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) + + self._load_base_style() + + if not self._select_and_load_profile(): + return + + if self._try_to_update(): + return + + self._load_app_styles() + self._load_app_translations() + + self._create_dependencies() + self._start_threads() + + if self._uri is not None: + self._ms.add_contact(self._uri) + + self._app.lastWindowClosed.connect(self._app.quit) + + self._execute_app() + + self._stop_app() + + # ----------------------------------------------------------------------------------------------------------------- + # App executing + # ----------------------------------------------------------------------------------------------------------------- + + def _execute_app(self): + while True: + try: + self._app.exec_() + except Exception as ex: + util.log('Unhandled exception: ' + str(ex)) + else: + break + + def _stop_app(self): + self._plugin_loader.stop() + self._stop_threads() + self._file_transfer_handler.stop() + self._tray.hide() + self._save_profile() + self._settings.close() + self._kill_toxav() + self._kill_tox() + + # ----------------------------------------------------------------------------------------------------------------- + # App loading + # ----------------------------------------------------------------------------------------------------------------- + + def _load_base_style(self): + with open(util.join_path(util.get_styles_directory(), 'dark_style.qss')) as fl: + style = fl.read() + self._app.setStyleSheet(style) + + def _load_app_styles(self): + # application color scheme + if self._settings['theme'] == 'dark': + return + for theme in self._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() + self._app.setStyleSheet(style) + break + + def _load_login_screen_translations(self): + current_language, supported_languages = self._get_languages() + if current_language not in supported_languages: + return + lang_path = supported_languages[current_language] + translator = QtCore.QTranslator() + translator.load(util.get_translations_directory() + lang_path) + self._app.installTranslator(translator) + self._app.translator = translator + + def _load_icon(self): + icon_file = os.path.join(util.get_images_directory(), 'icon.png') + self._app.setWindowIcon(QtGui.QIcon(icon_file)) + + @staticmethod + def _get_languages(): + current_locale = QtCore.QLocale() + curr_language = current_locale.languageToString(current_locale.language()) + supported_languages = Settings.supported_languages() + + return curr_language, supported_languages + + def _load_app_translations(self): + 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) + + if self._path is not None: # toxygen was started with path to profile + self._load_existing_profile(self._path) + else: + auto_profile = Settings.get_auto_profile() + if auto_profile is None: # no default profile + result = self._select_profile() + if result is None: + return False + if result.is_new_profile(): # create new profile + if not self._create_new_profile(result.profile_path): + return False + else: # load existing profile + self._load_existing_profile(result.profile_path) + self._path = result.profile_path + else: # default profile + self._path = auto_profile + self._load_existing_profile(auto_profile) + + if Settings.is_active_profile(self._path): # profile is in use + profile_name = util.get_profile_name_from_path(self._path) + title = util_ui.tr('Profile {}').format(profile_name) + text = util_ui.tr( + 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?') + reply = util_ui.question(text, title) + if not reply: + return False + + self._settings.set_active_profile() + + return True + + # ----------------------------------------------------------------------------------------------------------------- + # Threads + # ----------------------------------------------------------------------------------------------------------------- + + def _start_threads(self, initial_start=True): + # init thread + self._init = threads.InitThread(self._tox, self._plugin_loader, self._settings, initial_start) + self._init.start() + + # 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() + + def _stop_threads(self, is_app_closing=True): + self._init.stop_thread() + + self._av_loop.stop_thread() + self._main_loop.stop_thread() + + if is_app_closing: + threads.stop_file_transfer_thread() + + # ----------------------------------------------------------------------------------------------------------------- + # Profiles + # ----------------------------------------------------------------------------------------------------------------- + + def _select_profile(self): + 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): + self._profile_manager = ProfileManager(self._toxes, profile_path) + data = self._profile_manager.open_profile() + if self._toxes.is_data_encrypted(data): + data = self._enter_password(data) + self._settings = Settings(self._toxes, profile_path.replace('.tox', '.json')) + self._tox = self._create_tox(data) + + def _create_new_profile(self, 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') + else: + profile_path = util.join_path(util.curr_directory(__file__), profile_name + '.tox') + if os.path.isfile(profile_path): + util_ui.message_box(util_ui.tr('Profile with this name already exists'), + 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') + self._path = profile_path + if result.password: + self._toxes.set_password(result.password) + self._settings = Settings(self._toxes, self._path.replace('.tox', '.json')) + 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)) + 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')) + + return False + current_language, supported_languages = self._get_languages() + if current_language in supported_languages: + self._settings['language'] = current_language + self._settings.save() + + return True + + def _get_create_profile_screen_result(self): + cps = CreateProfileScreen() + cps.show() + self._app.exec_() + + return cps.result + + def _save_profile(self, data=None): + data = data or self._tox.get_savedata() + self._profile_manager.save_profile(data) + + # ----------------------------------------------------------------------------------------------------------------- + # Other private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _enter_password(self, data): + """ + Show password screen + """ + 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() + + def _reset(self): + """ + Create new tox instance (new network settings) + :return: tox instance + """ + self._contacts_manager.reset_contacts_statuses() + self._stop_threads(False) + data = self._tox.get_savedata() + self._save_profile(data) + self._kill_toxav() + self._kill_tox() + # create new tox instance + self._tox = self._create_tox(data) + 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) + + 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() + + def _create_dependencies(self): + 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) + + 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._group_peer_factory) + 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) + 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._contacts_provider, messages_items_factory, self._profile, + self._calls_manager) + file_transfers_message_service = FileTransfersMessagesService(self._contacts_manager, messages_items_factory, + self._profile, self._ms) + self._file_transfer_handler = FileTransfersHandler(self._tox, self._settings, self._contacts_provider, + file_transfers_message_service, self._profile) + 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, + 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, + 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) + + self._tray.show() + self._ms.show() + + self._init_callbacks() + + def _try_to_update(self): + updating = updater.start_update_if_needed(self._version, self._settings) + if updating: + 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 _force_exit(self): + raise SystemExit() + + 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_profile(self): + if not self._profile.has_avatar(): + self._profile.reset_avatar(self._settings['identicons']) + + def _kill_toxav(self): + self._calls_manager.set_toxav(None) + self._tox.AV.kill() + + def _kill_tox(self): + self._tox.kill() diff --git a/toxygen/av/__init__.py b/toxygen/av/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/av/call.py b/toxygen/av/call.py new file mode 100644 index 0000000..d3e023b --- /dev/null +++ b/toxygen/av/call.py @@ -0,0 +1,58 @@ + + +class Call: + + def __init__(self, out_audio, out_video, in_audio=False, in_video=False): + self._in_audio = in_audio + self._in_video = in_video + self._out_audio = out_audio + self._out_video = out_video + self._is_active = False + + def get_is_active(self): + return self._is_active + + def set_is_active(self, value): + self._is_active = value + + is_active = property(get_is_active, set_is_active) + + # ----------------------------------------------------------------------------------------------------------------- + # Audio + # ----------------------------------------------------------------------------------------------------------------- + + def get_in_audio(self): + return self._in_audio + + def set_in_audio(self, value): + self._in_audio = value + + in_audio = property(get_in_audio, set_in_audio) + + def get_out_audio(self): + return self._out_audio + + def set_out_audio(self, value): + self._out_audio = value + + out_audio = property(get_out_audio, set_out_audio) + + # ----------------------------------------------------------------------------------------------------------------- + # Video + # ----------------------------------------------------------------------------------------------------------------- + + def get_in_video(self): + return self._in_video + + def set_in_video(self, value): + self._in_video = value + + in_video = property(get_in_video, set_in_video) + + def get_out_video(self): + return self._out_video + + def set_out_video(self, value): + self._out_video = value + + out_video = property(get_out_video, set_out_video) diff --git a/toxygen/calls.py b/toxygen/av/calls.py similarity index 81% rename from toxygen/calls.py rename to toxygen/av/calls.py index 3d02110..d5f2fe7 100644 --- a/toxygen/calls.py +++ b/toxygen/av/calls.py @@ -1,77 +1,20 @@ import pyaudio import time import threading -import settings -from toxav_enums import * +from wrapper.toxav_enums import * import cv2 import itertools import numpy as np -import screen_sharing -# TODO: play sound until outgoing call will be started or cancelled +from av import screen_sharing +from av.call import Call +import common.tox_save -class Call: +class AV(common.tox_save.ToxAvSave): - def __init__(self, out_audio, out_video, in_audio=False, in_video=False): - self._in_audio = in_audio - self._in_video = in_video - self._out_audio = out_audio - self._out_video = out_video - self._is_active = False - - def get_is_active(self): - return self._is_active - - def set_is_active(self, value): - self._is_active = value - - is_active = property(get_is_active, set_is_active) - - # ----------------------------------------------------------------------------------------------------------------- - # Audio - # ----------------------------------------------------------------------------------------------------------------- - - def get_in_audio(self): - return self._in_audio - - def set_in_audio(self, value): - self._in_audio = value - - in_audio = property(get_in_audio, set_in_audio) - - def get_out_audio(self): - return self._out_audio - - def set_out_audio(self, value): - self._out_audio = value - - out_audio = property(get_out_audio, set_out_audio) - - # ----------------------------------------------------------------------------------------------------------------- - # Video - # ----------------------------------------------------------------------------------------------------------------- - - def get_in_video(self): - return self._in_video - - def set_in_video(self, value): - self._in_video = value - - in_video = property(get_in_video, set_in_video) - - def get_out_video(self): - return self._out_video - - def set_out_video(self, value): - self._out_video = value - - out_video = property(get_out_video, set_out_video) - - -class AV: - - def __init__(self, toxav): - self._toxav = toxav + def __init__(self, toxav, settings): + super().__init__(toxav) + self._settings = settings self._running = True self._calls = {} # dict: key - friend number, value - Call instance @@ -174,7 +117,7 @@ class AV: rate=self._audio_rate, channels=self._audio_channels, input=True, - input_device_index=settings.Settings.get_instance().audio['input'], + input_device_index=self._settings.audio['input'], frames_per_buffer=self._audio_sample_count * 10) self._audio_thread = threading.Thread(target=self.send_audio) @@ -203,15 +146,14 @@ class AV: return self._video_running = True - s = settings.Settings.get_instance() self._video_width = s.video['width'] self._video_height = s.video['height'] if s.video['device'] == -1: - self._video = screen_sharing.DesktopGrabber(s.video['x'], s.video['y'], - s.video['width'], s.video['height']) + 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(s.video['device']) + 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) @@ -241,7 +183,7 @@ class AV: self._out_stream = self._audio.open(format=pyaudio.paInt16, channels=channels_count, rate=rate, - output_device_index=settings.Settings.get_instance().audio['output'], + output_device_index=self._settings.audio['output'], output=True) self._out_stream.write(samples) diff --git a/toxygen/av/calls_manager.py b/toxygen/av/calls_manager.py new file mode 100644 index 0000000..5a48672 --- /dev/null +++ b/toxygen/av/calls_manager.py @@ -0,0 +1,116 @@ +import threading +import cv2 +import av.calls +from messenger.messages import * +from ui import av_widgets +import common.event as event + + +class CallsManager: + + def __init__(self, toxav, settings, screen, contacts_manager): + 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._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 + + def set_toxav(self, toxav): + self._call.set_toxav(toxav) + + # ----------------------------------------------------------------------------------------------------------------- + # Events + # ----------------------------------------------------------------------------------------------------------------- + + def get_call_started_event(self): + return self._call_started_event + + call_started_event = property(get_call_started_event) + + def get_call_finished_event(self): + return self._call_finished_event + + call_finished_event = property(get_call_finished_event) + + # ----------------------------------------------------------------------------------------------------------------- + # AV support + # ----------------------------------------------------------------------------------------------------------------- + + def call_click(self, audio=True, video=False): + """User clicked audio button in main window""" + num = self._contacts_manager.get_active_number() + 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']: + return + self._call(num, audio, video) + self._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) + + def incoming_call(self, audio, video, friend_number): + """ + Incoming call from friend. + """ + 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() + else: + friend.actions = True + text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") + self._call_widgets[friend_number] = self._get_incoming_call_widget(friend_number, text, friend.name) + self._call_widgets[friend_number].set_pixmap(friend.get_pixmap()) + self._call_widgets[friend_number].show() + + def accept_call(self, friend_number, audio, video): + """ + Accept incoming call with audio or video + """ + 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] + + def stop_call(self, friend_number, by_friend): + """ + Stop call with friend + """ + 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._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(): + if is_video: + cv2.destroyWindow(str(friend_number)) + + threading.Timer(2.0, destroy_window).start() + self._call_finished_event(friend_number, is_declined) + + def friend_exit(self, friend_number): + if friend_number in self._call: + self._call.finish_call(friend_number, True) + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _get_incoming_call_widget(self, friend_number, text, friend_name): + return av_widgets.IncomingCallWidget(self._settings, self, friend_number, text, friend_name) diff --git a/toxygen/screen_sharing.py b/toxygen/av/screen_sharing.py similarity index 100% rename from toxygen/screen_sharing.py rename to toxygen/av/screen_sharing.py diff --git a/toxygen/bootstrap/__init__.py b/toxygen/bootstrap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/bootstrap.py b/toxygen/bootstrap/bootstrap.py similarity index 63% rename from toxygen/bootstrap.py rename to toxygen/bootstrap/bootstrap.py index 0589940..fad68c4 100644 --- a/toxygen/bootstrap.py +++ b/toxygen/bootstrap/bootstrap.py @@ -1,11 +1,13 @@ import random import urllib.request -from util import log, curr_directory -import settings +from utils.util import * from PyQt5 import QtNetwork, QtCore import json +DEFAULT_NODES_COUNT = 4 + + class Node: def __init__(self, node): @@ -18,48 +20,42 @@ class Node: priority = property(get_priority) def get_data(self): - return bytes(self._ip, 'utf-8'), self._port, self._tox_key + return self._ip, self._port, self._tox_key -def generate_nodes(): - with open(curr_directory() + '/nodes.json', 'rt') as fl: +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) - sorted_nodes = sorted(nodes, key=lambda x: x.priority)[-4:] + 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 save_nodes(nodes): - if not nodes: - return - print('Saving nodes...') - with open(curr_directory() + '/nodes.json', 'wb') as fl: - fl.write(nodes) - - -def download_nodes_list(): +def download_nodes_list(settings): url = 'https://nodes.tox.chat/json' - s = settings.Settings.get_instance() - if not s['download_nodes_list']: + if not settings['download_nodes_list']: return - if not s['proxy_type']: # no proxy + 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) + _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 s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) - proxy.setHostName(s['proxy_host']) - proxy.setPort(s['proxy_port']) + 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() @@ -70,6 +66,18 @@ def download_nodes_list(): QtCore.QThread.msleep(1) QtCore.QCoreApplication.processEvents() data = bytes(reply.readAll().data()) - save_nodes(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): + if not nodes: + return + print('Saving nodes...') + with open(_get_nodes_path(), 'wb') as fl: + fl.write(nodes) diff --git a/toxygen/bootstrap/nodes.json b/toxygen/bootstrap/nodes.json new file mode 100644 index 0000000..5314998 --- /dev/null +++ b/toxygen/bootstrap/nodes.json @@ -0,0 +1 @@ +{"nodes":[{"ipv4":"80.211.19.83","ipv6":"-","port":33445,"public_key":"A2D7BF17C10A12C339B9F4E8DD77DEEE8457D580535A6F0D0F9AF04B8B4C4420","status_udp":true,"status_tcp":true}]} \ No newline at end of file diff --git a/toxygen/callbacks.py b/toxygen/callbacks.py deleted file mode 100644 index b59d17c..0000000 --- a/toxygen/callbacks.py +++ /dev/null @@ -1,469 +0,0 @@ -from PyQt5 import QtCore, QtGui, QtWidgets -from notifications import * -from settings import Settings -from profile import Profile -from toxcore_enums_and_consts import * -from toxav_enums import * -from tox import bin_to_string -from plugin_support import PluginLoader -import queue -import threading -import util -import cv2 -import numpy as np - -# ----------------------------------------------------------------------------------------------------------------- -# Threads -# ----------------------------------------------------------------------------------------------------------------- - - -class InvokeEvent(QtCore.QEvent): - EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) - - def __init__(self, fn, *args, **kwargs): - QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) - self.fn = fn - self.args = args - self.kwargs = kwargs - - -class Invoker(QtCore.QObject): - - def event(self, event): - event.fn(*event.args, **event.kwargs) - return True - - -_invoker = Invoker() - - -def invoke_in_main_thread(fn, *args, **kwargs): - QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) - - -class FileTransfersThread(threading.Thread): - - def __init__(self): - self._queue = queue.Queue() - self._timeout = 0.01 - self._continue = True - super().__init__() - - def execute(self, function, *args, **kwargs): - self._queue.put((function, args, kwargs)) - - def stop(self): - self._continue = False - - def run(self): - while self._continue: - try: - function, args, kwargs = self._queue.get(timeout=self._timeout) - function(*args, **kwargs) - except queue.Empty: - pass - except queue.Full: - util.log('Queue is Full in _thread') - except Exception as ex: - util.log('Exception in _thread: ' + str(ex)) - - -_thread = FileTransfersThread() - - -def start(): - _thread.start() - - -def stop(): - _thread.stop() - _thread.join() - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - current user -# ----------------------------------------------------------------------------------------------------------------- - - -def self_connection_status(tox_link): - """ - Current user changed connection status (offline, UDP, TCP) - """ - def wrapped(tox, connection, user_data): - print('Connection status: ', str(connection)) - profile = Profile.get_instance() - if profile.status is None: - status = tox_link.self_get_status() - invoke_in_main_thread(profile.set_status, status) - elif connection == TOX_CONNECTION['NONE']: - invoke_in_main_thread(profile.set_status, None) - return wrapped - - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - friends -# ----------------------------------------------------------------------------------------------------------------- - - -def friend_status(tox, friend_num, new_status, user_data): - """ - Check friend's status (none, busy, away) - """ - print("Friend's #{} status changed!".format(friend_num)) - profile = Profile.get_instance() - friend = profile.get_friend_by_number(friend_num) - if friend.status is None and Settings.get_instance()['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) - invoke_in_main_thread(QtCore.QTimer.singleShot, 5000, lambda: profile.send_files(friend_num)) - invoke_in_main_thread(profile.update_filtration) - - -def friend_connection_status(tox, friend_num, new_status, user_data): - """ - Check friend's connection status (offline, udp, tcp) - """ - print("Friend #{} connection status: {}".format(friend_num, new_status)) - profile = Profile.get_instance() - friend = profile.get_friend_by_number(friend_num) - if new_status == TOX_CONNECTION['NONE']: - invoke_in_main_thread(profile.friend_exit, friend_num) - invoke_in_main_thread(profile.update_filtration) - if Settings.get_instance()['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) - elif friend.status is None: - invoke_in_main_thread(profile.send_avatar, friend_num) - invoke_in_main_thread(PluginLoader.get_instance().friend_online, friend_num) - - -def friend_name(tox, friend_num, name, size, user_data): - """ - Friend changed his name - """ - profile = Profile.get_instance() - print('New name friend #' + str(friend_num)) - invoke_in_main_thread(profile.new_name, friend_num, name) - - -def friend_status_message(tox, friend_num, status_message, size, user_data): - """ - :return: function for callback friend_status_message. It updates friend's status message - and calls window repaint - """ - profile = Profile.get_instance() - friend = profile.get_friend_by_number(friend_num) - invoke_in_main_thread(friend.set_status_message, status_message) - print('User #{} has new status'.format(friend_num)) - invoke_in_main_thread(profile.send_messages, friend_num) - if profile.get_active_number() == friend_num: - invoke_in_main_thread(profile.set_active) - - -def friend_message(window, tray): - """ - New message from friend - """ - def wrapped(tox, friend_number, message_type, message, size, user_data): - profile = Profile.get_instance() - settings = Settings.get_instance() - message = str(message, 'utf-8') - invoke_in_main_thread(profile.new_message, friend_number, message_type, message) - if not window.isActiveWindow(): - friend = profile.get_friend_by_number(friend_number) - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: - invoke_in_main_thread(tray_notification, friend.name, message, tray, window) - if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['MESSAGE']) - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png')) - return wrapped - - -def friend_request(tox, public_key, message, message_size, user_data): - """ - Called when user get new friend request - """ - print('Friend request') - profile = Profile.get_instance() - key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) - tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE) - if tox_id not in Settings.get_instance()['blocked']: - invoke_in_main_thread(profile.process_friend_request, tox_id, str(message, 'utf-8')) - - -def friend_typing(tox, friend_number, typing, user_data): - invoke_in_main_thread(Profile.get_instance().friend_typing, friend_number, typing) - - -def friend_read_receipt(tox, friend_number, message_id, user_data): - profile = Profile.get_instance() - profile.get_friend_by_number(friend_number).dec_receipt() - if friend_number == profile.get_active_number(): - invoke_in_main_thread(profile.receipt) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - file transfers -# ----------------------------------------------------------------------------------------------------------------- - - -def tox_file_recv(window, tray): - """ - New incoming file - """ - def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data): - profile = Profile.get_instance() - settings = Settings.get_instance() - if file_type == TOX_FILE_KIND['DATA']: - print('File') - try: - file_name = str(file_name[:file_name_size], 'utf-8') - except: - file_name = 'toxygen_file' - invoke_in_main_thread(profile.incoming_file_transfer, - friend_number, - file_number, - size, - file_name) - if not window.isActiveWindow(): - friend = profile.get_friend_by_number(friend_number) - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: - file_from = QtWidgets.QApplication.translate("Callback", "File from") - invoke_in_main_thread(tray_notification, file_from + ' ' + friend.name, file_name, tray, window) - if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER']) - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png')) - else: # AVATAR - print('Avatar') - invoke_in_main_thread(profile.incoming_avatar, - friend_number, - file_number, - size) - return wrapped - - -def file_recv_chunk(tox, friend_number, file_number, position, chunk, length, user_data): - """ - Incoming chunk - """ - _thread.execute(Profile.get_instance().incoming_chunk, friend_number, file_number, position, - chunk[:length] if length else None) - - -def file_chunk_request(tox, friend_number, file_number, position, size, user_data): - """ - Outgoing chunk - """ - Profile.get_instance().outgoing_chunk(friend_number, file_number, position, size) - - -def file_recv_control(tox, friend_number, file_number, file_control, user_data): - """ - Friend cancelled, paused or resumed file transfer - """ - if file_control == TOX_FILE_CONTROL['CANCEL']: - invoke_in_main_thread(Profile.get_instance().cancel_transfer, friend_number, file_number, True) - elif file_control == TOX_FILE_CONTROL['PAUSE']: - invoke_in_main_thread(Profile.get_instance().pause_transfer, friend_number, file_number, True) - elif file_control == TOX_FILE_CONTROL['RESUME']: - invoke_in_main_thread(Profile.get_instance().resume_transfer, friend_number, file_number, True) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - custom packets -# ----------------------------------------------------------------------------------------------------------------- - - -def lossless_packet(tox, friend_number, data, length, user_data): - """ - Incoming lossless packet - """ - data = data[:length] - plugin = PluginLoader.get_instance() - invoke_in_main_thread(plugin.callback_lossless, friend_number, data) - - -def lossy_packet(tox, friend_number, data, length, user_data): - """ - Incoming lossy packet - """ - data = data[:length] - plugin = PluginLoader.get_instance() - invoke_in_main_thread(plugin.callback_lossy, friend_number, data) - - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - audio -# ----------------------------------------------------------------------------------------------------------------- - -def call_state(toxav, friend_number, mask, user_data): - """ - New call state - """ - print(friend_number, mask) - if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: - invoke_in_main_thread(Profile.get_instance().stop_call, friend_number, True) - else: - Profile.get_instance().call.toxav_call_state_cb(friend_number, mask) - - -def call(toxav, friend_number, audio, video, user_data): - """ - Incoming call from friend - """ - print(friend_number, audio, video) - invoke_in_main_thread(Profile.get_instance().incoming_call, audio, video, friend_number) - - -def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data): - """ - New audio chunk - """ - Profile.get_instance().call.audio_chunk( - bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), - audio_channels_count, - rate) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - video -# ----------------------------------------------------------------------------------------------------------------- - - -def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data): - """ - Creates yuv frame from y, u, v and shows it using OpenCV - For yuv => bgr we need this YUV420 frame: - - width - ------------------------- - | | - | Y | height - | | - ------------------------- - | | | - | U even | U odd | height // 4 - | | | - ------------------------- - | | | - | V even | V odd | height // 4 - | | | - ------------------------- - - width // 2 width // 2 - - It can be created from initial y, u, v using slices - """ - try: - y_size = abs(max(width, abs(ystride))) - u_size = abs(max(width // 2, abs(ustride))) - v_size = abs(max(width // 2, abs(vstride))) - - y = np.asarray(y[:y_size * height], dtype=np.uint8).reshape(height, y_size) - u = np.asarray(u[:u_size * height // 2], dtype=np.uint8).reshape(height // 2, u_size) - v = np.asarray(v[:v_size * height // 2], dtype=np.uint8).reshape(height // 2, v_size) - - width -= width % 4 - height -= height % 4 - - frame = np.zeros((int(height * 1.5), width), dtype=np.uint8) - - frame[:height, :] = y[:height, :width] - frame[height:height * 5 // 4, :width // 2] = u[:height // 2:2, :width // 2] - frame[height:height * 5 // 4, width // 2:] = u[1:height // 2:2, :width // 2] - - frame[height * 5 // 4:, :width // 2] = v[:height // 2:2, :width // 2] - frame[height * 5 // 4:, width // 2:] = v[1:height // 2:2, :width // 2] - - frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) - - invoke_in_main_thread(cv2.imshow, str(friend_number), frame) - except Exception as ex: - print(ex) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - groups -# ----------------------------------------------------------------------------------------------------------------- - - -def group_invite(tox, friend_number, gc_type, data, length, user_data): - invoke_in_main_thread(Profile.get_instance().group_invite, friend_number, gc_type, - bytes(data[:length])) - - -def show_gc_notification(window, tray, message, group_number, peer_number): - profile = Profile.get_instance() - settings = Settings.get_instance() - chat = profile.get_group_by_number(group_number) - peer_name = chat.get_peer_name(peer_number) - if not window.isActiveWindow() and (profile.name in message or settings['group_notifications']): - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: - invoke_in_main_thread(tray_notification, chat.name + ' ' + peer_name, message, tray, window) - if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['MESSAGE']) - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png')) - - -def group_message(window, tray): - def wrapped(tox, group_number, peer_number, message, length, user_data): - message = str(message[:length], 'utf-8') - invoke_in_main_thread(Profile.get_instance().new_gc_message, group_number, - peer_number, TOX_MESSAGE_TYPE['NORMAL'], message) - show_gc_notification(window, tray, message, group_number, peer_number) - return wrapped - - -def group_action(window, tray): - def wrapped(tox, group_number, peer_number, message, length, user_data): - message = str(message[:length], 'utf-8') - invoke_in_main_thread(Profile.get_instance().new_gc_message, group_number, - peer_number, TOX_MESSAGE_TYPE['ACTION'], message) - show_gc_notification(window, tray, message, group_number, peer_number) - return wrapped - - -def group_title(tox, group_number, peer_number, title, length, user_data): - invoke_in_main_thread(Profile.get_instance().new_gc_title, group_number, - title[:length]) - - -def group_namelist_change(tox, group_number, peer_number, change, user_data): - invoke_in_main_thread(Profile.get_instance().update_gc, group_number) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - initialization -# ----------------------------------------------------------------------------------------------------------------- - - -def init_callbacks(tox, window, tray): - """ - Initialization of all callbacks. - :param tox: tox instance - :param window: main window - :param tray: tray (for notifications) - """ - tox.callback_self_connection_status(self_connection_status(tox), 0) - - tox.callback_friend_status(friend_status, 0) - tox.callback_friend_message(friend_message(window, tray), 0) - tox.callback_friend_connection_status(friend_connection_status, 0) - tox.callback_friend_name(friend_name, 0) - tox.callback_friend_status_message(friend_status_message, 0) - tox.callback_friend_request(friend_request, 0) - tox.callback_friend_typing(friend_typing, 0) - tox.callback_friend_read_receipt(friend_read_receipt, 0) - - tox.callback_file_recv(tox_file_recv(window, tray), 0) - tox.callback_file_recv_chunk(file_recv_chunk, 0) - tox.callback_file_chunk_request(file_chunk_request, 0) - tox.callback_file_recv_control(file_recv_control, 0) - - toxav = tox.AV - toxav.callback_call_state(call_state, 0) - toxav.callback_call(call, 0) - toxav.callback_audio_receive_frame(callback_audio, 0) - toxav.callback_video_receive_frame(video_receive_frame, 0) - - tox.callback_friend_lossless_packet(lossless_packet, 0) - tox.callback_friend_lossy_packet(lossy_packet, 0) - - tox.callback_group_invite(group_invite) - tox.callback_group_message(group_message(window, tray)) - tox.callback_group_action(group_action(window, tray)) - tox.callback_group_title(group_title) - tox.callback_group_namelist_change(group_namelist_change) diff --git a/toxygen/common/__init__.py b/toxygen/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/common/event.py b/toxygen/common/event.py new file mode 100644 index 0000000..687a34d --- /dev/null +++ b/toxygen/common/event.py @@ -0,0 +1,26 @@ + + +class Event: + + def __init__(self): + self._callbacks = set() + + def __iadd__(self, callback): + self.add_callback(callback) + + return self + + def __isub__(self, callback): + self.remove_callback(callback) + + return self + + def __call__(self, *args, **kwargs): + for callback in self._callbacks: + callback(*args, **kwargs) + + def add_callback(self, callback): + self._callbacks.add(callback) + + def remove_callback(self, callback): + self._callbacks.discard(callback) diff --git a/toxygen/common/provider.py b/toxygen/common/provider.py new file mode 100644 index 0000000..d16edb4 --- /dev/null +++ b/toxygen/common/provider.py @@ -0,0 +1,13 @@ + + +class Provider: + + def __init__(self, get_item_action): + self._get_item_action = get_item_action + self._item = None + + def get_item(self): + if self._item is None: + self._item = self._get_item_action() + + return self._item diff --git a/toxygen/common/tox_save.py b/toxygen/common/tox_save.py new file mode 100644 index 0000000..09c159b --- /dev/null +++ b/toxygen/common/tox_save.py @@ -0,0 +1,18 @@ + + +class ToxSave: + + def __init__(self, tox): + self._tox = tox + + def set_tox(self, tox): + self._tox = tox + + +class ToxAvSave: + + def __init__(self, toxav): + self._toxav = toxav + + def set_toxav(self, toxav): + self._toxav = toxav diff --git a/toxygen/contacts/__init__.py b/toxygen/contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/basecontact.py b/toxygen/contacts/basecontact.py similarity index 56% rename from toxygen/basecontact.py rename to toxygen/contacts/basecontact.py index e1243a4..2058890 100644 --- a/toxygen/basecontact.py +++ b/toxygen/contacts/basecontact.py @@ -1,6 +1,9 @@ -from settings import * +from user_data.settings import * from PyQt5 import QtCore, QtGui -from toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE +from wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE +import utils.util as util +import common.event as event +import contacts.common as common class BaseContact: @@ -11,16 +14,21 @@ class BaseContact: Base class for all contacts. """ - def __init__(self, name, status_message, widget, tox_id): + def __init__(self, profile_manager, name, status_message, widget, tox_id): """ :param name: name, example: 'Toxygen user' :param status_message: status message, example: 'Toxing on Toxygen' :param widget: ContactItem instance :param tox_id: tox id of contact """ + self._profile_manager = profile_manager self._name, self._status_message = name, status_message self._status, self._widget = None, widget self._tox_id = tox_id + self._name_changed_event = event.Event() + self._status_message_changed_event = event.Event() + self._status_changed_event = event.Event() + self._avatar_changed_event = event.Event() self.init_widget() # ----------------------------------------------------------------------------------------------------------------- @@ -31,12 +39,20 @@ class BaseContact: return self._name def set_name(self, value): - self._name = str(value, 'utf-8') + if self._name == value: + return + self._name = value self._widget.name.setText(self._name) self._widget.name.repaint() + self._name_changed_event(self._name) name = property(get_name, set_name) + def get_name_changed_event(self): + return self._name_changed_event + + name_changed_event = property(get_name_changed_event) + # ----------------------------------------------------------------------------------------------------------------- # Status message # ----------------------------------------------------------------------------------------------------------------- @@ -45,12 +61,20 @@ class BaseContact: return self._status_message def set_status_message(self, value): - self._status_message = str(value, 'utf-8') + if self._status_message == value: + return + self._status_message = value self._widget.status_message.setText(self._status_message) self._widget.status_message.repaint() + self._status_message_changed_event(self._status_message) status_message = property(get_status_message, set_status_message) + def get_status_message_changed_event(self): + return self._status_message_changed_event + + status_message_changed_event = property(get_status_message_changed_event) + # ----------------------------------------------------------------------------------------------------------------- # Status # ----------------------------------------------------------------------------------------------------------------- @@ -59,11 +83,19 @@ class BaseContact: return self._status def set_status(self, value): + if self._status == value: + return self._status = value self._widget.connection_status.update(value) + self._status_changed_event(self._status) status = property(get_status, set_status) + def get_status_changed_event(self): + return self._status_changed_event + + status_changed_event = property(get_status_changed_event) + # ----------------------------------------------------------------------------------------------------------------- # TOX ID. WARNING: for friend it will return public key, for profile - full address # ----------------------------------------------------------------------------------------------------------------- @@ -81,24 +113,25 @@ class BaseContact: """ Tries to load avatar of contact or uses default avatar """ - prefix = ProfileHelper.get_path() + 'avatars/' - avatar_path = prefix + '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image - avatar_path = curr_directory() + '/images/avatar.png' + avatar_path = self.get_avatar_path() width = self._widget.avatar_label.width() pixmap = QtGui.QPixmap(avatar_path) self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) self._widget.avatar_label.repaint() + self._avatar_changed_event(avatar_path) - def reset_avatar(self): - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if os.path.isfile(avatar_path): + def reset_avatar(self, generate_new): + avatar_path = self.get_avatar_path() + if os.path.isfile(avatar_path) and not avatar_path == self._get_default_avatar_path(): os.remove(avatar_path) + if generate_new: + self.set_avatar(common.generate_avatar(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])) + else: self.load_avatar() def set_avatar(self, avatar): - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + avatar_path = self.get_contact_avatar_path() with open(avatar_path, 'wb') as f: f.write(avatar) self.load_avatar() @@ -106,13 +139,42 @@ class BaseContact: def get_pixmap(self): return self._widget.avatar_label.pixmap() + def get_avatar_path(self): + avatar_path = self.get_contact_avatar_path() + if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image + avatar_path = self._get_default_avatar_path() + + return avatar_path + + def get_contact_avatar_path(self): + directory = util.join_path(self._profile_manager.get_dir(), 'avatars') + + return util.join_path(directory, '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])) + + def has_avatar(self): + path = self.get_contact_avatar_path() + + return util.file_exists(path) + + def get_avatar_changed_event(self): + return self._avatar_changed_event + + avatar_changed_event = property(get_avatar_changed_event) + # ----------------------------------------------------------------------------------------------------------------- # Widgets # ----------------------------------------------------------------------------------------------------------------- def init_widget(self): - if self._widget is not None: - self._widget.name.setText(self._name) - self._widget.status_message.setText(self._status_message) - self._widget.connection_status.update(self._status) - self.load_avatar() + self._widget.name.setText(self._name) + self._widget.status_message.setText(self._status_message) + self._widget.connection_status.update(self._status) + self.load_avatar() + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + @staticmethod + def _get_default_avatar_path(): + return util.join_path(util.get_images_directory(), 'avatar.png') diff --git a/toxygen/contacts/common.py b/toxygen/contacts/common.py new file mode 100644 index 0000000..27750a2 --- /dev/null +++ b/toxygen/contacts/common.py @@ -0,0 +1,50 @@ +from pydenticon import Generator +import hashlib + + +# ----------------------------------------------------------------------------------------------------------------- +# Typing notifications +# ----------------------------------------------------------------------------------------------------------------- + +class BaseTypingNotificationHandler: + + DEFAULT_HANDLER = None + + def __init__(self): + pass + + def send(self, tox, is_typing): + pass + + +class FriendTypingNotificationHandler(BaseTypingNotificationHandler): + + def __init__(self, friend_number): + super().__init__() + self._friend_number = friend_number + + def send(self, tox, is_typing): + tox.self_set_typing(self._friend_number, is_typing) + + +BaseTypingNotificationHandler.DEFAULT_HANDLER = BaseTypingNotificationHandler() + + +# ----------------------------------------------------------------------------------------------------------------- +# Identicons support +# ----------------------------------------------------------------------------------------------------------------- + + +def generate_avatar(public_key): + foreground = ['rgb(45,79,255)', 'rgb(185, 66, 244)', 'rgb(185, 66, 244)', + 'rgb(254,180,44)', 'rgb(252, 2, 2)', 'rgb(109, 198, 0)', + 'rgb(226,121,234)', 'rgb(130, 135, 124)', + 'rgb(30,179,253)', 'rgb(160, 157, 0)', + 'rgb(232,77,65)', 'rgb(102, 4, 4)', + 'rgb(49,203,115)', + 'rgb(141,69,170)'] + generator = Generator(5, 5, foreground=foreground, background='rgba(42,42,42,0)') + digest = hashlib.sha256(public_key.encode('utf-8')).hexdigest() + identicon = generator.generate(digest, 220, 220, padding=(10, 10, 10, 10)) + + return identicon diff --git a/toxygen/contact.py b/toxygen/contacts/contact.py similarity index 61% rename from toxygen/contact.py rename to toxygen/contacts/contact.py index 9f27a1d..e88acf2 100644 --- a/toxygen/contact.py +++ b/toxygen/contacts/contact.py @@ -1,9 +1,8 @@ -from PyQt5 import QtCore, QtGui -from history import * -import basecontact -import util -from messages import * -import file_transfers as ft +from history.database import * +from contacts import basecontact, common +from messenger.messages import * +from contacts.contact_menu import * +from file_transfers import file_transfers as ft import re @@ -13,12 +12,12 @@ class Contact(basecontact.BaseContact): Properties: number, message getter, history etc. Base class for friend and gc classes """ - def __init__(self, message_getter, number, name, status_message, widget, tox_id): + def __init__(self, profile_manager, message_getter, number, name, status_message, widget, tox_id): """ :param message_getter: gets messages from db :param number: number of friend. """ - super().__init__(name, status_message, widget, tox_id) + super().__init__(profile_manager, name, status_message, widget, tox_id) self._number = number self._new_messages = False self._visible = True @@ -44,18 +43,22 @@ class Contact(basecontact.BaseContact): """ :param first_time: friend became active, load first part of messages """ - if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')): - return - if self._message_getter is None: - return - data = list(self._message_getter.get(PAGE_SIZE)) - if data is not None and len(data): - data.reverse() - else: - return - data = list(map(lambda tupl: TextMessage(*tupl), data)) - self._corr = data + self._corr - self._history_loaded = True + try: + if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')): + return + if self._message_getter is None: + return + data = list(self._message_getter.get(PAGE_SIZE)) + if data is not None and len(data): + data.reverse() + else: + return + data = list(map(lambda p: self._get_text_message(p), data)) + self._corr = data + self._corr + except: + pass + finally: + self._history_loaded = True def load_all_corr(self): """ @@ -66,7 +69,7 @@ class Contact(basecontact.BaseContact): data = list(self._message_getter.get_all()) if data is not None and len(data): data.reverse() - data = list(map(lambda tupl: TextMessage(*tupl), data)) + data = list(map(lambda p: self._get_text_message(p), data)) self._corr = data + self._corr self._history_loaded = True @@ -75,8 +78,8 @@ class Contact(basecontact.BaseContact): Get data to save in db :return: list of unsaved messages or [] """ - messages = list(filter(lambda x: x.get_type() <= 1, self._corr)) - return list(map(lambda x: x.get_data(), messages[-self._unsaved_messages:])) if self._unsaved_messages else [] + messages = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']), self._corr)) + return messages[-self._unsaved_messages:] if self._unsaved_messages else [] def get_corr(self): return self._corr[:] @@ -86,16 +89,31 @@ class Contact(basecontact.BaseContact): :param message: text or file transfer message """ self._corr.append(message) - if message.get_type() <= 1: + if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): self._unsaved_messages += 1 def get_last_message_text(self): - messages = list(filter(lambda x: x.get_type() <= 1 and x.get_owner() != MESSAGE_OWNER['FRIEND'], self._corr)) + messages = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']) + and m.author.type != MESSAGE_AUTHOR['FRIEND'], self._corr)) if messages: - return messages[-1].get_data()[0] + return messages[-1].text else: return '' + def remove_messages_widgets(self): + for message in self._corr: + message.remove_widget() + + def get_message(self, _filter): + return list(filter(lambda m: _filter(m), self._corr))[0] + + @staticmethod + def _get_text_message(params): + (message, author_type, author_name, unix_time, message_type, unique_id) = params + author = MessageAuthor(author_name, author_type) + + return TextMessage(message, author, unix_time, message_type, unique_id) + # ----------------------------------------------------------------------------------------------------------------- # Unsent messages # ----------------------------------------------------------------------------------------------------------------- @@ -104,19 +122,21 @@ class Contact(basecontact.BaseContact): """ :return list of unsent messages """ - messages = filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) + messages = filter(lambda m: m.author is not None and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr) return list(messages) def get_unsent_messages_for_saving(self): """ :return list of unsent messages for saving """ - messages = filter(lambda x: x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) - return list(map(lambda x: x.get_data(), messages)) + messages = filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']) + and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr) + return list(messages) - def mark_as_sent(self): + def mark_as_sent(self, tox_message_id): try: - message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0] + message = list(filter(lambda m: m.author is not None and m.author.type == MESSAGE_AUTHOR['NOT_SENT'] + 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)) @@ -125,9 +145,9 @@ class Contact(basecontact.BaseContact): # Message deletion # ----------------------------------------------------------------------------------------------------------------- - def delete_message(self, time): - elem = list(filter(lambda x: type(x) in (TextMessage, GroupChatMessage) and x.get_data()[2] == time, self._corr))[0] - tmp = list(filter(lambda x: x.get_type() <= 1, self._corr)) + def delete_message(self, message_id): + elem = list(filter(lambda m: m.message_id == message_id, self._corr))[0] + tmp = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']), self._corr)) if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages: self._unsaved_messages -= 1 self._corr.remove(elem) @@ -138,14 +158,14 @@ class Contact(basecontact.BaseContact): """ Delete old messages (reduces RAM usage if messages saving is not enabled) """ - def save_message(x): - if x.get_type() == 2 and (x.get_status() >= 2 or x.get_status() is None): + def save_message(m): + if m.type == MESSAGE_TYPE['FILE_TRANSFER'] and (m.state not in ACTIVE_FILE_TRANSFERS): return True - return x.get_owner() == MESSAGE_OWNER['NOT_SENT'] + return m.author is not None and m.author.type == MESSAGE_AUTHOR['NOT_SENT'] old = filter(save_message, self._corr[:-SAVE_MESSAGES]) self._corr = list(old) + self._corr[-SAVE_MESSAGES:] - text_messages = filter(lambda x: x.get_type() <= 1, self._corr) + text_messages = filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']), self._corr) self._unsaved_messages = min(self._unsaved_messages, len(list(text_messages))) self._search_index = 0 @@ -158,12 +178,14 @@ class Contact(basecontact.BaseContact): self._search_index = 0 # don't delete data about active file transfer if not save_unsent: - self._corr = list(filter(lambda x: x.get_type() == 2 and - x.get_status() in ft.ACTIVE_FILE_TRANSFERS, self._corr)) + self._corr = list(filter(lambda m: m.type == MESSAGE_TYPE['FILE_TRANSFER'] and + m.state in ft.ACTIVE_FILE_TRANSFERS, self._corr)) self._unsaved_messages = 0 else: - self._corr = list(filter(lambda x: (x.get_type() == 2 and x.get_status() in ft.ACTIVE_FILE_TRANSFERS) - or (x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT']), + self._corr = list(filter(lambda m: (m.type == MESSAGE_TYPE['FILE_TRANSFER'] + and m.state in ft.ACTIVE_FILE_TRANSFERS) + or (m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']) + and m.author.type == MESSAGE_AUTHOR['NOT_SENT']), self._corr)) self._unsaved_messages = len(self.get_unsent_messages()) @@ -179,9 +201,9 @@ class Contact(basecontact.BaseContact): while True: l = len(self._corr) for i in range(self._search_index - 1, -l - 1, -1): - if self._corr[i].get_type() > 1: + if self._corr[i].type not in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): continue - message = self._corr[i].get_data()[0] + message = self._corr[i].text if re.search(self._search_string, message, re.IGNORECASE) is not None: self._search_index = i return i @@ -194,9 +216,9 @@ class Contact(basecontact.BaseContact): if not self._search_index: return None for i in range(self._search_index + 1, 0): - if self._corr[i].get_type() > 1: + if self._corr[i].type not in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): continue - message = self._corr[i].get_data()[0] + message = self._corr[i].text if re.search(self._search_string, message, re.IGNORECASE) is not None: self._search_index = i return i @@ -229,6 +251,9 @@ class Contact(basecontact.BaseContact): def set_alias(self, alias): self._alias = bool(alias) + def has_alias(self): + return self._alias + # ----------------------------------------------------------------------------------------------------------------- # Visibility in friends' list # ----------------------------------------------------------------------------------------------------------------- @@ -241,10 +266,6 @@ class Contact(basecontact.BaseContact): visibility = property(get_visibility, set_visibility) - def set_widget(self, widget): - self._widget = widget - self.init_widget() - # ----------------------------------------------------------------------------------------------------------------- # Unread messages and other actions from friend # ----------------------------------------------------------------------------------------------------------------- @@ -276,7 +297,7 @@ class Contact(basecontact.BaseContact): messages = property(get_messages) # ----------------------------------------------------------------------------------------------------------------- - # Friend's number (can be used in toxcore) + # Friend's or group's number (can be used in toxcore) # ----------------------------------------------------------------------------------------------------------------- def get_number(self): @@ -286,3 +307,27 @@ class Contact(basecontact.BaseContact): self._number = value number = property(get_number, set_number) + + # ----------------------------------------------------------------------------------------------------------------- + # Typing notifications + # ----------------------------------------------------------------------------------------------------------------- + + def get_typing_notification_handler(self): + return common.BaseTypingNotificationHandler.DEFAULT_HANDLER + + typing_notification_handler = property(get_typing_notification_handler) + + # ----------------------------------------------------------------------------------------------------------------- + # Context menu support + # ----------------------------------------------------------------------------------------------------------------- + + def get_context_menu_generator(self): + return BaseContactMenuGenerator(self) + + # ----------------------------------------------------------------------------------------------------------------- + # Filtration support + # ----------------------------------------------------------------------------------------------------------------- + + def set_widget(self, widget): + self._widget = widget + self.init_widget() diff --git a/toxygen/contacts/contact_menu.py b/toxygen/contacts/contact_menu.py new file mode 100644 index 0000000..8178d31 --- /dev/null +++ b/toxygen/contacts/contact_menu.py @@ -0,0 +1,229 @@ +from PyQt5 import QtWidgets +import utils.ui as util_ui + + +# ----------------------------------------------------------------------------------------------------------------- +# Builder +# ----------------------------------------------------------------------------------------------------------------- + +def _create_menu(menu_name, parent): + menu_name = menu_name or '' + + return QtWidgets.QMenu(menu_name) if parent is None else parent.addMenu(menu_name) + + +class ContactMenuBuilder: + + def __init__(self): + self._actions = {} + self._submenus = {} + self._name = None + self._index = 0 + + def with_name(self, name): + self._name = name + + return self + + def with_action(self, text, handler): + self._add_action(text, handler) + + return self + + def with_optional_action(self, text, handler, show_action): + if show_action: + self._add_action(text, handler) + + return self + + def with_actions(self, actions): + for action in actions: + (text, handler) = action + self._add_action(text, handler) + + return self + + def with_submenu(self, submenu_builder): + self._add_submenu(submenu_builder) + + return self + + def with_optional_submenu(self, submenu_builder): + if submenu_builder is not None: + self._add_submenu(submenu_builder) + + return self + + def build(self, parent=None): + menu = _create_menu(self._name, parent) + + for i in range(self._index): + if i in self._actions: + text, handler = self._actions[i] + action = menu.addAction(text) + action.triggered.connect(handler) + else: + submenu_builder = self._submenus[i] + submenu = submenu_builder.build(menu) + menu.addMenu(submenu) + + return menu + + def _add_submenu(self, submenu): + self._submenus[self._index] = submenu + self._index += 1 + + def _add_action(self, text, handler): + self._actions[self._index] = (text, handler) + self._index += 1 + +# ----------------------------------------------------------------------------------------------------------------- +# Generators +# ----------------------------------------------------------------------------------------------------------------- + + +class BaseContactMenuGenerator: + + def __init__(self, contact): + self._contact = contact + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + return ContactMenuBuilder().build() + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _generate_copy_menu_builder(self, main_screen): + copy_menu_builder = ContactMenuBuilder() + (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)) + ) + + return copy_menu_builder + + 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) + 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)) + ) + + return history_menu_builder + + +class FriendMenuGenerator(BaseContactMenuGenerator): + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen) + copy_menu_builder = self._generate_copy_menu_builder(main_screen) + plugins_menu_builder = self._generate_plugins_menu_builder(plugin_loader, number) + 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') + + builder = ContactMenuBuilder() + menu = (builder + .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('Notes'), lambda: main_screen.show_note(self._contact)) + .with_optional_submenu(plugins_menu_builder) + .with_optional_submenu(groups_menu_builder) + ).build() + + return menu + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + @staticmethod + def _generate_plugins_menu_builder(plugin_loader, number): + if plugin_loader is None: + return None + plugins_actions = plugin_loader.get_menu(number) + if not len(plugins_actions): + return None + plugins_menu_builder = ContactMenuBuilder() + (plugins_menu_builder + .with_name(util_ui.tr('Plugins')) + .with_actions(plugins_actions) + ) + + return plugins_menu_builder + + def _generate_groups_menu(self, contacts_manager, groups_service): + chats = contacts_manager.get_group_chats() + if not len(chats) or self._contact.status is None: + return None + groups_menu_builder = ContactMenuBuilder() + (groups_menu_builder + .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]) + ) + + return groups_menu_builder + + +class GroupMenuGenerator(BaseContactMenuGenerator): + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + copy_menu_builder = self._generate_copy_menu_builder(main_screen) + history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen) + + builder = ContactMenuBuilder() + menu = (builder + .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'), + lambda: groups_service.show_group_management_screen(self._contact), + self._contact.is_self_founder()) + .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'), + 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'), + lambda: groups_service.reconnect_to_group(self._contact.number)) + .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('Notes'), lambda: main_screen.show_note(self._contact)) + ).build() + + return menu + + +class GroupPeerMenuGenerator(BaseContactMenuGenerator): + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + copy_menu_builder = self._generate_copy_menu_builder(main_screen) + history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen) + + builder = ContactMenuBuilder() + menu = (builder + .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'), + lambda: contacts_manager.remove_group_peer(self._contact)) + .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) + ).build() + + return menu diff --git a/toxygen/contacts/contact_provider.py b/toxygen/contacts/contact_provider.py new file mode 100644 index 0000000..76e8e79 --- /dev/null +++ b/toxygen/contacts/contact_provider.py @@ -0,0 +1,107 @@ +import common.tox_save as tox_save + + +class ContactProvider(tox_save.ToxSave): + + def __init__(self, tox, friend_factory, group_factory, group_peer_factory): + super().__init__(tox) + self._friend_factory = friend_factory + self._group_factory = group_factory + self._group_peer_factory = group_peer_factory + self._cache = {} # key - contact's public key, value - contact instance + + # ----------------------------------------------------------------------------------------------------------------- + # Friends + # ----------------------------------------------------------------------------------------------------------------- + + def get_friend_by_number(self, friend_number): + public_key = self._tox.friend_get_public_key(friend_number) + + return self.get_friend_by_public_key(public_key) + + def get_friend_by_public_key(self, public_key): + friend = self._get_contact_from_cache(public_key) + if friend is not None: + return friend + friend = self._friend_factory.create_friend_by_public_key(public_key) + self._add_to_cache(public_key, friend) + + return friend + + def get_all_friends(self): + friend_numbers = self._tox.self_get_friend_list() + friends = map(lambda n: self.get_friend_by_number(n), friend_numbers) + + return list(friends) + + # ----------------------------------------------------------------------------------------------------------------- + # Groups + # ----------------------------------------------------------------------------------------------------------------- + + def get_all_groups(self): + group_numbers = range(self._tox.group_get_number_groups()) + 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) + + return self.get_group_by_public_key(public_key) + + def get_group_by_public_key(self, public_key): + group = self._get_contact_from_cache(public_key) + if group is not None: + return group + group = self._group_factory.create_group_by_public_key(public_key) + self._add_to_cache(public_key, group) + + return group + + # ----------------------------------------------------------------------------------------------------------------- + # Group peers + # ----------------------------------------------------------------------------------------------------------------- + + def get_all_group_peers(self): + return list() + + 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) + + def get_group_peer_by_public_key(self, group, public_key): + peer = group.get_peer_by_public_key(public_key) + + return self._get_group_peer(group, peer) + + # ----------------------------------------------------------------------------------------------------------------- + # All contacts + # ----------------------------------------------------------------------------------------------------------------- + + def get_all(self): + return self.get_all_friends() + self.get_all_groups() + self.get_all_group_peers() + + # ----------------------------------------------------------------------------------------------------------------- + # Caching + # ----------------------------------------------------------------------------------------------------------------- + + def clear_cache(self): + self._cache.clear() + + def remove_contact_from_cache(self, contact_public_key): + if contact_public_key in self._cache: + del self._cache[contact_public_key] + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _get_contact_from_cache(self, public_key): + return self._cache[public_key] if public_key in self._cache else None + + def _add_to_cache(self, public_key, contact): + self._cache[public_key] = contact + + def _get_group_peer(self, group, peer): + return self._group_peer_factory.create_group_peer(group, peer) diff --git a/toxygen/contacts/contacts_manager.py b/toxygen/contacts/contacts_manager.py new file mode 100644 index 0000000..87a61ff --- /dev/null +++ b/toxygen/contacts/contacts_manager.py @@ -0,0 +1,575 @@ +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 + + +class ContactsManager(ToxSave): + """ + Represents contacts list. + """ + + def __init__(self, tox, settings, screen, profile_manager, contact_provider, history, tox_dns, + messages_items_factory): + super().__init__(tox) + self._settings = settings + self._screen = screen + self._profile_manager = profile_manager + self._contact_provider = contact_provider + self._tox_dns = tox_dns + self._messages_items_factory = messages_items_factory + self._messages = screen.messages + self._contacts, self._active_contact = [], -1 + self._active_contact_changed = Event() + self._sorting = settings['sorting'] + self._filter_string = '' + screen.contacts_filter.setCurrentIndex(int(self._sorting)) + self._history = history + self._load_contacts() + + def get_contact(self, num): + if num < 0 or num >= len(self._contacts): + return None + return self._contacts[num] + + def get_curr_contact(self): + return self._contacts[self._active_contact] if self._active_contact + 1 else None + + def save_profile(self): + data = self._tox.get_savedata() + self._profile_manager.save_profile(data) + + def is_friend_active(self, friend_number): + if not self.is_active_a_friend(): + return False + + return self.get_curr_contact().number == friend_number + + def is_group_active(self, group_number): + if self.is_active_a_friend(): + return False + + return self.get_curr_contact().number == group_number + + def is_contact_active(self, contact): + return self._contacts[self._active_contact].tox_id == contact.tox_id + + # ----------------------------------------------------------------------------------------------------------------- + # Reconnection support + # ----------------------------------------------------------------------------------------------------------------- + + def reset_contacts_statuses(self): + for contact in self._contacts: + contact.status = None + + # ----------------------------------------------------------------------------------------------------------------- + # Work with active friend + # ----------------------------------------------------------------------------------------------------------------- + + def get_active(self): + return self._active_contact + + def set_active(self, value): + """ + Change current active friend or update info + :param value: number of new active friend in friend's list + """ + if value is None and self._active_contact == -1: # nothing to update + return + if value == -1: # all friends were deleted + self._screen.account_name.setText('') + self._screen.account_status.setText('') + self._screen.account_status.setToolTip('') + self._active_contact = -1 + self._screen.account_avatar.setHidden(True) + self._messages.clear() + self._screen.messageEdit.clear() + return + try: + self._screen.typing.setVisible(False) + current_contact = self.get_curr_contact() + if current_contact is not None: + # TODO: send when needed + current_contact.typing_notification_handler.send(self._tox, False) + current_contact.remove_messages_widgets() # TODO: if required + self._unsubscribe_from_events(current_contact) + + if self._active_contact + 1 and self._active_contact != value: + try: + current_contact.curr_text = self._screen.messageEdit.toPlainText() + except: + pass + contact = self._contacts[value] + self._subscribe_to_events(contact) + contact.remove_invalid_unsent_files() + if self._active_contact != value: + self._screen.messageEdit.setPlainText(contact.curr_text) + self._active_contact = value + contact.reset_messages() + if not self._settings['save_history']: + contact.delete_old_messages() + self._messages.clear() + contact.load_corr() + corr = contact.get_corr()[-PAGE_SIZE:] + for message in corr: + if message.type == MESSAGE_TYPE['FILE_TRANSFER']: + self._messages_items_factory.create_file_transfer_item(message) + elif message.type == MESSAGE_TYPE['INLINE']: + self._messages_items_factory.create_inline_item(message) + else: + self._messages_items_factory.create_message_item(message) + self._messages.scrollToBottom() + # if value in self._call: + # self._screen.active_call() + # elif value in self._incoming_calls: + # self._screen.incoming_call() + # else: + # self._screen.call_finished() + 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)) + raise + + active_contact = property(get_active, set_active) + + def get_active_contact_changed(self): + return self._active_contact_changed + + active_contact_changed = property(get_active_contact_changed) + + def update(self): + if self._active_contact + 1: + self.set_active(self._active_contact) + + def is_active_a_friend(self): + return type(self.get_curr_contact()) is Friend + + def is_active_a_group(self): + return type(self.get_curr_contact()) is GroupChat + + def is_active_a_group_chat_peer(self): + return type(self.get_curr_contact()) is GroupPeerContact + + # ----------------------------------------------------------------------------------------------------------------- + # Filtration + # ----------------------------------------------------------------------------------------------------------------- + + def filtration_and_sorting(self, sorting=0, filter_str=''): + """ + Filtration of friends list + :param sorting: 0 - no sorting, 1 - online only, 2 - online first, 3 - by name, + 4 - online and by name, 5 - online first and by name + :param filter_str: show contacts which name contains this substring + """ + filter_str = filter_str.lower() + current_contact = self.get_curr_contact() + + if sorting > 5 or sorting < 0: + sorting = 0 + + if sorting in (1, 2, 4, 5): # online first + self._contacts = sorted(self._contacts, key=lambda x: int(x.status is not None), reverse=True) + sort_by_name = sorting in (4, 5) + # save results of previous sorting + online_friends = filter(lambda x: x.status is not None, self._contacts) + online_friends_count = len(list(online_friends)) + part1 = self._contacts[:online_friends_count] + part2 = self._contacts[online_friends_count:] + key_lambda = lambda x: x.name.lower() if sort_by_name else x.number + part1 = sorted(part1, key=key_lambda) + part2 = sorted(part2, key=key_lambda) + self._contacts = part1 + part2 + elif sorting == 0: + contacts = sorted(self._contacts, key=lambda c: c.number) + friends = filter(lambda c: type(c) is Friend, contacts) + groups = filter(lambda c: type(c) is GroupChat, contacts) + group_peers = filter(lambda c: type(c) is GroupPeerContact, contacts) + self._contacts = list(friends) + list(groups) + list(group_peers) + else: + self._contacts = sorted(self._contacts, key=lambda x: x.name.lower()) + + # change item widgets + for index, contact in enumerate(self._contacts): + list_item = self._screen.friends_list.item(index) + item_widget = self._screen.friends_list.itemWidget(list_item) + contact.set_widget(item_widget) + + for index, friend in enumerate(self._contacts): + filtered_by_name = filter_str in friend.name.lower() + friend.visibility = (friend.status is not None or sorting not in (1, 4)) and filtered_by_name + # show friend even if it's hidden when there any unread messages/actions + friend.visibility = friend.visibility or friend.messages or friend.actions + item = self._screen.friends_list.item(index) + item_widget = self._screen.friends_list.itemWidget(item) + item.setSizeHint(QtCore.QSize(250, item_widget.height() if friend.visibility else 0)) + + # save soring results + self._sorting, self._filter_string = sorting, filter_str + self._settings['sorting'] = self._sorting + self._settings.save() + + # update active contact + if current_contact is not None: + index = self._contacts.index(current_contact) + self.set_active(index) + + def update_filtration(self): + """ + Update list of contacts when 1 of friends change connection status + """ + self.filtration_and_sorting(self._sorting, self._filter_string) + + # ----------------------------------------------------------------------------------------------------------------- + # Contact getters + # ----------------------------------------------------------------------------------------------------------------- + + def get_friend_by_number(self, number): + return list(filter(lambda c: c.number == number and type(c) is Friend, self._contacts))[0] + + def get_group_by_number(self, number): + return list(filter(lambda c: c.number == number and type(c) is GroupChat, self._contacts))[0] + + 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) + + return self.get_contact_by_tox_id(peer.public_key) + + def check_if_contact_exists(self, tox_id): + return any(filter(lambda c: c.tox_id == tox_id, self._contacts)) + + def get_contact_by_tox_id(self, tox_id): + return list(filter(lambda c: c.tox_id == tox_id, self._contacts))[0] + + def get_active_number(self): + return self.get_curr_contact().number if self._active_contact + 1 else -1 + + def get_active_name(self): + return self.get_curr_contact().name if self._active_contact + 1 else '' + + def is_active_online(self): + return self._active_contact + 1 and self.get_curr_contact().status is not None + + # ----------------------------------------------------------------------------------------------------------------- + # Work with friends (remove, block, set alias, get public key) + # ----------------------------------------------------------------------------------------------------------------- + + def set_alias(self, num): + """ + Set new alias for friend + """ + friend = self._contacts[num] + name = friend.name + text = util_ui.tr("Enter new alias for friend {} or leave empty to use friend's name:").format(name) + title = util_ui.tr('Set alias') + text, ok = util_ui.text_dialog(text, title, name) + if not ok: + return + aliases = self._settings['friends_aliases'] + if text: + friend.name = text + try: + index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) + aliases[index] = (friend.tox_id, text) + except: + aliases.append((friend.tox_id, text)) + friend.set_alias(text) + else: # use default name + friend.name = self._tox.friend_get_name(friend.number) + friend.set_alias('') + try: + index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) + del aliases[index] + except: + pass + self._settings.save() + + def friend_public_key(self, num): + return self._contacts[num].tox_id + + def delete_friend(self, num): + """ + Removes friend from contact list + :param num: number of friend in list + """ + friend = self._contacts[num] + self._cleanup_contact_data(friend) + self._tox.friend_delete(friend.number) + self._delete_contact(num) + + def add_friend(self, tox_id): + """ + Adds friend to list + """ + self._tox.friend_add_norequest(tox_id) + self._add_friend(tox_id) + self.update_filtration() + + def block_user(self, tox_id): + """ + Block user with specified tox id (or public key) - delete from friends list and ignore friend requests + """ + tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] + if tox_id == self._tox.self_get_address[:TOX_PUBLIC_KEY_SIZE * 2]: + return + if tox_id not in self._settings['blocked']: + self._settings['blocked'].append(tox_id) + self._settings.save() + try: + num = self._tox.friend_by_public_key(tox_id) + self.delete_friend(num) + self.save_profile() + except: # not in friend list + pass + + def unblock_user(self, tox_id, add_to_friend_list): + """ + Unblock user + :param tox_id: tox id of contact + :param add_to_friend_list: add this contact to friend list or not + """ + self._settings['blocked'].remove(tox_id) + self._settings.save() + if add_to_friend_list: + self.add_friend(tox_id) + self.save_profile() + + # ----------------------------------------------------------------------------------------------------------------- + # Groups support + # ----------------------------------------------------------------------------------------------------------------- + + def get_group_chats(self): + return list(filter(lambda c: type(c) is GroupChat, self._contacts)) + + def add_group(self, group_number): + group = self._contact_provider.get_group_by_number(group_number) + index = len(self._contacts) + self._contacts.append(group) + group.reset_avatar(self._settings['identicons']) + self._save_profile() + self.set_active(index) + self.update_filtration() + + def delete_group(self, group_number): + group = self.get_group_by_number(group_number) + self._cleanup_contact_data(group) + num = self._contacts.index(group) + self._delete_contact(num) + + # ----------------------------------------------------------------------------------------------------------------- + # Groups private messaging + # ----------------------------------------------------------------------------------------------------------------- + + def add_group_peer(self, group, peer): + contact = self._contact_provider.get_group_peer_by_id(group, peer.id) + if self.check_if_contact_exists(contact.tox_id): + return + self._contacts.append(contact) + contact.reset_avatar(self._settings['identicons']) + self._save_profile() + + 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) + + 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) + + def get_gc_peer_name(self, name): + group = self.get_curr_contact() + + names = sorted(group.get_peers_names()) + if name in names: # return next nick + index = names.index(name) + index = (index + 1) % len(names) + + return names[index] + + suggested_names = list(filter(lambda x: x.startswith(name), names)) + if not len(suggested_names): + return '\t' + + return suggested_names[0] + + # ----------------------------------------------------------------------------------------------------------------- + # Friend requests + # ----------------------------------------------------------------------------------------------------------------- + + def send_friend_request(self, tox_id, message): + """ + Function tries to send request to contact with specified id + :param tox_id: id of new contact or tox dns 4 value + :param message: additional message + :return: True on success else error string + """ + try: + message = message or 'Hello! Add me to your contact list please' + if '@' in tox_id: # value like groupbot@toxme.io + tox_id = self._tox_dns.lookup(tox_id) + if tox_id is None: + raise Exception('TOX DNS lookup failed') + if len(tox_id) == TOX_PUBLIC_KEY_SIZE * 2: # public key + self.add_friend(tox_id) + title = util_ui.tr('Friend added') + text = util_ui.tr('Friend added without sending friend request') + util_ui.message_box(text, title) + else: + self._tox.friend_add(tox_id, message.encode('utf-8')) + tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] + self._add_friend(tox_id) + self.update_filtration() + self.save_profile() + return True + except Exception as ex: # wrong data + util.log('Friend request failed with ' + str(ex)) + return str(ex) + + def process_friend_request(self, tox_id, message): + """ + Accept or ignore friend request + :param tox_id: tox id of contact + :param message: message + """ + if tox_id in self._settings['blocked']: + return + try: + text = util_ui.tr('User {} wants to add you to contact list. Message:\n{}') + reply = util_ui.question(text.format(tox_id, message), util_ui.tr('Friend request')) + if reply: # accepted + self.add_friend(tox_id) + 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)) + + def can_send_typing_notification(self): + return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer() + + # ----------------------------------------------------------------------------------------------------------------- + # Contacts numbers update + # ----------------------------------------------------------------------------------------------------------------- + + def update_friends_numbers(self): + for friend in self._contact_provider.get_all_friends(): + friend.number = self._tox.friend_by_public_key(friend.tox_id) + self.update_filtration() + + def update_groups_numbers(self): + groups = self._contact_provider.get_all_groups() + for i in range(len(groups)): + chat_id = self._tox.group_get_chat_id(i) + group = self.get_contact_by_tox_id(chat_id) + group.number = i + self.update_filtration() + + def update_groups_lists(self): + groups = self._contact_provider.get_all_groups() + for group in groups: + group.remove_all_peers_except_self() + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _load_contacts(self): + self._load_friends() + self._load_groups() + if len(self._contacts): + self.set_active(0) + for contact in filter(lambda c: not c.has_avatar(), self._contacts): + contact.reset_avatar(self._settings['identicons']) + self.update_filtration() + + def _load_friends(self): + self._contacts.extend(self._contact_provider.get_all_friends()) + + def _load_groups(self): + self._contacts.extend(self._contact_provider.get_all_groups()) + + # ----------------------------------------------------------------------------------------------------------------- + # Current contact subscriptions + # ----------------------------------------------------------------------------------------------------------------- + + def _subscribe_to_events(self, contact): + contact.name_changed_event.add_callback(self._current_contact_name_changed) + contact.status_changed_event.add_callback(self._current_contact_status_changed) + contact.status_message_changed_event.add_callback(self._current_contact_status_message_changed) + contact.avatar_changed_event.add_callback(self._current_contact_avatar_changed) + + def _unsubscribe_from_events(self, contact): + contact.name_changed_event.remove_callback(self._current_contact_name_changed) + contact.status_changed_event.remove_callback(self._current_contact_status_changed) + contact.status_message_changed_event.remove_callback(self._current_contact_status_message_changed) + contact.avatar_changed_event.remove_callback(self._current_contact_avatar_changed) + + def _current_contact_name_changed(self, name): + self._screen.account_name.setText(name) + + def _current_contact_status_changed(self, status): + pass + + def _current_contact_status_message_changed(self, status_message): + self._screen.account_status.setText(status_message) + + def _current_contact_avatar_changed(self, avatar_path): + self._set_current_contact_avatar(avatar_path) + + def _set_current_contact_data(self, contact): + self._screen.account_name.setText(contact.name) + self._screen.account_status.setText(contact.status_message) + self._set_current_contact_avatar(contact.get_avatar_path()) + + def _set_current_contact_avatar(self, avatar_path): + width = self._screen.account_avatar.width() + pixmap = QtGui.QPixmap(avatar_path) + self._screen.account_avatar.setPixmap(pixmap.scaled(width, width, + QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) + + def _add_friend(self, tox_id): + self._history.add_friend_to_db(tox_id) + friend = self._contact_provider.get_friend_by_public_key(tox_id) + index = len(self._contacts) + self._contacts.append(friend) + if not friend.has_avatar(): + friend.reset_avatar(self._settings['identicons']) + self._save_profile() + self.set_active(index) + + def _save_profile(self): + data = self._tox.get_savedata() + self._profile_manager.save_profile(data) + + def _cleanup_contact_data(self, contact): + try: + index = list(map(lambda x: x[0], self._settings['friends_aliases'])).index(contact.tox_id) + del self._settings['friends_aliases'][index] + except: + pass + if contact.tox_id in self._settings['notes']: + del self._settings['notes'][contact.tox_id] + self._settings.save() + self._history.delete_history(contact) + if contact.has_avatar(): + avatar_path = contact.get_contact_avatar_path() + remove(avatar_path) + + def _delete_contact(self, num): + self.set_active(-1 if len(self._contacts) == 1 else 0) + + self._contact_provider.remove_contact_from_cache(self._contacts[num].tox_id) + del self._contacts[num] + self._screen.friends_list.takeItem(num) + self._save_profile() + + self.update_filtration() diff --git a/toxygen/contacts/friend.py b/toxygen/contacts/friend.py new file mode 100644 index 0000000..5c8eabb --- /dev/null +++ b/toxygen/contacts/friend.py @@ -0,0 +1,74 @@ +from contacts import contact, common +from messenger.messages import * +import os +from contacts.contact_menu import * + + +class Friend(contact.Contact): + """ + Friend in list of friends. + """ + + def __init__(self, profile_manager, message_getter, number, name, status_message, widget, tox_id): + super().__init__(profile_manager, message_getter, number, name, status_message, widget, tox_id) + self._receipts = 0 + self._typing_notification_handler = common.FriendTypingNotificationHandler(number) + + # ----------------------------------------------------------------------------------------------------------------- + # File transfers support + # ----------------------------------------------------------------------------------------------------------------- + + def insert_inline(self, before_message_id, inline): + """ + Update status of active transfer and load inline if needed + """ + try: + tr = list(filter(lambda m: m.message_id == before_message_id, self._corr))[0] + i = self._corr.index(tr) + if inline: # inline was loaded + self._corr.insert(i, inline) + return i - len(self._corr) + except: + pass + + def get_unsent_files(self): + messages = filter(lambda m: type(m) is UnsentFileMessage, self._corr) + return list(messages) + + def clear_unsent_files(self): + self._corr = list(filter(lambda m: type(m) is not UnsentFileMessage, self._corr)) + + def remove_invalid_unsent_files(self): + def is_valid(message): + if type(message) is not UnsentFileMessage: + return True + if message.data is not None: + return True + return os.path.exists(message.path) + + self._corr = list(filter(is_valid, self._corr)) + + def delete_one_unsent_file(self, message_id): + self._corr = list(filter(lambda m: not (type(m) is UnsentFileMessage and m.message_id == message_id), + self._corr)) + + # ----------------------------------------------------------------------------------------------------------------- + # Full status + # ----------------------------------------------------------------------------------------------------------------- + + def get_full_status(self): + return self._status_message + + # ----------------------------------------------------------------------------------------------------------------- + # Typing notifications + # ----------------------------------------------------------------------------------------------------------------- + + def get_typing_notification_handler(self): + return self._typing_notification_handler + + # ----------------------------------------------------------------------------------------------------------------- + # Context menu support + # ----------------------------------------------------------------------------------------------------------------- + + def get_context_menu_generator(self): + return FriendMenuGenerator(self) diff --git a/toxygen/contacts/friend_factory.py b/toxygen/contacts/friend_factory.py new file mode 100644 index 0000000..8ebafd6 --- /dev/null +++ b/toxygen/contacts/friend_factory.py @@ -0,0 +1,44 @@ +from contacts.friend import Friend +from common.tox_save import ToxSave + + +class FriendFactory(ToxSave): + + def __init__(self, profile_manager, settings, tox, db, items_factory): + super().__init__(tox) + self._profile_manager = profile_manager + self._settings = settings + self._db = db + self._items_factory = items_factory + + def create_friend_by_public_key(self, public_key): + friend_number = self._tox.friend_by_public_key(public_key) + + return self.create_friend_by_number(friend_number) + + def create_friend_by_number(self, friend_number): + aliases = self._settings['friends_aliases'] + tox_id = self._tox.friend_get_public_key(friend_number) + try: + alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] + except: + alias = '' + item = self._create_friend_item() + name = alias or self._tox.friend_get_name(friend_number) or tox_id + status_message = self._tox.friend_get_status_message(friend_number) + message_getter = self._db.messages_getter(tox_id) + friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, tox_id) + friend.set_alias(alias) + + return friend + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _create_friend_item(self): + """ + Method-factory + :return: new widget for friend instance + """ + return self._items_factory.create_contact_item() diff --git a/toxygen/contacts/group_chat.py b/toxygen/contacts/group_chat.py new file mode 100644 index 0000000..19ebc8e --- /dev/null +++ b/toxygen/contacts/group_chat.py @@ -0,0 +1,137 @@ +from contacts import contact +from contacts.contact_menu import GroupMenuGenerator +import utils.util as util +from groups.group_peer import GroupChatPeer +from wrapper import toxcore_enums_and_consts as constants +from common.tox_save import ToxSave +from groups.group_ban import GroupBan + + +class GroupChat(contact.Contact, ToxSave): + + def __init__(self, tox, profile_manager, message_getter, number, name, status_message, widget, tox_id, is_private): + super().__init__(profile_manager, message_getter, number, name, status_message, widget, tox_id) + ToxSave.__init__(self, tox) + + self._is_private = is_private + self._password = str() + self._peers_limit = 512 + self._peers = [] + self._add_self_to_gc() + + def remove_invalid_unsent_files(self): + pass + + def get_context_menu_generator(self): + return GroupMenuGenerator(self) + + # ----------------------------------------------------------------------------------------------------------------- + # Properties + # ----------------------------------------------------------------------------------------------------------------- + + def get_is_private(self): + return self._is_private + + def set_is_private(self, is_private): + self._is_private = is_private + + is_private = property(get_is_private, set_is_private) + + def get_password(self): + return self._password + + def set_password(self, password): + self._password = password + + password = property(get_password, set_password) + + def get_peers_limit(self): + return self._peers_limit + + def set_peers_limit(self, peers_limit): + self._peers_limit = peers_limit + + peers_limit = property(get_peers_limit, set_peers_limit) + + # ----------------------------------------------------------------------------------------------------------------- + # Peers methods + # ----------------------------------------------------------------------------------------------------------------- + + def get_self_peer(self): + return self._peers[0] + + def get_self_name(self): + return self._peers[0].name + + def get_self_role(self): + return self._peers[0].role + + def is_self_moderator_or_founder(self): + return self.get_self_role() <= constants.TOX_GROUP_ROLE['MODERATOR'] + + def is_self_founder(self): + return self.get_self_role() == constants.TOX_GROUP_ROLE['FOUNDER'] + + def add_peer(self, peer_id, is_current_user=False): + peer = GroupChatPeer(peer_id, + self._tox.group_peer_get_name(self._number, peer_id), + self._tox.group_peer_get_status(self._number, peer_id), + self._tox.group_peer_get_role(self._number, peer_id), + self._tox.group_peer_get_public_key(self._number, peer_id), + is_current_user) + self._peers.append(peer) + + def remove_peer(self, peer_id): + if peer_id == self.get_self_peer().id: # we were kicked or banned + self.remove_all_peers_except_self() + else: + peer = self.get_peer_by_id(peer_id) + self._peers.remove(peer) + + def get_peer_by_id(self, peer_id): + peers = list(filter(lambda p: p.id == peer_id, self._peers)) + + return peers[0] + + def get_peer_by_public_key(self, public_key): + peers = list(filter(lambda p: p.public_key == public_key, self._peers)) + + return peers[0] + + 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) + + def get_peers(self): + return self._peers[:] + + 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 + + bans = property(get_bans) + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + @staticmethod + def _get_default_avatar_path(): + return util.join_path(util.get_images_directory(), 'group.png') + + def _add_self_to_gc(self): + peer_id = self._tox.group_self_get_peer_id(self._number) + self.add_peer(peer_id, True) diff --git a/toxygen/contacts/group_factory.py b/toxygen/contacts/group_factory.py new file mode 100644 index 0000000..4083438 --- /dev/null +++ b/toxygen/contacts/group_factory.py @@ -0,0 +1,53 @@ +from contacts.group_chat import GroupChat +from common.tox_save import ToxSave +import wrapper.toxcore_enums_and_consts as constants + + +class GroupFactory(ToxSave): + + def __init__(self, profile_manager, settings, tox, db, items_factory): + super().__init__(tox) + self._profile_manager = profile_manager + self._settings = settings + self._db = db + self._items_factory = items_factory + + def create_group_by_public_key(self, public_key): + group_number = self._get_group_number_by_chat_id(public_key) + + return self.create_group_by_number(group_number) + + def create_group_by_number(self, group_number): + aliases = self._settings['friends_aliases'] + tox_id = self._tox.group_get_chat_id(group_number) + try: + alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] + except: + alias = '' + item = self._create_group_item() + name = alias or self._tox.group_get_name(group_number) or tox_id + status_message = self._tox.group_get_topic(group_number) + message_getter = self._db.messages_getter(tox_id) + is_private = self._tox.group_get_privacy_state(group_number) == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE'] + group = GroupChat(self._tox, self._profile_manager, message_getter, group_number, name, status_message, + item, tox_id, is_private) + group.set_alias(alias) + + return group + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _create_group_item(self): + """ + Method-factory + :return: new widget for group instance + """ + return self._items_factory.create_contact_item() + + def _get_group_number_by_chat_id(self, chat_id): + for i in range(self._tox.group_get_number_groups()): + if self._tox.group_get_chat_id(i) == chat_id: + return i + return -1 diff --git a/toxygen/contacts/group_peer_contact.py b/toxygen/contacts/group_peer_contact.py new file mode 100644 index 0000000..8854198 --- /dev/null +++ b/toxygen/contacts/group_peer_contact.py @@ -0,0 +1,20 @@ +import contacts.contact +from contacts.contact_menu import GroupPeerMenuGenerator + + +class GroupPeerContact(contacts.contact.Contact): + + def __init__(self, profile_manager, message_getter, peer_number, name, widget, tox_id, group_pk): + super().__init__(profile_manager, message_getter, peer_number, name, str(), widget, tox_id) + self._group_pk = group_pk + + def get_group_pk(self): + return self._group_pk + + group_pk = property(get_group_pk) + + def remove_invalid_unsent_files(self): + pass + + def get_context_menu_generator(self): + return GroupPeerMenuGenerator(self) diff --git a/toxygen/contacts/group_peer_factory.py b/toxygen/contacts/group_peer_factory.py new file mode 100644 index 0000000..38b3a20 --- /dev/null +++ b/toxygen/contacts/group_peer_factory.py @@ -0,0 +1,23 @@ +from common.tox_save import ToxSave +from contacts.group_peer_contact import GroupPeerContact + + +class GroupPeerFactory(ToxSave): + + def __init__(self, tox, profile_manager, db, items_factory): + super().__init__(tox) + self._profile_manager = profile_manager + self._db = db + self._items_factory = items_factory + + def create_group_peer(self, group, peer): + item = self._create_group_peer_item() + message_getter = self._db.messages_getter(peer.public_key) + group_peer_contact = GroupPeerContact(self._profile_manager, message_getter, peer.id, peer.name, + item, peer.public_key, group.tox_id) + group_peer_contact.status = peer.status + + return group_peer_contact + + def _create_group_peer_item(self): + return self._items_factory.create_contact_item() diff --git a/toxygen/contacts/profile.py b/toxygen/contacts/profile.py new file mode 100644 index 0000000..81220af --- /dev/null +++ b/toxygen/contacts/profile.py @@ -0,0 +1,87 @@ +from contacts import basecontact +import random +import threading +import common.tox_save as tox_save +from middleware.threads import invoke_in_main_thread + + +class Profile(basecontact.BaseContact, tox_save.ToxSave): + """ + Profile of current toxygen user. + """ + def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action): + """ + :param tox: tox instance + :param screen: ref to main screen + """ + basecontact.BaseContact.__init__(self, + profile_manager, + tox.self_get_name(), + tox.self_get_status_message(), + screen, + tox.self_get_address()) + tox_save.ToxSave.__init__(self, tox) + self._screen = screen + self._messages = screen.messages + self._contacts_provider = contacts_provider + self._reset_action = reset_action + self._waiting_for_reconnection = False + self._timer = None + + # ----------------------------------------------------------------------------------------------------------------- + # Edit current user's data + # ----------------------------------------------------------------------------------------------------------------- + + def change_status(self): + """ + Changes status of user (online, away, busy) + """ + if self._status is not None: + self.set_status((self._status + 1) % 3) + + def set_status(self, status): + super().set_status(status) + if status is not None: + self._tox.self_set_status(status) + elif not self._waiting_for_reconnection: + self._waiting_for_reconnection = True + self._timer = threading.Timer(50, self._reconnect) + self._timer.start() + + def set_name(self, value): + if self.name == value: + return + super().set_name(value) + self._tox.self_set_name(self._name) + + def set_status_message(self, value): + super().set_status_message(value) + self._tox.self_set_status_message(self._status_message) + + def set_new_nospam(self): + """Sets new nospam part of tox id""" + self._tox.self_set_nospam(random.randint(0, 4294967295)) # no spam - uint32 + self._tox_id = self._tox.self_get_address() + + return self._tox_id + + # ----------------------------------------------------------------------------------------------------------------- + # Reset + # ----------------------------------------------------------------------------------------------------------------- + + def restart(self): + """ + Recreate tox instance + """ + self.status = None + invoke_in_main_thread(self._reset_action) + + def _reconnect(self): + self._waiting_for_reconnection = False + contacts = self._contacts_provider.get_all_friends() + all_friends_offline = all(list(map(lambda x: x.status is None, contacts))) + if self.status is None or (all_friends_offline and len(contacts)): + self._waiting_for_reconnection = True + self.restart() + self._timer = threading.Timer(50, self._reconnect) + self._timer.start() diff --git a/toxygen/file_transfers/__init__.py b/toxygen/file_transfers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/file_transfers.py b/toxygen/file_transfers/file_transfers.py similarity index 68% rename from toxygen/file_transfers.py rename to toxygen/file_transfers/file_transfers.py index 7e0b193..0f04e5b 100644 --- a/toxygen/file_transfers.py +++ b/toxygen/file_transfers/file_transfers.py @@ -1,20 +1,21 @@ -from toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL +from wrapper.toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL from os.path import basename, getsize, exists, dirname from os import remove, rename, chdir -from time import time, sleep -from tox import Tox -import settings -from PyQt5 import QtCore +from time import time +from wrapper.tox import Tox +from common.event import Event +from middleware.threads import invoke_in_main_thread -TOX_FILE_TRANSFER_STATE = { +FILE_TRANSFER_STATE = { 'RUNNING': 0, 'PAUSED_BY_USER': 1, 'CANCELLED': 2, 'FINISHED': 3, 'PAUSED_BY_FRIEND': 4, 'INCOMING_NOT_STARTED': 5, - 'OUTGOING_NOT_STARTED': 6 + 'OUTGOING_NOT_STARTED': 6, + 'UNSENT': 7 } ACTIVE_FILE_TRANSFERS = (0, 1, 4, 5, 6) @@ -25,102 +26,106 @@ DO_NOT_SHOW_ACCEPT_BUTTON = (2, 3, 4, 6) SHOW_PROGRESS_BAR = (0, 1, 4) -ALLOWED_FILES = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png') - def is_inline(file_name): - return file_name in ALLOWED_FILES or file_name.startswith('qTox_Screenshot_') + allowed_inlines = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png') + + return file_name in allowed_inlines or file_name.startswith('qTox_Image_') -class StateSignal(QtCore.QObject): - - signal = QtCore.pyqtSignal(int, float, int) # state, progress, time in sec - - -class TransferFinishedSignal(QtCore.QObject): - - signal = QtCore.pyqtSignal(int, int) # friend number, file number - - -class FileTransfer(QtCore.QObject): +class FileTransfer: """ Superclass for file transfers """ def __init__(self, path, tox, friend_number, size, file_number=None): - QtCore.QObject.__init__(self) self._path = path self._tox = tox self._friend_number = friend_number - self.state = TOX_FILE_TRANSFER_STATE['RUNNING'] + self._state = FILE_TRANSFER_STATE['RUNNING'] self._file_number = file_number self._creation_time = None self._size = float(size) self._done = 0 - self._state_changed = StateSignal() - self._finished = TransferFinishedSignal() - self._file_id = None - - def set_tox(self, tox): - self._tox = tox + self._state_changed_event = Event() + self._finished_event = Event() + self._file_id = self._file = None def set_state_changed_handler(self, handler): - self._state_changed.signal.connect(handler) + self._state_changed_event += lambda *args: invoke_in_main_thread(handler, *args) def set_transfer_finished_handler(self, handler): - self._finished.signal.connect(handler) - - def signal(self): - percentage = self._done / self._size if self._size else 0 - if self._creation_time is None or not percentage: - t = -1 - else: - t = ((time() - self._creation_time) / percentage) * (1 - percentage) - self._state_changed.signal.emit(self.state, percentage, int(t)) - - def finished(self): - self._finished.signal.emit(self._friend_number, self._file_number) + self._finished_event += lambda *args: invoke_in_main_thread(handler, *args) def get_file_number(self): return self._file_number + file_number = property(get_file_number) + + def get_state(self): + return self._state + + def set_state(self, value): + self._state = value + self._signal() + + state = property(get_state, set_state) + def get_friend_number(self): return self._friend_number - def get_id(self): + friend_number = property(get_friend_number) + + def get_file_id(self): return self._file_id + file_id = property(get_file_id) + def get_path(self): return self._path + path = property(get_path) + + def get_size(self): + return self._size + + size = property(get_size) + def cancel(self): self.send_control(TOX_FILE_CONTROL['CANCEL']) - if hasattr(self, '_file'): + if self._file is not None: self._file.close() - self.signal() + self._signal() def cancelled(self): - if hasattr(self, '_file'): - sleep(0.1) + if self._file is not None: self._file.close() - self.state = TOX_FILE_TRANSFER_STATE['CANCELLED'] - self.signal() + self.set_state(FILE_TRANSFER_STATE['CANCELLED']) def pause(self, by_friend): if not by_friend: self.send_control(TOX_FILE_CONTROL['PAUSE']) else: - self.state = TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'] - self.signal() + self.set_state(FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']) def send_control(self, control): if self._tox.file_control(self._friend_number, self._file_number, control): - self.state = control - self.signal() + self.set_state(control) def get_file_id(self): return self._tox.file_get_file_id(self._friend_number, self._file_number) + def _signal(self): + percentage = self._done / self._size if self._size else 0 + if self._creation_time is None or not percentage: + t = -1 + else: + t = ((time() - self._creation_time) / percentage) * (1 - percentage) + self._state_changed_event(self.state, percentage, int(t)) + + def _finished(self): + self._finished_event(self._friend_number, self._file_number) + # ----------------------------------------------------------------------------------------------------------------- # Send file # ----------------------------------------------------------------------------------------------------------------- @@ -130,12 +135,14 @@ class SendTransfer(FileTransfer): def __init__(self, path, tox, friend_number, kind=TOX_FILE_KIND['DATA'], file_id=None): if path is not None: - self._file = open(path, 'rb') + fl = open(path, 'rb') size = getsize(path) else: + fl = None size = 0 - super(SendTransfer, self).__init__(path, tox, friend_number, size) - self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + super().__init__(path, tox, friend_number, size) + self._file = fl + self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] self._file_number = tox.file_send(friend_number, kind, size, file_id, bytes(basename(path), 'utf-8') if path else b'') self._file_id = self.get_file_id() @@ -153,12 +160,12 @@ class SendTransfer(FileTransfer): data = self._file.read(size) self._tox.file_send_chunk(self._friend_number, self._file_number, position, data) self._done += size + self._signal() else: - if hasattr(self, '_file'): + if self._file is not None: self._file.close() - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] - self.finished() - self.signal() + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() class SendAvatar(SendTransfer): @@ -168,11 +175,11 @@ class SendAvatar(SendTransfer): def __init__(self, path, tox, friend_number): if path is None: - hash = None + avatar_hash = None else: with open(path, 'rb') as fl: - hash = Tox.hash(fl.read()) - super(SendAvatar, self).__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], hash) + avatar_hash = Tox.hash(fl.read()) + super().__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], avatar_hash) class SendFromBuffer(FileTransfer): @@ -181,8 +188,8 @@ class SendFromBuffer(FileTransfer): """ def __init__(self, tox, friend_number, data, file_name): - super(SendFromBuffer, self).__init__(None, tox, friend_number, len(data)) - self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + super().__init__(None, tox, friend_number, len(data)) + self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] self._data = data self._file_number = tox.file_send(friend_number, TOX_FILE_KIND['DATA'], len(data), None, bytes(file_name, 'utf-8')) @@ -190,6 +197,8 @@ class SendFromBuffer(FileTransfer): def get_data(self): return self._data + data = property(get_data) + def send_chunk(self, position, size): if self._creation_time is None: self._creation_time = time() @@ -198,18 +207,18 @@ class SendFromBuffer(FileTransfer): self._tox.file_send_chunk(self._friend_number, self._file_number, position, data) self._done += size else: - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] - self.finished() - self.signal() + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() + self._signal() class SendFromFileBuffer(SendTransfer): def __init__(self, *args): - super(SendFromFileBuffer, self).__init__(*args) + super().__init__(*args) def send_chunk(self, position, size): - super(SendFromFileBuffer, self).send_chunk(position, size) + super().send_chunk(position, size) if not size: chdir(dirname(self._path)) remove(self._path) @@ -222,7 +231,7 @@ class SendFromFileBuffer(SendTransfer): class ReceiveTransfer(FileTransfer): def __init__(self, path, tox, friend_number, size, file_number, position=0): - super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number) + super().__init__(path, tox, friend_number, size, file_number) self._file = open(self._path, 'wb') self._file_size = position self._file.truncate(position) @@ -231,11 +240,12 @@ class ReceiveTransfer(FileTransfer): self._done = position def cancel(self): - super(ReceiveTransfer, self).cancel() + super().cancel() remove(self._path) def total_size(self): self._missed.add(self._file_size) + return min(self._missed) def write_chunk(self, position, data): @@ -248,8 +258,8 @@ class ReceiveTransfer(FileTransfer): self._creation_time = time() if data is None: self._file.close() - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] - self.finished() + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() else: data = bytearray(data) if self._file_size < position: @@ -264,7 +274,7 @@ class ReceiveTransfer(FileTransfer): if position + l > self._file_size: self._file_size = position + l self._done += l - self.signal() + self._signal() class ReceiveToBuffer(FileTransfer): @@ -273,19 +283,21 @@ class ReceiveToBuffer(FileTransfer): """ def __init__(self, tox, friend_number, size, file_number): - super(ReceiveToBuffer, self).__init__(None, tox, friend_number, size, file_number) + super().__init__(None, tox, friend_number, size, file_number) self._data = bytes() self._data_size = 0 def get_data(self): return self._data + data = property(get_data) + def write_chunk(self, position, data): if self._creation_time is None: self._creation_time = time() if data is None: - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] - self.finished() + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() else: data = bytes(data) l = len(data) @@ -295,7 +307,7 @@ class ReceiveToBuffer(FileTransfer): if position + l > self._data_size: self._data_size = position + l self._done += l - self.signal() + self._signal() class ReceiveAvatar(ReceiveTransfer): @@ -304,20 +316,17 @@ class ReceiveAvatar(ReceiveTransfer): """ MAX_AVATAR_SIZE = 512 * 1024 - def __init__(self, tox, friend_number, size, file_number): - path = settings.ProfileHelper.get_path() + 'avatars/{}.png'.format(tox.friend_get_public_key(friend_number)) - super(ReceiveAvatar, self).__init__(path + '.tmp', tox, friend_number, size, file_number) + def __init__(self, path, tox, friend_number, size, file_number): + full_path = path + '.tmp' + super().__init__(full_path, tox, friend_number, size, file_number) if size > self.MAX_AVATAR_SIZE: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - remove(path + '.tmp') + remove(full_path) elif not size: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - if exists(path): - remove(path) - self._file.close() - remove(path + '.tmp') + remove(full_path) elif exists(path): hash = self.get_file_id() with open(path, 'rb') as fl: @@ -326,22 +335,17 @@ class ReceiveAvatar(ReceiveTransfer): if hash == existing_hash: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - remove(path + '.tmp') + remove(full_path) else: self.send_control(TOX_FILE_CONTROL['RESUME']) else: self.send_control(TOX_FILE_CONTROL['RESUME']) def write_chunk(self, position, data): - super(ReceiveAvatar, self).write_chunk(position, data) - if self.state: + if data is None: avatar_path = self._path[:-4] if exists(avatar_path): chdir(dirname(avatar_path)) remove(avatar_path) rename(self._path, avatar_path) - self.finished(True) - - def finished(self, emit=False): - if emit: - super().finished() + super().write_chunk(position, data) diff --git a/toxygen/file_transfers/file_transfers_handler.py b/toxygen/file_transfers/file_transfers_handler.py new file mode 100644 index 0000000..114383b --- /dev/null +++ b/toxygen/file_transfers/file_transfers_handler.py @@ -0,0 +1,304 @@ +from messenger.messages import * +from ui.contact_items import * +import utils.util as util +from common.tox_save import ToxSave + + +class FileTransfersHandler(ToxSave): + + def __init__(self, tox, settings, contact_provider, file_transfers_message_service, profile): + super().__init__(tox) + self._settings = settings + self._contact_provider = contact_provider + self._file_transfers_message_service = file_transfers_message_service + self._file_transfers = {} + # key = (friend number, file number), value - transfer instance + self._paused_file_transfers = dict(settings['paused_file_transfers']) + # key - file id, value: [path, friend number, is incoming, start position] + self._insert_inline_before = {} + # key = (friend number, file number), value - message id + + profile.avatar_changed_event.add_callback(self._send_avatar_to_contacts) + + def stop(self): + self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {} + self._settings.save() + + # ----------------------------------------------------------------------------------------------------------------- + # File transfers support + # ----------------------------------------------------------------------------------------------------------------- + + def incoming_file_transfer(self, friend_number, file_number, size, file_name): + """ + New transfer + :param friend_number: number of friend who sent file + :param file_number: file number + :param size: file size in bytes + :param file_name: file name without path + """ + friend = self._get_friend_by_number(friend_number) + 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) + accepted = True + if file_id in self._paused_file_transfers: + (path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[file_id] + pos = start_position if os.path.exists(path) else 0 + if pos >= size: + self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) + return + self._tox.file_seek(friend_number, file_number, pos) + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + self.accept_transfer(path, friend_number, file_number, size, False, pos) + elif inline and size < 1024 * 1024: + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + self.accept_transfer('', friend_number, file_number, size, True) + elif auto: + path = self._settings['auto_accept_path'] or util.curr_directory() + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + self.accept_transfer(path + '/' + file_name, friend_number, file_number, size) + else: + accepted = False + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + + def cancel_transfer(self, friend_number, file_number, already_cancelled=False): + """ + Stop transfer + :param friend_number: number of friend + :param file_number: file number + :param already_cancelled: was cancelled by friend + """ + if (friend_number, file_number) in self._file_transfers: + tr = self._file_transfers[(friend_number, file_number)] + if not already_cancelled: + tr.cancel() + else: + tr.cancelled() + if (friend_number, file_number) in self._file_transfers: + del tr + del self._file_transfers[(friend_number, file_number)] + elif not already_cancelled: + 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) + + def pause_transfer(self, friend_number, file_number, by_friend=False): + """ + Pause transfer with specified data + """ + tr = self._file_transfers[(friend_number, file_number)] + tr.pause(by_friend) + + def resume_transfer(self, friend_number, file_number, by_friend=False): + """ + Resume transfer with specified data + """ + tr = self._file_transfers[(friend_number, file_number)] + if by_friend: + tr.state = FILE_TRANSFER_STATE['RUNNING'] + else: + tr.send_control(TOX_FILE_CONTROL['RESUME']) + + def accept_transfer(self, path, friend_number, file_number, size, inline=False, from_position=0): + """ + :param path: path for saving + :param friend_number: friend number + :param file_number: file number + :param size: file size + :param inline: is inline image + :param from_position: position for start + """ + path = self._generate_valid_path(path, from_position) + friend = self._get_friend_by_number(friend_number) + if not inline: + rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number, from_position) + else: + rt = ReceiveToBuffer(self._tox, friend_number, size, file_number) + rt.set_transfer_finished_handler(self.transfer_finished) + message = friend.get_message(lambda m: m.type == MESSAGE_TYPE['FILE_TRANSFER'] + and m.state in (FILE_TRANSFER_STATE['INCOMING_NOT_STARTED'], + FILE_TRANSFER_STATE['RUNNING']) + and m.file_number == file_number) + rt.set_state_changed_handler(message.transfer_updated) + self._file_transfers[(friend_number, file_number)] = rt + rt.send_control(TOX_FILE_CONTROL['RESUME']) + if inline: + self._insert_inline_before[(friend_number, file_number)] = message.message_id + + def send_screenshot(self, data, friend_number): + """ + Send screenshot + :param data: raw data - png format + :param friend_number: friend number + """ + self.send_inline(data, 'toxygen_inline.png', friend_number) + + def send_sticker(self, path, friend_number): + with open(path, 'rb') as fl: + data = fl.read() + self.send_inline(data, 'sticker.png', friend_number) + + def send_inline(self, data, file_name, friend_number, is_resend=False): + friend = self._get_friend_by_number(friend_number) + if friend.status is None and not is_resend: + self._file_transfers_message_service.add_unsent_file_message(friend, file_name, data) + return + elif friend.status is None and is_resend: + raise RuntimeError() + st = SendFromBuffer(self._tox, friend.number, data, file_name) + self._send_file_add_set_handlers(st, friend, file_name, True) + + def send_file(self, path, friend_number, is_resend=False, file_id=None): + """ + Send file to current active friend + :param path: file path + :param friend_number: friend_number + :param is_resend: is 'offline' message + :param file_id: file id of transfer + """ + friend = self._get_friend_by_number(friend_number) + 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') + return + st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id) + file_name = os.path.basename(path) + self._send_file_add_set_handlers(st, friend, file_name) + + def incoming_chunk(self, friend_number, file_number, position, data): + """ + Incoming chunk + """ + self._file_transfers[(friend_number, file_number)].write_chunk(position, data) + + def outgoing_chunk(self, friend_number, file_number, position, size): + """ + Outgoing chunk + """ + self._file_transfers[(friend_number, file_number)].send_chunk(position, size) + + def transfer_finished(self, friend_number, file_number): + transfer = self._file_transfers[(friend_number, file_number)] + t = type(transfer) + if t is ReceiveAvatar: + self._get_friend_by_number(friend_number).load_avatar() + elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings['allow_inline']): # inline image + print('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) + 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: + for fl in files: + data, path = fl.data, fl.path + if data is not None: + self.send_inline(data, path, friend_number, True) + else: + self.send_file(path, friend_number, True) + friend.clear_unsent_files() + for key in self._paused_file_transfers.keys(): + (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] + elif ft_friend_number == friend_number and not is_incoming: + 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)) + + def friend_exit(self, friend_number): + for friend_num, file_num in self._file_transfers.keys(): + if friend_num != friend_number: + 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] + elif type(ft) is ReceiveTransfer and ft.state != FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: + self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, True, ft.total_size()] + self.cancel_transfer(friend_num, file_num, True) + + # ----------------------------------------------------------------------------------------------------------------- + # Avatars support + # ----------------------------------------------------------------------------------------------------------------- + + def send_avatar(self, friend_number, avatar_path=None): + """ + :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 + + def incoming_avatar(self, friend_number, file_number, size): + """ + Friend changed avatar + :param friend_number: friend number + :param file_number: file number + :param size: size of avatar or 0 (default avatar) + """ + friend = self._get_friend_by_number(friend_number) + 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 + ra.set_transfer_finished_handler(self.transfer_finished) + elif not size: + friend.reset_avatar(self._settings['identicons']) + + def _send_avatar_to_contacts(self, _): + friends = self._get_all_friends() + for friend in filter(self._is_friend_online, friends): + self.send_avatar(friend.number) + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _is_friend_online(self, friend_number): + friend = self._get_friend_by_number(friend_number) + + return friend.status is not None + + def _get_friend_by_number(self, friend_number): + return self._contact_provider.get_friend_by_number(friend_number) + + def _get_all_friends(self): + return self._contact_provider.get_all_friends() + + def _send_file_add_set_handlers(self, st, friend, file_name, inline=False): + st.set_transfer_finished_handler(self.transfer_finished) + file_number = st.get_file_number() + self._file_transfers[(friend.number, file_number)] = st + tm = self._file_transfers_message_service.add_outgoing_transfer_message(friend, st.size, file_name, file_number) + st.set_state_changed_handler(tm.transfer_updated) + if inline: + self._insert_inline_before[(friend.number, file_number)] = tm.message_id + + @staticmethod + def _generate_valid_path(path, from_position): + path, file_name = os.path.split(path) + new_file_name, i = file_name, 1 + if not from_position: + while os.path.isfile(join_path(path, new_file_name)): # file with same name already exists + if '.' in file_name: # has extension + d = file_name.rindex('.') + else: # no extension + d = len(file_name) + new_file_name = file_name[:d] + ' ({})'.format(i) + file_name[d:] + i += 1 + path = join_path(path, new_file_name) + + return path diff --git a/toxygen/file_transfers/file_transfers_messages_service.py b/toxygen/file_transfers/file_transfers_messages_service.py new file mode 100644 index 0000000..4509183 --- /dev/null +++ b/toxygen/file_transfers/file_transfers_messages_service.py @@ -0,0 +1,78 @@ +from messenger.messenger import * +import utils.util as util +from file_transfers.file_transfers import * + + +class FileTransfersMessagesService: + + def __init__(self, contacts_manager, messages_items_factory, profile, main_screen): + self._contacts_manager = contacts_manager + self._messages_items_factory = messages_items_factory + self._profile = profile + self._messages = main_screen.messages + + def add_incoming_transfer_message(self, friend, accepted, size, file_name, file_number): + author = MessageAuthor(friend.name, MESSAGE_AUTHOR['FRIEND']) + status = FILE_TRANSFER_STATE['RUNNING'] if accepted else FILE_TRANSFER_STATE['INCOMING_NOT_STARTED'] + tm = TransferMessage(author, util.get_unix_time(), status, size, file_name, friend.number, file_number) + + if self._is_friend_active(friend.number): + self._create_file_transfer_item(tm) + self._messages.scrollToBottom() + else: + friend.actions = True + + friend.append_message(tm) + + return tm + + def add_outgoing_transfer_message(self, friend, size, file_name, file_number): + author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['ME']) + status = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + tm = TransferMessage(author, util.get_unix_time(), status, size, file_name, friend.number, file_number) + + if self._is_friend_active(friend.number): + self._create_file_transfer_item(tm) + self._messages.scrollToBottom() + + friend.append_message(tm) + + return tm + + def add_inline_message(self, transfer, index): + if not self._is_friend_active(transfer.friend_number): + return + count = self._messages.count() + if count + index + 1 >= 0: + self._create_inline_item(transfer.data, count + index + 1) + + def add_unsent_file_message(self, friend, file_path, data): + author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['ME']) + size = os.path.getsize(file_path) if data is None else len(data) + tm = UnsentFileMessage(file_path, data, util.get_unix_time(), author, size, friend.number) + friend.append_message(tm) + + if self._is_friend_active(friend.number): + self._create_unsent_file_item(tm) + self._messages.scrollToBottom() + + return tm + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _is_friend_active(self, friend_number): + if not self._contacts_manager.is_active_a_friend(): + return False + + return friend_number == self._contacts_manager.get_active_number() + + def _create_file_transfer_item(self, tm): + return self._messages_items_factory.create_file_transfer_item(tm) + + def _create_inline_item(self, data, position): + return self._messages_items_factory.create_inline_item(data, False, position) + + def _create_unsent_file_item(self, tm): + return self._messages_items_factory.create_unsent_file_item(tm) diff --git a/toxygen/friend.py b/toxygen/friend.py deleted file mode 100644 index d912708..0000000 --- a/toxygen/friend.py +++ /dev/null @@ -1,75 +0,0 @@ -import contact -from messages import * -import os - - -class Friend(contact.Contact): - """ - Friend in list of friends. - """ - - def __init__(self, message_getter, number, name, status_message, widget, tox_id): - super().__init__(message_getter, number, name, status_message, widget, tox_id) - self._receipts = 0 - - # ----------------------------------------------------------------------------------------------------------------- - # File transfers support - # ----------------------------------------------------------------------------------------------------------------- - - def update_transfer_data(self, file_number, status, inline=None): - """ - Update status of active transfer and load inline if needed - """ - try: - tr = list(filter(lambda x: x.get_type() == MESSAGE_TYPE['FILE_TRANSFER'] and x.is_active(file_number), - self._corr))[0] - tr.set_status(status) - i = self._corr.index(tr) - if inline: # inline was loaded - self._corr.insert(i, inline) - return i - len(self._corr) - except: - pass - - def get_unsent_files(self): - messages = filter(lambda x: type(x) is UnsentFile, self._corr) - return messages - - def clear_unsent_files(self): - self._corr = list(filter(lambda x: type(x) is not UnsentFile, self._corr)) - - def remove_invalid_unsent_files(self): - def is_valid(message): - if type(message) is not UnsentFile: - return True - if message.get_data()[1] is not None: - return True - return os.path.exists(message.get_data()[0]) - self._corr = list(filter(is_valid, self._corr)) - - def delete_one_unsent_file(self, time): - self._corr = list(filter(lambda x: not (type(x) is UnsentFile and x.get_data()[2] == time), self._corr)) - - # ----------------------------------------------------------------------------------------------------------------- - # History support - # ----------------------------------------------------------------------------------------------------------------- - - def get_receipts(self): - return self._receipts - - receipts = property(get_receipts) # read receipts - - def inc_receipts(self): - self._receipts += 1 - - def dec_receipt(self): - if self._receipts: - self._receipts -= 1 - self.mark_as_sent() - - # ----------------------------------------------------------------------------------------------------------------- - # Full status - # ----------------------------------------------------------------------------------------------------------------- - - def get_full_status(self): - return self._status_message diff --git a/toxygen/group_chat.py b/toxygen/group_chat.py deleted file mode 100644 index f7921a1..0000000 --- a/toxygen/group_chat.py +++ /dev/null @@ -1,49 +0,0 @@ -import contact -import util -from PyQt5 import QtGui, QtCore -import toxcore_enums_and_consts as constants - - -class GroupChat(contact.Contact): - - def __init__(self, name, status_message, widget, tox, group_number): - super().__init__(None, group_number, name, status_message, widget, None) - self._tox = tox - self.set_status(constants.TOX_USER_STATUS['NONE']) - - def set_name(self, name): - self._tox.group_set_title(self._number, name) - super().set_name(name) - - def send_message(self, message): - self._tox.group_message_send(self._number, message.encode('utf-8')) - - def new_title(self, title): - super().set_name(title) - - def load_avatar(self): - path = util.curr_directory() + '/images/group.png' - width = self._widget.avatar_label.width() - pixmap = QtGui.QPixmap(path) - self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation)) - self._widget.avatar_label.repaint() - - def remove_invalid_unsent_files(self): - pass - - def get_names(self): - peers_count = self._tox.group_number_peers(self._number) - names = [] - for i in range(peers_count): - name = self._tox.group_peername(self._number, i) - names.append(name) - names = sorted(names, key=lambda n: n.lower()) - return names - - def get_full_status(self): - names = self.get_names() - return '\n'.join(names) - - def get_peer_name(self, peer_number): - return self._tox.group_peername(self._number, peer_number) diff --git a/toxygen/groups/__init__.py b/toxygen/groups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/groups/group_ban.py b/toxygen/groups/group_ban.py new file mode 100644 index 0000000..89ecc7e --- /dev/null +++ b/toxygen/groups/group_ban.py @@ -0,0 +1,23 @@ + + +class GroupBan: + + def __init__(self, ban_id, ban_target, ban_time): + self._ban_id = ban_id + self._ban_target = ban_target + self._ban_time = ban_time + + def get_ban_id(self): + return self._ban_id + + ban_id = property(get_ban_id) + + def get_ban_target(self): + return self._ban_target + + ban_target = property(get_ban_target) + + def get_ban_time(self): + return self._ban_time + + ban_time = property(get_ban_time) diff --git a/toxygen/groups/group_invite.py b/toxygen/groups/group_invite.py new file mode 100644 index 0000000..a2eed47 --- /dev/null +++ b/toxygen/groups/group_invite.py @@ -0,0 +1,23 @@ + + +class GroupInvite: + + def __init__(self, friend_public_key, chat_name, invite_data): + self._friend_public_key = friend_public_key + self._chat_name = chat_name + self._invite_data = invite_data[:] + + def get_friend_public_key(self): + return self._friend_public_key + + friend_public_key = property(get_friend_public_key) + + def get_chat_name(self): + return self._chat_name + + chat_name = property(get_chat_name) + + def get_invite_data(self): + return self._invite_data[:] + + invite_data = property(get_invite_data) diff --git a/toxygen/groups/group_peer.py b/toxygen/groups/group_peer.py new file mode 100644 index 0000000..4eaf255 --- /dev/null +++ b/toxygen/groups/group_peer.py @@ -0,0 +1,70 @@ + + +class GroupChatPeer: + """ + Represents peer in group chat. + """ + + def __init__(self, peer_id, name, status, role, public_key, is_current_user=False, is_muted=False): + self._peer_id = peer_id + self._name = name + self._status = status + self._role = role + self._public_key = public_key + self._is_current_user = is_current_user + self._is_muted = is_muted + + # ----------------------------------------------------------------------------------------------------------------- + # Readonly properties + # ----------------------------------------------------------------------------------------------------------------- + + def get_id(self): + return self._peer_id + + id = property(get_id) + + def get_public_key(self): + return self._public_key + + public_key = property(get_public_key) + + def get_is_current_user(self): + return self._is_current_user + + is_current_user = property(get_is_current_user) + + # ----------------------------------------------------------------------------------------------------------------- + # Read-write properties + # ----------------------------------------------------------------------------------------------------------------- + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + name = property(get_name, set_name) + + def get_status(self): + return self._status + + def set_status(self, status): + self._status = status + + status = property(get_status, set_status) + + def get_role(self): + return self._role + + def set_role(self, role): + self._role = role + + role = property(get_role, set_role) + + def get_is_muted(self): + return self._is_muted + + def set_is_muted(self, is_muted): + self._is_muted = is_muted + + is_muted = property(get_is_muted, set_is_muted) diff --git a/toxygen/groups/groups_service.py b/toxygen/groups/groups_service.py new file mode 100644 index 0000000..b8fc7cc --- /dev/null +++ b/toxygen/groups/groups_service.py @@ -0,0 +1,242 @@ +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 + + +class GroupsService(tox_save.ToxSave): + + def __init__(self, tox, contacts_manager, contacts_provider, main_screen, widgets_factory_provider): + super().__init__(tox) + self._contacts_manager = contacts_manager + self._contacts_provider = contacts_provider + self._main_screen = main_screen + self._peers_list_widget = main_screen.peers_list + self._widgets_factory_provider = widgets_factory_provider + self._group_invites = [] + self._screen = None + + def set_tox(self, tox): + super().set_tox(tox) + for group in self._get_all_groups(): + group.set_tox(tox) + + # ----------------------------------------------------------------------------------------------------------------- + # Groups creation + # ----------------------------------------------------------------------------------------------------------------- + + def create_new_gc(self, name, privacy_state, nick, status): + group_number = self._tox.group_new(privacy_state, name, nick, status) + if group_number == -1: + return + + self._add_new_group_by_number(group_number) + group = self._get_group_by_number(group_number) + group.status = constants.TOX_USER_STATUS['NONE'] + self._contacts_manager.update_filtration() + + def join_gc_by_id(self, chat_id, password, nick, status): + group_number = self._tox.group_join(chat_id, password, nick, status) + self._add_new_group_by_number(group_number) + + # ----------------------------------------------------------------------------------------------------------------- + # Groups reconnect and leaving + # ----------------------------------------------------------------------------------------------------------------- + + def leave_group(self, group_number): + self._tox.group_leave(group_number) + self._contacts_manager.delete_group(group_number) + + def disconnect_from_group(self, group_number): + self._tox.group_disconnect(group_number) + group = self._get_group_by_number(group_number) + group.status = None + self._clear_peers_list(group) + + def reconnect_to_group(self, group_number): + self._tox.group_reconnect(group_number) + group = self._get_group_by_number(group_number) + group.status = constants.TOX_USER_STATUS['NONE'] + self._clear_peers_list(group) + + # ----------------------------------------------------------------------------------------------------------------- + # Group invites + # ----------------------------------------------------------------------------------------------------------------- + + def invite_friend(self, friend_number, group_number): + self._tox.group_invite_friend(group_number, friend_number) + + def process_group_invite(self, friend_number, group_name, invite_data): + friend = self._get_friend_by_number(friend_number) + invite = GroupInvite(friend.tox_id, group_name, invite_data) + self._group_invites.append(invite) + self._update_invites_button_state() + + def accept_group_invite(self, invite, name, status, password): + pk = invite.friend_public_key + friend = self._get_friend_by_public_key(pk) + self._join_gc_via_invite(invite.invite_data, friend.number, name, status, password) + self._delete_group_invite(invite) + self._update_invites_button_state() + + def decline_group_invite(self, invite): + self._delete_group_invite(invite) + self._main_screen.update_gc_invites_button_state() + + def get_group_invites(self): + return self._group_invites[:] + + group_invites = property(get_group_invites) + + def get_group_invites_count(self): + return len(self._group_invites) + + group_invites_count = property(get_group_invites_count) + + # ----------------------------------------------------------------------------------------------------------------- + # Group info methods + # ----------------------------------------------------------------------------------------------------------------- + + def update_group_info(self, group): + group.name = self._tox.group_get_name(group.number) + group.status_message = self._tox.group_get_topic(group.number) + + def set_group_topic(self, group): + if not group.is_self_moderator_or_founder(): + return + text = util_ui.tr('New topic for group "{}":'.format(group.name)) + title = util_ui.tr('Set group topic') + topic, ok = util_ui.text_dialog(text, title, group.status_message) + if not ok or not topic: + return + self._tox.group_set_topic(group.number, topic) + group.status_message = topic + + def show_group_management_screen(self, group): + widgets_factory = self._get_widgets_factory() + self._screen = widgets_factory.create_group_management_screen(group) + self._screen.show() + + def show_group_settings_screen(self, group): + widgets_factory = self._get_widgets_factory() + self._screen = widgets_factory.create_group_settings_screen(group) + self._screen.show() + + def set_group_password(self, group, password): + if group.password == password: + return + self._tox.group_founder_set_password(group.number, password) + group.password = password + + def set_group_peers_limit(self, group, peers_limit): + if group.peers_limit == peers_limit: + return + self._tox.group_founder_set_peer_limit(group.number, peers_limit) + group.peers_limit = peers_limit + + def set_group_privacy_state(self, group, privacy_state): + is_private = privacy_state == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE'] + if group.is_private == is_private: + return + self._tox.group_founder_set_privacy_state(group.number, privacy_state) + group.is_private = is_private + + # ----------------------------------------------------------------------------------------------------------------- + # Peers list + # ----------------------------------------------------------------------------------------------------------------- + + def generate_peers_list(self): + if not self._contacts_manager.is_active_a_group(): + return + group = self._contacts_manager.get_curr_contact() + PeersListGenerator().generate(group.peers, self, self._peers_list_widget, group.tox_id) + + def peer_selected(self, chat_id, peer_id): + widgets_factory = self._get_widgets_factory() + group = self._get_group_by_public_key(chat_id) + self_peer = group.get_self_peer() + if self_peer.id != peer_id: + self._screen = widgets_factory.create_peer_screen_window(group, peer_id) + else: + self._screen = widgets_factory.create_self_peer_screen_window(group) + self._screen.show() + + # ----------------------------------------------------------------------------------------------------------------- + # Peers actions + # ----------------------------------------------------------------------------------------------------------------- + + def set_new_peer_role(self, group, peer, role): + self._tox.group_mod_set_role(group.number, peer.id, role) + peer.role = role + self.generate_peers_list() + + def toggle_ignore_peer(self, group, peer, ignore): + self._tox.group_toggle_ignore(group.number, peer.id, ignore) + peer.is_muted = ignore + + def set_self_info(self, group, name, status): + self._tox.group_self_set_name(group.number, name) + self._tox.group_self_set_status(group.number, status) + self_peer = group.get_self_peer() + self_peer.name = name + self_peer.status = status + self.generate_peers_list() + + # ----------------------------------------------------------------------------------------------------------------- + # Bans support + # ----------------------------------------------------------------------------------------------------------------- + + def show_bans_list(self, group): + widgets_factory = self._get_widgets_factory() + self._screen = widgets_factory.create_groups_bans_screen(group) + self._screen.show() + + def ban_peer(self, group, peer_id, ban_type): + self._tox.group_mod_ban_peer(group.number, peer_id, ban_type) + + def kick_peer(self, group, peer_id): + self._tox.group_mod_remove_peer(group.number, peer_id) + + def cancel_ban(self, group_number, ban_id): + self._tox.group_mod_remove_ban(group_number, ban_id) + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _add_new_group_by_number(self, group_number): + self._contacts_manager.add_group(group_number) + + def _get_group_by_number(self, group_number): + return self._contacts_provider.get_group_by_number(group_number) + + def _get_group_by_public_key(self, public_key): + return self._contacts_provider.get_group_by_public_key(public_key) + + def _get_all_groups(self): + return self._contacts_provider.get_all_groups() + + def _get_friend_by_number(self, friend_number): + return self._contacts_provider.get_friend_by_number(friend_number) + + def _get_friend_by_public_key(self, public_key): + return self._contacts_provider.get_friend_by_public_key(public_key) + + def _clear_peers_list(self, group): + group.remove_all_peers_except_self() + self.generate_peers_list() + + def _delete_group_invite(self, invite): + if invite in self._group_invites: + self._group_invites.remove(invite) + + def _join_gc_via_invite(self, invite_data, friend_number, nick, status, password): + group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, status, password) + self._add_new_group_by_number(group_number) + + def _update_invites_button_state(self): + self._main_screen.update_gc_invites_button_state() + + def _get_widgets_factory(self): + return self._widgets_factory_provider.get_item() diff --git a/toxygen/groups/peers_list.py b/toxygen/groups/peers_list.py new file mode 100644 index 0000000..17495f5 --- /dev/null +++ b/toxygen/groups/peers_list.py @@ -0,0 +1,104 @@ +from ui.group_peers_list import PeerItem, PeerTypeItem +from wrapper.toxcore_enums_and_consts import * +from ui.widgets import * + + +# ----------------------------------------------------------------------------------------------------------------- +# Builder +# ----------------------------------------------------------------------------------------------------------------- + + +class PeerListBuilder: + + def __init__(self): + self._peers = {} + self._titles = {} + self._index = 0 + self._handler = None + + def with_click_handler(self, handler): + self._handler = handler + + return self + + def with_title(self, title): + self._titles[self._index] = title + self._index += 1 + + return self + + def with_peers(self, peers): + for peer in peers: + self._add_peer(peer) + + return self + + def build(self, list_widget): + list_widget.clear() + + for i in range(self._index): + if i in self._peers: + peer = self._peers[i] + self._add_peer_item(peer, list_widget) + else: + title = self._titles[i] + self._add_peer_type_item(title, list_widget) + + def _add_peer_item(self, peer, parent): + item = PeerItem(peer, self._handler, parent.width(), parent) + self._add_item(parent, item) + + def _add_peer_type_item(self, text, parent): + item = PeerTypeItem(text, parent.width(), parent) + self._add_item(parent, item) + + @staticmethod + def _add_item(parent, item): + elem = QtWidgets.QListWidgetItem(parent) + elem.setSizeHint(QtCore.QSize(parent.width(), item.height())) + parent.addItem(elem) + parent.setItemWidget(elem, item) + + def _add_peer(self, peer): + self._peers[self._index] = peer + self._index += 1 + +# ----------------------------------------------------------------------------------------------------------------- +# Generators +# ----------------------------------------------------------------------------------------------------------------- + + +class PeersListGenerator: + + @staticmethod + def generate(peers_list, groups_service, list_widget, chat_id): + admin_title = util_ui.tr('Administrator') + moderators_title = util_ui.tr('Moderators') + users_title = util_ui.tr('Users') + observers_title = util_ui.tr('Observers') + + admins = list(filter(lambda p: p.role == TOX_GROUP_ROLE['FOUNDER'], peers_list)) + moderators = list(filter(lambda p: p.role == TOX_GROUP_ROLE['MODERATOR'], peers_list)) + users = list(filter(lambda p: p.role == TOX_GROUP_ROLE['USER'], peers_list)) + observers = list(filter(lambda p: p.role == TOX_GROUP_ROLE['OBSERVER'], peers_list)) + + builder = (PeerListBuilder() + .with_click_handler(lambda peer_id: groups_service.peer_selected(chat_id, peer_id))) + if len(admins): + (builder + .with_title(admin_title) + .with_peers(admins)) + if len(moderators): + (builder + .with_title(moderators_title) + .with_peers(moderators)) + if len(users): + (builder + .with_title(users_title) + .with_peers(users)) + if len(observers): + (builder + .with_title(observers_title) + .with_peers(observers)) + + builder.build(list_widget) diff --git a/toxygen/history.py b/toxygen/history.py deleted file mode 100644 index 586981a..0000000 --- a/toxygen/history.py +++ /dev/null @@ -1,215 +0,0 @@ -from sqlite3 import connect -import settings -from os import chdir -import os.path -from toxes import ToxES - - -PAGE_SIZE = 42 - -TIMEOUT = 11 - -SAVE_MESSAGES = 250 - -MESSAGE_OWNER = { - 'ME': 0, - 'FRIEND': 1, - 'NOT_SENT': 2 -} - - -class History: - - def __init__(self, name): - self._name = name - chdir(settings.ProfileHelper.get_path()) - path = settings.ProfileHelper.get_path() + self._name + '.hstr' - if os.path.exists(path): - decr = ToxES.get_instance() - try: - with open(path, 'rb') as fin: - data = fin.read() - if decr.is_data_encrypted(data): - data = decr.pass_decrypt(data) - with open(path, 'wb') as fout: - fout.write(data) - except: - os.remove(path) - db = connect(name + '.hstr', timeout=TIMEOUT) - cursor = db.cursor() - cursor.execute('CREATE TABLE IF NOT EXISTS friends(' - ' tox_id TEXT PRIMARY KEY' - ')') - db.close() - - def save(self): - encr = ToxES.get_instance() - if encr.has_password(): - path = settings.ProfileHelper.get_path() + self._name + '.hstr' - with open(path, 'rb') as fin: - data = fin.read() - data = encr.pass_encrypt(bytes(data)) - with open(path, 'wb') as fout: - fout.write(data) - - def export(self, directory): - path = settings.ProfileHelper.get_path() + self._name + '.hstr' - new_path = directory + self._name + '.hstr' - with open(path, 'rb') as fin: - data = fin.read() - encr = ToxES.get_instance() - if encr.has_password(): - data = encr.pass_encrypt(data) - with open(new_path, 'wb') as fout: - fout.write(data) - - def add_friend_to_db(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr', timeout=TIMEOUT) - try: - cursor = db.cursor() - cursor.execute('INSERT INTO friends VALUES (?);', (tox_id, )) - cursor.execute('CREATE TABLE id' + tox_id + '(' - ' id INTEGER PRIMARY KEY,' - ' message TEXT,' - ' owner INTEGER,' - ' unix_time REAL,' - ' message_type INTEGER' - ')') - db.commit() - except: - print('Database is locked!') - db.rollback() - finally: - db.close() - - def delete_friend_from_db(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr', timeout=TIMEOUT) - try: - cursor = db.cursor() - cursor.execute('DELETE FROM friends WHERE tox_id=?;', (tox_id, )) - cursor.execute('DROP TABLE id' + tox_id + ';') - db.commit() - except: - print('Database is locked!') - db.rollback() - finally: - db.close() - - def friend_exists_in_db(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr', timeout=TIMEOUT) - cursor = db.cursor() - cursor.execute('SELECT 0 FROM friends WHERE tox_id=?', (tox_id, )) - result = cursor.fetchone() - db.close() - return result is not None - - def save_messages_to_db(self, tox_id, messages_iter): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr', timeout=TIMEOUT) - try: - cursor = db.cursor() - cursor.executemany('INSERT INTO id' + tox_id + '(message, owner, unix_time, message_type) ' - 'VALUES (?, ?, ?, ?);', messages_iter) - db.commit() - except: - print('Database is locked!') - db.rollback() - finally: - db.close() - - def update_messages(self, tox_id, unsent_time): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr', timeout=TIMEOUT) - try: - cursor = db.cursor() - cursor.execute('UPDATE id' + tox_id + ' SET owner = 0 ' - 'WHERE unix_time < ' + str(unsent_time) + ' AND owner = 2;') - db.commit() - except: - print('Database is locked!') - db.rollback() - finally: - db.close() - - def delete_message(self, tox_id, time): - start, end = str(time - 0.01), str(time + 0.01) - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr', timeout=TIMEOUT) - try: - cursor = db.cursor() - cursor.execute('DELETE FROM id' + tox_id + ' WHERE unix_time < ' + end + ' AND unix_time > ' + - start + ';') - db.commit() - except: - print('Database is locked!') - db.rollback() - finally: - db.close() - - def delete_messages(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr', timeout=TIMEOUT) - try: - cursor = db.cursor() - cursor.execute('DELETE FROM id' + tox_id + ';') - db.commit() - except: - print('Database is locked!') - db.rollback() - finally: - db.close() - - def messages_getter(self, tox_id): - return History.MessageGetter(self._name, tox_id) - - class MessageGetter: - - def __init__(self, name, tox_id): - self._count = 0 - self._name = name - self._tox_id = tox_id - self._db = self._cursor = None - - def connect(self): - chdir(settings.ProfileHelper.get_path()) - self._db = connect(self._name + '.hstr', timeout=TIMEOUT) - self._cursor = self._db.cursor() - self._cursor.execute('SELECT message, owner, unix_time, message_type FROM id' + self._tox_id + - ' ORDER BY unix_time DESC;') - - def disconnect(self): - self._db.close() - - def get_one(self): - self.connect() - self.skip() - data = self._cursor.fetchone() - self._count += 1 - self.disconnect() - return data - - def get_all(self): - self.connect() - data = self._cursor.fetchall() - self.disconnect() - self._count = len(data) - return data - - def get(self, count): - self.connect() - self.skip() - data = self._cursor.fetchmany(count) - self.disconnect() - self._count += len(data) - return data - - def skip(self): - if self._count: - self._cursor.fetchmany(self._count) - - def delete_one(self): - if self._count: - self._count -= 1 diff --git a/toxygen/history/__init__.py b/toxygen/history/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/history/database.py b/toxygen/history/database.py new file mode 100644 index 0000000..751c74b --- /dev/null +++ b/toxygen/history/database.py @@ -0,0 +1,201 @@ +from sqlite3 import connect +import os.path +import utils.util as util + + +TIMEOUT = 11 + +SAVE_MESSAGES = 500 + +MESSAGE_AUTHOR = { + 'ME': 0, + 'FRIEND': 1, + 'NOT_SENT': 2, + 'GC_PEER': 3 +} + +CONTACT_TYPE = { + 'FRIEND': 0, + 'GC_PEER': 1, + 'GC_PEER_PRIVATE': 2 +} + + +class Database: + + def __init__(self, path, toxes): + self._path, self._toxes = path, 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) + + # ----------------------------------------------------------------------------------------------------------------- + # Public methods + # ----------------------------------------------------------------------------------------------------------------- + + def save(self): + if self._toxes.has_password(): + with open(self._path, 'rb') as fin: + data = fin.read() + data = self._toxes.pass_encrypt(bytes(data)) + with open(self._path, 'wb') as fout: + fout.write(data) + + def export(self, directory): + new_path = util.join_path(directory, self._name) + with open(self._path, 'rb') as fin: + data = fin.read() + if self._toxes.has_password(): + data = self._toxes.pass_encrypt(data) + with open(new_path, 'wb') as fout: + fout.write(data) + + def add_friend_to_db(self, tox_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('CREATE TABLE IF NOT EXISTS id' + tox_id + '(' + ' id INTEGER PRIMARY KEY,' + ' author_name TEXT,' + ' message TEXT,' + ' author_type INTEGER,' + ' unix_time REAL,' + ' message_type INTEGER' + ')') + db.commit() + except: + print('Database is locked!') + db.rollback() + finally: + db.close() + + def delete_friend_from_db(self, tox_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('DROP TABLE id' + tox_id + ';') + db.commit() + except: + print('Database is locked!') + db.rollback() + finally: + db.close() + + def save_messages_to_db(self, tox_id, messages_iter): + db = self._connect() + try: + cursor = db.cursor() + cursor.executemany('INSERT INTO id' + tox_id + + '(message, author_name, author_type, unix_time, message_type) ' + + 'VALUES (?, ?, ?, ?, ?, ?);', messages_iter) + db.commit() + except: + print('Database is locked!') + db.rollback() + finally: + db.close() + + def update_messages(self, tox_id, message_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('UPDATE id' + tox_id + ' SET author = 0 ' + 'WHERE id = ' + str(message_id) + ' AND author = 2;') + db.commit() + except: + print('Database is locked!') + db.rollback() + finally: + db.close() + + def delete_message(self, tox_id, unique_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('DELETE FROM id' + tox_id + ' WHERE id = ' + str(unique_id) + ';') + db.commit() + except: + print('Database is locked!') + db.rollback() + finally: + db.close() + + def delete_messages(self, tox_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('DELETE FROM id' + tox_id + ';') + db.commit() + except: + print('Database is locked!') + db.rollback() + finally: + db.close() + + def messages_getter(self, tox_id): + self.add_friend_to_db(tox_id) + + return Database.MessageGetter(self._path, tox_id) + + # ----------------------------------------------------------------------------------------------------------------- + # Messages loading + # ----------------------------------------------------------------------------------------------------------------- + + class MessageGetter: + + def __init__(self, path, tox_id): + self._count = 0 + self._path = path + self._tox_id = tox_id + self._db = self._cursor = None + + def get_one(self): + return self.get(1) + + def get_all(self): + self._connect() + data = self._cursor.fetchall() + self._disconnect() + self._count = len(data) + return data + + def get(self, count): + self._connect() + self.skip() + data = self._cursor.fetchmany(count) + self._disconnect() + self._count += len(data) + return data + + def skip(self): + if self._count: + self._cursor.fetchmany(self._count) + + def delete_one(self): + if self._count: + self._count -= 1 + + def _connect(self): + self._db = connect(self._path, timeout=TIMEOUT) + self._cursor = self._db.cursor() + self._cursor.execute('SELECT message, author_type, author_name, unix_time, message_type, id FROM id' + + self._tox_id + ' ORDER BY unix_time DESC;') + + def _disconnect(self): + self._db.close() + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _connect(self): + return connect(self._path, timeout=TIMEOUT) diff --git a/toxygen/history/history.py b/toxygen/history/history.py new file mode 100644 index 0000000..bd7e353 --- /dev/null +++ b/toxygen/history/history.py @@ -0,0 +1,138 @@ +from history.history_logs_generators import * + + +class History: + + def __init__(self, contact_provider, db, settings, main_screen, messages_items_factory): + self._contact_provider = contact_provider + self._db = db + self._settings = settings + self._messages = main_screen.messages + self._messages_items_factory = messages_items_factory + self._is_loading = False + self._contacts_manager = None + + def __del__(self): + del self._db + + def set_contacts_manager(self, contacts_manager): + self._contacts_manager = contacts_manager + + # ----------------------------------------------------------------------------------------------------------------- + # History support + # ----------------------------------------------------------------------------------------------------------------- + + def save_history(self): + """ + Save history to db + """ + if 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']: + messages = friend.get_corr_for_saving() + else: + messages = friend.get_unsent_messages_for_saving() + self._db.delete_messages(friend.tox_id) + messages = map(lambda m: (m.text, m.author.name, m.author.type, m.time, m.type), messages) + self._db.save_messages_to_db(friend.tox_id, messages) + + self._db.save() + + def clear_history(self, friend, save_unsent=False): + """ + Clear chat history + """ + friend.clear_corr(save_unsent) + self._db.delete_friend_from_db(friend.tox_id) + + def export_history(self, contact, as_text=True): + extension = 'txt' if as_text else 'html' + file_name, _ = util_ui.save_file_dialog(util_ui.tr('Choose file name'), extension) + + if not file_name: + return + + if not file_name.endswith('.' + extension): + file_name += '.' + extension + + history = self.generate_history(contact, as_text) + with open(file_name, 'wt') as fl: + fl.write(history) + + def delete_message(self, message): + contact = self._contacts_manager.get_curr_contact() + if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): + if message.is_saved(): + self._db.delete_message(contact.tox_id, message.id) + contact.delete_message(message.message_id) + + def load_history(self, friend): + """ + Tries to load next part of messages + """ + if self._is_loading: + return + self._is_loading = True + friend.load_corr(False) + messages = friend.get_corr() + if not messages: + self._is_loading = False + return + messages.reverse() + messages = messages[self._messages.count():self._messages.count() + PAGE_SIZE] + for message in messages: + message_type = message.get_type() + if message_type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): # text message + self._create_message_item(message) + elif message_type == MESSAGE_TYPE['FILE_TRANSFER']: # file transfer + if message.state == FILE_TRANSFER_STATE['UNSENT']: + self._create_unsent_file_item(message) + else: + self._create_file_transfer_item(message) + elif message_type == MESSAGE_TYPE['INLINE']: # inline image + self._create_inline_item(message) + else: # info message + self._create_message_item(message) + self._is_loading = False + + def get_message_getter(self, friend_public_key): + self._db.add_friend_to_db(friend_public_key) + + return self._db.messages_getter(friend_public_key) + + def delete_history(self, friend): + self._db.delete_friend_from_db(friend.tox_id) + + def add_friend_to_db(self, tox_id): + self._db.add_friend_to_db(tox_id) + + @staticmethod + def generate_history(contact, as_text=True, _range=None): + if _range is None: + contact.load_all_corr() + corr = contact.get_corr() + elif _range[1] + 1: + corr = contact.get_corr()[_range[0]:_range[1] + 1] + else: + corr = contact.get_corr()[_range[0]:] + + generator = TextHistoryGenerator(corr, contact.name) if as_text else HtmlHistoryGenerator(corr, contact.name) + + return generator.generate() + + # ----------------------------------------------------------------------------------------------------------------- + # Items creation + # ----------------------------------------------------------------------------------------------------------------- + + def _create_message_item(self, message): + return self._messages_items_factory.create_message_item(message, False) + + def _create_unsent_file_item(self, message): + return self._messages_items_factory.create_unsent_file_item(message, False) + + def _create_file_transfer_item(self, message): + return self._messages_items_factory.create_file_transfer_item(message, False) + + def _create_inline_item(self, message): + return self._messages_items_factory.create_inline_item(message, False) diff --git a/toxygen/history/history_logs_generators.py b/toxygen/history/history_logs_generators.py new file mode 100644 index 0000000..b8d0a56 --- /dev/null +++ b/toxygen/history/history_logs_generators.py @@ -0,0 +1,48 @@ +from messenger.messages import * +import utils.util as util + + +class HistoryLogsGenerator: + + def __init__(self, history, contact_name): + self._history = history + self._contact_name = contact_name + + def generate(self): + return str() + + @staticmethod + def _get_message_time(message): + return util.convert_time(message.time) if message.author.type != MESSAGE_AUTHOR['NOT_SENT'] else 'Unsent' + + +class HtmlHistoryGenerator(HistoryLogsGenerator): + + def __init__(self, history, contact_name): + super().__init__(history, contact_name) + + def generate(self): + arr = [] + for message in self._history: + if type(message) is TextMessage: + x = '[{}] {}: {}
' + arr.append(x.format(self._get_message_time(message), message.author.name, message.text)) + s = '
'.join(arr) + html = '{}{}' + + return html.format(self._contact_name, s) + + +class TextHistoryGenerator(HistoryLogsGenerator): + + def __init__(self, history, contact_name): + super().__init__(history, contact_name) + + def generate(self): + arr = [self._contact_name] + for message in self._history: + if type(message) is TextMessage: + x = '[{}] {}: {}\n' + arr.append(x.format(self._get_message_time(message), message.author.name, message.text)) + + return '\n'.join(arr) diff --git a/toxygen/items_factory.py b/toxygen/items_factory.py deleted file mode 100644 index 44a00ad..0000000 --- a/toxygen/items_factory.py +++ /dev/null @@ -1,68 +0,0 @@ -from PyQt5 import QtWidgets, QtCore -from list_items import * - - -class ItemsFactory: - - def __init__(self, friends_list, messages): - self._friends = friends_list - self._messages = messages - - def friend_item(self): - item = ContactItem() - elem = QtWidgets.QListWidgetItem(self._friends) - elem.setSizeHint(QtCore.QSize(250, item.height())) - self._friends.addItem(elem) - self._friends.setItemWidget(elem, item) - return item - - def message_item(self, text, time, name, sent, message_type, append, pixmap): - item = MessageItem(text, time, name, sent, message_type, self._messages) - if pixmap is not None: - item.set_avatar(pixmap) - elem = QtWidgets.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - return item - - def inline_item(self, data, append): - elem = QtWidgets.QListWidgetItem() - item = InlineImageItem(data, self._messages.width(), elem) - elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - return item - - def unsent_file_item(self, file_name, size, name, time, append): - item = UnsentFileItem(file_name, - size, - name, - time, - self._messages.width()) - elem = QtWidgets.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - return item - - def file_transfer_item(self, data, append): - data.append(self._messages.width()) - item = FileTransferItem(*data) - elem = QtWidgets.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - return item diff --git a/toxygen/libtox.py b/toxygen/libtox.py deleted file mode 100644 index 752798f..0000000 --- a/toxygen/libtox.py +++ /dev/null @@ -1,59 +0,0 @@ -from platform import system -from ctypes import CDLL -import util - - -class LibToxCore: - - def __init__(self): - if system() == 'Windows': - self._libtoxcore = CDLL(util.curr_directory() + '/libs/libtox.dll') - elif system() == 'Darwin': - self._libtoxcore = CDLL('libtoxcore.dylib') - else: - # libtoxcore and libsodium must be installed in your os - try: - self._libtoxcore = CDLL('libtoxcore.so') - except: - self._libtoxcore = CDLL(util.curr_directory() + '/libs/libtoxcore.so') - - def __getattr__(self, item): - return self._libtoxcore.__getattr__(item) - - -class LibToxAV: - - def __init__(self): - if system() == 'Windows': - # on Windows av api is in libtox.dll - self._libtoxav = CDLL(util.curr_directory() + '/libs/libtox.dll') - elif system() == 'Darwin': - self._libtoxav = CDLL('libtoxav.dylib') - else: - # /usr/lib/libtoxav.so must exists - try: - self._libtoxav = CDLL('libtoxav.so') - except: - self._libtoxav = CDLL(util.curr_directory() + '/libs/libtoxav.so') - - def __getattr__(self, item): - return self._libtoxav.__getattr__(item) - - -class LibToxEncryptSave: - - def __init__(self): - if system() == 'Windows': - # on Windows profile encryption api is in libtox.dll - self._lib_tox_encrypt_save = CDLL(util.curr_directory() + '/libs/libtox.dll') - elif system() == 'Darwin': - self._lib_tox_encrypt_save = CDLL('libtoxencryptsave.dylib') - else: - # /usr/lib/libtoxencryptsave.so must exists - try: - self._lib_tox_encrypt_save = CDLL('libtoxencryptsave.so') - except: - self._lib_tox_encrypt_save = CDLL(util.curr_directory() + '/libs/libtoxencryptsave.so') - - def __getattr__(self, item): - return self._lib_tox_encrypt_save.__getattr__(item) diff --git a/toxygen/loginscreen.py b/toxygen/loginscreen.py deleted file mode 100644 index 77aa5ba..0000000 --- a/toxygen/loginscreen.py +++ /dev/null @@ -1,103 +0,0 @@ -from PyQt5 import QtWidgets, QtCore -from widgets import * - - -class NickEdit(LineEdit): - - def __init__(self, parent): - super(NickEdit, self).__init__(parent) - self.parent = parent - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Return: - self.parent.create_profile() - else: - super(NickEdit, self).keyPressEvent(event) - - -class LoginScreen(CenteredWidget): - - def __init__(self): - super(LoginScreen, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.resize(400, 200) - self.setMinimumSize(QtCore.QSize(400, 200)) - self.setMaximumSize(QtCore.QSize(400, 200)) - self.new_profile = QtWidgets.QPushButton(self) - self.new_profile.setGeometry(QtCore.QRect(20, 150, 171, 27)) - self.new_profile.clicked.connect(self.create_profile) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(20, 70, 101, 17)) - self.new_name = NickEdit(self) - self.new_name.setGeometry(QtCore.QRect(20, 100, 171, 31)) - self.load_profile = QtWidgets.QPushButton(self) - self.load_profile.setGeometry(QtCore.QRect(220, 150, 161, 27)) - self.load_profile.clicked.connect(self.load_ex_profile) - self.default = QtWidgets.QCheckBox(self) - self.default.setGeometry(QtCore.QRect(220, 110, 131, 22)) - self.groupBox = QtWidgets.QGroupBox(self) - self.groupBox.setGeometry(QtCore.QRect(210, 40, 181, 151)) - self.comboBox = QtWidgets.QComboBox(self.groupBox) - self.comboBox.setGeometry(QtCore.QRect(10, 30, 161, 27)) - self.groupBox_2 = QtWidgets.QGroupBox(self) - self.groupBox_2.setGeometry(QtCore.QRect(10, 40, 191, 151)) - self.toxygen = QtWidgets.QLabel(self) - self.groupBox.raise_() - self.groupBox_2.raise_() - self.comboBox.raise_() - self.default.raise_() - self.load_profile.raise_() - self.new_name.raise_() - self.new_profile.raise_() - self.toxygen.setGeometry(QtCore.QRect(160, 8, 90, 25)) - font = QtGui.QFont() - font.setFamily("Impact") - font.setPointSize(16) - self.toxygen.setFont(font) - self.toxygen.setObjectName("toxygen") - self.type = 0 - self.number = -1 - self.load_as_default = False - self.name = None - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.new_name.setPlaceholderText(QtWidgets.QApplication.translate("login", "Profile name")) - self.setWindowTitle(QtWidgets.QApplication.translate("login", "Log in")) - self.new_profile.setText(QtWidgets.QApplication.translate("login", "Create")) - self.label.setText(QtWidgets.QApplication.translate("login", "Profile name:")) - self.load_profile.setText(QtWidgets.QApplication.translate("login", "Load profile")) - self.default.setText(QtWidgets.QApplication.translate("login", "Use as default")) - self.groupBox.setTitle(QtWidgets.QApplication.translate("login", "Load existing profile")) - self.groupBox_2.setTitle(QtWidgets.QApplication.translate("login", "Create new profile")) - self.toxygen.setText(QtWidgets.QApplication.translate("login", "toxygen")) - - def create_profile(self): - self.type = 1 - self.name = self.new_name.text() - self.close() - - def load_ex_profile(self): - if not self.create_only: - self.type = 2 - self.number = self.comboBox.currentIndex() - self.load_as_default = self.default.isChecked() - self.close() - - def update_select(self, data): - list_of_profiles = [] - for elem in data: - list_of_profiles.append(elem) - self.comboBox.addItems(list_of_profiles) - self.create_only = not list_of_profiles - - def update_on_close(self, func): - self.onclose = func - - def closeEvent(self, event): - self.onclose(self.type, self.number, self.load_as_default, self.name) - event.accept() diff --git a/toxygen/main.py b/toxygen/main.py index d630bb6..eca3ac3 100644 --- a/toxygen/main.py +++ b/toxygen/main.py @@ -1,485 +1,49 @@ -import sys -from loginscreen import LoginScreen -import profile -from settings import * -from PyQt5 import QtCore, QtGui, QtWidgets -from bootstrap import generate_nodes, download_nodes_list -from mainscreen import MainWindow -from callbacks import init_callbacks, stop, start -from util import curr_directory, program_version, remove -import styles.style # reqired for styles loading -import platform -import toxes -from passwordscreen import PasswordScreen, UnlockAppScreen, SetProfilePasswordScreen -from plugin_support import PluginLoader -import updater +import app +from user_data.settings import * +import utils.util as util +import argparse -class Toxygen: - - def __init__(self, path_or_uri=None): - super(Toxygen, self).__init__() - self.tox = self.ms = self.init = self.app = self.tray = self.mainloop = self.avloop = None - if path_or_uri is None: - self.uri = self.path = None - elif path_or_uri.startswith('tox:'): - self.path = None - self.uri = path_or_uri[4:] - else: - self.path = path_or_uri - self.uri = None - - def enter_pass(self, data): - """ - Show password screen - """ - tmp = [data] - p = PasswordScreen(toxes.ToxES.get_instance(), tmp) - p.show() - self.app.lastWindowClosed.connect(self.app.quit) - self.app.exec_() - if tmp[0] == data: - raise SystemExit() - else: - return tmp[0] - - def main(self): - """ - Main function of app. loads login screen if needed and starts main screen - """ - app = QtWidgets.QApplication(sys.argv) - app.setWindowIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) - self.app = app - - if platform.system() == 'Linux': - QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) - - with open(curr_directory() + '/styles/dark_style.qss') as fl: - style = fl.read() - app.setStyleSheet(style) - - encrypt_save = toxes.ToxES() - - if self.path is not None: - path = os.path.dirname(self.path) + '/' - name = os.path.basename(self.path)[:-4] - data = ProfileHelper(path, name).open_profile() - if encrypt_save.is_data_encrypted(data): - data = self.enter_pass(data) - settings = Settings(name) - self.tox = profile.tox_factory(data, settings) - else: - auto_profile = Settings.get_auto_profile() - if not auto_profile[0]: - # show login screen if default profile not found - current_locale = QtCore.QLocale() - curr_lang = current_locale.languageToString(current_locale.language()) - langs = Settings.supported_languages() - if curr_lang in langs: - lang_path = langs[curr_lang] - translator = QtCore.QTranslator() - translator.load(curr_directory() + '/translations/' + lang_path) - app.installTranslator(translator) - app.translator = translator - ls = LoginScreen() - ls.setWindowIconText("Toxygen") - profiles = ProfileHelper.find_profiles() - ls.update_select(map(lambda x: x[1], profiles)) - _login = self.Login(profiles) - ls.update_on_close(_login.login_screen_close) - ls.show() - app.exec_() - if not _login.t: - return - elif _login.t == 1: # create new profile - _login.name = _login.name.strip() - name = _login.name if _login.name else 'toxygen_user' - pr = map(lambda x: x[1], ProfileHelper.find_profiles()) - if name in list(pr): - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle( - QtWidgets.QApplication.translate("MainWindow", "Error")) - text = (QtWidgets.QApplication.translate("MainWindow", - 'Profile with this name already exists')) - msgBox.setText(text) - msgBox.exec_() - return - self.tox = profile.tox_factory() - self.tox.self_set_name(bytes(_login.name, 'utf-8') if _login.name else b'Toxygen User') - self.tox.self_set_status_message(b'Toxing on Toxygen') - reply = QtWidgets.QMessageBox.question(None, - 'Profile {}'.format(name), - QtWidgets.QApplication.translate("login", - 'Do you want to set profile password?'), - QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: - set_pass = SetProfilePasswordScreen(encrypt_save) - set_pass.show() - self.app.lastWindowClosed.connect(self.app.quit) - self.app.exec_() - reply = QtWidgets.QMessageBox.question(None, - 'Profile {}'.format(name), - QtWidgets.QApplication.translate("login", - 'Do you want to save profile in default folder? If no, profile will be saved in program folder'), - QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: - path = Settings.get_default_path() - else: - path = curr_directory() + '/' - try: - ProfileHelper(path, name).save_profile(self.tox.get_savedata()) - except Exception as ex: - print(str(ex)) - log('Profile creation exception: ' + str(ex)) - msgBox = QtWidgets.QMessageBox() - msgBox.setText(QtWidgets.QApplication.translate("login", - 'Profile saving error! Does Toxygen have permission to write to this directory?')) - msgBox.exec_() - return - path = Settings.get_default_path() - settings = Settings(name) - if curr_lang in langs: - settings['language'] = curr_lang - settings.save() - else: # load existing profile - path, name = _login.get_data() - if _login.default: - Settings.set_auto_profile(path, name) - data = ProfileHelper(path, name).open_profile() - if encrypt_save.is_data_encrypted(data): - data = self.enter_pass(data) - settings = Settings(name) - self.tox = profile.tox_factory(data, settings) - else: - path, name = auto_profile - data = ProfileHelper(path, name).open_profile() - if encrypt_save.is_data_encrypted(data): - data = self.enter_pass(data) - settings = Settings(name) - self.tox = profile.tox_factory(data, settings) - - if Settings.is_active_profile(path, name): # profile is in use - reply = QtWidgets.QMessageBox.question(None, - 'Profile {}'.format(name), - QtWidgets.QApplication.translate("login", 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?'), - QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No) - if reply != QtWidgets.QMessageBox.Yes: - return - else: - settings.set_active_profile() - - # application color scheme - for theme in settings.built_in_themes().keys(): - if settings['theme'] == theme: - with open(curr_directory() + settings.built_in_themes()[theme]) as fl: - style = fl.read() - app.setStyleSheet(style) - - lang = Settings.supported_languages()[settings['language']] - translator = QtCore.QTranslator() - translator.load(curr_directory() + '/translations/' + lang) - app.installTranslator(translator) - app.translator = translator - - # tray icon - self.tray = QtWidgets.QSystemTrayIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) - self.tray.setObjectName('tray') - - self.ms = MainWindow(self.tox, self.reset, self.tray) - app.aboutToQuit.connect(self.ms.close_window) - - class Menu(QtWidgets.QMenu): - - def newStatus(self, status): - if not Settings.get_instance().locked: - profile.Profile.get_instance().set_status(status) - self.aboutToShowHandler() - self.hide() - - def aboutToShowHandler(self): - status = profile.Profile.get_instance().status - act = self.act - if status is None or Settings.get_instance().locked: - self.actions()[1].setVisible(False) - else: - self.actions()[1].setVisible(True) - act.actions()[0].setChecked(False) - act.actions()[1].setChecked(False) - act.actions()[2].setChecked(False) - act.actions()[status].setChecked(True) - self.actions()[2].setVisible(not Settings.get_instance().locked) - - def languageChange(self, *args, **kwargs): - self.actions()[0].setText(QtWidgets.QApplication.translate('tray', 'Open Toxygen')) - self.actions()[1].setText(QtWidgets.QApplication.translate('tray', 'Set status')) - self.actions()[2].setText(QtWidgets.QApplication.translate('tray', 'Exit')) - self.act.actions()[0].setText(QtWidgets.QApplication.translate('tray', 'Online')) - self.act.actions()[1].setText(QtWidgets.QApplication.translate('tray', 'Away')) - self.act.actions()[2].setText(QtWidgets.QApplication.translate('tray', 'Busy')) - - m = Menu() - show = m.addAction(QtWidgets.QApplication.translate('tray', 'Open Toxygen')) - sub = m.addMenu(QtWidgets.QApplication.translate('tray', 'Set status')) - onl = sub.addAction(QtWidgets.QApplication.translate('tray', 'Online')) - away = sub.addAction(QtWidgets.QApplication.translate('tray', 'Away')) - busy = sub.addAction(QtWidgets.QApplication.translate('tray', 'Busy')) - onl.setCheckable(True) - away.setCheckable(True) - busy.setCheckable(True) - m.act = sub - exit = m.addAction(QtWidgets.QApplication.translate('tray', 'Exit')) - - def show_window(): - s = Settings.get_instance() - - def show(): - if not self.ms.isActiveWindow(): - self.ms.setWindowState(self.ms.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) - self.ms.activateWindow() - self.ms.show() - if not s.locked: - show() - else: - def correct_pass(): - show() - s.locked = False - s.unlockScreen = False - if not s.unlockScreen: - s.unlockScreen = True - self.p = UnlockAppScreen(toxes.ToxES.get_instance(), correct_pass) - self.p.show() - - def tray_activated(reason): - if reason == QtWidgets.QSystemTrayIcon.DoubleClick: - show_window() - - def close_app(): - if not Settings.get_instance().locked: - settings.closing = True - self.ms.close() - - show.triggered.connect(show_window) - exit.triggered.connect(close_app) - m.aboutToShow.connect(lambda: m.aboutToShowHandler()) - onl.triggered.connect(lambda: m.newStatus(0)) - away.triggered.connect(lambda: m.newStatus(1)) - busy.triggered.connect(lambda: m.newStatus(2)) - - self.tray.setContextMenu(m) - self.tray.show() - self.tray.activated.connect(tray_activated) - - self.ms.show() - - updating = False - if settings['update'] and updater.updater_available() and updater.connection_available(): # auto update - version = updater.check_for_updates() - if version is not None: - if settings['update'] == 2: - updater.download(version) - updating = True - else: - reply = QtWidgets.QMessageBox.question(None, - 'Toxygen', - QtWidgets.QApplication.translate("login", - 'Update for Toxygen was found. Download and install it?'), - QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: - updater.download(version) - updating = True - - if updating: - data = self.tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - settings.close() - del self.tox - return - - plugin_helper = PluginLoader(self.tox, settings) # plugin support - plugin_helper.load() - - start() - # init thread - self.init = self.InitThread(self.tox, self.ms, self.tray) - self.init.start() - - # starting threads for tox iterate and toxav iterate - self.mainloop = self.ToxIterateThread(self.tox) - self.mainloop.start() - self.avloop = self.ToxAVIterateThread(self.tox.AV) - self.avloop.start() - - if self.uri is not None: - self.ms.add_contact(self.uri) - - app.lastWindowClosed.connect(app.quit) - app.exec_() - - self.init.stop = True - self.mainloop.stop = True - self.avloop.stop = True - plugin_helper.stop() - stop() - self.mainloop.wait() - self.init.wait() - self.avloop.wait() - self.tray.hide() - data = self.tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - settings.close() - del self.tox - - def reset(self): - """ - Create new tox instance (new network settings) - :return: tox instance - """ - self.mainloop.stop = True - self.init.stop = True - self.avloop.stop = True - self.mainloop.wait() - self.init.wait() - self.avloop.wait() - data = self.tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - del self.tox - # create new tox instance - self.tox = profile.tox_factory(data, Settings.get_instance()) - # init thread - self.init = self.InitThread(self.tox, self.ms, self.tray) - self.init.start() - - # starting threads for tox iterate and toxav iterate - self.mainloop = self.ToxIterateThread(self.tox) - self.mainloop.start() - - self.avloop = self.ToxAVIterateThread(self.tox.AV) - self.avloop.start() - - plugin_helper = PluginLoader.get_instance() - plugin_helper.set_tox(self.tox) - - return self.tox - - # ----------------------------------------------------------------------------------------------------------------- - # Inner classes - # ----------------------------------------------------------------------------------------------------------------- - - class InitThread(QtCore.QThread): - - def __init__(self, tox, ms, tray): - QtCore.QThread.__init__(self) - self.tox, self.ms, self.tray = tox, ms, tray - self.stop = False - - def run(self): - # initializing callbacks - init_callbacks(self.tox, self.ms, self.tray) - # download list of nodes if needed - download_nodes_list() - # bootstrap - try: - for data in generate_nodes(): - if self.stop: - return - self.tox.bootstrap(*data) - self.tox.add_tcp_relay(*data) - except: - pass - for _ in range(10): - if self.stop: - return - self.msleep(1000) - while not self.tox.self_get_connection_status(): - try: - for data in generate_nodes(): - if self.stop: - return - self.tox.bootstrap(*data) - self.tox.add_tcp_relay(*data) - except: - pass - finally: - self.msleep(5000) - - class ToxIterateThread(QtCore.QThread): - - def __init__(self, tox): - QtCore.QThread.__init__(self) - self.tox = tox - self.stop = False - - def run(self): - while not self.stop: - self.tox.iterate() - self.msleep(self.tox.iteration_interval()) - - class ToxAVIterateThread(QtCore.QThread): - - def __init__(self, toxav): - QtCore.QThread.__init__(self) - self.toxav = toxav - self.stop = False - - def run(self): - while not self.stop: - self.toxav.iterate() - self.msleep(self.toxav.iteration_interval()) - - class Login: - - def __init__(self, arr): - self.arr = arr - - def login_screen_close(self, t, number=-1, default=False, name=None): - """ Function which processes data from login screen - :param t: 0 - window was closed, 1 - new profile was created, 2 - profile loaded - :param number: num of chosen profile in list (-1 by default) - :param default: was or not chosen profile marked as default - :param name: name of new profile - """ - self.t = t - self.num = number - self.default = default - self.name = name - - def get_data(self): - return self.arr[self.num] +__maintainer__ = 'Ingvar' +__version__ = '0.5.0' def clean(): - """Removes all windows libs from libs folder""" - d = curr_directory() + '/libs/' - remove(d) + """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__) + + def main(): - if len(sys.argv) == 1: - toxygen = Toxygen() - else: # started with argument(s) - arg = sys.argv[1] - if arg == '--version': - print('Toxygen v' + program_version) - return - elif arg == '--help': - print('Usage:\ntoxygen path_to_profile\ntoxygen tox_id\ntoxygen --version\ntoxygen --reset') - return - elif arg == '--clean': - clean() - return - elif arg == '--reset': - reset() - return - else: - toxygen = Toxygen(arg) + 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() + + if args.version: + print_toxygen_version() + return + + if args.clean: + clean() + return + + if args.reset: + reset() + return + + toxygen = app.App(__version__, args.profile, args.uri) toxygen.main() diff --git a/toxygen/mainscreen.py b/toxygen/mainscreen.py deleted file mode 100644 index 7d7b9e7..0000000 --- a/toxygen/mainscreen.py +++ /dev/null @@ -1,757 +0,0 @@ -from menu import * -from profile import * -from list_items import * -from widgets import MultilineEdit, ComboBox -import plugin_support -from mainscreen_widgets import * -import settings -import toxes - - -class MainWindow(QtWidgets.QMainWindow, Singleton): - - def __init__(self, tox, reset, tray): - super().__init__() - Singleton.__init__(self) - self.reset = reset - self.tray = tray - self.setAcceptDrops(True) - self.initUI(tox) - self._saved = False - if settings.Settings.get_instance()['show_welcome_screen']: - self.ws = WelcomeScreen() - - 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.menuProfile = QtWidgets.QMenu(self.menubar) - self.menuProfile.setObjectName("menuProfile") - self.menuSettings = QtWidgets.QMenu(self.menubar) - self.menuSettings.setObjectName("menuSettings") - self.menuPlugins = QtWidgets.QMenu(self.menubar) - self.menuPlugins.setObjectName("menuPlugins") - self.menuAbout = QtWidgets.QMenu(self.menubar) - self.menuAbout.setObjectName("menuAbout") - - self.actionAdd_friend = QtWidgets.QAction(window) - self.actionAdd_gc = QtWidgets.QAction(window) - self.actionAdd_friend.setObjectName("actionAdd_friend") - self.actionprofilesettings = QtWidgets.QAction(window) - self.actionprofilesettings.setObjectName("actionprofilesettings") - self.actionPrivacy_settings = QtWidgets.QAction(window) - self.actionPrivacy_settings.setObjectName("actionPrivacy_settings") - self.actionInterface_settings = QtWidgets.QAction(window) - self.actionInterface_settings.setObjectName("actionInterface_settings") - self.actionNotifications = QtWidgets.QAction(window) - self.actionNotifications.setObjectName("actionNotifications") - self.actionNetwork = QtWidgets.QAction(window) - self.actionNetwork.setObjectName("actionNetwork") - self.actionAbout_program = QtWidgets.QAction(window) - self.actionAbout_program.setObjectName("actionAbout_program") - self.updateSettings = QtWidgets.QAction(window) - self.actionSettings = QtWidgets.QAction(window) - self.actionSettings.setObjectName("actionSettings") - self.audioSettings = QtWidgets.QAction(window) - self.videoSettings = QtWidgets.QAction(window) - self.pluginData = QtWidgets.QAction(window) - self.importPlugin = QtWidgets.QAction(window) - self.reloadPlugins = QtWidgets.QAction(window) - self.lockApp = QtWidgets.QAction(window) - self.menuProfile.addAction(self.actionAdd_friend) - self.menuProfile.addAction(self.actionAdd_gc) - self.menuProfile.addAction(self.actionSettings) - self.menuProfile.addAction(self.lockApp) - self.menuSettings.addAction(self.actionPrivacy_settings) - self.menuSettings.addAction(self.actionInterface_settings) - self.menuSettings.addAction(self.actionNotifications) - self.menuSettings.addAction(self.actionNetwork) - self.menuSettings.addAction(self.audioSettings) - self.menuSettings.addAction(self.videoSettings) - self.menuSettings.addAction(self.updateSettings) - self.menuPlugins.addAction(self.pluginData) - self.menuPlugins.addAction(self.importPlugin) - self.menuPlugins.addAction(self.reloadPlugins) - self.menuAbout.addAction(self.actionAbout_program) - - self.menubar.addAction(self.menuProfile.menuAction()) - self.menubar.addAction(self.menuSettings.menuAction()) - self.menubar.addAction(self.menuPlugins.menuAction()) - self.menubar.addAction(self.menuAbout.menuAction()) - - self.actionAbout_program.triggered.connect(self.about_program) - self.actionNetwork.triggered.connect(self.network_settings) - self.actionAdd_friend.triggered.connect(self.add_contact) - self.actionAdd_gc.triggered.connect(self.create_gc) - self.actionSettings.triggered.connect(self.profile_settings) - self.actionPrivacy_settings.triggered.connect(self.privacy_settings) - self.actionInterface_settings.triggered.connect(self.interface_settings) - self.actionNotifications.triggered.connect(self.notification_settings) - self.audioSettings.triggered.connect(self.audio_settings) - self.videoSettings.triggered.connect(self.video_settings) - self.updateSettings.triggered.connect(self.update_settings) - self.pluginData.triggered.connect(self.plugins_menu) - self.lockApp.triggered.connect(self.lock_app) - self.importPlugin.triggered.connect(self.import_plugin) - self.reloadPlugins.triggered.connect(self.reload_plugins) - - def languageChange(self, *args, **kwargs): - self.retranslateUi() - - def event(self, event): - if event.type() == QtCore.QEvent.WindowActivate: - self.tray.setIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) - self.messages.repaint() - return super(MainWindow, self).event(event) - - def retranslateUi(self): - self.lockApp.setText(QtWidgets.QApplication.translate("MainWindow", "Lock")) - self.menuPlugins.setTitle(QtWidgets.QApplication.translate("MainWindow", "Plugins")) - self.pluginData.setText(QtWidgets.QApplication.translate("MainWindow", "List of plugins")) - self.menuProfile.setTitle(QtWidgets.QApplication.translate("MainWindow", "Profile")) - self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings")) - self.menuAbout.setTitle(QtWidgets.QApplication.translate("MainWindow", "About")) - self.actionAdd_friend.setText(QtWidgets.QApplication.translate("MainWindow", "Add contact")) - self.actionAdd_gc.setText(QtWidgets.QApplication.translate("MainWindow", "Create group chat")) - self.actionprofilesettings.setText(QtWidgets.QApplication.translate("MainWindow", "Profile")) - self.actionPrivacy_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Privacy")) - self.actionInterface_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Interface")) - self.actionNotifications.setText(QtWidgets.QApplication.translate("MainWindow", "Notifications")) - self.actionNetwork.setText(QtWidgets.QApplication.translate("MainWindow", "Network")) - self.actionAbout_program.setText(QtWidgets.QApplication.translate("MainWindow", "About program")) - self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Settings")) - self.audioSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Audio")) - self.videoSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Video")) - self.updateSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Updates")) - self.contact_name.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "Search")) - self.sendMessageButton.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Send message")) - self.callButton.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Start audio call with friend")) - self.online_contacts.clear() - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "All")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online first")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Name")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online and by name")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online first and by name")) - ind = Settings.get_instance()['sorting'] - d = {0: 0, 1: 1, 2: 2, 3: 4, 1 | 4: 4, 2 | 4: 5} - self.online_contacts.setCurrentIndex(d[ind]) - self.importPlugin.setText(QtWidgets.QApplication.translate("MainWindow", "Import plugin")) - self.reloadPlugins.setText(QtWidgets.QApplication.translate("MainWindow", "Reload plugins")) - - def setup_right_bottom(self, Form): - Form.resize(650, 60) - self.messageEdit = MessageArea(Form, self) - self.messageEdit.setGeometry(QtCore.QRect(0, 3, 450, 55)) - self.messageEdit.setObjectName("messageEdit") - font = QtGui.QFont() - font.setPointSize(11) - font.setFamily(settings.Settings.get_instance()['font']) - self.messageEdit.setFont(font) - - self.sendMessageButton = QtWidgets.QPushButton(Form) - self.sendMessageButton.setGeometry(QtCore.QRect(565, 3, 60, 55)) - self.sendMessageButton.setObjectName("sendMessageButton") - - self.menuButton = MenuButton(Form, self.show_menu) - self.menuButton.setGeometry(QtCore.QRect(QtCore.QRect(455, 3, 55, 55))) - - pixmap = QtGui.QPixmap('send.png') - icon = QtGui.QIcon(pixmap) - self.sendMessageButton.setIcon(icon) - self.sendMessageButton.setIconSize(QtCore.QSize(45, 60)) - - pixmap = QtGui.QPixmap('menu.png') - icon = QtGui.QIcon(pixmap) - self.menuButton.setIcon(icon) - self.menuButton.setIconSize(QtCore.QSize(40, 40)) - - self.sendMessageButton.clicked.connect(self.send_message) - - QtCore.QMetaObject.connectSlotsByName(Form) - - def setup_left_center_menu(self, Form): - Form.resize(270, 25) - self.search_label = QtWidgets.QLabel(Form) - self.search_label.setGeometry(QtCore.QRect(3, 2, 20, 20)) - pixmap = QtGui.QPixmap() - pixmap.load(curr_directory() + '/images/search.png') - self.search_label.setScaledContents(False) - self.search_label.setPixmap(pixmap) - - self.contact_name = LineEdit(Form) - self.contact_name.setGeometry(QtCore.QRect(0, 0, 150, 25)) - self.contact_name.setObjectName("contact_name") - self.contact_name.textChanged.connect(self.filtering) - - self.online_contacts = ComboBox(Form) - self.online_contacts.setGeometry(QtCore.QRect(150, 0, 120, 25)) - self.online_contacts.activated[int].connect(lambda x: self.filtering()) - self.search_label.raise_() - - QtCore.QMetaObject.connectSlotsByName(Form) - - def setup_left_top(self, Form): - Form.setCursor(QtCore.Qt.PointingHandCursor) - Form.setMinimumSize(QtCore.QSize(270, 75)) - Form.setMaximumSize(QtCore.QSize(270, 75)) - Form.setBaseSize(QtCore.QSize(270, 75)) - self.avatar_label = Form.avatar_label = QtWidgets.QLabel(Form) - self.avatar_label.setGeometry(QtCore.QRect(5, 5, 64, 64)) - self.avatar_label.setScaledContents(False) - self.avatar_label.setAlignment(QtCore.Qt.AlignCenter) - self.name = Form.name = DataLabel(Form) - Form.name.setGeometry(QtCore.QRect(75, 15, 150, 25)) - font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) - font.setPointSize(14) - font.setBold(True) - Form.name.setFont(font) - Form.name.setObjectName("name") - self.status_message = Form.status_message = DataLabel(Form) - Form.status_message.setGeometry(QtCore.QRect(75, 35, 170, 25)) - font.setPointSize(12) - font.setBold(False) - Form.status_message.setFont(font) - Form.status_message.setObjectName("status_message") - self.connection_status = Form.connection_status = StatusCircle(Form) - Form.connection_status.setGeometry(QtCore.QRect(230, 10, 32, 32)) - self.avatar_label.mouseReleaseEvent = self.profile_settings - self.status_message.mouseReleaseEvent = self.profile_settings - self.name.mouseReleaseEvent = self.profile_settings - self.connection_status.raise_() - Form.connection_status.setObjectName("connection_status") - - def setup_right_top(self, Form): - Form.resize(650, 75) - self.account_avatar = QtWidgets.QLabel(Form) - self.account_avatar.setGeometry(QtCore.QRect(10, 5, 64, 64)) - self.account_avatar.setScaledContents(False) - self.account_name = DataLabel(Form) - self.account_name.setGeometry(QtCore.QRect(100, 0, 400, 25)) - self.account_name.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) - font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) - font.setPointSize(14) - font.setBold(True) - self.account_name.setFont(font) - self.account_name.setObjectName("account_name") - self.account_status = DataLabel(Form) - self.account_status.setGeometry(QtCore.QRect(100, 20, 400, 25)) - self.account_status.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) - font.setPointSize(12) - font.setBold(False) - self.account_status.setFont(font) - self.account_status.setObjectName("account_status") - self.callButton = QtWidgets.QPushButton(Form) - self.callButton.setGeometry(QtCore.QRect(550, 5, 50, 50)) - self.callButton.setObjectName("callButton") - self.callButton.clicked.connect(lambda: self.profile.call_click(True)) - self.videocallButton = QtWidgets.QPushButton(Form) - self.videocallButton.setGeometry(QtCore.QRect(550, 5, 50, 50)) - self.videocallButton.setObjectName("videocallButton") - self.videocallButton.clicked.connect(lambda: self.profile.call_click(True, True)) - self.update_call_state('call') - self.typing = QtWidgets.QLabel(Form) - self.typing.setGeometry(QtCore.QRect(500, 25, 50, 30)) - pixmap = QtGui.QPixmap(QtCore.QSize(50, 30)) - pixmap.load(curr_directory() + '/images/typing.png') - self.typing.setScaledContents(False) - self.typing.setPixmap(pixmap.scaled(50, 30, QtCore.Qt.KeepAspectRatio)) - self.typing.setVisible(False) - QtCore.QMetaObject.connectSlotsByName(Form) - - def setup_left_center(self, widget): - self.friends_list = QtWidgets.QListWidget(widget) - self.friends_list.setObjectName("friends_list") - self.friends_list.setGeometry(0, 0, 270, 310) - self.friends_list.clicked.connect(self.friend_click) - self.friends_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.friends_list.customContextMenuRequested.connect(self.friend_right_click) - self.friends_list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.friends_list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - self.friends_list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.friends_list.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) - - def setup_right_center(self, widget): - self.messages = QtWidgets.QListWidget(widget) - self.messages.setGeometry(0, 0, 620, 310) - self.messages.setObjectName("messages") - self.messages.setSpacing(1) - self.messages.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - self.messages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.messages.focusOutEvent = lambda event: self.messages.clearSelection() - self.messages.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) - - def load(pos): - if not pos: - self.profile.load_history() - self.messages.verticalScrollBar().setValue(1) - self.messages.verticalScrollBar().valueChanged.connect(load) - self.messages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.messages.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - - def initUI(self, tox): - self.setMinimumSize(920, 500) - s = Settings.get_instance() - self.setGeometry(s['x'], s['y'], s['width'], s['height']) - self.setWindowTitle('Toxygen') - os.chdir(curr_directory() + '/images/') - menu = QtWidgets.QWidget() - main = QtWidgets.QWidget() - grid = QtWidgets.QGridLayout() - search = QtWidgets.QWidget() - name = QtWidgets.QWidget() - info = QtWidgets.QWidget() - main_list = QtWidgets.QWidget() - messages = QtWidgets.QWidget() - message_buttons = QtWidgets.QWidget() - self.setup_left_center_menu(search) - self.setup_left_top(name) - self.setup_right_center(messages) - self.setup_right_top(info) - self.setup_right_bottom(message_buttons) - self.setup_left_center(main_list) - self.setup_menu(menu) - if not Settings.get_instance()['mirror_mode']: - grid.addWidget(search, 2, 0) - grid.addWidget(name, 1, 0) - grid.addWidget(messages, 2, 1, 2, 1) - grid.addWidget(info, 1, 1) - grid.addWidget(message_buttons, 4, 1) - grid.addWidget(main_list, 3, 0, 2, 1) - grid.setColumnMinimumWidth(1, 500) - grid.setColumnMinimumWidth(0, 270) - else: - grid.addWidget(search, 2, 1) - grid.addWidget(name, 1, 1) - grid.addWidget(messages, 2, 0, 2, 1) - grid.addWidget(info, 1, 0) - grid.addWidget(message_buttons, 4, 0) - grid.addWidget(main_list, 3, 1, 2, 1) - grid.setColumnMinimumWidth(0, 500) - grid.setColumnMinimumWidth(1, 270) - - grid.addWidget(menu, 0, 0, 1, 2) - grid.setSpacing(0) - grid.setContentsMargins(0, 0, 0, 0) - grid.setRowMinimumHeight(0, 25) - grid.setRowMinimumHeight(1, 75) - grid.setRowMinimumHeight(2, 25) - grid.setRowMinimumHeight(3, 320) - grid.setRowMinimumHeight(4, 55) - grid.setColumnStretch(1, 1) - grid.setRowStretch(3, 1) - main.setLayout(grid) - self.setCentralWidget(main) - self.messageEdit.setFocus() - self.user_info = name - self.friend_info = info - self.retranslateUi() - self.profile = Profile(tox, self) - - def closeEvent(self, event): - s = Settings.get_instance() - if not s['close_to_tray'] or s.closing: - if not self._saved: - self._saved = True - self.profile.save_history() - self.profile.close() - s['x'] = self.geometry().x() - s['y'] = self.geometry().y() - s['width'] = self.width() - s['height'] = self.height() - s.save() - QtWidgets.QApplication.closeAllWindows() - event.accept() - elif QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): - event.ignore() - self.hide() - - def close_window(self): - Settings.get_instance().closing = True - self.close() - - def resizeEvent(self, *args, **kwargs): - self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 155) - self.friends_list.setGeometry(0, 0, 270, self.height() - 125) - - self.videocallButton.setGeometry(QtCore.QRect(self.width() - 330, 10, 50, 50)) - self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 10, 50, 50)) - self.typing.setGeometry(QtCore.QRect(self.width() - 450, 20, 50, 30)) - - self.messageEdit.setGeometry(QtCore.QRect(55, 0, self.width() - 395, 55)) - self.menuButton.setGeometry(QtCore.QRect(0, 0, 55, 55)) - self.sendMessageButton.setGeometry(QtCore.QRect(self.width() - 340, 0, 70, 55)) - - self.account_name.setGeometry(QtCore.QRect(100, 15, self.width() - 560, 25)) - self.account_status.setGeometry(QtCore.QRect(100, 35, self.width() - 560, 25)) - self.messageEdit.setFocus() - self.profile.update() - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Escape and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): - self.hide() - elif event.key() == QtCore.Qt.Key_C and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): - rows = list(map(lambda x: self.messages.row(x), self.messages.selectedItems())) - indexes = (rows[0] - self.messages.count(), rows[-1] - self.messages.count()) - s = self.profile.export_history(self.profile.active_friend, True, indexes) - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(s) - elif event.key() == QtCore.Qt.Key_Z and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): - self.messages.clearSelection() - elif event.key() == QtCore.Qt.Key_F and event.modifiers() & QtCore.Qt.ControlModifier: - self.show_search_field() - else: - super(MainWindow, self).keyPressEvent(event) - - # ----------------------------------------------------------------------------------------------------------------- - # Functions which called when user click in menu - # ----------------------------------------------------------------------------------------------------------------- - - def about_program(self): - import util - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "About")) - text = (QtWidgets.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.
Version: ')) - github = '
Github' - submit_a_bug = '
Submit a bug' - msgBox.setText(text + util.program_version + github + submit_a_bug) - msgBox.exec_() - - def network_settings(self): - self.n_s = NetworkSettings(self.reset) - self.n_s.show() - - def plugins_menu(self): - self.p_s = PluginsSettings() - self.p_s.show() - - def add_contact(self, link=''): - self.a_c = AddContact(link or '') - self.a_c.show() - - def create_gc(self): - self.profile.create_group_chat() - - def profile_settings(self, *args): - self.p_s = ProfileSettings() - self.p_s.show() - - def privacy_settings(self): - self.priv_s = PrivacySettings() - self.priv_s.show() - - def notification_settings(self): - self.notif_s = NotificationsSettings() - self.notif_s.show() - - def interface_settings(self): - self.int_s = InterfaceSettings() - self.int_s.show() - - def audio_settings(self): - self.audio_s = AudioSettings() - self.audio_s.show() - - def video_settings(self): - self.video_s = VideoSettings() - self.video_s.show() - - def update_settings(self): - self.update_s = UpdateSettings() - self.update_s.show() - - def reload_plugins(self): - plugin_loader = plugin_support.PluginLoader.get_instance() - if plugin_loader is not None: - plugin_loader.reload() - - def import_plugin(self): - import util - directory = QtWidgets.QFileDialog.getExistingDirectory(self, - QtWidgets.QApplication.translate("MainWindow", 'Choose folder with plugin'), - util.curr_directory(), - QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) - if directory: - src = directory + '/' - dest = curr_directory() + '/plugins/' - util.copy(src, dest) - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle( - QtWidgets.QApplication.translate("MainWindow", "Restart Toxygen")) - msgBox.setText( - QtWidgets.QApplication.translate("MainWindow", 'Plugin will be loaded after restart')) - msgBox.exec_() - - def lock_app(self): - if toxes.ToxES.get_instance().has_password(): - Settings.get_instance().locked = True - self.hide() - else: - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle( - QtWidgets.QApplication.translate("MainWindow", "Cannot lock app")) - msgBox.setText( - QtWidgets.QApplication.translate("MainWindow", 'Error. Profile password is not set.')) - msgBox.exec_() - - def show_menu(self): - if not hasattr(self, 'menu'): - self.menu = DropdownMenu(self) - self.menu.setGeometry(QtCore.QRect(0 if Settings.get_instance()['mirror_mode'] else 270, - self.height() - 120, - 180, - 120)) - self.menu.show() - - # ----------------------------------------------------------------------------------------------------------------- - # Messages, calls and file transfers - # ----------------------------------------------------------------------------------------------------------------- - - def send_message(self): - text = self.messageEdit.toPlainText() - self.profile.send_message(text) - - def send_file(self): - self.menu.hide() - if self.profile.active_friend + 1and self.profile.is_active_a_friend(): - choose = QtWidgets.QApplication.translate("MainWindow", 'Choose file') - name = QtWidgets.QFileDialog.getOpenFileName(self, choose, options=QtWidgets.QFileDialog.DontUseNativeDialog) - if name[0]: - self.profile.send_file(name[0]) - - def send_screenshot(self, hide=False): - self.menu.hide() - if self.profile.active_friend + 1 and self.profile.is_active_a_friend(): - self.sw = ScreenShotWindow(self) - self.sw.show() - if hide: - self.hide() - - def send_smiley(self): - self.menu.hide() - if self.profile.active_friend + 1: - self.smiley = SmileyWindow(self) - self.smiley.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(), - self.y() + self.height() - 200, - self.smiley.width(), - self.smiley.height())) - self.smiley.show() - - def send_sticker(self): - self.menu.hide() - if self.profile.active_friend + 1 and self.profile.is_active_a_friend(): - self.sticker = StickerWindow(self) - self.sticker.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(), - self.y() + self.height() - 200, - self.sticker.width(), - self.sticker.height())) - self.sticker.show() - - def active_call(self): - self.update_call_state('finish_call') - - def incoming_call(self): - self.update_call_state('incoming_call') - - def call_finished(self): - self.update_call_state('call') - - def update_call_state(self, state): - os.chdir(curr_directory() + '/images/') - - pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(state)) - icon = QtGui.QIcon(pixmap) - self.callButton.setIcon(icon) - self.callButton.setIconSize(QtCore.QSize(50, 50)) - - pixmap = QtGui.QPixmap(curr_directory() + '/images/{}_video.png'.format(state)) - icon = QtGui.QIcon(pixmap) - self.videocallButton.setIcon(icon) - self.videocallButton.setIconSize(QtCore.QSize(35, 35)) - - # ----------------------------------------------------------------------------------------------------------------- - # Functions which called when user open context menu in friends list - # ----------------------------------------------------------------------------------------------------------------- - - def friend_right_click(self, pos): - item = self.friends_list.itemAt(pos) - num = self.friends_list.indexFromItem(item).row() - friend = Profile.get_instance().get_friend(num) - if friend is None: - return - settings = Settings.get_instance() - allowed = friend.tox_id in settings['auto_accept_from_friends'] - auto = QtWidgets.QApplication.translate("MainWindow", 'Disallow auto accept') if allowed else QtWidgets.QApplication.translate("MainWindow", 'Allow auto accept') - if item is not None: - self.listMenu = QtWidgets.QMenu() - is_friend = type(friend) is Friend - if is_friend: - set_alias_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set alias')) - set_alias_item.triggered.connect(lambda: self.set_alias(num)) - - history_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Chat history')) - clear_history_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Clear history')) - export_to_text_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Export as text')) - export_to_html_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Export as HTML')) - - copy_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Copy')) - copy_name_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Name')) - copy_status_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Status message')) - if is_friend: - copy_key_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Public key')) - - auto_accept_item = self.listMenu.addAction(auto) - remove_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Remove friend')) - block_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Block friend')) - notes_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Notes')) - - chats = self.profile.get_group_chats() - if len(chats) and self.profile.is_active_online(): - invite_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Invite to group chat')) - for i in range(len(chats)): - name, number = chats[i] - item = invite_menu.addAction(name) - item.triggered.connect(lambda: self.invite_friend_to_gc(num, number)) - - plugins_loader = plugin_support.PluginLoader.get_instance() - if plugins_loader is not None: - submenu = plugins_loader.get_menu(self.listMenu, num) - if len(submenu): - plug = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins')) - plug.addActions(submenu) - copy_key_item.triggered.connect(lambda: self.copy_friend_key(num)) - remove_item.triggered.connect(lambda: self.remove_friend(num)) - block_item.triggered.connect(lambda: self.block_friend(num)) - auto_accept_item.triggered.connect(lambda: self.auto_accept(num, not allowed)) - notes_item.triggered.connect(lambda: self.show_note(friend)) - else: - leave_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Leave chat')) - set_title_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set title')) - leave_item.triggered.connect(lambda: self.leave_gc(num)) - set_title_item.triggered.connect(lambda: self.set_title(num)) - clear_history_item.triggered.connect(lambda: self.clear_history(num)) - copy_name_item.triggered.connect(lambda: self.copy_name(friend)) - copy_status_item.triggered.connect(lambda: self.copy_status(friend)) - export_to_text_item.triggered.connect(lambda: self.export_history(num)) - export_to_html_item.triggered.connect(lambda: self.export_history(num, False)) - parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) - self.listMenu.move(parent_position + pos) - self.listMenu.show() - - def show_note(self, friend): - s = Settings.get_instance() - note = s['notes'][friend.tox_id] if friend.tox_id in s['notes'] else '' - user = QtWidgets.QApplication.translate("MainWindow", 'Notes about user') - user = '{} {}'.format(user, friend.name) - - def save_note(text): - if friend.tox_id in s['notes']: - del s['notes'][friend.tox_id] - if text: - s['notes'][friend.tox_id] = text - s.save() - self.note = MultilineEdit(user, note, save_note) - self.note.show() - - def export_history(self, num, as_text=True): - s = self.profile.export_history(num, as_text) - extension = 'txt' if as_text else 'html' - file_name, _ = QtWidgets.QFileDialog.getSaveFileName(None, - QtWidgets.QApplication.translate("MainWindow", - 'Choose file name'), - curr_directory(), - filter=extension, - options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) - - if file_name: - if not file_name.endswith('.' + extension): - file_name += '.' + extension - with open(file_name, 'wt') as fl: - fl.write(s) - - def set_alias(self, num): - self.profile.set_alias(num) - - def remove_friend(self, num): - self.profile.delete_friend(num) - - def block_friend(self, num): - friend = self.profile.get_friend(num) - self.profile.block_user(friend.tox_id) - - def copy_friend_key(self, num): - tox_id = self.profile.friend_public_key(num) - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(tox_id) - - def copy_name(self, friend): - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(friend.name) - - def copy_status(self, friend): - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(friend.status_message) - - def clear_history(self, num): - self.profile.clear_history(num) - - def leave_gc(self, num): - self.profile.leave_gc(num) - - def set_title(self, num): - self.profile.set_title(num) - - def auto_accept(self, num, value): - settings = Settings.get_instance() - tox_id = self.profile.friend_public_key(num) - if value: - settings['auto_accept_from_friends'].append(tox_id) - else: - settings['auto_accept_from_friends'].remove(tox_id) - settings.save() - - def invite_friend_to_gc(self, friend_number, group_number): - self.profile.invite_friend(friend_number, group_number) - - # ----------------------------------------------------------------------------------------------------------------- - # Functions which called when user click somewhere else - # ----------------------------------------------------------------------------------------------------------------- - - def friend_click(self, index): - num = index.row() - self.profile.set_active(num) - - def mouseReleaseEvent(self, event): - pos = self.connection_status.pos() - x, y = pos.x() + self.user_info.pos().x(), pos.y() + self.user_info.pos().y() - if (x < event.x() < x + 32) and (y < event.y() < y + 32): - self.profile.change_status() - else: - super(MainWindow, self).mouseReleaseEvent(event) - - def show(self): - super().show() - self.profile.update() - - def filtering(self): - ind = self.online_contacts.currentIndex() - d = {0: 0, 1: 1, 2: 2, 3: 4, 4: 1 | 4, 5: 2 | 4} - self.profile.filtration_and_sorting(d[ind], self.contact_name.text()) - - def show_search_field(self): - if hasattr(self, 'search_field') and self.search_field.isVisible(): - return - if self.profile.get_curr_friend() is None: - return - self.search_field = SearchScreen(self.messages, self.messages.width(), self.messages.parent()) - x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40 - self.search_field.setGeometry(x, y, self.messages.width(), 40) - self.messages.setGeometry(x, self.messages.y(), self.messages.width(), self.messages.height() - 40) - self.search_field.show() diff --git a/toxygen/menu.py b/toxygen/menu.py deleted file mode 100644 index 17f4e17..0000000 --- a/toxygen/menu.py +++ /dev/null @@ -1,1095 +0,0 @@ -from PyQt5 import QtCore, QtGui, QtWidgets -from settings import * -from profile import Profile -from util import curr_directory, copy -from widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow -import pyaudio -import toxes -import plugin_support -import updater - - -class AddContact(CenteredWidget): - """Add contact form""" - - def __init__(self, tox_id=''): - super(AddContact, self).__init__() - self.initUI(tox_id) - self._adding = False - - def initUI(self, tox_id): - self.setObjectName('AddContact') - self.resize(568, 306) - self.sendRequestButton = QtWidgets.QPushButton(self) - self.sendRequestButton.setGeometry(QtCore.QRect(50, 270, 471, 31)) - self.sendRequestButton.setMinimumSize(QtCore.QSize(0, 0)) - self.sendRequestButton.setBaseSize(QtCore.QSize(0, 0)) - self.sendRequestButton.setObjectName("sendRequestButton") - self.sendRequestButton.clicked.connect(self.add_friend) - self.tox_id = LineEdit(self) - self.tox_id.setGeometry(QtCore.QRect(50, 40, 471, 27)) - self.tox_id.setObjectName("lineEdit") - self.tox_id.setText(tox_id) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(50, 10, 80, 20)) - self.error_label = DataLabel(self) - self.error_label.setGeometry(QtCore.QRect(120, 10, 420, 20)) - font = QtGui.QFont() - font.setFamily(Settings.get_instance()['font']) - font.setPointSize(10) - font.setWeight(30) - self.error_label.setFont(font) - self.error_label.setStyleSheet("QLabel { color: #BC1C1C; }") - self.label.setObjectName("label") - self.message_edit = QtWidgets.QTextEdit(self) - self.message_edit.setGeometry(QtCore.QRect(50, 110, 471, 151)) - self.message_edit.setObjectName("textEdit") - self.message = QtWidgets.QLabel(self) - self.message.setGeometry(QtCore.QRect(50, 70, 101, 31)) - self.message.setFont(font) - self.message.setObjectName("label_2") - self.retranslateUi() - self.message_edit.setText('Hello! Add me to your contact list please') - font.setPointSize(12) - font.setBold(True) - self.label.setFont(font) - self.message.setFont(font) - QtCore.QMetaObject.connectSlotsByName(self) - - def add_friend(self): - if self._adding: - return - self._adding = True - profile = Profile.get_instance() - send = profile.send_friend_request(self.tox_id.text().strip(), self.message_edit.toPlainText()) - self._adding = False - if send is True: - # request was successful - self.close() - else: # print error data - self.error_label.setText(send) - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate('AddContact', "Add contact")) - self.sendRequestButton.setText(QtWidgets.QApplication.translate("Form", "Send request")) - self.label.setText(QtWidgets.QApplication.translate('AddContact', "TOX ID:")) - self.message.setText(QtWidgets.QApplication.translate('AddContact', "Message:")) - self.tox_id.setPlaceholderText(QtWidgets.QApplication.translate('AddContact', "TOX ID or public key of contact")) - - -class ProfileSettings(CenteredWidget): - """Form with profile settings such as name, status, TOX ID""" - def __init__(self): - super(ProfileSettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("ProfileSettingsForm") - self.setMinimumSize(QtCore.QSize(700, 600)) - self.setMaximumSize(QtCore.QSize(700, 600)) - self.nick = LineEdit(self) - self.nick.setGeometry(QtCore.QRect(30, 60, 350, 27)) - profile = Profile.get_instance() - self.nick.setText(profile.name) - self.status = QtWidgets.QComboBox(self) - self.status.setGeometry(QtCore.QRect(400, 60, 200, 27)) - self.status_message = LineEdit(self) - self.status_message.setGeometry(QtCore.QRect(30, 130, 350, 27)) - self.status_message.setText(profile.status_message) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(40, 30, 91, 25)) - font = QtGui.QFont() - font.setFamily(Settings.get_instance()['font']) - font.setPointSize(18) - font.setWeight(75) - font.setBold(True) - self.label.setFont(font) - self.label_2 = QtWidgets.QLabel(self) - self.label_2.setGeometry(QtCore.QRect(40, 100, 100, 25)) - self.label_2.setFont(font) - self.label_3 = QtWidgets.QLabel(self) - self.label_3.setGeometry(QtCore.QRect(40, 180, 100, 25)) - self.label_3.setFont(font) - self.tox_id = QtWidgets.QLabel(self) - self.tox_id.setGeometry(QtCore.QRect(15, 210, 685, 21)) - font.setPointSize(10) - self.tox_id.setFont(font) - s = profile.tox_id - self.tox_id.setText(s) - self.copyId = QtWidgets.QPushButton(self) - self.copyId.setGeometry(QtCore.QRect(40, 250, 180, 30)) - self.copyId.clicked.connect(self.copy) - self.export = QtWidgets.QPushButton(self) - self.export.setGeometry(QtCore.QRect(230, 250, 180, 30)) - self.export.clicked.connect(self.export_profile) - self.new_nospam = QtWidgets.QPushButton(self) - self.new_nospam.setGeometry(QtCore.QRect(420, 250, 180, 30)) - self.new_nospam.clicked.connect(self.new_no_spam) - self.copy_pk = QtWidgets.QPushButton(self) - self.copy_pk.setGeometry(QtCore.QRect(40, 300, 180, 30)) - self.copy_pk.clicked.connect(self.copy_public_key) - self.new_avatar = QtWidgets.QPushButton(self) - self.new_avatar.setGeometry(QtCore.QRect(230, 300, 180, 30)) - self.delete_avatar = QtWidgets.QPushButton(self) - self.delete_avatar.setGeometry(QtCore.QRect(420, 300, 180, 30)) - self.delete_avatar.clicked.connect(self.reset_avatar) - self.new_avatar.clicked.connect(self.set_avatar) - self.profilepass = QtWidgets.QLabel(self) - self.profilepass.setGeometry(QtCore.QRect(40, 340, 300, 30)) - font.setPointSize(18) - self.profilepass.setFont(font) - self.password = LineEdit(self) - self.password.setGeometry(QtCore.QRect(40, 380, 300, 30)) - self.password.setEchoMode(QtWidgets.QLineEdit.Password) - self.leave_blank = QtWidgets.QLabel(self) - self.leave_blank.setGeometry(QtCore.QRect(350, 380, 300, 30)) - self.confirm_password = LineEdit(self) - self.confirm_password.setGeometry(QtCore.QRect(40, 420, 300, 30)) - self.confirm_password.setEchoMode(QtWidgets.QLineEdit.Password) - self.set_password = QtWidgets.QPushButton(self) - self.set_password.setGeometry(QtCore.QRect(40, 470, 300, 30)) - self.set_password.clicked.connect(self.new_password) - self.not_match = QtWidgets.QLabel(self) - self.not_match.setGeometry(QtCore.QRect(350, 420, 300, 30)) - self.not_match.setVisible(False) - self.not_match.setStyleSheet('QLabel { color: #BC1C1C; }') - self.warning = QtWidgets.QLabel(self) - self.warning.setGeometry(QtCore.QRect(40, 510, 500, 30)) - self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') - self.default = QtWidgets.QPushButton(self) - self.default.setGeometry(QtCore.QRect(40, 550, 620, 30)) - path, name = Settings.get_auto_profile() - self.auto = path + name == ProfileHelper.get_path() + Settings.get_instance().name - self.default.clicked.connect(self.auto_profile) - self.retranslateUi() - if profile.status is not None: - self.status.setCurrentIndex(profile.status) - else: - self.status.setVisible(False) - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.export.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Export profile")) - self.setWindowTitle(QtWidgets.QApplication.translate("ProfileSettingsForm", "Profile settings")) - self.label.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Name:")) - self.label_2.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Status:")) - self.label_3.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "TOX ID:")) - self.copyId.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Copy TOX ID")) - self.new_avatar.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "New avatar")) - self.delete_avatar.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Reset avatar")) - self.new_nospam.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "New NoSpam")) - self.profilepass.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Profile password")) - self.password.setPlaceholderText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Password (at least 8 symbols)")) - self.confirm_password.setPlaceholderText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Confirm password")) - self.set_password.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Set password")) - self.not_match.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Passwords do not match")) - self.leave_blank.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Leaving blank will reset current password")) - self.warning.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "There is no way to recover lost passwords")) - self.status.addItem(QtWidgets.QApplication.translate("ProfileSettingsForm", "Online")) - self.status.addItem(QtWidgets.QApplication.translate("ProfileSettingsForm", "Away")) - self.status.addItem(QtWidgets.QApplication.translate("ProfileSettingsForm", "Busy")) - self.copy_pk.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Copy public key")) - if self.auto: - self.default.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Mark as not default profile")) - else: - self.default.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Mark as default profile")) - - def auto_profile(self): - if self.auto: - Settings.reset_auto_profile() - else: - Settings.set_auto_profile(ProfileHelper.get_path(), Settings.get_instance().name) - self.auto = not self.auto - if self.auto: - self.default.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Mark as not default profile")) - else: - self.default.setText( - QtWidgets.QApplication.translate("ProfileSettingsForm", "Mark as default profile")) - - def new_password(self): - if self.password.text() == self.confirm_password.text(): - if not len(self.password.text()) or len(self.password.text()) >= 8: - e = toxes.ToxES.get_instance() - e.set_password(self.password.text()) - self.close() - else: - self.not_match.setText( - QtWidgets.QApplication.translate("ProfileSettingsForm", "Password must be at least 8 symbols")) - self.not_match.setVisible(True) - else: - self.not_match.setText(QtWidgets.QApplication.translate("ProfileSettingsForm", "Passwords do not match")) - self.not_match.setVisible(True) - - def copy(self): - clipboard = QtWidgets.QApplication.clipboard() - profile = Profile.get_instance() - clipboard.setText(profile.tox_id) - pixmap = QtGui.QPixmap(curr_directory() + '/images/accept.png') - icon = QtGui.QIcon(pixmap) - self.copyId.setIcon(icon) - self.copyId.setIconSize(QtCore.QSize(10, 10)) - - def copy_public_key(self): - clipboard = QtWidgets.QApplication.clipboard() - profile = Profile.get_instance() - clipboard.setText(profile.tox_id[:64]) - pixmap = QtGui.QPixmap(curr_directory() + '/images/accept.png') - icon = QtGui.QIcon(pixmap) - self.copy_pk.setIcon(icon) - self.copy_pk.setIconSize(QtCore.QSize(10, 10)) - - def new_no_spam(self): - self.tox_id.setText(Profile.get_instance().new_nospam()) - - def reset_avatar(self): - Profile.get_instance().reset_avatar() - - def set_avatar(self): - choose = QtWidgets.QApplication.translate("ProfileSettingsForm", "Choose avatar") - name = QtWidgets.QFileDialog.getOpenFileName(self, choose, None, 'Images (*.png)', - options=QtWidgets.QFileDialog.DontUseNativeDialog) - if name[0]: - bitmap = QtGui.QPixmap(name[0]) - bitmap.scaled(QtCore.QSize(128, 128), aspectRatioMode=QtCore.Qt.KeepAspectRatio, - transformMode=QtCore.Qt.SmoothTransformation) - - byte_array = QtCore.QByteArray() - buffer = QtCore.QBuffer(byte_array) - buffer.open(QtCore.QIODevice.WriteOnly) - bitmap.save(buffer, 'PNG') - Profile.get_instance().set_avatar(bytes(byte_array.data())) - - def export_profile(self): - directory = QtWidgets.QFileDialog.getExistingDirectory(self, '', curr_directory(), - QtWidgets.QFileDialog.DontUseNativeDialog) + '/' - if directory != '/': - reply = QtWidgets.QMessageBox.question(None, - QtWidgets.QApplication.translate("ProfileSettingsForm", - 'Use new path'), - QtWidgets.QApplication.translate("ProfileSettingsForm", - 'Do you want to move your profile to this location?'), - QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No) - settings = Settings.get_instance() - settings.export(directory) - profile = Profile.get_instance() - profile.export_db(directory) - ProfileHelper.get_instance().export_profile(directory, reply == QtWidgets.QMessageBox.Yes) - - def closeEvent(self, event): - profile = Profile.get_instance() - profile.set_name(self.nick.text()) - profile.set_status_message(self.status_message.text().encode('utf-8')) - profile.set_status(self.status.currentIndex()) - - -class NetworkSettings(CenteredWidget): - """Network settings form: UDP, Ipv6 and proxy""" - def __init__(self, reset): - super(NetworkSettings, self).__init__() - self.reset = reset - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("NetworkSettings") - self.resize(300, 400) - self.setMinimumSize(QtCore.QSize(300, 400)) - self.setMaximumSize(QtCore.QSize(300, 400)) - self.setBaseSize(QtCore.QSize(300, 400)) - self.ipv = QtWidgets.QCheckBox(self) - self.ipv.setGeometry(QtCore.QRect(20, 10, 97, 22)) - self.ipv.setObjectName("ipv") - self.udp = QtWidgets.QCheckBox(self) - self.udp.setGeometry(QtCore.QRect(150, 10, 97, 22)) - self.udp.setObjectName("udp") - self.proxy = QtWidgets.QCheckBox(self) - self.proxy.setGeometry(QtCore.QRect(20, 40, 97, 22)) - self.http = QtWidgets.QCheckBox(self) - self.http.setGeometry(QtCore.QRect(20, 70, 97, 22)) - self.proxy.setObjectName("proxy") - self.proxyip = LineEdit(self) - self.proxyip.setGeometry(QtCore.QRect(40, 130, 231, 27)) - self.proxyip.setObjectName("proxyip") - self.proxyport = LineEdit(self) - self.proxyport.setGeometry(QtCore.QRect(40, 190, 231, 27)) - self.proxyport.setObjectName("proxyport") - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(40, 100, 66, 17)) - self.label_2 = QtWidgets.QLabel(self) - self.label_2.setGeometry(QtCore.QRect(40, 165, 66, 17)) - self.reconnect = QtWidgets.QPushButton(self) - self.reconnect.setGeometry(QtCore.QRect(40, 230, 231, 30)) - self.reconnect.clicked.connect(self.restart_core) - settings = Settings.get_instance() - self.ipv.setChecked(settings['ipv6_enabled']) - self.udp.setChecked(settings['udp_enabled']) - self.proxy.setChecked(settings['proxy_type']) - self.proxyip.setText(settings['proxy_host']) - self.proxyport.setText(str(settings['proxy_port'])) - self.http.setChecked(settings['proxy_type'] == 1) - self.warning = QtWidgets.QLabel(self) - self.warning.setGeometry(QtCore.QRect(5, 270, 290, 60)) - self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') - self.nodes = QtWidgets.QCheckBox(self) - self.nodes.setGeometry(QtCore.QRect(20, 350, 270, 22)) - self.nodes.setChecked(settings['download_nodes_list']) - self.retranslateUi() - self.proxy.stateChanged.connect(lambda x: self.activate()) - self.activate() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("NetworkSettings", "Network settings")) - self.ipv.setText(QtWidgets.QApplication.translate("Form", "IPv6")) - self.udp.setText(QtWidgets.QApplication.translate("Form", "UDP")) - self.proxy.setText(QtWidgets.QApplication.translate("Form", "Proxy")) - self.label.setText(QtWidgets.QApplication.translate("Form", "IP:")) - self.label_2.setText(QtWidgets.QApplication.translate("Form", "Port:")) - self.reconnect.setText(QtWidgets.QApplication.translate("NetworkSettings", "Restart TOX core")) - self.http.setText(QtWidgets.QApplication.translate("Form", "HTTP")) - self.nodes.setText(QtWidgets.QApplication.translate("Form", "Download nodes list from tox.chat")) - self.warning.setText(QtWidgets.QApplication.translate("Form", "WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) - - def activate(self): - bl = self.proxy.isChecked() - self.proxyip.setEnabled(bl) - self.http.setEnabled(bl) - self.proxyport.setEnabled(bl) - - def restart_core(self): - try: - settings = Settings.get_instance() - settings['ipv6_enabled'] = self.ipv.isChecked() - settings['udp_enabled'] = self.udp.isChecked() - settings['proxy_type'] = 2 - int(self.http.isChecked()) if self.proxy.isChecked() else 0 - settings['proxy_host'] = str(self.proxyip.text()) - settings['proxy_port'] = int(self.proxyport.text()) - settings['download_nodes_list'] = self.nodes.isChecked() - settings.save() - # recreate tox instance - Profile.get_instance().reset(self.reset) - self.close() - except Exception as ex: - log('Exception in restart: ' + str(ex)) - - -class PrivacySettings(CenteredWidget): - """Privacy settings form: history, typing notifications""" - - def __init__(self): - super(PrivacySettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("privacySettings") - self.resize(370, 600) - self.setMinimumSize(QtCore.QSize(370, 600)) - self.setMaximumSize(QtCore.QSize(370, 600)) - self.saveHistory = QtWidgets.QCheckBox(self) - self.saveHistory.setGeometry(QtCore.QRect(10, 20, 350, 22)) - self.saveUnsentOnly = QtWidgets.QCheckBox(self) - self.saveUnsentOnly.setGeometry(QtCore.QRect(10, 60, 350, 22)) - - self.fileautoaccept = QtWidgets.QCheckBox(self) - self.fileautoaccept.setGeometry(QtCore.QRect(10, 100, 350, 22)) - - self.typingNotifications = QtWidgets.QCheckBox(self) - self.typingNotifications.setGeometry(QtCore.QRect(10, 140, 350, 30)) - self.inlines = QtWidgets.QCheckBox(self) - self.inlines.setGeometry(QtCore.QRect(10, 180, 350, 30)) - self.auto_path = QtWidgets.QLabel(self) - self.auto_path.setGeometry(QtCore.QRect(10, 230, 350, 30)) - self.path = QtWidgets.QPlainTextEdit(self) - self.path.setGeometry(QtCore.QRect(10, 265, 350, 45)) - self.change_path = QtWidgets.QPushButton(self) - self.change_path.setGeometry(QtCore.QRect(10, 320, 350, 30)) - settings = Settings.get_instance() - self.typingNotifications.setChecked(settings['typing_notifications']) - self.fileautoaccept.setChecked(settings['allow_auto_accept']) - self.saveHistory.setChecked(settings['save_history']) - self.inlines.setChecked(settings['allow_inline']) - self.saveUnsentOnly.setChecked(settings['save_unsent_only']) - self.saveUnsentOnly.setEnabled(settings['save_history']) - self.saveHistory.stateChanged.connect(self.update) - self.path.setPlainText(settings['auto_accept_path'] or curr_directory()) - self.change_path.clicked.connect(self.new_path) - self.block_user_label = QtWidgets.QLabel(self) - self.block_user_label.setGeometry(QtCore.QRect(10, 360, 350, 30)) - self.block_id = QtWidgets.QPlainTextEdit(self) - self.block_id.setGeometry(QtCore.QRect(10, 390, 350, 30)) - self.block = QtWidgets.QPushButton(self) - self.block.setGeometry(QtCore.QRect(10, 430, 350, 30)) - self.block.clicked.connect(lambda: Profile.get_instance().block_user(self.block_id.toPlainText()) or self.close()) - self.blocked_users_label = QtWidgets.QLabel(self) - self.blocked_users_label.setGeometry(QtCore.QRect(10, 470, 350, 30)) - self.comboBox = QtWidgets.QComboBox(self) - self.comboBox.setGeometry(QtCore.QRect(10, 500, 350, 30)) - self.comboBox.addItems(settings['blocked']) - self.unblock = QtWidgets.QPushButton(self) - self.unblock.setGeometry(QtCore.QRect(10, 540, 350, 30)) - self.unblock.clicked.connect(lambda: self.unblock_user()) - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("privacySettings", "Privacy settings")) - self.saveHistory.setText(QtWidgets.QApplication.translate("privacySettings", "Save chat history")) - self.fileautoaccept.setText(QtWidgets.QApplication.translate("privacySettings", "Allow file auto accept")) - self.typingNotifications.setText(QtWidgets.QApplication.translate("privacySettings", "Send typing notifications")) - self.auto_path.setText(QtWidgets.QApplication.translate("privacySettings", "Auto accept default path:")) - self.change_path.setText(QtWidgets.QApplication.translate("privacySettings", "Change")) - self.inlines.setText(QtWidgets.QApplication.translate("privacySettings", "Allow inlines")) - self.block_user_label.setText(QtWidgets.QApplication.translate("privacySettings", "Block by public key:")) - self.blocked_users_label.setText(QtWidgets.QApplication.translate("privacySettings", "Blocked users:")) - self.unblock.setText(QtWidgets.QApplication.translate("privacySettings", "Unblock")) - self.block.setText(QtWidgets.QApplication.translate("privacySettings", "Block user")) - self.saveUnsentOnly.setText(QtWidgets.QApplication.translate("privacySettings", "Save unsent messages only")) - - def update(self, new_state): - self.saveUnsentOnly.setEnabled(new_state) - if not new_state: - self.saveUnsentOnly.setChecked(False) - - def unblock_user(self): - if not self.comboBox.count(): - return - title = QtWidgets.QApplication.translate("privacySettings", "Add to friend list") - info = QtWidgets.QApplication.translate("privacySettings", "Do you want to add this user to friend list?") - reply = QtWidgets.QMessageBox.question(None, title, info, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) - Profile.get_instance().unblock_user(self.comboBox.currentText(), reply == QtWidgets.QMessageBox.Yes) - self.close() - - def closeEvent(self, event): - settings = Settings.get_instance() - settings['typing_notifications'] = self.typingNotifications.isChecked() - settings['allow_auto_accept'] = self.fileautoaccept.isChecked() - - if settings['save_history'] and not self.saveHistory.isChecked(): # clear history - reply = QtWidgets.QMessageBox.question(None, - QtWidgets.QApplication.translate("privacySettings", - 'Chat history'), - QtWidgets.QApplication.translate("privacySettings", - 'History will be cleaned! Continue?'), - QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: - Profile.get_instance().clear_history() - settings['save_history'] = self.saveHistory.isChecked() - else: - settings['save_history'] = self.saveHistory.isChecked() - if self.saveUnsentOnly.isChecked() and not settings['save_unsent_only']: - reply = QtWidgets.QMessageBox.question(None, - QtWidgets.QApplication.translate("privacySettings", - 'Chat history'), - QtWidgets.QApplication.translate("privacySettings", - 'History will be cleaned! Continue?'), - QtWidgets.QMessageBox.Yes, - QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: - Profile.get_instance().clear_history(None, True) - settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() - else: - settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() - settings['auto_accept_path'] = self.path.toPlainText() - settings['allow_inline'] = self.inlines.isChecked() - settings.save() - - def new_path(self): - directory = QtWidgets.QFileDialog.getExistingDirectory(options=QtWidgets.QFileDialog.DontUseNativeDialog) + '/' - if directory != '/': - self.path.setPlainText(directory) - - -class NotificationsSettings(CenteredWidget): - """Notifications settings form""" - - def __init__(self): - super(NotificationsSettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("notificationsForm") - self.resize(350, 210) - self.setMinimumSize(QtCore.QSize(350, 210)) - self.setMaximumSize(QtCore.QSize(350, 210)) - self.enableNotifications = QtWidgets.QCheckBox(self) - self.enableNotifications.setGeometry(QtCore.QRect(10, 20, 340, 18)) - self.callsSound = QtWidgets.QCheckBox(self) - self.callsSound.setGeometry(QtCore.QRect(10, 170, 340, 18)) - self.soundNotifications = QtWidgets.QCheckBox(self) - self.soundNotifications.setGeometry(QtCore.QRect(10, 70, 340, 18)) - self.groupNotifications = QtWidgets.QCheckBox(self) - self.groupNotifications.setGeometry(QtCore.QRect(10, 120, 340, 18)) - font = QtGui.QFont() - s = Settings.get_instance() - font.setFamily(s['font']) - font.setPointSize(12) - self.callsSound.setFont(font) - self.soundNotifications.setFont(font) - self.enableNotifications.setFont(font) - self.groupNotifications.setFont(font) - self.enableNotifications.setChecked(s['notifications']) - self.soundNotifications.setChecked(s['sound_notifications']) - self.groupNotifications.setChecked(s['group_notifications']) - self.callsSound.setChecked(s['calls_sound']) - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("notificationsForm", "Notification settings")) - self.enableNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable notifications")) - self.groupNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Notify about all messages in groups")) - self.callsSound.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable call\'s sound")) - self.soundNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable sound notifications")) - - def closeEvent(self, *args, **kwargs): - settings = Settings.get_instance() - settings['notifications'] = self.enableNotifications.isChecked() - settings['sound_notifications'] = self.soundNotifications.isChecked() - settings['group_notifications'] = self.groupNotifications.isChecked() - settings['calls_sound'] = self.callsSound.isChecked() - settings.save() - - -class InterfaceSettings(CenteredWidget): - """Interface settings form""" - def __init__(self): - super(InterfaceSettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("interfaceForm") - self.setMinimumSize(QtCore.QSize(400, 650)) - self.setMaximumSize(QtCore.QSize(400, 650)) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(30, 10, 370, 20)) - settings = Settings.get_instance() - font = QtGui.QFont() - font.setPointSize(14) - font.setBold(True) - font.setFamily(settings['font']) - self.label.setFont(font) - self.themeSelect = QtWidgets.QComboBox(self) - self.themeSelect.setGeometry(QtCore.QRect(30, 40, 120, 30)) - self.themeSelect.addItems(list(settings.built_in_themes().keys())) - theme = settings['theme'] - if theme in settings.built_in_themes().keys(): - index = list(settings.built_in_themes().keys()).index(theme) - else: - index = 0 - self.themeSelect.setCurrentIndex(index) - self.lang_choose = QtWidgets.QComboBox(self) - self.lang_choose.setGeometry(QtCore.QRect(30, 110, 120, 30)) - supported = sorted(Settings.supported_languages().keys(), reverse=True) - for key in supported: - self.lang_choose.insertItem(0, key) - if settings['language'] == key: - self.lang_choose.setCurrentIndex(0) - self.lang = QtWidgets.QLabel(self) - self.lang.setGeometry(QtCore.QRect(30, 80, 370, 20)) - self.lang.setFont(font) - self.mirror_mode = QtWidgets.QCheckBox(self) - self.mirror_mode.setGeometry(QtCore.QRect(30, 160, 370, 20)) - self.mirror_mode.setChecked(settings['mirror_mode']) - self.smileys = QtWidgets.QCheckBox(self) - self.smileys.setGeometry(QtCore.QRect(30, 190, 120, 20)) - self.smileys.setChecked(settings['smileys']) - self.smiley_pack_label = QtWidgets.QLabel(self) - self.smiley_pack_label.setGeometry(QtCore.QRect(30, 230, 370, 20)) - self.smiley_pack_label.setFont(font) - self.smiley_pack = QtWidgets.QComboBox(self) - self.smiley_pack.setGeometry(QtCore.QRect(30, 260, 160, 30)) - sm = smileys.SmileyLoader.get_instance() - self.smiley_pack.addItems(sm.get_packs_list()) - try: - ind = sm.get_packs_list().index(settings['smiley_pack']) - except: - ind = sm.get_packs_list().index('default') - self.smiley_pack.setCurrentIndex(ind) - self.messages_font_size_label = QtWidgets.QLabel(self) - self.messages_font_size_label.setGeometry(QtCore.QRect(30, 300, 370, 20)) - self.messages_font_size_label.setFont(font) - self.messages_font_size = QtWidgets.QComboBox(self) - self.messages_font_size.setGeometry(QtCore.QRect(30, 330, 160, 30)) - self.messages_font_size.addItems([str(x) for x in range(10, 25)]) - self.messages_font_size.setCurrentIndex(settings['message_font_size'] - 10) - - self.unread = QtWidgets.QPushButton(self) - self.unread.setGeometry(QtCore.QRect(30, 470, 340, 30)) - self.unread.clicked.connect(self.select_color) - - self.compact_mode = QtWidgets.QCheckBox(self) - self.compact_mode.setGeometry(QtCore.QRect(30, 380, 370, 20)) - self.compact_mode.setChecked(settings['compact_mode']) - - self.close_to_tray = QtWidgets.QCheckBox(self) - self.close_to_tray.setGeometry(QtCore.QRect(30, 410, 370, 20)) - self.close_to_tray.setChecked(settings['close_to_tray']) - - self.show_avatars = QtWidgets.QCheckBox(self) - self.show_avatars.setGeometry(QtCore.QRect(30, 440, 370, 20)) - self.show_avatars.setChecked(settings['show_avatars']) - - self.choose_font = QtWidgets.QPushButton(self) - self.choose_font.setGeometry(QtCore.QRect(30, 510, 340, 30)) - self.choose_font.clicked.connect(self.new_font) - - self.import_smileys = QtWidgets.QPushButton(self) - self.import_smileys.setGeometry(QtCore.QRect(30, 550, 340, 30)) - self.import_smileys.clicked.connect(self.import_sm) - - self.import_stickers = QtWidgets.QPushButton(self) - self.import_stickers.setGeometry(QtCore.QRect(30, 590, 340, 30)) - self.import_stickers.clicked.connect(self.import_st) - - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.show_avatars.setText(QtWidgets.QApplication.translate("interfaceForm", "Show avatars in chat")) - self.setWindowTitle(QtWidgets.QApplication.translate("interfaceForm", "Interface settings")) - self.label.setText(QtWidgets.QApplication.translate("interfaceForm", "Theme:")) - self.lang.setText(QtWidgets.QApplication.translate("interfaceForm", "Language:")) - self.smileys.setText(QtWidgets.QApplication.translate("interfaceForm", "Smileys")) - self.smiley_pack_label.setText(QtWidgets.QApplication.translate("interfaceForm", "Smiley pack:")) - self.mirror_mode.setText(QtWidgets.QApplication.translate("interfaceForm", "Mirror mode")) - self.messages_font_size_label.setText(QtWidgets.QApplication.translate("interfaceForm", "Messages font size:")) - self.unread.setText(QtWidgets.QApplication.translate("interfaceForm", "Select unread messages notification color")) - self.compact_mode.setText(QtWidgets.QApplication.translate("interfaceForm", "Compact contact list")) - self.import_smileys.setText(QtWidgets.QApplication.translate("interfaceForm", "Import smiley pack")) - self.import_stickers.setText(QtWidgets.QApplication.translate("interfaceForm", "Import sticker pack")) - self.close_to_tray.setText(QtWidgets.QApplication.translate("interfaceForm", "Close to tray")) - self.choose_font.setText(QtWidgets.QApplication.translate("interfaceForm", "Select font")) - - def import_st(self): - directory = QtWidgets.QFileDialog.getExistingDirectory(self, - QtWidgets.QApplication.translate("MainWindow", - 'Choose folder with sticker pack'), - curr_directory(), - QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) - - if directory: - src = directory + '/' - dest = curr_directory() + '/stickers/' + os.path.basename(directory) + '/' - copy(src, dest) - - def import_sm(self): - directory = QtWidgets.QFileDialog.getExistingDirectory(self, - QtWidgets.QApplication.translate("MainWindow", - 'Choose folder with smiley pack'), - curr_directory(), - QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) - - if directory: - src = directory + '/' - dest = curr_directory() + '/smileys/' + os.path.basename(directory) + '/' - copy(src, dest) - - def new_font(self): - settings = Settings.get_instance() - font, ok = QtWidgets.QFontDialog.getFont(QtGui.QFont(settings['font'], 10), self) - if ok: - settings['font'] = font.family() - settings.save() - msgBox = QtWidgets.QMessageBox() - text = QtWidgets.QApplication.translate("interfaceForm", 'Restart app to apply settings') - msgBox.setWindowTitle(QtWidgets.QApplication.translate("interfaceForm", 'Restart required')) - msgBox.setText(text) - msgBox.exec_() - - def select_color(self): - settings = Settings.get_instance() - col = QtWidgets.QColorDialog.getColor(QtGui.QColor(settings['unread_color'])) - - if col.isValid(): - name = col.name() - settings['unread_color'] = name - settings.save() - - def closeEvent(self, event): - settings = Settings.get_instance() - settings['theme'] = str(self.themeSelect.currentText()) - try: - theme = settings['theme'] - app = QtWidgets.QApplication.instance() - with open(curr_directory() + settings.built_in_themes()[theme]) as fl: - style = fl.read() - app.setStyleSheet(style) - except IsADirectoryError: - app.setStyleSheet('') # for default style - settings['smileys'] = self.smileys.isChecked() - restart = False - if settings['mirror_mode'] != self.mirror_mode.isChecked(): - settings['mirror_mode'] = self.mirror_mode.isChecked() - restart = True - if settings['compact_mode'] != self.compact_mode.isChecked(): - settings['compact_mode'] = self.compact_mode.isChecked() - restart = True - if settings['show_avatars'] != self.show_avatars.isChecked(): - settings['show_avatars'] = self.show_avatars.isChecked() - restart = True - settings['smiley_pack'] = self.smiley_pack.currentText() - settings['close_to_tray'] = self.close_to_tray.isChecked() - smileys.SmileyLoader.get_instance().load_pack() - language = self.lang_choose.currentText() - if settings['language'] != language: - settings['language'] = language - text = self.lang_choose.currentText() - path = Settings.supported_languages()[text] - app = QtWidgets.QApplication.instance() - app.removeTranslator(app.translator) - app.translator.load(curr_directory() + '/translations/' + path) - app.installTranslator(app.translator) - settings['message_font_size'] = self.messages_font_size.currentIndex() + 10 - Profile.get_instance().update() - settings.save() - if restart: - msgBox = QtWidgets.QMessageBox() - text = QtWidgets.QApplication.translate("interfaceForm", 'Restart app to apply settings') - msgBox.setWindowTitle(QtWidgets.QApplication.translate("interfaceForm", 'Restart required')) - msgBox.setText(text) - msgBox.exec_() - - -class AudioSettings(CenteredWidget): - """ - Audio calls settings form - """ - - def __init__(self): - super(AudioSettings, self).__init__() - self.initUI() - self.retranslateUi() - self.center() - - def initUI(self): - self.setObjectName("audioSettingsForm") - self.resize(400, 150) - self.setMinimumSize(QtCore.QSize(400, 150)) - self.setMaximumSize(QtCore.QSize(400, 150)) - self.in_label = QtWidgets.QLabel(self) - self.in_label.setGeometry(QtCore.QRect(25, 5, 350, 20)) - self.out_label = QtWidgets.QLabel(self) - self.out_label.setGeometry(QtCore.QRect(25, 65, 350, 20)) - settings = Settings.get_instance() - font = QtGui.QFont() - font.setPointSize(16) - font.setBold(True) - font.setFamily(settings['font']) - self.in_label.setFont(font) - self.out_label.setFont(font) - self.input = QtWidgets.QComboBox(self) - self.input.setGeometry(QtCore.QRect(25, 30, 350, 30)) - self.output = QtWidgets.QComboBox(self) - self.output.setGeometry(QtCore.QRect(25, 90, 350, 30)) - p = pyaudio.PyAudio() - self.in_indexes, self.out_indexes = [], [] - for i in range(p.get_device_count()): - device = p.get_device_info_by_index(i) - if device["maxInputChannels"]: - self.input.addItem(str(device["name"])) - self.in_indexes.append(i) - if device["maxOutputChannels"]: - self.output.addItem(str(device["name"])) - self.out_indexes.append(i) - self.input.setCurrentIndex(self.in_indexes.index(settings.audio['input'])) - self.output.setCurrentIndex(self.out_indexes.index(settings.audio['output'])) - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("audioSettingsForm", "Audio settings")) - self.in_label.setText(QtWidgets.QApplication.translate("audioSettingsForm", "Input device:")) - self.out_label.setText(QtWidgets.QApplication.translate("audioSettingsForm", "Output device:")) - - def closeEvent(self, event): - settings = Settings.get_instance() - settings.audio['input'] = self.in_indexes[self.input.currentIndex()] - settings.audio['output'] = self.out_indexes[self.output.currentIndex()] - settings.save() - - -class DesktopAreaSelectionWindow(RubberBandWindow): - - def mouseReleaseEvent(self, event): - if self.rubberband.isVisible(): - self.rubberband.hide() - rect = self.rubberband.geometry() - width, height = rect.width(), rect.height() - if width >= 8 and height >= 8: - self.parent.save(rect.x(), rect.y(), width - (width % 4), height - (height % 4)) - self.close() - - -class VideoSettings(CenteredWidget): - """ - Audio calls settings form - """ - - def __init__(self): - super().__init__() - self.initUI() - self.retranslateUi() - self.center() - self.desktopAreaSelection = None - - def initUI(self): - self.setObjectName("videoSettingsForm") - self.resize(400, 120) - self.setMinimumSize(QtCore.QSize(400, 120)) - self.setMaximumSize(QtCore.QSize(400, 120)) - self.in_label = QtWidgets.QLabel(self) - self.in_label.setGeometry(QtCore.QRect(25, 5, 350, 20)) - settings = Settings.get_instance() - font = QtGui.QFont() - font.setPointSize(16) - font.setBold(True) - font.setFamily(settings['font']) - self.in_label.setFont(font) - self.video_size = QtWidgets.QComboBox(self) - self.video_size.setGeometry(QtCore.QRect(25, 70, 350, 30)) - self.input = QtWidgets.QComboBox(self) - self.input.setGeometry(QtCore.QRect(25, 30, 350, 30)) - self.input.currentIndexChanged.connect(self.selectionChanged) - self.button = QtWidgets.QPushButton(self) - self.button.clicked.connect(self.button_clicked) - self.button.setGeometry(QtCore.QRect(25, 70, 350, 30)) - import cv2 - self.devices = [-1] - screen = QtWidgets.QApplication.primaryScreen() - size = screen.size() - self.frame_max_sizes = [(size.width(), size.height())] - desktop = QtWidgets.QApplication.translate("videoSettingsForm", "Desktop") - self.input.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) - - 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.input.addItem('Device #' + str(i)) - try: - index = self.devices.index(settings.video['device']) - self.input.setCurrentIndex(index) - except: - print('Video devices error!') - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("videoSettingsForm", "Video settings")) - self.in_label.setText(QtWidgets.QApplication.translate("videoSettingsForm", "Device:")) - self.button.setText(QtWidgets.QApplication.translate("videoSettingsForm", "Select region")) - - def button_clicked(self): - self.desktopAreaSelection = DesktopAreaSelectionWindow(self) - - def closeEvent(self, event): - if self.input.currentIndex() == 0: - return - try: - settings = Settings.get_instance() - settings.video['device'] = self.devices[self.input.currentIndex()] - text = self.video_size.currentText() - settings.video['width'] = int(text.split(' ')[0]) - settings.video['height'] = int(text.split(' ')[-1]) - settings.save() - except Exception as ex: - print('Saving video settings error: ' + str(ex)) - - def save(self, x, y, width, height): - self.desktopAreaSelection = None - settings = Settings.get_instance() - settings.video['device'] = -1 - settings.video['width'] = width - settings.video['height'] = height - settings.video['x'] = x - settings.video['y'] = y - settings.save() - - def selectionChanged(self): - if self.input.currentIndex() == 0: - self.button.setVisible(True) - self.video_size.setVisible(False) - else: - self.button.setVisible(False) - self.video_size.setVisible(True) - width, height = self.frame_max_sizes[self.input.currentIndex()] - self.video_size.clear() - dims = [ - (320, 240), - (640, 360), - (640, 480), - (720, 480), - (1280, 720), - (1920, 1080), - (2560, 1440) - ] - for w, h in dims: - if w <= width and h <= height: - self.video_size.addItem(str(w) + ' * ' + str(h)) - - -class PluginsSettings(CenteredWidget): - """ - Plugins settings form - """ - - def __init__(self): - super(PluginsSettings, self).__init__() - self.initUI() - self.center() - self.retranslateUi() - - def initUI(self): - self.resize(400, 210) - self.setMinimumSize(QtCore.QSize(400, 210)) - self.setMaximumSize(QtCore.QSize(400, 210)) - self.comboBox = QtWidgets.QComboBox(self) - self.comboBox.setGeometry(QtCore.QRect(30, 10, 340, 30)) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(30, 40, 340, 90)) - self.label.setWordWrap(True) - self.button = QtWidgets.QPushButton(self) - self.button.setGeometry(QtCore.QRect(30, 130, 340, 30)) - self.button.clicked.connect(self.button_click) - self.open = QtWidgets.QPushButton(self) - self.open.setGeometry(QtCore.QRect(30, 170, 340, 30)) - self.open.clicked.connect(self.open_plugin) - self.pl_loader = plugin_support.PluginLoader.get_instance() - self.update_list() - self.comboBox.currentIndexChanged.connect(self.show_data) - self.show_data() - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate('PluginsForm', "Plugins")) - self.open.setText(QtWidgets.QApplication.translate('PluginsForm', "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: - msgBox = QtWidgets.QMessageBox() - text = QtWidgets.QApplication.translate("PluginsForm", 'No GUI found for this plugin') - msgBox.setWindowTitle(QtWidgets.QApplication.translate("PluginsForm", 'Error')) - msgBox.setText(text) - msgBox.exec_() - - def update_list(self): - self.comboBox.clear() - data = self.pl_loader.get_plugins_list() - self.comboBox.addItems(list(map(lambda x: x[0], data))) - self.data = data - - def show_data(self): - ind = self.comboBox.currentIndex() - if len(self.data): - plugin = self.data[ind] - descr = plugin[2] or QtWidgets.QApplication.translate("PluginsForm", "No description available") - self.label.setText(descr) - if plugin[1]: - self.button.setText(QtWidgets.QApplication.translate("PluginsForm", "Disable plugin")) - else: - self.button.setText(QtWidgets.QApplication.translate("PluginsForm", "Enable plugin")) - else: - self.open.setVisible(False) - self.button.setVisible(False) - self.label.setText(QtWidgets.QApplication.translate("PluginsForm", "No plugins found")) - - def button_click(self): - ind = self.comboBox.currentIndex() - plugin = self.data[ind] - self.pl_loader.toggle_plugin(plugin[-1]) - plugin[1] = not plugin[1] - if plugin[1]: - self.button.setText(QtWidgets.QApplication.translate("PluginsForm", "Disable plugin")) - else: - self.button.setText(QtWidgets.QApplication.translate("PluginsForm", "Enable plugin")) - - -class UpdateSettings(CenteredWidget): - """ - Updates settings form - """ - - def __init__(self): - super(UpdateSettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("updateSettingsForm") - self.resize(400, 150) - self.setMinimumSize(QtCore.QSize(400, 120)) - self.setMaximumSize(QtCore.QSize(400, 120)) - self.in_label = QtWidgets.QLabel(self) - self.in_label.setGeometry(QtCore.QRect(25, 5, 350, 20)) - settings = Settings.get_instance() - font = QtGui.QFont() - font.setPointSize(16) - font.setBold(True) - font.setFamily(settings['font']) - self.in_label.setFont(font) - self.autoupdate = QtWidgets.QComboBox(self) - self.autoupdate.setGeometry(QtCore.QRect(25, 30, 350, 30)) - self.button = QtWidgets.QPushButton(self) - self.button.setGeometry(QtCore.QRect(25, 70, 350, 30)) - self.button.setEnabled(settings['update']) - self.button.clicked.connect(self.update_client) - - self.retranslateUi() - self.autoupdate.setCurrentIndex(settings['update']) - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("updateSettingsForm", "Update settings")) - self.in_label.setText(QtWidgets.QApplication.translate("updateSettingsForm", "Select update mode:")) - self.button.setText(QtWidgets.QApplication.translate("updateSettingsForm", "Update Toxygen")) - self.autoupdate.addItem(QtWidgets.QApplication.translate("updateSettingsForm", "Disabled")) - self.autoupdate.addItem(QtWidgets.QApplication.translate("updateSettingsForm", "Manual")) - self.autoupdate.addItem(QtWidgets.QApplication.translate("updateSettingsForm", "Auto")) - - def closeEvent(self, event): - settings = Settings.get_instance() - settings['update'] = self.autoupdate.currentIndex() - settings.save() - - def update_client(self): - if not updater.connection_available(): - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle( - QtWidgets.QApplication.translate("updateSettingsForm", "Error")) - text = (QtWidgets.QApplication.translate("updateSettingsForm", 'Problems with internet connection')) - msgBox.setText(text) - msgBox.exec_() - return - if not updater.updater_available(): - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle( - QtWidgets.QApplication.translate("updateSettingsForm", "Error")) - text = (QtWidgets.QApplication.translate("updateSettingsForm", 'Updater not found')) - msgBox.setText(text) - msgBox.exec_() - return - version = updater.check_for_updates() - if version is not None: - updater.download(version) - QtWidgets.QApplication.closeAllWindows() - else: - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle( - QtWidgets.QApplication.translate("updateSettingsForm", "No updates found")) - text = (QtWidgets.QApplication.translate("updateSettingsForm", 'Toxygen is up to date')) - msgBox.setText(text) - msgBox.exec_() diff --git a/toxygen/messages.py b/toxygen/messages.py deleted file mode 100644 index 8d9f4a3..0000000 --- a/toxygen/messages.py +++ /dev/null @@ -1,113 +0,0 @@ - - -MESSAGE_TYPE = { - 'TEXT': 0, - 'ACTION': 1, - 'FILE_TRANSFER': 2, - 'INLINE': 3, - 'INFO_MESSAGE': 4, - 'GC_TEXT': 5, - 'GC_ACTION': 6 -} - - -class Message: - - def __init__(self, message_type, owner, time): - self._time = time - self._type = message_type - self._owner = owner - - def get_type(self): - return self._type - - def get_owner(self): - return self._owner - - def mark_as_sent(self): - self._owner = 0 - - -class TextMessage(Message): - """ - Plain text or action message - """ - - def __init__(self, message, owner, time, message_type): - super(TextMessage, self).__init__(message_type, owner, time) - self._message = message - - def get_data(self): - return self._message, self._owner, self._time, self._type - - -class GroupChatMessage(TextMessage): - - def __init__(self, message, owner, time, message_type, name): - super().__init__(message, owner, time, message_type) - self._user_name = name - - def get_data(self): - return self._message, self._owner, self._time, self._type, self._user_name - - -class TransferMessage(Message): - """ - Message with info about file transfer - """ - - def __init__(self, owner, time, status, size, name, friend_number, file_number): - super(TransferMessage, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], owner, time) - self._status = status - self._size = size - self._file_name = name - self._friend_number, self._file_number = friend_number, file_number - - def is_active(self, file_number): - return self._file_number == file_number and self._status not in (2, 3) - - def get_friend_number(self): - return self._friend_number - - def get_file_number(self): - return self._file_number - - def get_status(self): - return self._status - - def set_status(self, value): - self._status = value - - def get_data(self): - return self._file_name, self._size, self._time, self._owner, self._friend_number, self._file_number, self._status - - -class UnsentFile(Message): - def __init__(self, path, data, time): - super(UnsentFile, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], 0, time) - self._data, self._path = data, path - - def get_data(self): - return self._path, self._data, self._time - - def get_status(self): - return None - - -class InlineImage(Message): - """ - Inline image - """ - - def __init__(self, data): - super(InlineImage, self).__init__(MESSAGE_TYPE['INLINE'], None, None) - self._data = data - - def get_data(self): - return self._data - - -class InfoMessage(TextMessage): - - def __init__(self, message, time): - super(InfoMessage, self).__init__(message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/__init__.py b/toxygen/messenger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/messenger/messages.py b/toxygen/messenger/messages.py new file mode 100644 index 0000000..e777c4b --- /dev/null +++ b/toxygen/messenger/messages.py @@ -0,0 +1,239 @@ +from history.database import MESSAGE_AUTHOR +import os.path +from ui.messages_widgets import * + + +MESSAGE_TYPE = { + 'TEXT': 0, + 'ACTION': 1, + 'FILE_TRANSFER': 2, + 'INLINE': 3, + 'INFO_MESSAGE': 4 +} + +PAGE_SIZE = 42 + + +class MessageAuthor: + + def __init__(self, author_name, author_type): + self._name = author_name + self._type = author_type + + def get_name(self): + return self._name + + name = property(get_name) + + def get_type(self): + return self._type + + def set_type(self, value): + self._type = value + + type = property(get_type, set_type) + + +class Message: + + MESSAGE_ID = 0 + + def __init__(self, message_type, author, time): + self._time = time + self._type = message_type + self._author = author + self._widget = None + self._message_id = self._get_id() + + def get_type(self): + return self._type + + type = property(get_type) + + def get_author(self): + return self._author + + author = property(get_author) + + def get_time(self): + return self._time + + time = property(get_time) + + def get_message_id(self): + return self._message_id + + message_id = property(get_message_id) + + def get_widget(self, *args): + self._widget = self._create_widget(*args) + + return self._widget + + widget = property(get_widget) + + def remove_widget(self): + self._widget = None + + def mark_as_sent(self): + self._author.type = MESSAGE_AUTHOR['ME'] + if self._widget is not None: + self._widget.mark_as_sent() + + def _create_widget(self, *args): + pass + + @staticmethod + def _get_id(): + Message.MESSAGE_ID += 1 + + return int(Message.MESSAGE_ID) + + +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) + self._message = message + self._id = message_id + + def get_text(self): + return self._message + + text = property(get_text) + + def get_id(self): + return self._id + + id = property(get_id) + + def is_saved(self): + return self._id > 0 + + def _create_widget(self, *args): + return MessageItem(self, *args) + + +class OutgoingTextMessage(TextMessage): + + def __init__(self, message, owner, time, message_type, tox_message_id=0): + super().__init__(message, owner, time, message_type) + self._tox_message_id = tox_message_id + + def get_tox_message_id(self): + return self._tox_message_id + + def set_tox_message_id(self, tox_message_id): + self._tox_message_id = tox_message_id + + tox_message_id = property(get_tox_message_id, set_tox_message_id) + + +class GroupChatMessage(TextMessage): + + def __init__(self, id, message, owner, time, message_type, name): + super().__init__(id, message, owner, time, message_type) + self._user_name = name + + +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) + self._state = state + self._size = size + self._file_name = file_name + self._friend_number, self._file_number = friend_number, file_number + + def is_active(self, file_number): + if self._file_number != file_number: + return False + + return self._state not in (FILE_TRANSFER_STATE['FINISHED'], FILE_TRANSFER_STATE['CANCELLED']) + + def get_friend_number(self): + return self._friend_number + + friend_number = property(get_friend_number) + + def get_file_number(self): + return self._file_number + + file_number = property(get_file_number) + + def get_state(self): + return self._state + + def set_state(self, value): + self._state = value + + state = property(get_state, set_state) + + def get_size(self): + return self._size + + size = property(get_size) + + def get_file_name(self): + return self._file_name + + file_name = property(get_file_name) + + def transfer_updated(self, state, percentage, time): + self._state = state + if self._widget is not None: + self._widget.update_transfer_state(state, percentage, time) + + def _create_widget(self, *args): + return FileTransferItem(self, *args) + + +class UnsentFileMessage(TransferMessage): + + def __init__(self, path, data, time, author, size, friend_number): + file_name = os.path.basename(path) + super().__init__(author, time, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) + self._data, self._path = data, path + + def get_data(self): + return self._data + + data = property(get_data) + + def get_path(self): + return self._path + + path = property(get_path) + + def _create_widget(self, *args): + return UnsentFileItem(self, *args) + + +class InlineImageMessage(Message): + """ + Inline image + """ + + def __init__(self, data): + super().__init__(MESSAGE_TYPE['INLINE'], None, None) + self._data = data + + def get_data(self): + return self._data + + data = property(get_data) + + def _create_widget(self, *args): + return InlineImageItem(self, *args) + + +class InfoMessage(TextMessage): + + def __init__(self, message, time): + super().__init__(message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py new file mode 100644 index 0000000..e859135 --- /dev/null +++ b/toxygen/messenger/messenger.py @@ -0,0 +1,310 @@ +import common.tox_save as tox_save +from messenger.messages import * + + +class Messenger(tox_save.ToxSave): + + def __init__(self, tox, plugin_loader, screen, contacts_manager, contacts_provider, items_factory, profile, + calls_manager): + super().__init__(tox) + self._plugin_loader = plugin_loader + self._screen = screen + self._contacts_manager = contacts_manager + self._contacts_provider = contacts_provider + self._items_factory = items_factory + self._profile = profile + self._profile_name = profile.name + + profile.name_changed_event.add_callback(self._on_profile_name_changed) + calls_manager.call_started_event.add_callback(self._on_call_started) + calls_manager.call_finished_event.add_callback(self._on_call_finished) + + def get_last_message(self): + contact = self._contacts_manager.get_curr_contact() + if contact is None: + return str() + + return contact.get_last_message_text() + + # ----------------------------------------------------------------------------------------------------------------- + # Messaging - friends + # ----------------------------------------------------------------------------------------------------------------- + + def new_message(self, friend_number, message_type, message): + """ + Current user gets new message + :param friend_number: friend_num of friend who sent message + :param message_type: message type - plain text or action message (/me) + :param message: text of message + """ + t = util.get_unix_time() + friend = self._get_friend_by_number(friend_number) + text_message = TextMessage(message, MessageAuthor(friend.name, MESSAGE_AUTHOR['FRIEND']), t, message_type) + self._add_message(text_message, friend) + + def send_message(self): + text = self._screen.messageEdit.toPlainText() + + plugin_command_prefix = '/plugin ' + if text.startswith(plugin_command_prefix): + self._plugin_loader.command(text[len(plugin_command_prefix):]) + self._screen.messageEdit.clear() + return + + action_message_prefix = '/me ' + if text.startswith(action_message_prefix): + message_type = TOX_MESSAGE_TYPE['ACTION'] + text = text[len(action_message_prefix):] + else: + message_type = TOX_MESSAGE_TYPE['NORMAL'] + + if self._contacts_manager.is_active_a_friend(): + self.send_message_to_friend(text, message_type) + elif self._contacts_manager.is_active_a_group(): + self.send_message_to_group(text, message_type) + elif self._contacts_manager.is_active_a_group_chat_peer(): + self.send_message_to_group_peer(text, message_type) + + def send_message_to_friend(self, text, message_type, friend_number=None): + """ + Send message + :param text: message text + :param friend_number: number of friend + """ + if friend_number is None: + friend_number = self._contacts_manager.get_active_number() + + if not text or friend_number < 0: + return + + friend = self._get_friend_by_number(friend_number) + messages = self._split_message(text.encode('utf-8')) + t = util.get_unix_time() + for message in messages: + if friend.status is not None: + message_id = self._tox.friend_send_message(friend_number, message_type, message) + else: + message_id = 0 + message_author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['NOT_SENT']) + message = OutgoingTextMessage(text, message_author, t, message_type, message_id) + friend.append_message(message) + if not self._contacts_manager.is_friend_active(friend_number): + return + self._create_message_item(message) + self._screen.messageEdit.clear() + self._screen.messages.scrollToBottom() + + def send_messages(self, friend_number): + """ + Send 'offline' messages to friend + """ + friend = self._get_friend_by_number(friend_number) + friend.load_corr() + messages = friend.get_unsent_messages() + try: + for message in messages: + 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)) + + # ----------------------------------------------------------------------------------------------------------------- + # Messaging - groups + # ----------------------------------------------------------------------------------------------------------------- + + def send_message_to_group(self, text, message_type, group_number=None): + if group_number is None: + group_number = self._contacts_manager.get_active_number() + + if not text or group_number < 0: + return + + group = self._get_group_by_number(group_number) + messages = self._split_message(text.encode('utf-8')) + t = util.get_unix_time() + for message in messages: + self._tox.group_send_message(group_number, message_type, message) + message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) + message = OutgoingTextMessage(text, message_author, t, message_type) + group.append_message(message) + if not self._contacts_manager.is_group_active(group_number): + return + self._create_message_item(message) + self._screen.messageEdit.clear() + self._screen.messages.scrollToBottom() + + def new_group_message(self, group_number, message_type, message, peer_id): + """ + Current user gets new message + :param message_type: message type - plain text or action message (/me) + :param message: text of message + """ + t = util.get_unix_time() + group = self._get_group_by_number(group_number) + peer = group.get_peer_by_id(peer_id) + text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), t, message_type) + self._add_message(text_message, group) + + # ----------------------------------------------------------------------------------------------------------------- + # Messaging - group peers + # ----------------------------------------------------------------------------------------------------------------- + + def send_message_to_group_peer(self, text, message_type, group_number=None, peer_id=None): + if group_number is None or peer_id is None: + group_peer_contact = self._contacts_manager.get_curr_contact() + peer_id = group_peer_contact.number + group = self._get_group_by_public_key(group_peer_contact.group_pk) + group_number = group.number + + if not text or group_number < 0 or peer_id < 0: + return + + group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) + group = self._get_group_by_number(group_number) + messages = self._split_message(text.encode('utf-8')) + t = util.get_unix_time() + for message in messages: + self._tox.group_send_private_message(group_number, peer_id, message_type, message) + message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) + message = OutgoingTextMessage(text, message_author, t, message_type) + group_peer_contact.append_message(message) + if not self._contacts_manager.is_contact_active(group_peer_contact): + return + self._create_message_item(message) + self._screen.messageEdit.clear() + self._screen.messages.scrollToBottom() + + def new_group_private_message(self, group_number, message_type, message, peer_id): + """ + Current user gets new message + :param message: text of message + """ + t = util.get_unix_time() + group = self._get_group_by_number(group_number) + peer = group.get_peer_by_id(peer_id) + 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) + self._add_message(text_message, group_peer_contact) + + # ----------------------------------------------------------------------------------------------------------------- + # Message receipts + # ----------------------------------------------------------------------------------------------------------------- + + def receipt(self, friend_number, message_id): + friend = self._get_friend_by_number(friend_number) + friend.mark_as_sent(message_id) + + # ----------------------------------------------------------------------------------------------------------------- + # Typing notifications + # ----------------------------------------------------------------------------------------------------------------- + + def send_typing(self, typing): + """ + Send typing notification to a friend + """ + if not self._contacts_manager.can_send_typing_notification(): + return + contact = self._contacts_manager.get_curr_contact() + contact.typing_notification_handler.send(self._tox, typing) + + def friend_typing(self, friend_number, typing): + """ + Display incoming typing notification + """ + if self._contacts_manager.is_friend_active(friend_number): + self._screen.typing.setVisible(typing) + + # ----------------------------------------------------------------------------------------------------------------- + # Contact info updated + # ----------------------------------------------------------------------------------------------------------------- + + def new_friend_name(self, friend, old_name, new_name): + if old_name == new_name or friend.has_alias(): + return + message = util_ui.tr('User {} is now known as {}') + message = message.format(old_name, new_name) + if not self._contacts_manager.is_friend_active(friend.number): + friend.actions = True + self._add_info_message(friend.number, message) + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + @staticmethod + def _split_message(message): + messages = [] + while len(message) > TOX_MAX_MESSAGE_LENGTH: + size = TOX_MAX_MESSAGE_LENGTH * 4 // 5 + last_part = message[size:TOX_MAX_MESSAGE_LENGTH] + if b' ' in last_part: + index = last_part.index(b' ') + elif b',' in last_part: + index = last_part.index(b',') + elif b'.' in last_part: + index = last_part.index(b'.') + else: + index = TOX_MAX_MESSAGE_LENGTH - size - 1 + index += size + 1 + messages.append(message[:index]) + message = message[index:] + if message: + messages.append(message) + + return messages + + def _get_friend_by_number(self, friend_number): + return self._contacts_provider.get_friend_by_number(friend_number) + + def _get_group_by_number(self, group_number): + return self._contacts_provider.get_group_by_number(group_number) + + def _get_group_by_public_key(self, public_key): + return self._contacts_provider.get_group_by_public_key( public_key) + + def _on_profile_name_changed(self, new_name): + if self._profile_name == new_name: + return + message = util_ui.tr('User {} is now known as {}') + message = message.format(self._profile_name, new_name) + for friend in self._contacts_provider.get_all_friends(): + self._add_info_message(friend.number, message) + self._profile_name = new_name + + def _on_call_started(self, friend_number, audio, video, is_outgoing): + if is_outgoing: + text = util_ui.tr("Outgoing video call") if video else util_ui.tr("Outgoing audio call") + else: + text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") + self._add_info_message(friend_number, text) + + def _on_call_finished(self, friend_number, is_declined): + text = util_ui.tr("Call declined") if is_declined else util_ui.tr("Call finished") + self._add_info_message(friend_number, text) + + def _add_info_message(self, friend_number, text): + friend = self._get_friend_by_number(friend_number) + message = InfoMessage(text, util.get_unix_time()) + friend.append_message(message) + if self._contacts_manager.is_friend_active(friend_number): + self._create_info_message_item(message) + + def _create_info_message_item(self, message): + self._items_factory.create_message_item(message) + self._screen.messages.scrollToBottom() + + def _add_message(self, text_message, contact): + if self._contacts_manager.is_contact_active(contact): # add message to list + self._create_message_item(text_message) + self._screen.messages.scrollToBottom() + self._contacts_manager.get_curr_contact().append_message(text_message) + else: + contact.inc_messages() + contact.append_message(text_message) + if not contact.visibility: + self._contacts_manager.update_filtration() + + def _create_message_item(self, text_message): + # pixmap = self._contacts_manager.get_curr_contact().get_pixmap() + self._items_factory.create_message_item(text_message) diff --git a/toxygen/middleware/__init__.py b/toxygen/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py new file mode 100644 index 0000000..b9a4099 --- /dev/null +++ b/toxygen/middleware/callbacks.py @@ -0,0 +1,605 @@ +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 + +# TODO: refactoring. Use contact provider instead of manager + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - current user +# ----------------------------------------------------------------------------------------------------------------- + + +def self_connection_status(tox, profile): + """ + Current user changed connection status (offline, TCP, UDP) + """ + 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) + + return wrapped + + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - friends +# ----------------------------------------------------------------------------------------------------------------- + + +def friend_status(contacts_manager, file_transfer_handler, profile, settings): + def wrapped(tox, friend_number, new_status, user_data): + """ + Check friend's status (none, busy, away) + """ + print("Friend's #{} status changed!".format(friend_number)) + 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']: + sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) + invoke_in_main_thread(friend.set_status, new_status) + + def set_timer(): + t = threading.Timer(5, lambda: file_transfer_handler.send_files(friend_number)) + t.start() + invoke_in_main_thread(set_timer) + invoke_in_main_thread(contacts_manager.update_filtration) + + return wrapped + + +def friend_connection_status(contacts_manager, profile, settings, plugin_loader, file_transfer_handler, + messenger, calls_manager): + def wrapped(tox, friend_number, new_status, user_data): + """ + Check friend's connection status (offline, udp, tcp) + """ + print("Friend #{} connection status: {}".format(friend_number, 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) + invoke_in_main_thread(file_transfer_handler.friend_exit, friend_number) + invoke_in_main_thread(contacts_manager.update_filtration) + invoke_in_main_thread(messenger.friend_typing, friend_number, False) + invoke_in_main_thread(calls_manager.friend_exit, friend_number) + if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) + elif friend.status is None: + invoke_in_main_thread(file_transfer_handler.send_avatar, friend_number) + invoke_in_main_thread(plugin_loader.friend_online, friend_number) + + return wrapped + + +def friend_name(contacts_provider, messenger): + def wrapped(tox, friend_number, name, size, user_data): + """ + Friend changed his name + """ + print('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') + invoke_in_main_thread(friend.set_name, new_name) + invoke_in_main_thread(messenger.new_friend_name, friend, old_name, new_name) + + return wrapped + + +def friend_status_message(contacts_manager, messenger): + 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) + invoke_in_main_thread(friend.set_status_message, str(status_message, 'utf-8')) + print('User #{} has new status message'.format(friend_number)) + invoke_in_main_thread(messenger.send_messages, friend_number) + + return wrapped + + +def friend_message(messenger, contacts_manager, profile, settings, window, tray): + def wrapped(tox, friend_number, message_type, message, size, user_data): + """ + New message from friend + """ + message = str(message, 'utf-8') + invoke_in_main_thread(messenger.new_message, friend_number, message_type, message) + if not window.isActiveWindow(): + friend = contacts_manager.get_friend_by_number(friend_number) + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: + invoke_in_main_thread(tray_notification, friend.name, message, tray, window) + 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)) + + return wrapped + + +def friend_request(contacts_manager): + def wrapped(tox, public_key, message, message_size, user_data): + """ + Called when user get new friend request + """ + print('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')) + + return wrapped + + +def friend_typing(messenger): + def wrapped(tox, friend_number, typing, user_data): + invoke_in_main_thread(messenger.friend_typing, friend_number, typing) + + return wrapped + + +def friend_read_receipt(messenger): + def wrapped(tox, friend_number, message_id, user_data): + invoke_in_main_thread(messenger.receipt, friend_number, message_id) + + return wrapped + + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - file transfers +# ----------------------------------------------------------------------------------------------------------------- + + +def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager, settings): + """ + New incoming file + """ + 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') + try: + file_name = str(file_name[:file_name_size], 'utf-8') + except: + file_name = 'toxygen_file' + invoke_in_main_thread(file_transfer_handler.incoming_file_transfer, + friend_number, + file_number, + size, + file_name) + if not window.isActiveWindow(): + friend = contacts_manager.get_friend_by_number(friend_number) + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: + file_from = util_ui.tr("File from") + invoke_in_main_thread(tray_notification, file_from + ' ' + friend.name, file_name, tray, window) + if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER']) + 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') + invoke_in_main_thread(file_transfer_handler.incoming_avatar, + friend_number, + file_number, + size) + return wrapped + + +def file_recv_chunk(file_transfer_handler): + """ + Incoming chunk + """ + def wrapped(tox, friend_number, file_number, position, chunk, length, user_data): + chunk = chunk[:length] if length else None + execute(file_transfer_handler.incoming_chunk, friend_number, file_number, position, chunk) + + return wrapped + + +def file_chunk_request(file_transfer_handler): + """ + Outgoing chunk + """ + def wrapped(tox, friend_number, file_number, position, size, user_data): + execute(file_transfer_handler.outgoing_chunk, friend_number, file_number, position, size) + + return wrapped + + +def file_recv_control(file_transfer_handler): + """ + Friend cancelled, paused or resumed file transfer + """ + def wrapped(tox, friend_number, file_number, file_control, user_data): + if file_control == TOX_FILE_CONTROL['CANCEL']: + file_transfer_handler.cancel_transfer(friend_number, file_number, True) + elif file_control == TOX_FILE_CONTROL['PAUSE']: + file_transfer_handler.pause_transfer(friend_number, file_number, True) + elif file_control == TOX_FILE_CONTROL['RESUME']: + file_transfer_handler.resume_transfer(friend_number, file_number, True) + + return wrapped + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - custom packets +# ----------------------------------------------------------------------------------------------------------------- + + +def lossless_packet(plugin_loader): + def wrapped(tox, friend_number, data, length, user_data): + """ + Incoming lossless packet + """ + data = data[:length] + invoke_in_main_thread(plugin_loader.callback_lossless, friend_number, data) + + return wrapped + + +def lossy_packet(plugin_loader): + def wrapped(tox, friend_number, data, length, user_data): + """ + Incoming lossy packet + """ + data = data[:length] + invoke_in_main_thread(plugin_loader.callback_lossy, friend_number, data) + + return wrapped + + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - audio +# ----------------------------------------------------------------------------------------------------------------- + +def call_state(calls_manager): + def wrapped(toxav, friend_number, mask, user_data): + """ + New call state + """ + print(friend_number, mask) + 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) + + return wrapped + + +def call(calls_manager): + def wrapped(toxav, friend_number, audio, video, user_data): + """ + Incoming call from friend + """ + print(friend_number, audio, video) + invoke_in_main_thread(calls_manager.incoming_call, audio, video, friend_number) + + return wrapped + + +def callback_audio(calls_manager): + def wrapped(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data): + """ + New audio chunk + """ + calls_manager.call.audio_chunk( + bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), + audio_channels_count, + rate) + + return wrapped + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - video +# ----------------------------------------------------------------------------------------------------------------- + + +def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data): + """ + Creates yuv frame from y, u, v and shows it using OpenCV + For yuv => bgr we need this YUV420 frame: + + width + ------------------------- + | | + | Y | height + | | + ------------------------- + | | | + | U even | U odd | height // 4 + | | | + ------------------------- + | | | + | V even | V odd | height // 4 + | | | + ------------------------- + + width // 2 width // 2 + + It can be created from initial y, u, v using slices + """ + try: + y_size = abs(max(width, abs(ystride))) + u_size = abs(max(width // 2, abs(ustride))) + v_size = abs(max(width // 2, abs(vstride))) + + y = np.asarray(y[:y_size * height], dtype=np.uint8).reshape(height, y_size) + u = np.asarray(u[:u_size * height // 2], dtype=np.uint8).reshape(height // 2, u_size) + v = np.asarray(v[:v_size * height // 2], dtype=np.uint8).reshape(height // 2, v_size) + + width -= width % 4 + height -= height % 4 + + frame = np.zeros((int(height * 1.5), width), dtype=np.uint8) + + frame[:height, :] = y[:height, :width] + frame[height:height * 5 // 4, :width // 2] = u[:height // 2:2, :width // 2] + frame[height:height * 5 // 4, width // 2:] = u[1:height // 2:2, :width // 2] + + frame[height * 5 // 4:, :width // 2] = v[:height // 2:2, :width // 2] + frame[height * 5 // 4:, width // 2:] = v[1:height // 2:2, :width // 2] + + frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + invoke_in_main_thread(cv2.imshow, str(friend_number), frame) + except Exception as ex: + print(ex) + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - groups +# ----------------------------------------------------------------------------------------------------------------- + + +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): + 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']: + 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)) + + return wrapped + + +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): + 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(): + 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']: + 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)) + + return wrapped + + +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): + 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: + 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) + invoke_in_main_thread(tray_notification, title, text, 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 + + +def group_self_join(contacts_provider, contacts_manager, groups_service): + def wrapped(tox, group_number, user_data): + 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) + invoke_in_main_thread(contacts_manager.update_filtration) + + return wrapped + + +def group_peer_join(contacts_provider, groups_service): + def wrapped(tox, group_number, peer_id, user_data): + group = contacts_provider.get_group_by_number(group_number) + group.add_peer(peer_id) + invoke_in_main_thread(groups_service.generate_peers_list) + invoke_in_main_thread(groups_service.update_group_info, group) + + return wrapped + + +def group_peer_exit(contacts_provider, groups_service, contacts_manager): + def wrapped(tox, group_number, peer_id, message, length, user_data): + 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): + 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) + + return wrapped + + +def group_peer_status(contacts_provider, groups_service): + def wrapped(tox, group_number, peer_id, peer_status, user_data): + group = contacts_provider.get_group_by_number(group_number) + peer = group.get_peer_by_id(peer_id) + peer.status = peer_status + invoke_in_main_thread(groups_service.generate_peers_list) + + return wrapped + + +def group_topic(contacts_provider): + def wrapped(tox, group_number, peer_id, topic, length, user_data): + group = contacts_provider.get_group_by_number(group_number) + topic = str(topic[:length], 'utf-8') + invoke_in_main_thread(group.set_status_message, topic) + + 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 + # 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) + # TODO: add info message + + def wrapped(tox, group_number, mod_peer_id, peer_id, event_type, user_data): + group = contacts_provider.get_group_by_number(group_number) + + 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']: + update_peer_role(group, mod_peer_id, peer_id, TOX_GROUP_ROLE['USER']) + elif event_type == TOX_GROUP_MOD_EVENT['MODERATOR']: + update_peer_role(group, mod_peer_id, peer_id, TOX_GROUP_ROLE['MODERATOR']) + + invoke_in_main_thread(groups_service.generate_peers_list) + + return wrapped + + +def group_password(contacts_provider): + + def wrapped(tox_link, group_number, password, length, user_data): + password = str(password[:length], 'utf-8') + group = contacts_provider.get_group_by_number(group_number) + group.password = password + + return wrapped + + +def group_peer_limit(contacts_provider): + + def wrapped(tox_link, group_number, peer_limit, user_data): + group = contacts_provider.get_group_by_number(group_number) + group.peer_limit = peer_limit + + return wrapped + + +def group_privacy_state(contacts_provider): + + def wrapped(tox_link, group_number, privacy_state, user_data): + group = contacts_provider.get_group_by_number(group_number) + group.is_private = privacy_state == TOX_GROUP_PRIVACY_STATE['PRIVATE'] + + return wrapped + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - initialization +# ----------------------------------------------------------------------------------------------------------------- + + +def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, + calls_manager, file_transfer_handler, main_window, tray, messenger, groups_service, + contacts_provider): + """ + Initialization of all callbacks. + :param tox: Tox instance + :param profile: Profile instance + :param settings: Settings instance + :param contacts_manager: ContactsManager instance + :param contacts_manager: ContactsManager instance + :param calls_manager: CallsManager instance + :param file_transfer_handler: FileTransferHandler instance + :param plugin_loader: PluginLoader instance + :param main_window: MainWindow instance + :param tray: tray (for notifications) + :param messenger: Messenger instance + :param groups_service: GroupsService instance + :param contacts_provider: ContactsProvider instance + """ + # self callbacks + tox.callback_self_connection_status(self_connection_status(tox, profile)) + + # friend callbacks + tox.callback_friend_status(friend_status(contacts_manager, file_transfer_handler, profile, settings)) + tox.callback_friend_message(friend_message(messenger, contacts_manager, profile, settings, main_window, tray)) + tox.callback_friend_connection_status(friend_connection_status(contacts_manager, profile, settings, plugin_loader, + file_transfer_handler, messenger, calls_manager)) + tox.callback_friend_name(friend_name(contacts_provider, messenger)) + tox.callback_friend_status_message(friend_status_message(contacts_manager, messenger)) + tox.callback_friend_request(friend_request(contacts_manager)) + tox.callback_friend_typing(friend_typing(messenger)) + tox.callback_friend_read_receipt(friend_read_receipt(messenger)) + + # file transfer + tox.callback_file_recv(tox_file_recv(main_window, tray, profile, file_transfer_handler, + contacts_manager, settings)) + tox.callback_file_recv_chunk(file_recv_chunk(file_transfer_handler)) + tox.callback_file_chunk_request(file_chunk_request(file_transfer_handler)) + tox.callback_file_recv_control(file_recv_control(file_transfer_handler)) + + # av + toxav = tox.AV + toxav.callback_call_state(call_state(calls_manager), 0) + toxav.callback_call(call(calls_manager), 0) + toxav.callback_audio_receive_frame(callback_audio(calls_manager), 0) + toxav.callback_video_receive_frame(video_receive_frame, 0) + + # custom packets + tox.callback_friend_lossless_packet(lossless_packet(plugin_loader)) + tox.callback_friend_lossy_packet(lossy_packet(plugin_loader)) + + # gc callbacks + tox.callback_group_message(group_message(main_window, tray, tox, messenger, settings, profile), 0) + tox.callback_group_private_message(group_private_message(main_window, tray, tox, messenger, settings, profile), 0) + tox.callback_group_invite(group_invite(main_window, settings, tray, profile, groups_service, contacts_provider), 0) + tox.callback_group_self_join(group_self_join(contacts_provider, contacts_manager, groups_service), 0) + tox.callback_group_peer_join(group_peer_join(contacts_provider, groups_service), 0) + tox.callback_group_peer_exit(group_peer_exit(contacts_provider, groups_service, contacts_manager), 0) + tox.callback_group_peer_name(group_peer_name(contacts_provider, groups_service), 0) + tox.callback_group_peer_status(group_peer_status(contacts_provider, groups_service), 0) + tox.callback_group_topic(group_topic(contacts_provider), 0) + tox.callback_group_moderation(group_moderation(groups_service, contacts_provider, contacts_manager, messenger), 0) + tox.callback_group_password(group_password(contacts_provider), 0) + tox.callback_group_peer_limit(group_peer_limit(contacts_provider), 0) + tox.callback_group_privacy_state(group_privacy_state(contacts_provider), 0) diff --git a/toxygen/middleware/threads.py b/toxygen/middleware/threads.py new file mode 100644 index 0000000..5f9404b --- /dev/null +++ b/toxygen/middleware/threads.py @@ -0,0 +1,172 @@ +from bootstrap.bootstrap import * +import threading +import queue +from utils import util +import time +from PyQt5 import QtCore + + +# ----------------------------------------------------------------------------------------------------------------- +# Base threads +# ----------------------------------------------------------------------------------------------------------------- + +class BaseThread(threading.Thread): + + def __init__(self): + super().__init__() + self._stop_thread = False + + def stop_thread(self): + self._stop_thread = True + self.join() + + +class BaseQThread(QtCore.QThread): + + def __init__(self): + super().__init__() + self._stop_thread = False + + def stop_thread(self): + self._stop_thread = True + self.wait() + + +# ----------------------------------------------------------------------------------------------------------------- +# Toxcore threads +# ----------------------------------------------------------------------------------------------------------------- + +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 + 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 + try: + for data in generate_nodes(): + if self._stop_thread: + return + self._tox.bootstrap(*data) + self._tox.add_tcp_relay(*data) + except: + pass + + 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 self._stop_thread: + return + self._tox.bootstrap(*data) + self._tox.add_tcp_relay(*data) + except: + pass + finally: + time.sleep(5) + + +class ToxIterateThread(BaseQThread): + + def __init__(self, tox): + super().__init__() + self._tox = tox + + def run(self): + while not self._stop_thread: + self._tox.iterate() + time.sleep(self._tox.iteration_interval() / 1000) + + +class ToxAVIterateThread(BaseQThread): + + def __init__(self, toxav): + super().__init__() + self._toxav = toxav + + def run(self): + while not self._stop_thread: + self._toxav.iterate() + time.sleep(self._toxav.iteration_interval() / 1000) + + +# ----------------------------------------------------------------------------------------------------------------- +# File transfers thread +# ----------------------------------------------------------------------------------------------------------------- + +class FileTransfersThread(BaseQThread): + + def __init__(self): + super().__init__() + self._queue = queue.Queue() + self._timeout = 0.01 + + def execute(self, func, *args, **kwargs): + self._queue.put((func, args, kwargs)) + + def run(self): + while not self._stop_thread: + try: + func, args, kwargs = self._queue.get(timeout=self._timeout) + func(*args, **kwargs) + except queue.Empty: + pass + except queue.Full: + util.log('Queue is full in _thread') + except Exception as ex: + util.log('Exception in _thread: ' + str(ex)) + + +_thread = FileTransfersThread() + + +def start_file_transfer_thread(): + _thread.start() + + +def stop_file_transfer_thread(): + _thread.stop_thread() + + +def execute(func, *args, **kwargs): + _thread.execute(func, *args, **kwargs) + + +# ----------------------------------------------------------------------------------------------------------------- +# Invoking in main thread +# ----------------------------------------------------------------------------------------------------------------- + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) diff --git a/toxygen/middleware/tox_factory.py b/toxygen/middleware/tox_factory.py new file mode 100644 index 0000000..9ee5c01 --- /dev/null +++ b/toxygen/middleware/tox_factory.py @@ -0,0 +1,34 @@ +import user_data.settings +import wrapper.tox +import wrapper.toxcore_enums_and_consts as enums +import ctypes + + +def tox_factory(data=None, settings=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: + settings = user_data.settings.Settings.get_default_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 + + return wrapper.tox.Tox(tox_options) diff --git a/toxygen/network/__init__.py b/toxygen/network/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/network/tox_dns.py b/toxygen/network/tox_dns.py new file mode 100644 index 0000000..02e97f5 --- /dev/null +++ b/toxygen/network/tox_dns.py @@ -0,0 +1,65 @@ +import json +import urllib.request +import utils.util as util +from PyQt5 import QtNetwork, QtCore + + +class ToxDns: + + def __init__(self, settings): + self._settings = settings + + @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 not res['c']: + return res['tox_id'] + else: + raise LookupError() + + def lookup(self, email): + """ + TOX DNS 4 + :param email: data like 'groupbot@toxme.io' + :return: tox id on success else None + """ + 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 + for url in urls: + try: + return self._send_request(url, data) + except Exception as ex: + util.log('TOX DNS ERROR: ' + str(ex)) + else: # proxy + netman = QtNetwork.QNetworkAccessManager() + proxy = QtNetwork.QNetworkProxy() + if self._settings['proxy_type'] == 2: + proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy) + else: + proxy.setType(QtNetwork.QNetworkProxy.HttpProxy) + proxy.setHostName(self._settings['proxy_host']) + proxy.setPort(self._settings['proxy_port']) + netman.setProxy(proxy) + for url in urls: + try: + request = QtNetwork.QNetworkRequest() + request.setUrl(QtCore.QUrl(url)) + request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json") + reply = netman.post(request, bytes(json.dumps(data), 'utf-8')) + + while not reply.isFinished(): + QtCore.QThread.msleep(1) + QtCore.QCoreApplication.processEvents() + data = bytes(reply.readAll().data()) + result = json.loads(str(data, 'utf-8')) + if not result['c']: + return result['tox_id'] + except Exception as ex: + util.log('TOX DNS ERROR: ' + str(ex)) + + return None # error diff --git a/toxygen/nodes.json b/toxygen/nodes.json deleted file mode 100644 index 003bbc0..0000000 --- a/toxygen/nodes.json +++ /dev/null @@ -1 +0,0 @@ -{"last_scan":1516822981,"last_refresh":1516822982,"nodes":[{"ipv4":"node.tox.biribiri.org","ipv6":"-","port":33445,"tcp_ports":[3389,33445],"public_key":"F404ABAA1C99A9D37D61AB54898F56793E1DEF8BD46B1038B9D822E8460FAB67","maintainer":"nurupo","location":"US","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Welcome, stranger #7985. I'm up for 5d 14h 34m 34s, running since Jan 19 05:08:27 UTC. If I get outdated, please ping my maintainer at nurupo.contributions@gmail.com","last_ping":1516822981},{"ipv4":"nodes.tox.chat","ipv6":"-","port":33445,"tcp_ports":[3389,33445],"public_key":"6FC41E2BD381D37E9748FC0E0328CE086AF9598BECC8FEB7DDF2E440475F300E","maintainer":"Impyy","location":"NL","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Straps boots like no other","last_ping":1516822981},{"ipv4":"130.133.110.14","ipv6":"2001:6f8:1c3c:babe::14:1","port":33445,"tcp_ports":[33445],"public_key":"461FA3776EF0FA655F1A05477DF1B3B614F7D6B124F7DB1DD4FE3C08B03B640F","maintainer":"Manolis","location":"DE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Spline tox bootstrap node","last_ping":1516822981},{"ipv4":"205.185.116.116","ipv6":"-","port":33445,"tcp_ports":[3389,33445],"public_key":"A179B09749AC826FF01F37A9613F6B57118AE014D4196A0E1105A98F93A54702","maintainer":"Busindre","location":"US","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"198.98.51.198","ipv6":"2605:6400:1:fed5:22:45af:ec10:f329","port":33445,"tcp_ports":[33445,3389],"public_key":"1D5A5F2F5D6233058BF0259B09622FB40B482E4FA0931EB8FD3AB8E7BF7DAF6F","maintainer":"Busindre","location":"US","status_udp":true,"status_tcp":true,"version":"2014101200","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"85.172.30.117","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"8E7D0B859922EF569298B4D261A8CCB5FEA14FB91ED412A7603A585A25698832","maintainer":"ray65536","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Ray's Tox Node","last_ping":1516822981},{"ipv4":"194.249.212.109","ipv6":"2001:1470:fbfe::109","port":33445,"tcp_ports":[33445,3389],"public_key":"3CEE1F054081E7A011234883BC4FC39F661A55B73637A5AC293DDF1251D9432B","maintainer":"fluke571","location":"SI","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"185.25.116.107","ipv6":"2a00:7a60:0:746b::3","port":33445,"tcp_ports":[33445,3389],"public_key":"DA4E4ED4B697F2E9B000EEFE3A34B554ACD3F45F5C96EAEA2516DD7FF9AF7B43","maintainer":"MAH69K","location":"UA","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Saluton! Mia Tox ID: B229B7BD68FC66C2716EAB8671A461906321C764782D7B3EDBB650A315F6C458EF744CE89F07. Scribu! ;)","last_ping":1516822981},{"ipv4":"5.189.176.217","ipv6":"2a02:c200:1:10:3:1:605:1337","port":5190,"tcp_ports":[3389,33445,5190],"public_key":"2B2137E094F743AC8BD44652C55F41DFACC502F125E99E4FE24D40537489E32F","maintainer":"tastytea","location":"DE","status_udp":true,"status_tcp":true,"version":"","motd":"","last_ping":1516822981},{"ipv4":"217.182.143.254","ipv6":"2001:41d0:302:1000::e111","port":2306,"tcp_ports":[33445,2306,443],"public_key":"7AED21F94D82B05774F697B209628CD5A9AD17E0C073D9329076A4C28ED28147","maintainer":"pucetox","location":"FR","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"by pucetox,\nipv4/ipv6 UDP:2306 TCP:21/80/443/2306/33445\nsync your nodes here tox.0x10k.com/bootstrapd-conf , \n for communication: 1D1C0B992DEB6D7F18561176F7F5E572BCC7F2BA5CFA7E9E437B9134122CE96D906A6119F9D2","last_ping":1516822981},{"ipv4":"104.223.122.15","ipv6":"2607:ff48:aa81:800::35eb:1","port":33445,"tcp_ports":[3389,33445],"public_key":"0FB96EEBFB1650DDB52E70CF773DDFCABE25A95CC3BB50FC251082E4B63EF82A","maintainer":"ru_maniac","location":"US","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"built on: Tue Feb 21st 2017, 10:52:30 UTC+3\nplease note: running on TokTox Toxcore!\nmore info on the matter: goo.gl/Gz5KhK \u0026 goo.gl/i2TZJr\n\ntox id for queries and general info: EBD2A7B649ABB10ED9F47E5113F04000F39D46F087CEB62FCCE1069471FD6915256D197F2A97","last_ping":1516822981},{"ipv4":"tox.verdict.gg","ipv6":"-","port":33445,"tcp_ports":[33445,3389],"public_key":"1C5293AEF2114717547B39DA8EA6F1E331E5E358B35F9B6B5F19317911C5F976","maintainer":"Deliran","location":"DE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Praise The Sun!","last_ping":1516822981},{"ipv4":"d4rk4.ru","ipv6":"-","port":1813,"tcp_ports":[1813],"public_key":"53737F6D47FA6BD2808F378E339AF45BF86F39B64E79D6D491C53A1D522E7039","maintainer":"D4rk4","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"TOX ID: 35EDC07AEB18B163E07EE33F6CDDA63969F394FF6A617CEAB22A7EBBEAAAF854C0EDFBD46898","last_ping":1516822981},{"ipv4":"51.254.84.212","ipv6":"2001:41d0:a:1a3b::18","port":33445,"tcp_ports":[3389,33445],"public_key":"AEC204B9A4501412D5F0BB67D9C81B5DB3EE6ADA64122D32A3E9B093D544327D","maintainer":"a68366","location":"FR","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Since 26.12.2015","last_ping":1516822981},{"ipv4":"88.99.133.52","ipv6":"-","port":33445,"tcp_ports":[3389,33445],"public_key":"2D320F971EF2CA18004416C2AAE7BA52BF7949DB34EA8E2E21AF67BD367BE211","maintainer":"Skey","location":"FR","status_udp":true,"status_tcp":true,"version":"2014101200","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"92.54.84.70","ipv6":"-","port":33445,"tcp_ports":[],"public_key":"5625A62618CB4FCA70E147A71B29695F38CC65FF0CBD68AD46254585BE564802","maintainer":"t3mp","location":"RU","status_udp":true,"status_tcp":false,"version":"2016010100","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"tox.uplinklabs.net","ipv6":"tox.uplinklabs.net","port":33445,"tcp_ports":[3389,33445],"public_key":"1A56EA3EDF5DF4C0AEABBF3C2E4E603890F87E983CAC8A0D532A335F2C6E3E1F","maintainer":"AbacusAvenger","location":"US","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"i don't know what this is for","last_ping":1516822981},{"ipv4":"toxnode.nek0.net","ipv6":"toxnode.nek0.net","port":33445,"tcp_ports":[3389,33445],"public_key":"20965721D32CE50C3E837DD75B33908B33037E6225110BFF209277AEAF3F9639","maintainer":"Phsm","location":"UA","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"95.215.44.78","ipv6":"2a02:7aa0:1619::c6fe:d0cb","port":33445,"tcp_ports":[33445,3389],"public_key":"672DBE27B4ADB9D5FB105A6BB648B2F8FDB89B3323486A7A21968316E012023C","maintainer":"HooinKyoma","location":"SE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Thanx to Hooin Kyoma","last_ping":1516822981},{"ipv4":"163.172.136.118","ipv6":"2001:bc8:4400:2100::1c:50f","port":33445,"tcp_ports":[33445,3389],"public_key":"2C289F9F37C20D09DA83565588BF496FAB3764853FA38141817A72E3F18ACA0B","maintainer":"LittleVulpix","location":"FR","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"LittleTox - your friendly neighbourhood tox node!","last_ping":1516822981},{"ipv4":"sorunome.de","ipv6":"sorunome.de","port":33445,"tcp_ports":[3389,33445],"public_key":"02807CF4F8BB8FB390CC3794BDF1E8449E9A8392C5D3F2200019DA9F1E812E46","maintainer":"Sorunome","location":"DE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Keep calm and pony on","last_ping":1516822981},{"ipv4":"37.97.185.116","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"E59A0E71ADA20D35BD1B0957059D7EF7E7792B3D680AE25C6F4DBBA09114D165","maintainer":"Yani","location":"NL","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Yani's node of pleasure and leisure","last_ping":1516822981},{"ipv4":"80.87.193.193","ipv6":"2a01:230:2:6::46a8","port":33445,"tcp_ports":[3389,33445],"public_key":"B38255EE4B054924F6D79A5E6E5889EC94B6ADF6FE9906F97A3D01E3D083223A","maintainer":"linxon","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Tox DHT node by Linxon. Author ToxID: EC774ED05A7E71EEE2EBA939A27CD4FF403D7D79E1E685CFD0394B1770498217C6107E4D3C26","last_ping":1516822981},{"ipv4":"initramfs.io","ipv6":"-","port":33445,"tcp_ports":[3389,33445],"public_key":"3F0A45A268367C1BEA652F258C85F4A66DA76BCAA667A49E770BCC4917AB6A25","maintainer":"initramfs","location":"TW","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"initramfs' Tox DHT Node","last_ping":1516822981},{"ipv4":"hibiki.eve.moe","ipv6":"hibiki.eve.moe","port":33445,"tcp_ports":[33445],"public_key":"D3EB45181B343C2C222A5BCF72B760638E15ED87904625AAD351C594EEFAE03E","maintainer":"EveNeko","location":"FR","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"tox-bootstrapd@hibiki.eve.moe","last_ping":1516822981},{"ipv4":"tox.deadteam.org","ipv6":"tox.deadteam.org","port":33445,"tcp_ports":[33445],"public_key":"C7D284129E83877D63591F14B3F658D77FF9BA9BA7293AEB2BDFBFE1A803AF47","maintainer":"DeadTeam","location":"DE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Vive le TOX","last_ping":1516822981},{"ipv4":"46.229.52.198","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"813C8F4187833EF0655B10F7752141A352248462A567529A38B6BBF73E979307","maintainer":"Stranger","location":"UA","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Freedom to parrots!","last_ping":1516822981},{"ipv4":"node.tox.ngc.network","ipv6":"node.tox.ngc.network","port":33445,"tcp_ports":[3389,33445],"public_key":"A856243058D1DE633379508ADCAFCF944E40E1672FF402750EF712E30C42012A","maintainer":"Nolz","location":"DE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Unlike Others","last_ping":1516822981},{"ipv4":"149.56.140.5","ipv6":"2607:5300:0201:3100:0000:0000:0000:3ec2","port":33445,"tcp_ports":[3389,33445],"public_key":"7E5668E0EE09E19F320AD47902419331FFEE147BB3606769CFBE921A2A2FD34C","maintainer":"velusip","location":"CA","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Jera","last_ping":1516822981},{"ipv4":"185.14.30.213","ipv6":"2a00:1ca8:a7::e8b","port":443,"tcp_ports":[33445,3389,443],"public_key":"2555763C8C460495B14157D234DD56B86300A2395554BCAE4621AC345B8C1B1B","maintainer":"dvor","location":"NL","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Just another tox node.","last_ping":1516822981},{"ipv4":"tox.natalenko.name","ipv6":"tox.natalenko.name","port":33445,"tcp_ports":[33445],"public_key":"1CB6EBFD9D85448FA70D3CAE1220B76BF6FCE911B46ACDCF88054C190589650B","maintainer":"post-factum","location":"DE","status_udp":true,"status_tcp":true,"version":"","motd":"","last_ping":1516822981},{"ipv4":"136.243.141.187","ipv6":"2a01:4f8:212:2459::a:1337","port":443,"tcp_ports":[33445,3389,443],"public_key":"6EE1FADE9F55CC7938234CC07C864081FC606D8FE7B751EDA217F268F1078A39","maintainer":"CeBe","location":"DE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"uTox is the future! - maintained by CeBe - contact: tox@cebe.cc - tox: 7F50119368DC8FD3B1ECAF5D18E3F8854F0484CEC5BBF625D420B8E38638733C02486E387AF8","last_ping":1516822981},{"ipv4":"tox.abilinski.com","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"0E9D7FEE2AA4B42A4C18FE81C038E32FFD8D907AAA7896F05AA76C8D31A20065","maintainer":"flobe","location":"CA","status_udp":true,"status_tcp":true,"version":"","motd":"","last_ping":1516822981},{"ipv4":"m.loskiq.it","ipv6":"-","port":33445,"tcp_ports":[33445,3389],"public_key":"88124F3C18C6CFA8778B7679B7329A333616BD27A4DFB562D476681315CF143D","maintainer":"loskiq","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"https://t.me/loskiq","last_ping":1516822981},{"ipv4":"192.99.232.158","ipv6":"-","port":33445,"tcp_ports":[],"public_key":"7B6CB208C811DEA8782711CE0CAD456AAC0C7B165A0498A1AA7010D2F2EC996C","maintainer":"basiljose","location":"CA","status_udp":true,"status_tcp":false,"version":"2016010100","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"tmux.ru","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"7467AFA626D3246343170B309BA5BDC975DF3924FC9D7A5917FBFA9F5CD5CD38","maintainer":"nrn","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"https://t.me/nyoroon","last_ping":1516822981},{"ipv4":"37.48.122.22","ipv6":"2001:1af8:4700:a115:6::b","port":33445,"tcp_ports":[33445,3389],"public_key":"1B5A8AB25FFFB66620A531C4646B47F0F32B74C547B30AF8BD8266CA50A3AB59","maintainer":"Pokemon","location":"NL","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"Those who would give up essential Liberty, to purchase a little temporary Safety, deserve neither Liberty nor Safety","last_ping":1516822981},{"ipv4":"tox.novg.net","ipv6":"-","port":33445,"tcp_ports":[33445,3389],"public_key":"D527E5847F8330D628DAB1814F0A422F6DC9D0A300E6C357634EE2DA88C35463","maintainer":"blind_oracle","location":"NL","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"t0x-node1.weba.ru","ipv6":"-","port":33445,"tcp_ports":[3389,33445],"public_key":"5A59705F86B9FC0671FDF72ED9BB5E55015FF20B349985543DDD4B0656CA1C63","maintainer":"Amin","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"T0X-Node #1","last_ping":1516822981},{"ipv4":"109.195.99.39","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"EF937F61B4979B60BBF306752D8F32029A2A05CD2615B2E9FBFFEADD8E7D5032","maintainer":"NaCl","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"NaCl node respond","last_ping":1516822981},{"ipv4":"79.140.30.52","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"FFAC871E85B1E1487F87AE7C76726AE0E60318A85F6A1669E04C47EB8DC7C72D","maintainer":"warlomak","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"tox-easy-bootstrap","last_ping":1516822981},{"ipv4":"94.41.167.70","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"E519B2C1098999B60190012C7B53E8C43A73C535721036CD9DEC7CCA06741A7D","maintainer":"warlomak","location":"RU","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"tox-easy-bootstrap","last_ping":1516822981},{"ipv4":"104.223.122.204","ipv6":"-","port":33445,"tcp_ports":[3389],"public_key":"3925752E43BF2F8EB4E12B0E9414311064FF2D76707DC7D5D2CCB43F75081F6B","maintainer":"ru_maniac","location":"US","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"rmnc_third_node","last_ping":1516822981},{"ipv4":"77.55.211.53","ipv6":"-","port":53,"tcp_ports":[443,33445,3389],"public_key":"B9D109CC820C69A5D97A4A1A15708107C6BA85C13BC6188CC809D374AFF18E63","maintainer":"GDR!","location":"PL","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"GDR!'s tox-bootstrapd https://gdr.name/","last_ping":1516822922},{"ipv4":"boseburo.ddns.net","ipv6":"-","port":33445,"tcp_ports":[33445],"public_key":"AF3FC9FC3D121E82E362B4FA84A53E63F58C11C2BA61D988855289B8CABC9B18","maintainer":"LowEel","location":"DE","status_udp":true,"status_tcp":true,"version":"2016010100","motd":"This is the Bose Buro bootstrap daemon","last_ping":1516822981},{"ipv4":"46.101.197.175","ipv6":"2a03:b0c0:3:d0::ac:5001","port":443,"tcp_ports":[443,33445,3389],"public_key":"CD133B521159541FB1D326DE9850F5E56A6C724B5B8E5EB5CD8D950408E95707","maintainer":"clearmartin","location":"DE","status_udp":false,"status_tcp":true,"version":"2014101200","motd":"tox-bootstrapd","last_ping":1516822981},{"ipv4":"104.233.104.126","ipv6":"-","port":33445,"tcp_ports":[],"public_key":"EDEE8F2E839A57820DE3DA4156D88350E53D4161447068A3457EE8F59F362414","maintainer":"wildermesser","location":"CA","status_udp":false,"status_tcp":false,"version":"","motd":"","last_ping":0},{"ipv4":"195.93.190.6","ipv6":"2a01:d0:ffff:a8a::2","port":33445,"tcp_ports":[],"public_key":"FB4CE0DDEFEED45F26917053E5D24BDDA0FA0A3D83A672A9DA2375928B37023D","maintainer":"strngr","location":"UA","status_udp":false,"status_tcp":false,"version":"2016010100","motd":"tox node at strngr.name","last_ping":1516816803},{"ipv4":"193.124.186.205","ipv6":"2a02:f680:1:1100::542a","port":5228,"tcp_ports":[],"public_key":"9906D65F2A4751068A59D30505C5FC8AE1A95E0843AE9372EAFA3BAB6AC16C2C","maintainer":"Cactus","location":"RU","status_udp":false,"status_tcp":false,"version":"","motd":"","last_ping":0},{"ipv4":"85.21.144.224","ipv6":"-","port":33445,"tcp_ports":[],"public_key":"8F738BBC8FA9394670BCAB146C67A507B9907C8E564E28C2B59BEBB2FF68711B","maintainer":"himura","location":"RU","status_udp":false,"status_tcp":false,"version":"","motd":"","last_ping":0},{"ipv4":"37.187.122.30","ipv6":"-","port":33445,"tcp_ports":[],"public_key":"BEB71F97ED9C99C04B8489BB75579EB4DC6AB6F441B603D63533122F1858B51D","maintainer":"dolohow","location":"FR","status_udp":false,"status_tcp":false,"version":"2016010100","motd":"#stay frosty 8218DB335926393789859EDF2D79AC4CC805ADF73472D08165FEA51555502A58AE84FCE7C3D4","last_ping":1515853621},{"ipv4":"95.215.46.114","ipv6":"2a02:7aa0:1619::bdbd:17b8","port":33445,"tcp_ports":[],"public_key":"5823FB947FF24CF83DDFAC3F3BAA18F96EA2018B16CC08429CB97FA502F40C23","maintainer":"isotoxin","location":"SE","status_udp":false,"status_tcp":false,"version":"","motd":"","last_ping":0},{"ipv4":"tox.dumalogiya.ru","ipv6":"-","port":33445,"tcp_ports":[],"public_key":"2DAE6EB8C16131761A675D7C723F618FBA9D29DD8B4E0A39E7E3E8D7055EF113","maintainer":"mikhailnov","location":"RU","status_udp":false,"status_tcp":false,"version":"","motd":"","last_ping":0}]} \ No newline at end of file diff --git a/toxygen/notifications.py b/toxygen/notifications.py deleted file mode 100644 index 26a29ec..0000000 --- a/toxygen/notifications.py +++ /dev/null @@ -1,71 +0,0 @@ -from PyQt5 import QtCore, QtWidgets -from util import curr_directory -import wave -import pyaudio - - -SOUND_NOTIFICATION = { - 'MESSAGE': 0, - 'FRIEND_CONNECTION_STATUS': 1, - 'FILE_TRANSFER': 2 -} - - -def tray_notification(title, text, tray, window): - """ - Show tray notification and activate window icon - NOTE: different behaviour on different OS - :param title: Name of user who sent message or file - :param text: text of message or file info - :param tray: ref to tray icon - :param window: main window - """ - if QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): - if len(text) > 30: - text = text[:27] + '...' - tray.showMessage(title, text, QtWidgets.QSystemTrayIcon.NoIcon, 3000) - QtWidgets.QApplication.alert(window, 0) - - def message_clicked(): - window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) - window.activateWindow() - tray.messageClicked.connect(message_clicked) - - -class AudioFile: - chunk = 1024 - - def __init__(self, fl): - self.wf = wave.open(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 play(self): - data = self.wf.readframes(self.chunk) - while data: - self.stream.write(data) - data = self.wf.readframes(self.chunk) - - def close(self): - self.stream.close() - self.p.terminate() - - -def sound_notification(t): - """ - Plays sound notification - :param t: type of notification - """ - if t == SOUND_NOTIFICATION['MESSAGE']: - f = curr_directory() + '/sounds/message.wav' - elif t == SOUND_NOTIFICATION['FILE_TRANSFER']: - f = curr_directory() + '/sounds/file.wav' - else: - f = curr_directory() + '/sounds/contact.wav' - a = AudioFile(f) - a.play() - a.close() diff --git a/toxygen/notifications/__init__.py b/toxygen/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/notifications/sound.py b/toxygen/notifications/sound.py new file mode 100644 index 0000000..361cd05 --- /dev/null +++ b/toxygen/notifications/sound.py @@ -0,0 +1,54 @@ +import utils.util +import wave +import pyaudio +import os.path + + +SOUND_NOTIFICATION = { + 'MESSAGE': 0, + 'FRIEND_CONNECTION_STATUS': 1, + 'FILE_TRANSFER': 2 +} + + +class AudioFile: + chunk = 1024 + + def __init__(self, fl): + self.wf = wave.open(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 play(self): + data = self.wf.readframes(self.chunk) + while data: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + + def close(self): + self.stream.close() + self.p.terminate() + + +def sound_notification(t): + """ + Plays sound notification + :param t: type of notification + """ + if t == SOUND_NOTIFICATION['MESSAGE']: + f = get_file_path('message.wav') + elif t == SOUND_NOTIFICATION['FILE_TRANSFER']: + f = get_file_path('file.wav') + else: + f = get_file_path('contact.wav') + a = AudioFile(f) + a.play() + a.close() + + +def get_file_path(file_name): + return os.path.join(utils.util.get_sounds_directory(), file_name) diff --git a/toxygen/notifications/tray.py b/toxygen/notifications/tray.py new file mode 100644 index 0000000..4232253 --- /dev/null +++ b/toxygen/notifications/tray.py @@ -0,0 +1,22 @@ +from PyQt5 import QtCore, QtWidgets + + +def tray_notification(title, text, tray, window): + """ + Show tray notification and activate window icon + NOTE: different behaviour on different OS + :param title: Name of user who sent message or file + :param text: text of message or file info + :param tray: ref to tray icon + :param window: main window + """ + if QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + if len(text) > 30: + text = text[:27] + '...' + tray.showMessage(title, text, QtWidgets.QSystemTrayIcon.NoIcon, 3000) + QtWidgets.QApplication.alert(window, 0) + + def message_clicked(): + window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + window.activateWindow() + tray.messageClicked.connect(message_clicked) diff --git a/toxygen/plugin_support/__init__.py b/toxygen/plugin_support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/plugin_support.py b/toxygen/plugin_support/plugin_support.py similarity index 50% rename from toxygen/plugin_support.py rename to toxygen/plugin_support/plugin_support.py index 0ff7421..ed45910 100644 --- a/toxygen/plugin_support.py +++ b/toxygen/plugin_support/plugin_support.py @@ -1,36 +1,50 @@ -import util -import profile +import utils.util as util import os import importlib import inspect import plugins.plugin_super_class as pl -import toxes import sys -class PluginLoader(util.Singleton): +class Plugin: - def __init__(self, tox, settings): - super().__init__() - self._profile = profile.Profile.get_instance() + def __init__(self, plugin, is_active): + self._instance = plugin + self._is_active = is_active + + def get_instance(self): + return self._instance + + instance = property(get_instance) + + def get_is_active(self): + return self._is_active + + def set_is_active(self, is_active): + self._is_active = is_active + + is_active = property(get_is_active, set_is_active) + + +class PluginLoader: + + def __init__(self, settings, app): self._settings = settings - self._plugins = {} # dict. key - plugin unique short name, value - tuple (plugin instance, is active) - self._tox = tox - self._encr = toxes.ToxES.get_instance() + self._app = app + self._plugins = {} # dict. key - plugin unique short name, value - Plugin instance def set_tox(self, tox): """ New tox instance """ - self._tox = tox - for value in self._plugins.values(): - value[0].set_tox(tox) + for plugin in self._plugins.values(): + plugin.instance.set_tox(tox) def load(self): """ Load all plugins in plugins folder """ - path = util.curr_directory() + '/plugins/' + path = util.get_plugins_directory() if not os.path.exists(path): util.log('Plugin dir not found') return @@ -52,18 +66,19 @@ class PluginLoader(util.Singleton): for elem in dir(module): obj = getattr(module, elem) # looking for plugin class in module - if inspect.isclass(obj) and hasattr(obj, 'is_plugin') and obj.is_plugin: - print('Plugin', elem) - try: # create instance of plugin class - inst = obj(self._tox, self._profile, self._settings, self._encr) - autostart = inst.get_short_name() in self._settings['plugins'] - if autostart: - inst.start() - except Exception as ex: - util.log('Exception in module ' + name + ' Exception: ' + str(ex)) - continue - self._plugins[inst.get_short_name()] = [inst, autostart] # (inst, is active) - break + 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'] + if is_active: + instance.start() + except Exception as ex: + util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + continue + self._plugins[instance.get_short_name()] = Plugin(instance, is_active) + break def callback_lossless(self, friend_number, data): """ @@ -71,8 +86,8 @@ class PluginLoader(util.Singleton): """ l = data[0] - pl.LOSSLESS_FIRST_BYTE name = ''.join(chr(x) for x in data[1:l + 1]) - if name in self._plugins and self._plugins[name][1]: - self._plugins[name][0].lossless_packet(''.join(chr(x) for x in data[l + 1:]), friend_number) + if name in self._plugins and self._plugins[name].is_active: + self._plugins[name].instance.lossless_packet(''.join(chr(x) for x in data[l + 1:]), friend_number) def callback_lossy(self, friend_number, data): """ @@ -80,37 +95,38 @@ class PluginLoader(util.Singleton): """ l = data[0] - pl.LOSSY_FIRST_BYTE name = ''.join(chr(x) for x in data[1:l + 1]) - if name in self._plugins and self._plugins[name][1]: - self._plugins[name][0].lossy_packet(''.join(chr(x) for x in data[l + 1:]), friend_number) + if name in self._plugins and self._plugins[name].is_active: + self._plugins[name].instance.lossy_packet(''.join(chr(x) for x in data[l + 1:]), friend_number) def friend_online(self, friend_number): """ Friend with specified number is online """ - for elem in self._plugins.values(): - if elem[1]: - elem[0].friend_connected(friend_number) + for plugin in self._plugins.values(): + if plugin.is_active: + plugin.instance.friend_connected(friend_number) def get_plugins_list(self): """ Returns list of all plugins """ result = [] - for data in self._plugins.values(): + for plugin in self._plugins.values(): try: - result.append([data[0].get_name(), # plugin full name - data[1], # is enabled - data[0].get_description(), # plugin description - data[0].get_short_name()]) # key - short unique name + result.append([plugin.instance.get_name(), # plugin full name + plugin.is_active, # is enabled + plugin.instance.get_description(), # plugin description + plugin.instance.get_short_name()]) # key - short unique name except: continue + return result def plugin_window(self, key): """ Return window or None for specified plugin """ - return self._plugins[key][0].get_window() + return self._plugins[key].instance.get_window() def toggle_plugin(self, key): """ @@ -118,12 +134,12 @@ class PluginLoader(util.Singleton): :param key: plugin short name """ plugin = self._plugins[key] - if plugin[1]: - plugin[0].stop() + if plugin.is_active: + plugin.instance.stop() else: - plugin[0].start() - plugin[1] = not plugin[1] - if plugin[1]: + plugin.instance.start() + plugin.is_active = not plugin.is_active + if plugin.is_active: self._settings['plugins'].append(key) else: self._settings['plugins'].remove(key) @@ -135,30 +151,32 @@ class PluginLoader(util.Singleton): """ text = text.strip() name = text.split()[0] - if name in self._plugins and self._plugins[name][1]: - self._plugins[name][0].command(text[len(name) + 1:]) + if name in self._plugins and self._plugins[name].is_active: + self._plugins[name].instance.command(text[len(name) + 1:]) - def get_menu(self, menu, num): + def get_menu(self, num): """ Return list of items for menu """ result = [] - for elem in self._plugins.values(): - if elem[1]: - try: - result.extend(elem[0].get_menu(menu, num)) - except: - continue + for plugin in self._plugins.values(): + if not plugin.is_active: + continue + try: + result.extend(plugin.instance.get_menu(num)) + except: + continue return result def get_message_menu(self, menu, selected_text): result = [] - for elem in self._plugins.values(): - if elem[1]: - try: - result.extend(elem[0].get_message_menu(menu, selected_text)) - except: - continue + for plugin in self._plugins.values(): + if not plugin.is_active: + continue + try: + result.extend(plugin.instance.get_message_menu(menu, selected_text)) + except: + pass return result def stop(self): @@ -166,8 +184,8 @@ class PluginLoader(util.Singleton): App is closing, stop all plugins """ for key in list(self._plugins.keys()): - if self._plugins[key][1]: - self._plugins[key][0].close() + if self._plugins[key].is_active: + self._plugins[key].instance.close() del self._plugins[key] def reload(self): diff --git a/toxygen/plugins/plugin_super_class.py b/toxygen/plugins/plugin_super_class.py index c857c56..0056d36 100644 --- a/toxygen/plugins/plugin_super_class.py +++ b/toxygen/plugins/plugin_super_class.py @@ -1,5 +1,7 @@ import os from PyQt5 import QtCore, QtWidgets +import utils.ui as util_ui +import common.tox_save as tox_save MAX_SHORT_NAME_LENGTH = 5 @@ -26,25 +28,22 @@ def log(name, data): fl.write(str(data) + '\n') -class PluginSuperClass: +class PluginSuperClass(tox_save.ToxSave): """ Superclass for all plugins. Plugin is Python3 module with at least one class derived from PluginSuperClass. """ is_plugin = True - def __init__(self, name, short_name, tox=None, profile=None, settings=None, encrypt_save=None): + def __init__(self, name, short_name, app): """ - Constructor. In plugin __init__ should take only 4 last arguments + Constructor. In plugin __init__ should take only 1 last argument :param name: plugin full name :param short_name: plugin unique short name (length of short name should not exceed MAX_SHORT_NAME_LENGTH) - :param tox: tox instance - :param profile: profile instance - :param settings: profile settings - :param encrypt_save: ToxES instance. + :param app: App instance """ - self._settings = settings - self._profile = profile - self._tox = tox + tox = getattr(app, '_tox') + super().__init__(tox) + self._settings = getattr(app, '_settings') name = name.strip() short_name = short_name.strip() if not name or not short_name: @@ -52,7 +51,6 @@ class PluginSuperClass: self._name = name self._short_name = short_name[:MAX_SHORT_NAME_LENGTH] self._translator = None # translator for plugin's GUI - self._encrypt_save = encrypt_save # ----------------------------------------------------------------------------------------------------------------- # Get methods @@ -76,12 +74,11 @@ class PluginSuperClass: """ return self.__doc__ - def get_menu(self, menu, row_number): + def get_menu(self, row_number): """ This method creates items for menu which called on right click in list of friends - :param menu: menu instance :param row_number: number of selected row in list of contacts - :return list of QAction's + :return list of tuples (text, handler) """ return [] @@ -100,12 +97,6 @@ class PluginSuperClass: """ return None - def set_tox(self, tox): - """ - New tox instance - """ - self._tox = tox - # ----------------------------------------------------------------------------------------------------------------- # Plugin was stopped, started or new command received # ----------------------------------------------------------------------------------------------------------------- @@ -134,11 +125,9 @@ class PluginSuperClass: :param command: string with command """ if command == 'help': - msgbox = QtWidgets.QMessageBox() - title = QtWidgets.QApplication.translate("PluginWindow", "List of commands for plugin {}") - msgbox.setWindowTitle(title.format(self._name)) - msgbox.setText(QtWidgets.QApplication.translate("PluginWindow", "No commands available")) - msgbox.exec_() + text = util_ui.tr('No commands available') + title = util_ui.tr('List of commands for plugin {}').format(self._name) + util_ui.message_box(text, title) # ----------------------------------------------------------------------------------------------------------------- # Translations support diff --git a/toxygen/profile.py b/toxygen/profile.py deleted file mode 100644 index 16d117d..0000000 --- a/toxygen/profile.py +++ /dev/null @@ -1,1458 +0,0 @@ -from list_items import * -from PyQt5 import QtGui, QtWidgets -from friend import * -from settings import * -from toxcore_enums_and_consts import * -from ctypes import * -from util import log, Singleton, curr_directory -from tox_dns import tox_dns -from history import * -from file_transfers import * -import time -import calls -import avwidgets -import plugin_support -import basecontact -import items_factory -import cv2 -import threading -from group_chat import * -import re - - -class Profile(basecontact.BaseContact, Singleton): - """ - Profile of current toxygen user. Contains friends list, tox instance - """ - def __init__(self, tox, screen): - """ - :param tox: tox instance - :param screen: ref to main screen - """ - basecontact.BaseContact.__init__(self, - tox.self_get_name(), - tox.self_get_status_message(), - screen.user_info, - tox.self_get_address()) - Singleton.__init__(self) - self._screen = screen - self._messages = screen.messages - self._tox = tox - self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number) - self._call = calls.AV(tox.AV) # object with data about calls - self._call_widgets = {} # dict of incoming call widgets - self._incoming_calls = set() - self._load_history = True - self._waiting_for_reconnection = False - self._factory = items_factory.ItemsFactory(self._screen.friends_list, self._messages) - settings = Settings.get_instance() - self._sorting = settings['sorting'] - self._show_avatars = settings['show_avatars'] - self._filter_string = '' - self._friend_item_height = 40 if settings['compact_mode'] else 70 - self._paused_file_transfers = dict(settings['paused_file_transfers']) - # key - file id, value: [path, friend number, is incoming, start position] - screen.online_contacts.setCurrentIndex(int(self._sorting)) - aliases = settings['friends_aliases'] - data = tox.self_get_friend_list() - self._history = History(tox.self_get_public_key()) # connection to db - self._contacts, self._active_friend = [], -1 - for i in data: # creates list of friends - tox_id = tox.friend_get_public_key(i) - try: - alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] - except: - alias = '' - item = self.create_friend_item() - name = alias or tox.friend_get_name(i) or tox_id - status_message = tox.friend_get_status_message(i) - if not self._history.friend_exists_in_db(tox_id): - self._history.add_friend_to_db(tox_id) - message_getter = self._history.messages_getter(tox_id) - friend = Friend(message_getter, i, name, status_message, item, tox_id) - friend.set_alias(alias) - self._contacts.append(friend) - if len(self._contacts): - self.set_active(0) - self.filtration_and_sorting(self._sorting) - - # ----------------------------------------------------------------------------------------------------------------- - # Edit current user's data - # ----------------------------------------------------------------------------------------------------------------- - - def change_status(self): - """ - Changes status of user (online, away, busy) - """ - if self._status is not None: - self.set_status((self._status + 1) % 3) - - def set_status(self, status): - super(Profile, self).set_status(status) - if status is not None: - self._tox.self_set_status(status) - elif not self._waiting_for_reconnection: - self._waiting_for_reconnection = True - QtCore.QTimer.singleShot(50000, self.reconnect) - - def set_name(self, value): - if self.name == value: - return - tmp = self.name - super(Profile, self).set_name(value.encode('utf-8')) - self._tox.self_set_name(self._name.encode('utf-8')) - message = QtWidgets.QApplication.translate("MainWindow", 'User {} is now known as {}') - message = message.format(tmp, value) - for friend in self._contacts: - friend.append_message(InfoMessage(message, time.time())) - if self._active_friend + 1: - self.create_message_item(message, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - - def set_status_message(self, value): - super(Profile, self).set_status_message(value) - self._tox.self_set_status_message(self._status_message.encode('utf-8')) - - def new_nospam(self): - """Sets new nospam part of tox id""" - import random - self._tox.self_set_nospam(random.randint(0, 4294967295)) # no spam - uint32 - self._tox_id = self._tox.self_get_address() - return self._tox_id - - # ----------------------------------------------------------------------------------------------------------------- - # Filtration - # ----------------------------------------------------------------------------------------------------------------- - - def filtration_and_sorting(self, sorting=0, filter_str=''): - """ - Filtration of friends list - :param sorting: 0 - no sort, 1 - online only, 2 - online first, 4 - by name - :param filter_str: show contacts which name contains this substring - """ - filter_str = filter_str.lower() - settings = Settings.get_instance() - number = self.get_active_number() - is_friend = self.is_active_a_friend() - if sorting > 1: - if sorting & 2: - self._contacts = sorted(self._contacts, key=lambda x: int(x.status is not None), reverse=True) - if sorting & 4: - if not sorting & 2: - self._contacts = sorted(self._contacts, key=lambda x: x.name.lower()) - else: # save results of prev sorting - online_friends = filter(lambda x: x.status is not None, self._contacts) - count = len(list(online_friends)) - part1 = self._contacts[:count] - part2 = self._contacts[count:] - part1 = sorted(part1, key=lambda x: x.name.lower()) - part2 = sorted(part2, key=lambda x: x.name.lower()) - self._contacts = part1 + part2 - else: # sort by number - online_friends = filter(lambda x: x.status is not None, self._contacts) - count = len(list(online_friends)) - part1 = self._contacts[:count] - part2 = self._contacts[count:] - part1 = sorted(part1, key=lambda x: x.number) - part2 = sorted(part2, key=lambda x: x.number) - self._contacts = part1 + part2 - self._screen.friends_list.clear() - for contact in self._contacts: - contact.set_widget(self.create_friend_item()) - for index, friend in enumerate(self._contacts): - friend.visibility = (friend.status is not None or not (sorting & 1)) and (filter_str in friend.name.lower()) - friend.visibility = friend.visibility or friend.messages or friend.actions - if friend.visibility: - self._screen.friends_list.item(index).setSizeHint(QtCore.QSize(250, self._friend_item_height)) - else: - self._screen.friends_list.item(index).setSizeHint(QtCore.QSize(250, 0)) - self._sorting, self._filter_string = sorting, filter_str - settings['sorting'] = self._sorting - settings.save() - self.set_active_by_number_and_type(number, is_friend) - - def update_filtration(self): - """ - Update list of contacts when 1 of friends change connection status - """ - self.filtration_and_sorting(self._sorting, self._filter_string) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend getters - # ----------------------------------------------------------------------------------------------------------------- - - def get_friend_by_number(self, num): - return list(filter(lambda x: x.number == num and type(x) is Friend, self._contacts))[0] - - def get_friend(self, num): - if num < 0 or num >= len(self._contacts): - return None - return self._contacts[num] - - def get_curr_friend(self): - return self._contacts[self._active_friend] if self._active_friend + 1 else None - - # ----------------------------------------------------------------------------------------------------------------- - # Work with active friend - # ----------------------------------------------------------------------------------------------------------------- - - def get_active(self): - return self._active_friend - - def set_active(self, value=None): - """ - Change current active friend or update info - :param value: number of new active friend in friend's list or None to update active user's data - """ - if value is None and self._active_friend == -1: # nothing to update - return - if value == -1: # all friends were deleted - self._screen.account_name.setText('') - self._screen.account_status.setText('') - self._screen.account_status.setToolTip('') - self._active_friend = -1 - self._screen.account_avatar.setHidden(True) - self._messages.clear() - self._screen.messageEdit.clear() - return - try: - self.send_typing(False) - self._screen.typing.setVisible(False) - if value is not None: - if self._active_friend + 1 and self._active_friend != value: - try: - self.get_curr_friend().curr_text = self._screen.messageEdit.toPlainText() - except: - pass - friend = self._contacts[value] - friend.remove_invalid_unsent_files() - if self._active_friend != value: - self._screen.messageEdit.setPlainText(friend.curr_text) - self._active_friend = value - friend.reset_messages() - if not Settings.get_instance()['save_history']: - friend.delete_old_messages() - self._messages.clear() - friend.load_corr() - messages = friend.get_corr()[-PAGE_SIZE:] - self._load_history = False - for message in messages: - if message.get_type() <= 1: - data = message.get_data() - self.create_message_item(data[0], - data[2], - data[1], - data[3]) - elif message.get_type() == MESSAGE_TYPE['FILE_TRANSFER']: - if message.get_status() is None: - self.create_unsent_file_item(message) - continue - item = self.create_file_transfer_item(message) - if message.get_status() in ACTIVE_FILE_TRANSFERS: # active file transfer - try: - ft = self._file_transfers[(message.get_friend_number(), message.get_file_number())] - ft.set_state_changed_handler(item.update_transfer_state) - ft.signal() - except: - print('Incoming not started transfer - no info found') - elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline - self.create_inline_item(message.get_data()) - elif message.get_type() < 5: # info message - data = message.get_data() - self.create_message_item(data[0], - data[2], - '', - data[3]) - else: - data = message.get_data() - self.create_gc_message_item(data[0], data[2], data[1], data[4], data[3]) - self._messages.scrollToBottom() - self._load_history = True - if value in self._call: - self._screen.active_call() - elif value in self._incoming_calls: - self._screen.incoming_call() - else: - self._screen.call_finished() - else: - friend = self.get_curr_friend() - - self._screen.account_name.setText(friend.name) - self._screen.account_status.setText(friend.status_message) - self._screen.account_status.setToolTip(friend.get_full_status()) - if friend.tox_id is None: - avatar_path = curr_directory() + '/images/group.png' - else: - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(friend.tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if not os.path.isfile(avatar_path): # load default image - avatar_path = curr_directory() + '/images/avatar.png' - os.chdir(os.path.dirname(avatar_path)) - pixmap = QtGui.QPixmap(avatar_path) - self._screen.account_avatar.setPixmap(pixmap.scaled(64, 64, QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation)) - except Exception as ex: # no friend found. ignore - log('Friend value: ' + str(value)) - log('Error in set active: ' + str(ex)) - raise - - def set_active_by_number_and_type(self, number, is_friend): - for i in range(len(self._contacts)): - c = self._contacts[i] - if c.number == number and (type(c) is Friend == is_friend): - self._active_friend = i - break - - active_friend = property(get_active, set_active) - - def get_last_message(self): - if self._active_friend + 1: - return self.get_curr_friend().get_last_message_text() - else: - return '' - - def get_active_number(self): - return self.get_curr_friend().number if self._active_friend + 1 else -1 - - def get_active_name(self): - return self.get_curr_friend().name if self._active_friend + 1 else '' - - def is_active_online(self): - return self._active_friend + 1 and self.get_curr_friend().status is not None - - def new_name(self, number, name): - friend = self.get_friend_by_number(number) - tmp = friend.name - friend.set_name(name) - name = str(name, 'utf-8') - if friend.name == name and tmp != name: - message = QtWidgets.QApplication.translate("MainWindow", 'User {} is now known as {}') - message = message.format(tmp, name) - friend.append_message(InfoMessage(message, time.time())) - friend.actions = True - if number == self.get_active_number(): - self.create_message_item(message, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - self.set_active(None) - - def update(self): - if self._active_friend + 1: - self.set_active(self._active_friend) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend connection status callbacks - # ----------------------------------------------------------------------------------------------------------------- - - 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: - for fl in files: - data = fl.get_data() - if data[1] is not None: - self.send_inline(data[1], data[0], friend_number, True) - else: - self.send_file(data[0], friend_number, True) - friend.clear_unsent_files() - for key in list(self._paused_file_transfers.keys()): - data = self._paused_file_transfers[key] - if not os.path.exists(data[0]): - del self._paused_file_transfers[key] - elif data[1] == friend_number and not data[2]: - self.send_file(data[0], friend_number, True, key) - del self._paused_file_transfers[key] - if friend_number == self.get_active_number() and self.is_active_a_friend(): - self.update() - except Exception as ex: - print('Exception in file sending: ' + str(ex)) - - def friend_exit(self, friend_number): - """ - Friend with specified number quit - """ - self.get_friend_by_number(friend_number).status = None - self.friend_typing(friend_number, False) - if friend_number in self._call: - self._call.finish_call(friend_number, True) - for friend_num, file_num in list(self._file_transfers.keys()): - if friend_num == friend_number: - ft = self._file_transfers[(friend_num, file_num)] - if type(ft) is SendTransfer: - self._paused_file_transfers[ft.get_id()] = [ft.get_path(), friend_num, False, -1] - elif type(ft) is ReceiveTransfer and ft.state != TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - self._paused_file_transfers[ft.get_id()] = [ft.get_path(), friend_num, True, ft.total_size()] - self.cancel_transfer(friend_num, file_num, True) - - # ----------------------------------------------------------------------------------------------------------------- - # Typing notifications - # ----------------------------------------------------------------------------------------------------------------- - - def send_typing(self, typing): - """ - Send typing notification to a friend - """ - if Settings.get_instance()['typing_notifications'] and self._active_friend + 1: - try: - friend = self.get_curr_friend() - if friend.status is not None: - self._tox.self_set_typing(friend.number, typing) - except: - pass - - def friend_typing(self, friend_number, typing): - """ - Display incoming typing notification - """ - if friend_number == self.get_active_number() and self.is_active_a_friend(): - self._screen.typing.setVisible(typing) - - # ----------------------------------------------------------------------------------------------------------------- - # Private messages - # ----------------------------------------------------------------------------------------------------------------- - - def receipt(self): - i = 0 - while i < self._messages.count() and not self._messages.itemWidget(self._messages.item(i)).mark_as_sent(): - i += 1 - - def send_messages(self, friend_number): - """ - Send 'offline' messages to friend - """ - friend = self.get_friend_by_number(friend_number) - friend.load_corr() - messages = friend.get_unsent_messages() - try: - for message in messages: - self.split_and_send(friend_number, message.get_data()[-1], message.get_data()[0].encode('utf-8')) - friend.inc_receipts() - except Exception as ex: - log('Sending pending messages failed with ' + str(ex)) - - def split_and_send(self, number, message_type, message): - """ - Message splitting. Message length cannot be > TOX_MAX_MESSAGE_LENGTH - :param number: friend's number - :param message_type: type of message - :param message: message text - """ - while len(message) > TOX_MAX_MESSAGE_LENGTH: - size = TOX_MAX_MESSAGE_LENGTH * 4 // 5 - last_part = message[size:TOX_MAX_MESSAGE_LENGTH] - if b' ' in last_part: - index = last_part.index(b' ') - elif b',' in last_part: - index = last_part.index(b',') - elif b'.' in last_part: - index = last_part.index(b'.') - else: - index = TOX_MAX_MESSAGE_LENGTH - size - 1 - index += size + 1 - self._tox.friend_send_message(number, message_type, message[:index]) - message = message[index:] - self._tox.friend_send_message(number, message_type, message) - - def new_message(self, friend_num, message_type, message): - """ - Current user gets new message - :param friend_num: friend_num of friend who sent message - :param message_type: message type - plain text or action message (/me) - :param message: text of message - """ - if friend_num == self.get_active_number()and self.is_active_a_friend(): # add message to list - t = time.time() - self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) - self._messages.scrollToBottom() - self.get_curr_friend().append_message( - TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type)) - else: - friend = self.get_friend_by_number(friend_num) - friend.inc_messages() - friend.append_message( - TextMessage(message, MESSAGE_OWNER['FRIEND'], time.time(), message_type)) - if not friend.visibility: - self.update_filtration() - - def send_message(self, text, friend_num=None): - """ - Send message - :param text: message text - :param friend_num: num of friend - """ - if not self.is_active_a_friend(): - self.send_gc_message(text) - return - if friend_num is None: - friend_num = self.get_active_number() - if text.startswith('/plugin '): - plugin_support.PluginLoader.get_instance().command(text[8:]) - self._screen.messageEdit.clear() - elif text and friend_num + 1: - if text.startswith('/me '): - message_type = TOX_MESSAGE_TYPE['ACTION'] - text = text[4:] - else: - message_type = TOX_MESSAGE_TYPE['NORMAL'] - friend = self.get_friend_by_number(friend_num) - friend.inc_receipts() - if friend.status is not None: - self.split_and_send(friend.number, message_type, text.encode('utf-8')) - t = time.time() - if friend.number == self.get_active_number() and self.is_active_a_friend(): - self.create_message_item(text, t, MESSAGE_OWNER['NOT_SENT'], message_type) - self._screen.messageEdit.clear() - self._messages.scrollToBottom() - friend.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type)) - - def delete_message(self, time): - friend = self.get_curr_friend() - friend.delete_message(time) - self._history.delete_message(friend.tox_id, time) - self.update() - - # ----------------------------------------------------------------------------------------------------------------- - # History support - # ----------------------------------------------------------------------------------------------------------------- - - def save_history(self): - """ - Save history to db - """ - s = Settings.get_instance() - if hasattr(self, '_history'): - if s['save_history']: - for friend in filter(lambda x: type(x) is Friend, self._contacts): - if not self._history.friend_exists_in_db(friend.tox_id): - self._history.add_friend_to_db(friend.tox_id) - if not s['save_unsent_only']: - messages = friend.get_corr_for_saving() - else: - messages = friend.get_unsent_messages_for_saving() - self._history.delete_messages(friend.tox_id) - self._history.save_messages_to_db(friend.tox_id, messages) - unsent_messages = friend.get_unsent_messages() - unsent_time = unsent_messages[0].get_data()[2] if len(unsent_messages) else time.time() + 1 - self._history.update_messages(friend.tox_id, unsent_time) - self._history.save() - del self._history - - def clear_history(self, num=None, save_unsent=False): - """ - Clear chat history - """ - if num is not None: - friend = self._contacts[num] - friend.clear_corr(save_unsent) - if self._history.friend_exists_in_db(friend.tox_id): - self._history.delete_messages(friend.tox_id) - self._history.delete_friend_from_db(friend.tox_id) - else: # clear all history - for number in range(len(self._contacts)): - self.clear_history(number, save_unsent) - if num is None or num == self.get_active_number(): - self.update() - - def load_history(self): - """ - Tries to load next part of messages - """ - if not self._load_history: - return - self._load_history = False - friend = self.get_curr_friend() - friend.load_corr(False) - data = friend.get_corr() - if not data: - return - data.reverse() - data = data[self._messages.count():self._messages.count() + PAGE_SIZE] - for message in data: - if message.get_type() <= 1: # text message - data = message.get_data() - self.create_message_item(data[0], - data[2], - data[1], - data[3], - False) - elif message.get_type() == MESSAGE_TYPE['FILE_TRANSFER']: # file transfer - if message.get_status() is None: - self.create_unsent_file_item(message) - continue - item = self.create_file_transfer_item(message, False) - if message.get_status() in ACTIVE_FILE_TRANSFERS: # active file transfer - try: - ft = self._file_transfers[(message.get_friend_number(), message.get_file_number())] - ft.set_state_changed_handler(item.update_transfer_state) - ft.signal() - except: - print('Incoming not started transfer - no info found') - elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline image - self.create_inline_item(message.get_data(), False) - else: # info message - data = message.get_data() - self.create_message_item(data[0], - data[2], - '', - data[3], - False) - self._load_history = True - - def export_db(self, directory): - self._history.export(directory) - - def export_history(self, num, as_text=True, _range=None): - friend = self._contacts[num] - if _range is None: - friend.load_all_corr() - corr = friend.get_corr() - elif _range[1] + 1: - corr = friend.get_corr()[_range[0]:_range[1] + 1] - else: - corr = friend.get_corr()[_range[0]:] - arr = [] - new_line = '\n' if as_text else '
' - for message in corr: - if type(message) is TextMessage: - data = message.get_data() - if as_text: - x = '[{}] {}: {}\n' - else: - x = '[{}] {}: {}
' - arr.append(x.format(convert_time(data[2]) if data[1] != MESSAGE_OWNER['NOT_SENT'] else 'Unsent', - friend.name if data[1] == MESSAGE_OWNER['FRIEND'] else self.name, - data[0])) - s = new_line.join(arr) - if not as_text: - s = '{}{}'.format(friend.name, s) - return s - - # ----------------------------------------------------------------------------------------------------------------- - # Friend, message and file transfer items creation - # ----------------------------------------------------------------------------------------------------------------- - - def create_friend_item(self): - """ - Method-factory - :return: new widget for friend instance - """ - return self._factory.friend_item() - - def create_message_item(self, text, time, owner, message_type, append=True): - if message_type == MESSAGE_TYPE['INFO_MESSAGE']: - name = '' - elif owner == MESSAGE_OWNER['FRIEND']: - name = self.get_active_name() - else: - name = self._name - pixmap = None - if self._show_avatars: - if owner == MESSAGE_OWNER['FRIEND']: - pixmap = self.get_curr_friend().get_pixmap() - else: - pixmap = self.get_pixmap() - return self._factory.message_item(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'], - message_type, append, pixmap) - - def create_gc_message_item(self, text, time, owner, name, message_type, append=True): - pixmap = None - if self._show_avatars: - if owner == MESSAGE_OWNER['FRIEND']: - pixmap = self.get_curr_friend().get_pixmap() - else: - pixmap = self.get_pixmap() - return self._factory.message_item(text, time, name, True, - message_type - 5, append, pixmap) - - def create_file_transfer_item(self, tm, append=True): - data = list(tm.get_data()) - data[3] = self.get_friend_by_number(data[4]).name if data[3] else self._name - return self._factory.file_transfer_item(data, append) - - def create_unsent_file_item(self, message, append=True): - data = message.get_data() - return self._factory.unsent_file_item(os.path.basename(data[0]), - os.path.getsize(data[0]) if data[1] is None else len(data[1]), - self.name, - data[2], - append) - - def create_inline_item(self, data, append=True): - return self._factory.inline_item(data, append) - - # ----------------------------------------------------------------------------------------------------------------- - # Work with friends (remove, block, set alias, get public key) - # ----------------------------------------------------------------------------------------------------------------- - - def set_alias(self, num): - """ - Set new alias for friend - """ - friend = self._contacts[num] - name = friend.name - dialog = QtWidgets.QApplication.translate('MainWindow', - "Enter new alias for friend {} or leave empty to use friend's name:") - dialog = dialog.format(name) - title = QtWidgets.QApplication.translate('MainWindow', - 'Set alias') - text, ok = QtWidgets.QInputDialog.getText(None, - title, - dialog, - QtWidgets.QLineEdit.Normal, - name) - if ok: - settings = Settings.get_instance() - aliases = settings['friends_aliases'] - if text: - friend.name = bytes(text, 'utf-8') - try: - index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) - aliases[index] = (friend.tox_id, text) - except: - aliases.append((friend.tox_id, text)) - friend.set_alias(text) - else: # use default name - friend.name = bytes(self._tox.friend_get_name(friend.number), 'utf-8') - friend.set_alias('') - try: - index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) - del aliases[index] - except: - pass - settings.save() - if num == self.get_active_number() and self.is_active_a_friend(): - self.update() - - def friend_public_key(self, num): - return self._contacts[num].tox_id - - def delete_friend(self, num): - """ - Removes friend from contact list - :param num: number of friend in list - """ - friend = self._contacts[num] - settings = Settings.get_instance() - try: - index = list(map(lambda x: x[0], settings['friends_aliases'])).index(friend.tox_id) - del settings['friends_aliases'][index] - except: - pass - if friend.tox_id in settings['notes']: - del settings['notes'][friend.tox_id] - settings.save() - self.clear_history(num) - if self._history.friend_exists_in_db(friend.tox_id): - self._history.delete_friend_from_db(friend.tox_id) - self._tox.friend_delete(friend.number) - del self._contacts[num] - self._screen.friends_list.takeItem(num) - if num == self._active_friend: # active friend was deleted - if not len(self._contacts): # last friend was deleted - self.set_active(-1) - else: - self.set_active(0) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - - def add_friend(self, tox_id): - """ - Adds friend to list - """ - num = self._tox.friend_add_norequest(tox_id) # num - friend number - item = self.create_friend_item() - try: - if not self._history.friend_exists_in_db(tox_id): - self._history.add_friend_to_db(tox_id) - message_getter = self._history.messages_getter(tox_id) - except Exception as ex: # something is wrong - log('Accept friend request failed! ' + str(ex)) - message_getter = None - friend = Friend(message_getter, num, tox_id, '', item, tox_id) - self._contacts.append(friend) - - def block_user(self, tox_id): - """ - Block user with specified tox id (or public key) - delete from friends list and ignore friend requests - """ - tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] - if tox_id == self.tox_id[:TOX_PUBLIC_KEY_SIZE * 2]: - return - settings = Settings.get_instance() - if tox_id not in settings['blocked']: - settings['blocked'].append(tox_id) - settings.save() - try: - num = self._tox.friend_by_public_key(tox_id) - self.delete_friend(num) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - except: # not in friend list - pass - - def unblock_user(self, tox_id, add_to_friend_list): - """ - Unblock user - :param tox_id: tox id of contact - :param add_to_friend_list: add this contact to friend list or not - """ - s = Settings.get_instance() - s['blocked'].remove(tox_id) - s.save() - if add_to_friend_list: - self.add_friend(tox_id) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend requests - # ----------------------------------------------------------------------------------------------------------------- - - def send_friend_request(self, tox_id, message): - """ - Function tries to send request to contact with specified id - :param tox_id: id of new contact or tox dns 4 value - :param message: additional message - :return: True on success else error string - """ - try: - message = message or 'Hello! Add me to your contact list please' - if '@' in tox_id: # value like groupbot@toxme.io - tox_id = tox_dns(tox_id) - if tox_id is None: - raise Exception('TOX DNS lookup failed') - if len(tox_id) == TOX_PUBLIC_KEY_SIZE * 2: # public key - self.add_friend(tox_id) - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "Friend added")) - text = (QtWidgets.QApplication.translate("MainWindow", 'Friend added without sending friend request')) - msgBox.setText(text) - msgBox.exec_() - else: - result = self._tox.friend_add(tox_id, message.encode('utf-8')) - tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] - item = self.create_friend_item() - if not self._history.friend_exists_in_db(tox_id): - self._history.add_friend_to_db(tox_id) - message_getter = self._history.messages_getter(tox_id) - friend = Friend(message_getter, result, tox_id, '', item, tox_id) - self._contacts.append(friend) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - return True - except Exception as ex: # wrong data - log('Friend request failed with ' + str(ex)) - return str(ex) - - def process_friend_request(self, tox_id, message): - """ - Accept or ignore friend request - :param tox_id: tox id of contact - :param message: message - """ - try: - text = QtWidgets.QApplication.translate('MainWindow', 'User {} wants to add you to contact list. Message:\n{}') - info = text.format(tox_id, message) - fr_req = QtWidgets.QApplication.translate('MainWindow', 'Friend request') - reply = QtWidgets.QMessageBox.question(None, fr_req, info, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: # accepted - self.add_friend(tox_id) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - except Exception as ex: # something is wrong - log('Accept friend request failed! ' + str(ex)) - - # ----------------------------------------------------------------------------------------------------------------- - # Reset - # ----------------------------------------------------------------------------------------------------------------- - - def reset(self, restart): - """ - Recreate tox instance - :param restart: method which calls restart and returns new tox instance - """ - for contact in self._contacts: - if type(contact) is Friend: - self.friend_exit(contact.number) - else: - self.leave_gc(contact.number) - self._call.stop() - del self._call - del self._tox - self._tox = restart() - self._call = calls.AV(self._tox.AV) - self.status = None - for friend in self._contacts: - friend.number = self._tox.friend_by_public_key(friend.tox_id) # numbers update - self.update_filtration() - - def reconnect(self): - self._waiting_for_reconnection = False - if self.status is None or all(list(map(lambda x: x.status is None, self._contacts))) and len(self._contacts): - self._waiting_for_reconnection = True - self.reset(self._screen.reset) - QtCore.QTimer.singleShot(50000, self.reconnect) - - def close(self): - for friend in filter(lambda x: type(x) is Friend, self._contacts): - self.friend_exit(friend.number) - for i in range(len(self._contacts)): - del self._contacts[0] - if hasattr(self, '_call'): - self._call.stop() - del self._call - s = Settings.get_instance() - s['paused_file_transfers'] = dict(self._paused_file_transfers) if s['resend_files'] else {} - s.save() - - # ----------------------------------------------------------------------------------------------------------------- - # File transfers support - # ----------------------------------------------------------------------------------------------------------------- - - def incoming_file_transfer(self, friend_number, file_number, size, file_name): - """ - New transfer - :param friend_number: number of friend who sent file - :param file_number: file number - :param size: file size in bytes - :param file_name: file name without path - """ - settings = Settings.get_instance() - friend = self.get_friend_by_number(friend_number) - auto = settings['allow_auto_accept'] and friend.tox_id in settings['auto_accept_from_friends'] - inline = is_inline(file_name) and settings['allow_inline'] - file_id = self._tox.file_get_file_id(friend_number, file_number) - accepted = True - if file_id in self._paused_file_transfers: - data = self._paused_file_transfers[file_id] - pos = data[-1] if os.path.exists(data[0]) else 0 - if pos >= size: - self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) - return - self._tox.file_seek(friend_number, file_number, pos) - self.accept_transfer(None, data[0], friend_number, file_number, size, False, pos) - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['RUNNING'], - size, - file_name, - friend_number, - file_number) - elif inline and size < 1024 * 1024: - self.accept_transfer(None, '', friend_number, file_number, size, True) - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['RUNNING'], - size, - file_name, - friend_number, - file_number) - - elif auto: - path = settings['auto_accept_path'] or curr_directory() - self.accept_transfer(None, path + '/' + file_name, friend_number, file_number, size) - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['RUNNING'], - size, - file_name, - friend_number, - file_number) - else: - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED'], - size, - file_name, - friend_number, - file_number) - accepted = False - if friend_number == self.get_active_number() and self.is_active_a_friend(): - item = self.create_file_transfer_item(tm) - if accepted: - self._file_transfers[(friend_number, file_number)].set_state_changed_handler(item.update_transfer_state) - self._messages.scrollToBottom() - else: - friend.actions = True - - friend.append_message(tm) - - def cancel_transfer(self, friend_number, file_number, already_cancelled=False): - """ - Stop transfer - :param friend_number: number of friend - :param file_number: file number - :param already_cancelled: was cancelled by friend - """ - i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['CANCELLED']) - if (friend_number, file_number) in self._file_transfers: - tr = self._file_transfers[(friend_number, file_number)] - if not already_cancelled: - tr.cancel() - else: - tr.cancelled() - if (friend_number, file_number) in self._file_transfers: - del tr - del self._file_transfers[(friend_number, file_number)] - else: - if not already_cancelled: - self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) - if friend_number == self.get_active_number() and self.is_active_a_friend(): - tmp = self._messages.count() + i - if tmp >= 0: - self._messages.itemWidget( - self._messages.item(tmp)).update_transfer_state(TOX_FILE_TRANSFER_STATE['CANCELLED'], - 0, -1) - - def cancel_not_started_transfer(self, cancel_time): - self.get_curr_friend().delete_one_unsent_file(cancel_time) - self.update() - - def pause_transfer(self, friend_number, file_number, by_friend=False): - """ - Pause transfer with specified data - """ - tr = self._file_transfers[(friend_number, file_number)] - tr.pause(by_friend) - t = TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'] if by_friend else TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER'] - self.get_friend_by_number(friend_number).update_transfer_data(file_number, t) - - def resume_transfer(self, friend_number, file_number, by_friend=False): - """ - Resume transfer with specified data - """ - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['RUNNING']) - tr = self._file_transfers[(friend_number, file_number)] - if by_friend: - tr.state = TOX_FILE_TRANSFER_STATE['RUNNING'] - tr.signal() - else: - tr.send_control(TOX_FILE_CONTROL['RESUME']) - - def accept_transfer(self, item, path, friend_number, file_number, size, inline=False, from_position=0): - """ - :param item: transfer item. - :param path: path for saving - :param friend_number: friend number - :param file_number: file number - :param size: file size - :param inline: is inline image - :param from_position: position for start - """ - path, file_name = os.path.split(path) - new_file_name, i = file_name, 1 - if not from_position: - while os.path.isfile(path + '/' + new_file_name): # file with same name already exists - if '.' in file_name: # has extension - d = file_name.rindex('.') - else: # no extension - d = len(file_name) - new_file_name = file_name[:d] + ' ({})'.format(i) + file_name[d:] - i += 1 - path = os.path.join(path, new_file_name) - if not inline: - rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number, from_position) - else: - rt = ReceiveToBuffer(self._tox, friend_number, size, file_number) - rt.set_transfer_finished_handler(self.transfer_finished) - self._file_transfers[(friend_number, file_number)] = rt - self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['RESUME']) - if item is not None: - rt.set_state_changed_handler(item.update_transfer_state) - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['RUNNING']) - - def send_screenshot(self, data): - """ - Send screenshot to current active friend - :param data: raw data - png - """ - self.send_inline(data, 'toxygen_inline.png') - self._messages.repaint() - - def send_sticker(self, path): - with open(path, 'rb') as fl: - data = fl.read() - self.send_inline(data, 'sticker.png') - - def send_inline(self, data, file_name, friend_number=None, is_resend=False): - friend_number = friend_number or self.get_active_number() - friend = self.get_friend_by_number(friend_number) - if friend.status is None and not is_resend: - m = UnsentFile(file_name, data, time.time()) - friend.append_message(m) - self.update() - return - elif friend.status is None and is_resend: - raise RuntimeError() - st = SendFromBuffer(self._tox, friend.number, data, file_name) - st.set_transfer_finished_handler(self.transfer_finished) - self._file_transfers[(friend.number, st.get_file_number())] = st - tm = TransferMessage(MESSAGE_OWNER['ME'], - time.time(), - TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'], - len(data), - file_name, - friend.number, - st.get_file_number()) - item = self.create_file_transfer_item(tm) - friend.append_message(tm) - st.set_state_changed_handler(item.update_transfer_state) - self._messages.scrollToBottom() - - def send_file(self, path, number=None, is_resend=False, file_id=None): - """ - Send file to current active friend - :param path: file path - :param number: friend_number - :param is_resend: is 'offline' message - :param file_id: file id of transfer - """ - friend_number = self.get_active_number() if number is None else number - friend = self.get_friend_by_number(friend_number) - if friend.status is None and not is_resend: - m = UnsentFile(path, None, time.time()) - friend.append_message(m) - self.update() - return - elif friend.status is None and is_resend: - print('Error in sending') - raise RuntimeError() - st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id) - st.set_transfer_finished_handler(self.transfer_finished) - self._file_transfers[(friend_number, st.get_file_number())] = st - tm = TransferMessage(MESSAGE_OWNER['ME'], - time.time(), - TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'], - os.path.getsize(path), - os.path.basename(path), - friend_number, - st.get_file_number()) - if friend_number == self.get_active_number(): - item = self.create_file_transfer_item(tm) - st.set_state_changed_handler(item.update_transfer_state) - self._messages.scrollToBottom() - self._contacts[friend_number].append_message(tm) - - def incoming_chunk(self, friend_number, file_number, position, data): - """ - Incoming chunk - """ - self._file_transfers[(friend_number, file_number)].write_chunk(position, data) - - def outgoing_chunk(self, friend_number, file_number, position, size): - """ - Outgoing chunk - """ - self._file_transfers[(friend_number, file_number)].send_chunk(position, size) - - def transfer_finished(self, friend_number, file_number): - transfer = self._file_transfers[(friend_number, file_number)] - t = type(transfer) - if t is ReceiveAvatar: - self.get_friend_by_number(friend_number).load_avatar() - if friend_number == self.get_active_number() and self.is_active_a_friend(): - self.set_active(None) - elif t is ReceiveToBuffer or (t is SendFromBuffer and Settings.get_instance()['allow_inline']): # inline image - print('inline') - inline = InlineImage(transfer.get_data()) - i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['FINISHED'], - inline) - if friend_number == self.get_active_number() and self.is_active_a_friend(): - count = self._messages.count() - if count + i + 1 >= 0: - elem = QtWidgets.QListWidgetItem() - item = InlineImageItem(transfer.get_data(), self._messages.width(), elem) - elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) - self._messages.insertItem(count + i + 1, elem) - self._messages.setItemWidget(elem, item) - self._messages.scrollToBottom() - elif t is not SendAvatar: - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['FINISHED']) - del self._file_transfers[(friend_number, file_number)] - del transfer - - # ----------------------------------------------------------------------------------------------------------------- - # Avatars support - # ----------------------------------------------------------------------------------------------------------------- - - def send_avatar(self, friend_number): - """ - :param friend_number: number of friend who should get new avatar - """ - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if not os.path.isfile(avatar_path): # reset image - avatar_path = None - sa = SendAvatar(avatar_path, self._tox, friend_number) - self._file_transfers[(friend_number, sa.get_file_number())] = sa - - def incoming_avatar(self, friend_number, file_number, size): - """ - Friend changed avatar - :param friend_number: friend number - :param file_number: file number - :param size: size of avatar or 0 (default avatar) - """ - ra = ReceiveAvatar(self._tox, friend_number, size, file_number) - if ra.state != TOX_FILE_TRANSFER_STATE['CANCELLED']: - self._file_transfers[(friend_number, file_number)] = ra - ra.set_transfer_finished_handler(self.transfer_finished) - else: - self.get_friend_by_number(friend_number).load_avatar() - if self.get_active_number() == friend_number and self.is_active_a_friend(): - self.set_active(None) - - def reset_avatar(self): - super(Profile, self).reset_avatar() - for friend in filter(lambda x: x.status is not None, self._contacts): - self.send_avatar(friend.number) - - def set_avatar(self, data): - super(Profile, self).set_avatar(data) - for friend in filter(lambda x: x.status is not None, self._contacts): - self.send_avatar(friend.number) - - # ----------------------------------------------------------------------------------------------------------------- - # AV support - # ----------------------------------------------------------------------------------------------------------------- - - def get_call(self): - return self._call - - call = property(get_call) - - def call_click(self, audio=True, video=False): - """User clicked audio button in main window""" - num = self.get_active_number() - if not self.is_active_a_friend(): - return - if num not in self._call and self.is_active_online(): # start call - if not Settings.get_instance().audio['enabled']: - return - self._call(num, audio, video) - self._screen.active_call() - if video: - text = QtWidgets.QApplication.translate("incoming_call", "Outgoing video call") - else: - text = QtWidgets.QApplication.translate("incoming_call", "Outgoing audio call") - self.get_curr_friend().append_message(InfoMessage(text, time.time())) - self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - elif num in self._call: # finish or cancel call if you call with active friend - self.stop_call(num, False) - - def incoming_call(self, audio, video, friend_number): - """ - Incoming call from friend. - """ - if not Settings.get_instance().audio['enabled']: - return - friend = self.get_friend_by_number(friend_number) - if video: - text = QtWidgets.QApplication.translate("incoming_call", "Incoming video call") - else: - text = QtWidgets.QApplication.translate("incoming_call", "Incoming audio call") - friend.append_message(InfoMessage(text, time.time())) - self._incoming_calls.add(friend_number) - if friend_number == self.get_active_number(): - self._screen.incoming_call() - self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - else: - friend.actions = True - self._call_widgets[friend_number] = avwidgets.IncomingCallWidget(friend_number, text, friend.name) - self._call_widgets[friend_number].set_pixmap(friend.get_pixmap()) - self._call_widgets[friend_number].show() - - def accept_call(self, friend_number, audio, video): - """ - Accept incoming call with audio or video - """ - 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] - - def stop_call(self, friend_number, by_friend): - """ - Stop call with friend - """ - if friend_number in self._incoming_calls: - self._incoming_calls.remove(friend_number) - text = QtWidgets.QApplication.translate("incoming_call", "Call declined") - else: - text = QtWidgets.QApplication.translate("incoming_call", "Call finished") - self._screen.call_finished() - is_video = self._call.is_video_call(friend_number) - self._call.finish_call(friend_number, by_friend) # finish or decline call - if hasattr(self, '_call_widget'): - self._call_widget[friend_number].close() - del self._call_widget[friend_number] - - def destroy_window(): - if is_video: - cv2.destroyWindow(str(friend_number)) - - threading.Timer(2.0, destroy_window).start() - friend = self.get_friend_by_number(friend_number) - friend.append_message(InfoMessage(text, time.time())) - if friend_number == self.get_active_number(): - self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - - # ----------------------------------------------------------------------------------------------------------------- - # GC support - # ----------------------------------------------------------------------------------------------------------------- - - def is_active_a_friend(self): - return type(self.get_curr_friend()) is Friend - - def get_group_by_number(self, number): - groups = filter(lambda x: type(x) is GroupChat and x.number == number, self._contacts) - return list(groups)[0] - - def add_gc(self, number): - widget = self.create_friend_item() - gc = GroupChat('Group chat #' + str(number), '', widget, self._tox, number) - self._contacts.append(gc) - - def create_group_chat(self): - number = self._tox.add_av_groupchat() - self.add_gc(number) - - def leave_gc(self, num): - gc = self._contacts[num] - self._tox.del_groupchat(gc.number) - del self._contacts[num] - self._screen.friends_list.takeItem(num) - if num == self._active_friend: # active friend was deleted - if not len(self._contacts): # last friend was deleted - self.set_active(-1) - else: - self.set_active(0) - - def group_invite(self, friend_number, gc_type, data): - text = QtWidgets.QApplication.translate('MainWindow', 'User {} invites you to group chat. Accept?') - title = QtWidgets.QApplication.translate('MainWindow', 'Group chat invite') - friend = self.get_friend_by_number(friend_number) - reply = QtWidgets.QMessageBox.question(None, title, text.format(friend.name), QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: # accepted - if gc_type == TOX_GROUPCHAT_TYPE['TEXT']: - number = self._tox.join_groupchat(friend_number, data) - else: - number = self._tox.join_av_groupchat(friend_number, data) - self.add_gc(number) - - def new_gc_message(self, group_number, peer_number, message_type, message): - name = self._tox.group_peername(group_number, peer_number) - message_type += 5 - if group_number == self.get_active_number() and not self.is_active_a_friend(): # add message to list - t = time.time() - self.create_gc_message_item(message, t, MESSAGE_OWNER['FRIEND'], name, message_type) - self._messages.scrollToBottom() - self.get_curr_friend().append_message( - GroupChatMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type, name)) - else: - gc = self.get_group_by_number(group_number) - gc.inc_messages() - gc.append_message( - GroupChatMessage(message, MESSAGE_OWNER['FRIEND'], time.time(), message_type, name)) - if not gc.visibility: - self.update_filtration() - - def new_gc_title(self, group_number, title): - gc = self.get_group_by_number(group_number) - gc.new_title(title) - if not self.is_active_a_friend() and self.get_active_number() == group_number: - self.update() - - def update_gc(self, group_number): - count = self._tox.group_number_peers(group_number) - gc = self.get_group_by_number(group_number) - text = QtWidgets.QApplication.translate('MainWindow', '{} users in chat') - gc.status_message = text.format(str(count)).encode('utf-8') - if not self.is_active_a_friend() and self.get_active_number() == group_number: - self.update() - - def send_gc_message(self, text): - group_number = self.get_active_number() - if text.startswith('/me '): - text = text[4:] - self._tox.group_action_send(group_number, text.encode('utf-8')) - else: - self._tox.group_message_send(group_number, text.encode('utf-8')) - self._screen.messageEdit.clear() - - def set_title(self, num): - """ - Set new title for gc - """ - gc = self._contacts[num] - name = gc.name - dialog = QtWidgets.QApplication.translate('MainWindow', - "Enter new title for group {}:") - dialog = dialog.format(name) - title = QtWidgets.QApplication.translate('MainWindow', - 'Set title') - text, ok = QtWidgets.QInputDialog.getText(None, - title, - dialog, - QtWidgets.QLineEdit.Normal, - name) - if ok: - text = text.encode('utf-8') - self._tox.group_set_title(gc.number, text) - self.new_gc_title(gc.number, text) - - def get_group_chats(self): - chats = filter(lambda x: type(x) is GroupChat, self._contacts) - chats = map(lambda c: (c.name, c.number), chats) - return list(chats) - - def invite_friend(self, friend_num, group_number): - friend = self._contacts[friend_num] - self._tox.invite_friend(friend.number, group_number) - - def get_gc_peer_name(self, text): - gc = self.get_curr_friend() - if type(gc) is not GroupChat: - return '\t' - names = gc.get_names() - name = re.split("\s+", text)[-1] - suggested_names = list(filter(lambda x: x.startswith(name), names)) - if not len(suggested_names): - return '\t' - return suggested_names[0][len(name):] + ': ' - - -def tox_factory(data=None, settings=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: - settings = Settings.get_default_settings() - tox_options = Tox.options_new() - # see lines 393-401 - tox_options.contents.ipv6_enabled = settings['ipv6_enabled'] - 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'] - if data: # load existing profile - tox_options.contents.savedata_type = TOX_SAVEDATA_TYPE['TOX_SAVE'] - tox_options.contents.savedata_data = c_char_p(data) - tox_options.contents.savedata_length = len(data) - else: # create new profile - tox_options.contents.savedata_type = TOX_SAVEDATA_TYPE['NONE'] - tox_options.contents.savedata_data = None - tox_options.contents.savedata_length = 0 - return Tox(tox_options) diff --git a/toxygen/smileys/__init__.py b/toxygen/smileys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/smileys.py b/toxygen/smileys/smileys.py similarity index 76% rename from toxygen/smileys.py rename to toxygen/smileys/smileys.py index 52cb603..0391856 100644 --- a/toxygen/smileys.py +++ b/toxygen/smileys/smileys.py @@ -1,11 +1,11 @@ -import util +from utils import util import json import os from collections import OrderedDict from PyQt5 import QtCore -class SmileyLoader(util.Singleton): +class SmileyLoader: """ Class which loads smileys packs and insert smileys into messages """ @@ -25,7 +25,7 @@ class SmileyLoader(util.Singleton): pack_name = self._settings['smiley_pack'] if self._settings['smileys'] and self._curr_pack != pack_name: self._curr_pack = pack_name - path = self.get_smileys_path() + 'config.json' + path = util.join_path(self.get_smileys_path(), 'config.json') try: with open(path, encoding='utf8') as fl: self._smileys = json.loads(fl.read()) @@ -34,7 +34,7 @@ class SmileyLoader(util.Singleton): print('Smiley pack {} loaded'.format(pack_name)) keys, values, self._list = [], [], [] for key, value in tmp.items(): - value = self.get_smileys_path() + value + value = util.join_path(self.get_smileys_path(), value) if value not in values: keys.append(key) values.append(value) @@ -45,10 +45,11 @@ class SmileyLoader(util.Singleton): print('Smiley pack {} was not loaded. Error: {}'.format(pack_name, ex)) def get_smileys_path(self): - return util.curr_directory() + '/smileys/' + self._curr_pack + '/' if self._curr_pack is not None else None + return util.join_path(util.get_smileys_directory(), self._curr_pack) if self._curr_pack is not None else None - def get_packs_list(self): - d = util.curr_directory() + '/smileys/' + @staticmethod + def get_packs_list(): + d = util.get_smileys_directory() return [x[1] for x in os.walk(d)][0] def get_smileys(self): @@ -71,18 +72,3 @@ class SmileyLoader(util.Singleton): if file_name.endswith('.gif'): # animated smiley edit.addAnimation(QtCore.QUrl(file_name), self.get_smileys_path() + file_name) return ' '.join(arr) - - -def sticker_loader(): - """ - :return list of stickers - """ - result = [] - d = util.curr_directory() + '/stickers/' - keys = [x[1] for x in os.walk(d)][0] - for key in keys: - path = d + key + '/' - files = filter(lambda f: f.endswith('.png'), os.listdir(path)) - files = map(lambda f: str(path + f), files) - result.extend(files) - return result diff --git a/toxygen/stickers/__init__.py b/toxygen/stickers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/stickers/stickers.py b/toxygen/stickers/stickers.py new file mode 100644 index 0000000..14142c7 --- /dev/null +++ b/toxygen/stickers/stickers.py @@ -0,0 +1,18 @@ +import os +import utils.util as util + + +def load_stickers(): + """ + :return list of stickers + """ + result = [] + d = util.get_stickers_directory() + keys = [x[1] for x in os.walk(d)][0] + for key in keys: + path = util.join_path(d, key) + files = filter(lambda f: f.endswith('.png'), os.listdir(path)) + files = map(lambda f: util.join_path(path, f), files) + result.extend(files) + + return result diff --git a/toxygen/styles/dark_style.qss b/toxygen/styles/dark_style.qss index 0216f23..ece5ec3 100644 --- a/toxygen/styles/dark_style.qss +++ b/toxygen/styles/dark_style.qss @@ -1207,12 +1207,12 @@ MessageItem border: none; } -MessageEdit +MessageBrowser { border: none; } -MessageEdit::focus +MessageBrowser::focus { border: none; } @@ -1222,7 +1222,7 @@ MessageItem::focus border: none; } -MessageEdit:hover +MessageBrowser:hover { border: none; } @@ -1243,7 +1243,7 @@ QPushButton:hover background-color: #1E90FF; } -MessageEdit +MessageBrowser { background-color: transparent; } @@ -1253,7 +1253,7 @@ MessageEdit background-color: #1E90FF; } -#friends_list:item:selected +#friendsListWidget:item:selected { background-color: #333333; } @@ -1277,7 +1277,7 @@ QListWidget > QLabel color: #A9A9A9; } -#contact_name +#searchLineEdit { padding-left: 22px; } @@ -1322,3 +1322,14 @@ ClickableLabel:hover { background-color: #4A4949; } + +#warningLabel +{ + color: #BC1C1C; +} + +#groupInvitesPushButton +{ + background-color: #009c00; +} + diff --git a/toxygen/styles/style.qss b/toxygen/styles/style.qss index 26fbaf2..ff9f614 100644 --- a/toxygen/styles/style.qss +++ b/toxygen/styles/style.qss @@ -1,4 +1,4 @@ -#contact_name +#searchLineEdit { padding-left: 22px; } @@ -27,3 +27,14 @@ MessageEdit { background-color: transparent; } + +#warningLabel +{ + color: #BC1C1C; +} + +#groupInvitesPushButton +{ + background-color: #009c00; +} + diff --git a/toxygen/tox_dns.py b/toxygen/tox_dns.py deleted file mode 100644 index 26b9619..0000000 --- a/toxygen/tox_dns.py +++ /dev/null @@ -1,59 +0,0 @@ -import json -import urllib.request -from util import log -import settings -from PyQt5 import QtNetwork, QtCore - - -def tox_dns(email): - """ - TOX DNS 4 - :param email: data like 'groupbot@toxme.io' - :return: tox id on success else None - """ - site = email.split('@')[1] - data = {"action": 3, "name": "{}".format(email)} - urls = ('https://{}/api'.format(site), 'http://{}/api'.format(site)) - s = settings.Settings.get_instance() - if not s['proxy_type']: # no proxy - for url in urls: - try: - return send_request(url, data) - except Exception as ex: - log('TOX DNS ERROR: ' + str(ex)) - else: # proxy - netman = QtNetwork.QNetworkAccessManager() - proxy = QtNetwork.QNetworkProxy() - proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) - proxy.setHostName(s['proxy_host']) - proxy.setPort(s['proxy_port']) - netman.setProxy(proxy) - for url in urls: - try: - request = QtNetwork.QNetworkRequest() - request.setUrl(QtCore.QUrl(url)) - request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json") - reply = netman.post(request, bytes(json.dumps(data), 'utf-8')) - - while not reply.isFinished(): - QtCore.QThread.msleep(1) - QtCore.QCoreApplication.processEvents() - data = bytes(reply.readAll().data()) - result = json.loads(str(data, 'utf-8')) - if not result['c']: - return result['tox_id'] - except Exception as ex: - log('TOX DNS ERROR: ' + str(ex)) - - return None # error - - -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 not res['c']: - return res['tox_id'] - else: - raise LookupError() diff --git a/toxygen/toxcore_enums_and_consts.py b/toxygen/toxcore_enums_and_consts.py deleted file mode 100644 index a17d93e..0000000 --- a/toxygen/toxcore_enums_and_consts.py +++ /dev/null @@ -1,220 +0,0 @@ -TOX_USER_STATUS = { - 'NONE': 0, - 'AWAY': 1, - 'BUSY': 2, -} - -TOX_MESSAGE_TYPE = { - 'NORMAL': 0, - 'ACTION': 1, -} - -TOX_PROXY_TYPE = { - 'NONE': 0, - 'HTTP': 1, - 'SOCKS5': 2, -} - -TOX_SAVEDATA_TYPE = { - 'NONE': 0, - 'TOX_SAVE': 1, - 'SECRET_KEY': 2, -} - -TOX_ERR_OPTIONS_NEW = { - 'OK': 0, - 'MALLOC': 1, -} - -TOX_ERR_NEW = { - 'OK': 0, - 'NULL': 1, - 'MALLOC': 2, - 'PORT_ALLOC': 3, - 'PROXY_BAD_TYPE': 4, - 'PROXY_BAD_HOST': 5, - 'PROXY_BAD_PORT': 6, - 'PROXY_NOT_FOUND': 7, - 'LOAD_ENCRYPTED': 8, - 'LOAD_BAD_FORMAT': 9, -} - -TOX_ERR_BOOTSTRAP = { - 'OK': 0, - 'NULL': 1, - 'BAD_HOST': 2, - 'BAD_PORT': 3, -} - -TOX_CONNECTION = { - 'NONE': 0, - 'TCP': 1, - 'UDP': 2, -} - -TOX_ERR_SET_INFO = { - 'OK': 0, - 'NULL': 1, - 'TOO_LONG': 2, -} - -TOX_ERR_FRIEND_ADD = { - 'OK': 0, - 'NULL': 1, - 'TOO_LONG': 2, - 'NO_MESSAGE': 3, - 'OWN_KEY': 4, - 'ALREADY_SENT': 5, - 'BAD_CHECKSUM': 6, - 'SET_NEW_NOSPAM': 7, - 'MALLOC': 8, -} - -TOX_ERR_FRIEND_DELETE = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_BY_PUBLIC_KEY = { - 'OK': 0, - 'NULL': 1, - 'NOT_FOUND': 2, -} - -TOX_ERR_FRIEND_GET_PUBLIC_KEY = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_GET_LAST_ONLINE = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_QUERY = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, -} - -TOX_ERR_SET_TYPING = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_SEND_MESSAGE = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'SENDQ': 4, - 'TOO_LONG': 5, - 'EMPTY': 6, -} - -TOX_FILE_KIND = { - 'DATA': 0, - 'AVATAR': 1, -} - -TOX_FILE_CONTROL = { - 'RESUME': 0, - 'PAUSE': 1, - 'CANCEL': 2, -} - -TOX_ERR_FILE_CONTROL = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, - 'FRIEND_NOT_CONNECTED': 2, - 'NOT_FOUND': 3, - 'NOT_PAUSED': 4, - 'DENIED': 5, - 'ALREADY_PAUSED': 6, - 'SENDQ': 7, -} - -TOX_ERR_FILE_SEEK = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, - 'FRIEND_NOT_CONNECTED': 2, - 'NOT_FOUND': 3, - 'DENIED': 4, - 'INVALID_POSITION': 5, - 'SENDQ': 6, -} - -TOX_ERR_FILE_GET = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'NOT_FOUND': 3, -} - -TOX_ERR_FILE_SEND = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'NAME_TOO_LONG': 4, - 'TOO_MANY': 5, -} - -TOX_ERR_FILE_SEND_CHUNK = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'NOT_FOUND': 4, - 'NOT_TRANSFERRING': 5, - 'INVALID_LENGTH': 6, - 'SENDQ': 7, - 'WRONG_POSITION': 8, -} - -TOX_ERR_FRIEND_CUSTOM_PACKET = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'INVALID': 4, - 'EMPTY': 5, - 'TOO_LONG': 6, - 'SENDQ': 7, -} - -TOX_ERR_GET_PORT = { - 'OK': 0, - 'NOT_BOUND': 1, -} - -TOX_CHAT_CHANGE = { - 'PEER_ADD': 0, - 'PEER_DEL': 1, - 'PEER_NAME': 2 -} - -TOX_GROUPCHAT_TYPE = { - 'TEXT': 0, - 'AV': 1 -} - -TOX_PUBLIC_KEY_SIZE = 32 - -TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6 - -TOX_MAX_FRIEND_REQUEST_LENGTH = 1016 - -TOX_MAX_MESSAGE_LENGTH = 1372 - -TOX_MAX_NAME_LENGTH = 128 - -TOX_MAX_STATUS_MESSAGE_LENGTH = 1007 - -TOX_SECRET_KEY_SIZE = 32 - -TOX_FILE_ID_LENGTH = 32 - -TOX_HASH_LENGTH = 32 - -TOX_MAX_CUSTOM_PACKET_SIZE = 1373 diff --git a/toxygen/toxes.py b/toxygen/toxes.py deleted file mode 100644 index 5b7282f..0000000 --- a/toxygen/toxes.py +++ /dev/null @@ -1,28 +0,0 @@ -import util -import toxencryptsave - - -class ToxES(util.Singleton): - - def __init__(self): - super().__init__() - self._toxencryptsave = toxencryptsave.ToxEncryptSave() - self._passphrase = None - - def set_password(self, passphrase): - self._passphrase = passphrase - - def has_password(self): - return bool(self._passphrase) - - def is_password(self, password): - return self._passphrase == password - - def is_data_encrypted(self, data): - return len(data) > 0 and self._toxencryptsave.is_data_encrypted(data) - - def pass_encrypt(self, data): - return self._toxencryptsave.pass_encrypt(data, self._passphrase) - - def pass_decrypt(self, data): - return self._toxencryptsave.pass_decrypt(data, self._passphrase) diff --git a/toxygen/ui/__init__.py b/toxygen/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/avwidgets.py b/toxygen/ui/av_widgets.py similarity index 82% rename from toxygen/avwidgets.py rename to toxygen/ui/av_widgets.py index 8c81387..e5773a8 100644 --- a/toxygen/avwidgets.py +++ b/toxygen/ui/av_widgets.py @@ -1,17 +1,16 @@ from PyQt5 import QtCore, QtGui, QtWidgets -import widgets -import profile -import util +from ui import widgets +import utils.util as util import pyaudio import wave -import settings -from util import curr_directory class IncomingCallWidget(widgets.CenteredWidget): - def __init__(self, friend_number, text, name): - super(IncomingCallWidget, self).__init__() + def __init__(self, settings, calls_manager, friend_number, text, name): + super().__init__() + self._settings = settings + self._calls_manager = calls_manager self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint) self.resize(QtCore.QSize(500, 270)) self.avatar_label = QtWidgets.QLabel(self) @@ -21,7 +20,7 @@ class IncomingCallWidget(widgets.CenteredWidget): self.name.setGeometry(QtCore.QRect(90, 20, 300, 25)) self._friend_number = friend_number font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) + font.setFamily(settings['font']) font.setPointSize(16) font.setBold(True) self.name.setFont(font) @@ -34,13 +33,13 @@ class IncomingCallWidget(widgets.CenteredWidget): self.accept_video.setGeometry(QtCore.QRect(170, 100, 150, 150)) self.decline = QtWidgets.QPushButton(self) self.decline.setGeometry(QtCore.QRect(320, 100, 150, 150)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_audio.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'accept_audio.png')) icon = QtGui.QIcon(pixmap) self.accept_audio.setIcon(icon) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_video.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'accept_video.png')) icon = QtGui.QIcon(pixmap) self.accept_video.setIcon(icon) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/decline_call.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'decline_call.png')) icon = QtGui.QIcon(pixmap) self.decline.setIcon(icon) self.accept_audio.setIconSize(QtCore.QSize(150, 150)) @@ -90,11 +89,11 @@ class IncomingCallWidget(widgets.CenteredWidget): self.stream.close() self.p.terminate() - self.a = AudioFile(curr_directory() + '/sounds/call.wav') + self.a = AudioFile(util.join_path(util.get_sounds_directory(), 'call.wav')) self.a.play() self.a.close() - if settings.Settings.get_instance()['calls_sound']: + if self._settings['calls_sound']: self.thread = SoundPlay() self.thread.start() else: @@ -110,24 +109,21 @@ class IncomingCallWidget(widgets.CenteredWidget): if self._processing: return self._processing = True - pr = profile.Profile.get_instance() - pr.accept_call(self._friend_number, True, False) + self._calls_manager.accept_call(self._friend_number, True, False) self.stop() def accept_call_with_video(self): if self._processing: return self._processing = True - pr = profile.Profile.get_instance() - pr.accept_call(self._friend_number, True, True) + self._calls_manager.accept_call(self._friend_number, True, True) self.stop() def decline_call(self): if self._processing: return self._processing = True - pr = profile.Profile.get_instance() - pr.stop_call(self._friend_number, False) + self._calls_manager.stop_call(self._friend_number, False) self.stop() def set_pixmap(self, pixmap): diff --git a/toxygen/ui/contact_items.py b/toxygen/ui/contact_items.py new file mode 100644 index 0000000..7a32284 --- /dev/null +++ b/toxygen/ui/contact_items.py @@ -0,0 +1,97 @@ +from wrapper.toxcore_enums_and_consts import * +from PyQt5 import QtCore, QtGui, QtWidgets +from utils.util import * +from ui.widgets import DataLabel + + +class ContactItem(QtWidgets.QWidget): + """ + Contact in friends list + """ + + def __init__(self, settings, parent=None): + QtWidgets.QWidget.__init__(self, parent) + mode = settings['compact_mode'] + self.setBaseSize(QtCore.QSize(250, 40 if mode else 70)) + self.avatar_label = QtWidgets.QLabel(self) + size = 32 if mode else 64 + self.avatar_label.setGeometry(QtCore.QRect(3, 4, size, size)) + self.avatar_label.setScaledContents(False) + self.avatar_label.setAlignment(QtCore.Qt.AlignCenter) + self.name = DataLabel(self) + self.name.setGeometry(QtCore.QRect(50 if mode else 75, 3 if mode else 10, 150, 15 if mode else 25)) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(10 if mode else 12) + font.setBold(True) + self.name.setFont(font) + self.status_message = DataLabel(self) + self.status_message.setGeometry(QtCore.QRect(50 if mode else 75, 20 if mode else 30, 170, 15 if mode else 20)) + font.setPointSize(10) + font.setBold(False) + self.status_message.setFont(font) + self.connection_status = StatusCircle(self) + self.connection_status.setGeometry(QtCore.QRect(230, -2 if mode else 5, 32, 32)) + self.messages = UnreadMessagesCount(settings, self) + self.messages.setGeometry(QtCore.QRect(20 if mode else 52, 20 if mode else 50, 30, 20)) + + +class StatusCircle(QtWidgets.QWidget): + """ + Connection status + """ + def __init__(self, parent): + QtWidgets.QWidget.__init__(self, parent) + self.setGeometry(0, 0, 32, 32) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) + self.unread = False + + def update(self, status, unread_messages=None): + if unread_messages is None: + unread_messages = self.unread + else: + self.unread = unread_messages + if status == TOX_USER_STATUS['NONE']: + name = 'online' + elif status == TOX_USER_STATUS['AWAY']: + name = 'idle' + elif status == TOX_USER_STATUS['BUSY']: + name = 'busy' + else: + name = 'offline' + if unread_messages: + name += '_notification' + self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) + else: + self.label.setGeometry(QtCore.QRect(2, 0, 32, 32)) + pixmap = QtGui.QPixmap(join_path(get_images_directory(), '{}.png'.format(name))) + self.label.setPixmap(pixmap) + + +class UnreadMessagesCount(QtWidgets.QWidget): + + def __init__(self, settings, parent=None): + super().__init__(parent) + self._settings = settings + self.resize(30, 20) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(0, 0, 30, 20)) + self.label.setVisible(False) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(12) + font.setBold(True) + self.label.setFont(font) + self.label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter) + color = settings['unread_color'] + self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') + + def update(self, messages_count): + color = self._settings['unread_color'] + self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') + if messages_count: + self.label.setVisible(True) + self.label.setText(str(messages_count)) + else: + self.label.setVisible(False) diff --git a/toxygen/ui/create_profile_screen.py b/toxygen/ui/create_profile_screen.py new file mode 100644 index 0000000..512c141 --- /dev/null +++ b/toxygen/ui/create_profile_screen.py @@ -0,0 +1,52 @@ +from ui.widgets import * +from PyQt5 import uic +import utils.util as util +import utils.ui as util_ui + + +class CreateProfileScreenResult: + + def __init__(self, save_into_default_folder, password): + self._save_into_default_folder = save_into_default_folder + self._password = password + + def get_save_into_default_folder(self): + return self._save_into_default_folder + + save_into_default_folder = property(get_save_into_default_folder) + + def get_password(self): + return self._password + + password = property(get_password) + + +class CreateProfileScreen(CenteredWidget, DialogWithResult): + + def __init__(self): + CenteredWidget.__init__(self) + DialogWithResult.__init__(self) + uic.loadUi(util.get_views_path('create_profile_screen'), self) + self.center() + self.createProfile.clicked.connect(self._create_profile) + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('New profile settings')) + self.defaultFolder.setText(util_ui.tr('Save in default folder')) + self.programFolder.setText(util_ui.tr('Save in program folder')) + self.password.setPlaceholderText(util_ui.tr('Password')) + self.confirmPassword.setPlaceholderText(util_ui.tr('Confirm password')) + self.createProfile.setText(util_ui.tr('Create profile')) + self.passwordLabel.setText(util_ui.tr('Password (at least 8 symbols):')) + + def _create_profile(self): + password = self.password.text() + if password != self.confirmPassword.text(): + self.errorLabel.setText(util_ui.tr('Passwords do not match')) + return + if 0 < len(password) < 8: + self.errorLabel.setText(util_ui.tr('Password must be at least 8 symbols')) + return + result = CreateProfileScreenResult(self.defaultFolder.isChecked(), password) + self.close_with_result(result) diff --git a/toxygen/ui/group_bans_widgets.py b/toxygen/ui/group_bans_widgets.py new file mode 100644 index 0000000..b2758c7 --- /dev/null +++ b/toxygen/ui/group_bans_widgets.py @@ -0,0 +1,68 @@ +from ui.widgets import CenteredWidget +from PyQt5 import uic, QtWidgets, QtCore +import utils.util as util +import utils.ui as util_ui + + +class GroupBanItem(QtWidgets.QWidget): + + def __init__(self, ban, cancel_ban, can_cancel_ban, parent=None): + super().__init__(parent) + self._ban = ban + self._cancel_ban = cancel_ban + self._can_cancel_ban = can_cancel_ban + + uic.loadUi(util.get_views_path('gc_ban_item'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.banTargetLabel.setText(self._ban.ban_target) + ban_time = self._ban.ban_time + self.banTimeLabel.setText(util.unix_time_to_long_str(ban_time)) + + self.cancelPushButton.clicked.connect(self._cancel_ban) + self.cancelPushButton.setEnabled(self._can_cancel_ban) + + def _retranslate_ui(self): + self.cancelPushButton.setText(util_ui.tr('Cancel ban')) + + def _cancel_ban(self): + self._cancel_ban(self._ban.ban_id) + + +class GroupBansScreen(CenteredWidget): + + def __init__(self, groups_service, group): + super().__init__() + self._groups_service = groups_service + self._group = group + + uic.loadUi(util.get_views_path('bans_list_screen'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self._refresh_bans_list() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) + + def _refresh_bans_list(self): + self.bansListWidget.clear() + can_cancel_ban = self._group.is_self_moderator_or_founder() + for ban in self._group.bans: + self._create_ban_item(ban, can_cancel_ban) + + def _create_ban_item(self, ban, can_cancel_ban): + item = GroupBanItem(ban, self._on_ban_cancelled, can_cancel_ban, self.bansListWidget) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(item.width(), item.height())) + self.bansListWidget.addItem(elem) + self.bansListWidget.setItemWidget(elem, item) + + def _on_ban_cancelled(self, ban_id): + self._groups_service.cancel_ban(self._group.number, ban_id) + self._refresh_bans_list() diff --git a/toxygen/ui/group_invites_widgets.py b/toxygen/ui/group_invites_widgets.py new file mode 100644 index 0000000..d35aca1 --- /dev/null +++ b/toxygen/ui/group_invites_widgets.py @@ -0,0 +1,127 @@ +from PyQt5 import uic, QtWidgets +import utils.util as util +from ui.widgets import * + + +class GroupInviteItem(QtWidgets.QWidget): + + def __init__(self, parent, chat_name, avatar, friend_name): + super().__init__(parent) + uic.loadUi(util.get_views_path('gc_invite_item'), self) + + self.groupNameLabel.setText(chat_name) + self.friendNameLabel.setText(friend_name) + self.friendAvatarLabel.setPixmap(avatar) + + def is_selected(self): + return self.selectCheckBox.isChecked() + + def subscribe_checked_event(self, callback): + self.selectCheckBox.clicked.connect(callback) + + +class GroupInvitesScreen(CenteredWidget): + + def __init__(self, groups_service, profile, contacts_provider): + super().__init__() + self._groups_service = groups_service + self._profile = profile + self._contacts_provider = contacts_provider + + uic.loadUi(util.get_views_path('group_invites_screen'), self) + + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self._refresh_invites_list() + + self.nickLineEdit.setText(self._profile.name) + self.statusComboBox.setCurrentIndex(self._profile.status or 0) + + self.nickLineEdit.textChanged.connect(self._nick_changed) + self.acceptPushButton.clicked.connect(self._accept_invites) + self.declinePushButton.clicked.connect(self._decline_invites) + + self.invitesListWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.invitesListWidget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + + self._update_buttons_state() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Group chat invites')) + self.noInvitesLabel.setText(util_ui.tr('No group invites found')) + self.acceptPushButton.setText(util_ui.tr('Accept')) + self.declinePushButton.setText(util_ui.tr('Decline')) + self.statusComboBox.addItem(util_ui.tr('Online')) + self.statusComboBox.addItem(util_ui.tr('Away')) + self.statusComboBox.addItem(util_ui.tr('Busy')) + self.nickLineEdit.setPlaceholderText(util_ui.tr('Your nick in chat')) + self.passwordLineEdit.setPlaceholderText(util_ui.tr('Optional password')) + + def _get_friend(self, public_key): + return self._contacts_provider.get_friend_by_public_key(public_key) + + def _accept_invites(self): + nick = self.nickLineEdit.text() + password = self.passwordLineEdit.text() + status = self.statusComboBox.currentIndex() + + selected_invites = self._get_selected_invites() + for invite in selected_invites: + self._groups_service.accept_group_invite(invite, nick, status, password) + + self._refresh_invites_list() + self._close_window_if_needed() + + def _decline_invites(self): + selected_invites = self._get_selected_invites() + for invite in selected_invites: + self._groups_service.decline_group_invite(invite) + + self._refresh_invites_list() + self._close_window_if_needed() + + def _get_selected_invites(self): + all_invites = self._groups_service.get_group_invites() + selected = [] + items_count = len(all_invites) + for index in range(items_count): + list_item = self.invitesListWidget.item(index) + item_widget = self.invitesListWidget.itemWidget(list_item) + if item_widget.is_selected(): + selected.append(all_invites[index]) + + return selected + + def _refresh_invites_list(self): + self.invitesListWidget.clear() + invites = self._groups_service.get_group_invites() + for invite in invites: + self._create_invite_item(invite) + + def _create_invite_item(self, invite): + friend = self._get_friend(invite.friend_public_key) + item = GroupInviteItem(self.invitesListWidget, invite.chat_name, friend.get_pixmap(), friend.name) + item.subscribe_checked_event(self._item_selected) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(item.width(), item.height())) + self.invitesListWidget.addItem(elem) + self.invitesListWidget.setItemWidget(elem, item) + + def _item_selected(self): + self._update_buttons_state() + + def _nick_changed(self): + self._update_buttons_state() + + def _update_buttons_state(self): + nick = self.nickLineEdit.text() + selected_items = self._get_selected_invites() + self.acceptPushButton.setEnabled(bool(nick) and len(selected_items)) + self.declinePushButton.setEnabled(len(selected_items) > 0) + + def _close_window_if_needed(self): + if self._groups_service.group_invites_count == 0: + self.close() diff --git a/toxygen/ui/group_peers_list.py b/toxygen/ui/group_peers_list.py new file mode 100644 index 0000000..9d2632d --- /dev/null +++ b/toxygen/ui/group_peers_list.py @@ -0,0 +1,33 @@ +from ui.widgets import * +from wrapper.toxcore_enums_and_consts import * + + +class PeerItem(QtWidgets.QWidget): + + def __init__(self, peer, handler, width, parent=None): + super().__init__(parent) + self.resize(QtCore.QSize(width, 34)) + self.nameLabel = DataLabel(self) + self.nameLabel.setGeometry(5, 0, width - 5, 34) + name = peer.name + if peer.is_current_user: + name += util_ui.tr(' (You)') + self.nameLabel.setText(name) + if peer.status == TOX_USER_STATUS['NONE']: + style = 'QLabel {color: green}' + elif peer.status == TOX_USER_STATUS['AWAY']: + style = 'QLabel {color: yellow}' + else: + style = 'QLabel {color: red}' + self.nameLabel.setStyleSheet(style) + self.nameLabel.mousePressEvent = lambda x: handler(peer.id) + + +class PeerTypeItem(QtWidgets.QWidget): + + def __init__(self, text, width, parent=None): + super().__init__(parent) + self.resize(QtCore.QSize(width, 34)) + self.nameLabel = DataLabel(self) + self.nameLabel.setGeometry(5, 0, width - 5, 34) + self.nameLabel.setText(text) diff --git a/toxygen/ui/group_settings_widgets.py b/toxygen/ui/group_settings_widgets.py new file mode 100644 index 0000000..c32168b --- /dev/null +++ b/toxygen/ui/group_settings_widgets.py @@ -0,0 +1,77 @@ +from ui.widgets import CenteredWidget +from PyQt5 import uic +import utils.util as util +import utils.ui as util_ui + + +class GroupManagementScreen(CenteredWidget): + + def __init__(self, groups_service, group): + super().__init__() + self._groups_service = groups_service + self._group = group + + uic.loadUi(util.get_views_path('group_management_screen'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.passwordLineEdit.setText(self._group.password) + self.privacyStateComboBox.setCurrentIndex(1 if self._group.is_private else 0) + self.peersLimitSpinBox.setValue(self._group.peers_limit) + + self.savePushButton.clicked.connect(self._save) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Group "{}"').format(self._group.name)) + self.passwordLabel.setText(util_ui.tr('Password:')) + self.peerLimitLabel.setText(util_ui.tr('Peer limit:')) + self.privacyStateLabel.setText(util_ui.tr('Privacy state:')) + self.savePushButton.setText(util_ui.tr('Save')) + + self.privacyStateComboBox.clear() + self.privacyStateComboBox.addItem(util_ui.tr('Public')) + self.privacyStateComboBox.addItem(util_ui.tr('Private')) + + def _save(self): + password = self.passwordLineEdit.text() + privacy_state = self.privacyStateComboBox.currentIndex() + peers_limit = self.peersLimitSpinBox.value() + + self._groups_service.set_group_password(self._group, password) + self._groups_service.set_group_privacy_state(self._group, privacy_state) + self._groups_service.set_group_peers_limit(self._group, peers_limit) + + self.close() + + +class GroupSettingsScreen(CenteredWidget): + + def __init__(self, group): + super().__init__() + self._group = group + + uic.loadUi(util.get_views_path('gc_settings_screen'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.copyPasswordPushButton.clicked.connect(self._copy_password) + self.copyPasswordPushButton.setEnabled(bool(self._group.password)) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Group "{}"').format(self._group.name)) + if self._group.password: + password_label_text = '{} {}'.format(util_ui.tr('Password:'), self._group.password) + else: + password_label_text = util_ui.tr('Password is not set') + self.passwordLabel.setText(password_label_text) + self.peerLimitLabel.setText('{} {}'.format(util_ui.tr('Peer limit:'), self._group.peers_limit)) + privacy_state = util_ui.tr('Private') if self._group.is_private else util_ui.tr('Public') + self.privacyStateLabel.setText('{} {}'.format(util_ui.tr('Privacy state:'), privacy_state)) + self.copyPasswordPushButton.setText(util_ui.tr('Copy password')) + + def _copy_password(self): + util_ui.copy_to_clipboard(self._group.password) diff --git a/toxygen/ui/groups_widgets.py b/toxygen/ui/groups_widgets.py new file mode 100644 index 0000000..ad4b703 --- /dev/null +++ b/toxygen/ui/groups_widgets.py @@ -0,0 +1,123 @@ +from PyQt5 import uic +import utils.util as util +from ui.widgets import * +from wrapper.toxcore_enums_and_consts import * + + +class BaseGroupScreen(CenteredWidget): + + def __init__(self, groups_service, profile): + super().__init__() + self._groups_service = groups_service + self._profile = profile + + def _retranslate_ui(self): + self.nickLineEdit.setPlaceholderText(util_ui.tr('Your nick in chat')) + self.nickLabel.setText(util_ui.tr('Nickname:')) + self.statusLabel.setText(util_ui.tr('Status:')) + self.statusComboBox.addItem(util_ui.tr('Online')) + self.statusComboBox.addItem(util_ui.tr('Away')) + self.statusComboBox.addItem(util_ui.tr('Busy')) + + +class CreateGroupScreen(BaseGroupScreen): + + def __init__(self, groups_service, profile): + super().__init__(groups_service, profile) + uic.loadUi(util.get_views_path('create_group_screen'), self) + self.center() + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.statusComboBox.setCurrentIndex(self._profile.status or 0) + self.nickLineEdit.setText(self._profile.name) + + self.addGroupButton.clicked.connect(self._create_group) + self.groupNameLineEdit.textChanged.connect(self._group_name_changed) + self.nickLineEdit.textChanged.connect(self._nick_changed) + + def _retranslate_ui(self): + super()._retranslate_ui() + self.setWindowTitle(util_ui.tr('Create new group chat')) + self.groupNameLabel.setText(util_ui.tr('Group name:')) + self.groupTypeLabel.setText(util_ui.tr('Group type:')) + self.groupNameLineEdit.setPlaceholderText(util_ui.tr('Group\'s persistent name')) + self.addGroupButton.setText(util_ui.tr('Create group')) + self.groupTypeComboBox.addItem(util_ui.tr('Public')) + self.groupTypeComboBox.addItem(util_ui.tr('Private')) + self.groupTypeComboBox.setCurrentIndex(1) + + def _create_group(self): + group_name = self.groupNameLineEdit.text() + privacy_state = self.groupTypeComboBox.currentIndex() + nick = self.nickLineEdit.text() + status = self.statusComboBox.currentIndex() + self._groups_service.create_new_gc(group_name, privacy_state, nick, status) + self.close() + + def _nick_changed(self): + self._update_button_state() + + def _group_name_changed(self): + self._update_button_state() + + def _update_button_state(self): + is_nick_set = bool(self.nickLineEdit.text()) + is_group_name_set = bool(self.groupNameLineEdit.text()) + self.addGroupButton.setEnabled(is_nick_set and is_group_name_set) + + +class JoinGroupScreen(BaseGroupScreen): + + def __init__(self, groups_service, profile): + super().__init__(groups_service, profile) + uic.loadUi(util.get_views_path('join_group_screen'), self) + self.center() + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.statusComboBox.setCurrentIndex(self._profile.status or 0) + self.nickLineEdit.setText(self._profile.name) + + self.chatIdLineEdit.textChanged.connect(self._chat_id_changed) + self.joinGroupButton.clicked.connect(self._join_group) + self.nickLineEdit.textChanged.connect(self._nick_changed) + + def _retranslate_ui(self): + super()._retranslate_ui() + self.setWindowTitle(util_ui.tr('Join public group chat')) + self.chatIdLabel.setText(util_ui.tr('Group ID:')) + self.passwordLabel.setText(util_ui.tr('Password:')) + self.chatIdLineEdit.setPlaceholderText(util_ui.tr('Group\'s chat ID')) + self.joinGroupButton.setText(util_ui.tr('Join group')) + self.passwordLineEdit.setPlaceholderText(util_ui.tr('Optional password')) + + def _chat_id_changed(self): + self._update_button_state() + + def _nick_changed(self): + self._update_button_state() + + def _update_button_state(self): + chat_id = self._get_chat_id() + is_nick_set = bool(self.nickLineEdit.text()) + self.joinGroupButton.setEnabled(len(chat_id) == TOX_GROUP_CHAT_ID_SIZE * 2 and is_nick_set) + + def _join_group(self): + chat_id = self._get_chat_id() + password = self.passwordLineEdit.text() + nick = self.nickLineEdit.text() + status = self.statusComboBox.currentIndex() + self._groups_service.join_gc_by_id(chat_id, password, nick, status) + self.close() + + def _get_chat_id(self): + chat_id = self.chatIdLineEdit.text().strip() + if chat_id.startswith('tox:'): + chat_id = chat_id[4:] + + return chat_id diff --git a/toxygen/ui/items_factories.py b/toxygen/ui/items_factories.py new file mode 100644 index 0000000..7346f8f --- /dev/null +++ b/toxygen/ui/items_factories.py @@ -0,0 +1,90 @@ +from ui.contact_items import * +from ui.messages_widgets import * + + +class ContactItemsFactory: + + def __init__(self, settings, main_screen): + self._settings = settings + self._friends_list = main_screen.friends_list + + def create_contact_item(self): + item = ContactItem(self._settings) + elem = QtWidgets.QListWidgetItem(self._friends_list) + elem.setSizeHint(QtCore.QSize(250, 40 if self._settings['compact_mode'] else 70)) + self._friends_list.addItem(elem) + self._friends_list.setItemWidget(elem, item) + + return item + + +class MessagesItemsFactory: + + def __init__(self, settings, plugin_loader, smiley_loader, main_screen, delete_action): + self._file_transfers_handler = None + self._settings, self._plugin_loader = settings, plugin_loader + self._smiley_loader, self._delete_action = smiley_loader, delete_action + self._messages = main_screen.messages + self._message_edit = main_screen.messageEdit + + def set_file_transfers_handler(self, file_transfers_handler): + self._file_transfers_handler = file_transfers_handler + + def create_message_item(self, message, append=True, pixmap=None): + item = message.get_widget(self._settings, self._create_message_browser, + self._delete_action, self._messages) + if pixmap is not None: + item.set_avatar(pixmap) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(0, elem) + self._messages.setItemWidget(elem, item) + + return item + + def create_inline_item(self, message, append=True, position=0): + elem = QtWidgets.QListWidgetItem() + item = InlineImageItem(message.data, self._messages.width(), elem, self._messages) + elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(position, elem) + self._messages.setItemWidget(elem, item) + + return item + + def create_unsent_file_item(self, message, append=True): + item = message.get_widget(self._file_transfers_handler, self._settings, self._messages.width(), self._messages) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(0, elem) + self._messages.setItemWidget(elem, item) + + return item + + def create_file_transfer_item(self, message, append=True): + item = message.get_widget(self._file_transfers_handler, self._settings, self._messages.width(), self._messages) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(0, elem) + self._messages.setItemWidget(elem, item) + + return item + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _create_message_browser(self, text, width, message_type, parent=None): + return MessageBrowser(self._settings, self._message_edit, self._smiley_loader, self._plugin_loader, + text, width, message_type, parent) diff --git a/toxygen/ui/login_screen.py b/toxygen/ui/login_screen.py new file mode 100644 index 0000000..35e33b5 --- /dev/null +++ b/toxygen/ui/login_screen.py @@ -0,0 +1,77 @@ +from ui.widgets import * +from PyQt5 import uic +import utils.util as util +import utils.ui as util_ui +import os.path + + +class LoginScreenResult: + + def __init__(self, profile_path, load_as_default, password=None): + self._profile_path = profile_path + self._load_as_default = load_as_default + self._password = password + + def get_profile_path(self): + return self._profile_path + + profile_path = property(get_profile_path) + + def get_load_as_default(self): + return self._load_as_default + + load_as_default = property(get_load_as_default) + + def get_password(self): + return self._password + + password = property(get_password) + + def is_new_profile(self): + return not os.path.isfile(self._profile_path) + + +class LoginScreen(CenteredWidget, DialogWithResult): + + def __init__(self): + CenteredWidget.__init__(self) + DialogWithResult.__init__(self) + uic.loadUi(util.get_views_path('login_screen'), self) + self.center() + self._profiles = [] + self._update_ui() + + def update_select(self, profiles): + profiles = sorted(profiles, key=lambda p: p[1]) + self._profiles = list(profiles) + self.profilesComboBox.addItems(list(map(lambda p: p[1], profiles))) + self.loadProfilePushButton.setEnabled(len(profiles) > 0) + + def _update_ui(self): + self.profileNameLineEdit = LineEditWithEnterSupport(self._create_profile, self) + self.profileNameLineEdit.setGeometry(QtCore.QRect(20, 100, 160, 30)) + self._retranslate_ui() + self.createProfilePushButton.clicked.connect(self._create_profile) + self.loadProfilePushButton.clicked.connect(self._load_existing_profile) + + def _create_profile(self): + path = self.profileNameLineEdit.text() + load_as_default = self.defaultProfileCheckBox.isChecked() + result = LoginScreenResult(path, load_as_default) + self.close_with_result(result) + + def _load_existing_profile(self): + index = self.profilesComboBox.currentIndex() + load_as_default = self.defaultProfileCheckBox.isChecked() + path = util.join_path(self._profiles[index][0], self._profiles[index][1] + '.tox') + result = LoginScreenResult(path, load_as_default) + self.close_with_result(result) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Log in')) + self.profileNameLineEdit.setPlaceholderText(util_ui.tr('Profile name')) + self.createProfilePushButton.setText(util_ui.tr('Create')) + self.loadProfilePushButton.setText(util_ui.tr('Load profile')) + self.defaultProfileCheckBox.setText(util_ui.tr('Use as default')) + self.existingProfileGroupBox.setTitle(util_ui.tr('Load existing profile')) + self.newProfileGroupBox.setTitle(util_ui.tr('Create new profile')) diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py new file mode 100644 index 0000000..5a510a5 --- /dev/null +++ b/toxygen/ui/main_screen.py @@ -0,0 +1,718 @@ +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 + + +class MainWindow(QtWidgets.QMainWindow): + + def __init__(self, settings, tray): + super().__init__() + self._settings = settings + self._contacts_manager = None + self._tray = tray + self._widget_factory = None + self._modal_window = None + self._plugins_loader = None + self.setAcceptDrops(True) + self._saved = False + self._smiley_window = None + self._profile = self._toxes = self._messenger = None + self._file_transfer_handler = self._history_loader = self._groups_service = self._calls_manager = None + self._should_show_group_peers_list = False + self.initUI() + + def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader, + file_transfer_handler, history_loader, calls_manager, groups_service, toxes): + self._widget_factory = widget_factory + self._tray = tray + self._contacts_manager = contacts_manager + self._profile = profile + self._plugins_loader = plugins_loader + self._file_transfer_handler = file_transfer_handler + self._history_loader = history_loader + self._calls_manager = calls_manager + self._groups_service = groups_service + self._toxes = toxes + 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) + + self.update_gc_invites_button_state() + + def show(self): + super().show() + self._contacts_manager.update() + if self._settings['show_welcome_screen']: + self._modal_window = self._widget_factory.create_welcome_window() + + 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.menuProfile = QtWidgets.QMenu(self.menubar) + self.menuProfile.setObjectName("menuProfile") + self.menuGC = QtWidgets.QMenu(self.menubar) + self.menuSettings = QtWidgets.QMenu(self.menubar) + self.menuSettings.setObjectName("menuSettings") + self.menuPlugins = QtWidgets.QMenu(self.menubar) + self.menuPlugins.setObjectName("menuPlugins") + self.menuAbout = QtWidgets.QMenu(self.menubar) + self.menuAbout.setObjectName("menuAbout") + + self.actionAdd_friend = QtWidgets.QAction(window) + self.actionAdd_friend.setObjectName("actionAdd_friend") + self.actionprofilesettings = QtWidgets.QAction(window) + self.actionprofilesettings.setObjectName("actionprofilesettings") + self.actionPrivacy_settings = QtWidgets.QAction(window) + self.actionPrivacy_settings.setObjectName("actionPrivacy_settings") + self.actionInterface_settings = QtWidgets.QAction(window) + self.actionInterface_settings.setObjectName("actionInterface_settings") + self.actionNotifications = QtWidgets.QAction(window) + self.actionNotifications.setObjectName("actionNotifications") + self.actionNetwork = QtWidgets.QAction(window) + self.actionNetwork.setObjectName("actionNetwork") + self.actionAbout_program = QtWidgets.QAction(window) + self.actionAbout_program.setObjectName("actionAbout_program") + self.updateSettings = QtWidgets.QAction(window) + self.actionSettings = QtWidgets.QAction(window) + self.actionSettings.setObjectName("actionSettings") + self.audioSettings = QtWidgets.QAction(window) + self.videoSettings = QtWidgets.QAction(window) + self.pluginData = QtWidgets.QAction(window) + self.importPlugin = QtWidgets.QAction(window) + self.reloadPlugins = QtWidgets.QAction(window) + self.lockApp = QtWidgets.QAction(window) + self.createGC = QtWidgets.QAction(window) + self.joinGC = QtWidgets.QAction(window) + self.gc_invites = QtWidgets.QAction(window) + + self.menuProfile.addAction(self.actionAdd_friend) + self.menuProfile.addAction(self.actionSettings) + self.menuProfile.addAction(self.lockApp) + self.menuGC.addAction(self.createGC) + self.menuGC.addAction(self.joinGC) + self.menuGC.addAction(self.gc_invites) + self.menuSettings.addAction(self.actionPrivacy_settings) + self.menuSettings.addAction(self.actionInterface_settings) + self.menuSettings.addAction(self.actionNotifications) + self.menuSettings.addAction(self.actionNetwork) + self.menuSettings.addAction(self.audioSettings) + self.menuSettings.addAction(self.videoSettings) + self.menuSettings.addAction(self.updateSettings) + self.menuPlugins.addAction(self.pluginData) + self.menuPlugins.addAction(self.importPlugin) + self.menuPlugins.addAction(self.reloadPlugins) + self.menuAbout.addAction(self.actionAbout_program) + + self.menubar.addAction(self.menuProfile.menuAction()) + self.menubar.addAction(self.menuGC.menuAction()) + self.menubar.addAction(self.menuSettings.menuAction()) + self.menubar.addAction(self.menuPlugins.menuAction()) + self.menubar.addAction(self.menuAbout.menuAction()) + + self.actionAbout_program.triggered.connect(self.about_program) + self.actionNetwork.triggered.connect(self.network_settings) + self.actionAdd_friend.triggered.connect(self.add_contact_triggered) + self.createGC.triggered.connect(self.create_gc) + self.joinGC.triggered.connect(self.join_gc) + self.actionSettings.triggered.connect(self.profile_settings) + self.actionPrivacy_settings.triggered.connect(self.privacy_settings) + self.actionInterface_settings.triggered.connect(self.interface_settings) + self.actionNotifications.triggered.connect(self.notification_settings) + self.audioSettings.triggered.connect(self.audio_settings) + self.videoSettings.triggered.connect(self.video_settings) + self.updateSettings.triggered.connect(self.update_settings) + self.pluginData.triggered.connect(self.plugins_menu) + self.lockApp.triggered.connect(self.lock_app) + self.importPlugin.triggered.connect(self.import_plugin) + self.reloadPlugins.triggered.connect(self.reload_plugins) + self.gc_invites.triggered.connect(self._open_gc_invites_list) + + def languageChange(self, *args, **kwargs): + self.retranslateUi() + + def event(self, event): + if event.type() == QtCore.QEvent.WindowActivate: + self._tray.setIcon(QtGui.QIcon(util.join_path(util.get_images_directory(), 'icon.png'))) + self.messages.repaint() + return super().event(event) + + def retranslateUi(self): + self.lockApp.setText(util_ui.tr("Lock")) + self.menuPlugins.setTitle(util_ui.tr("Plugins")) + self.menuGC.setTitle(util_ui.tr("Group chats")) + self.pluginData.setText(util_ui.tr("List of plugins")) + self.menuProfile.setTitle(util_ui.tr("Profile")) + self.menuSettings.setTitle(util_ui.tr("Settings")) + self.menuAbout.setTitle(util_ui.tr("About")) + self.actionAdd_friend.setText(util_ui.tr("Add contact")) + self.createGC.setText(util_ui.tr("Create group chat")) + self.joinGC.setText(util_ui.tr("Join group chat")) + self.gc_invites.setText(util_ui.tr("Group invites")) + self.actionprofilesettings.setText(util_ui.tr("Profile")) + self.actionPrivacy_settings.setText(util_ui.tr("Privacy")) + self.actionInterface_settings.setText(util_ui.tr("Interface")) + self.actionNotifications.setText(util_ui.tr("Notifications")) + self.actionNetwork.setText(util_ui.tr("Network")) + self.actionAbout_program.setText(util_ui.tr("About 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.searchLineEdit.setPlaceholderText(util_ui.tr("Search")) + self.sendMessageButton.setToolTip(util_ui.tr("Send message")) + self.callButton.setToolTip(util_ui.tr("Start audio call with friend")) + self.contactsFilterComboBox.clear() + self.contactsFilterComboBox.addItem(util_ui.tr("All")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online first")) + self.contactsFilterComboBox.addItem(util_ui.tr("Name")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online and by name")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online first and by name")) + + def setup_right_bottom(self, Form): + Form.resize(650, 60) + self.messageEdit = MessageArea(Form, self) + self.messageEdit.setGeometry(QtCore.QRect(0, 3, 450, 55)) + font = QtGui.QFont() + font.setPointSize(11) + font.setFamily(self._settings['font']) + self.messageEdit.setFont(font) + + self.sendMessageButton = QtWidgets.QPushButton(Form) + self.sendMessageButton.setGeometry(QtCore.QRect(565, 3, 60, 55)) + + self.menuButton = MenuButton(Form, self.show_menu) + self.menuButton.setGeometry(QtCore.QRect(QtCore.QRect(455, 3, 55, 55))) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'send.png')) + icon = QtGui.QIcon(pixmap) + self.sendMessageButton.setIcon(icon) + self.sendMessageButton.setIconSize(QtCore.QSize(45, 60)) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'menu.png')) + icon = QtGui.QIcon(pixmap) + self.menuButton.setIcon(icon) + self.menuButton.setIconSize(QtCore.QSize(40, 40)) + + self.sendMessageButton.clicked.connect(self.send_message) + + QtCore.QMetaObject.connectSlotsByName(Form) + + def setup_left_column(self, left_column): + uic.loadUi(util.get_views_path('ms_left_column'), left_column) + + pixmap = QtGui.QPixmap() + pixmap.load(util.join_path(util.get_images_directory(), 'search.png')) + left_column.searchLabel.setPixmap(pixmap) + + self.name = DataLabel(left_column) + self.name.setGeometry(QtCore.QRect(75, 15, 150, 25)) + font = QtGui.QFont() + font.setFamily(self._settings['font']) + font.setPointSize(14) + font.setBold(True) + self.name.setFont(font) + + self.status_message = DataLabel(left_column) + self.status_message.setGeometry(QtCore.QRect(75, 35, 170, 25)) + + self.connection_status = StatusCircle(left_column) + self.connection_status.setGeometry(QtCore.QRect(230, 10, 32, 32)) + + left_column.contactsFilterComboBox.activated[int].connect(lambda x: self._filtering()) + + self.avatar_label = left_column.avatarLabel + self.searchLineEdit = left_column.searchLineEdit + self.contacts_filter = self.contactsFilterComboBox = left_column.contactsFilterComboBox + + self.groupInvitesPushButton = left_column.groupInvitesPushButton + + self.groupInvitesPushButton.clicked.connect(self._open_gc_invites_list) + self.avatar_label.mouseReleaseEvent = self.profile_settings + self.status_message.mouseReleaseEvent = self.profile_settings + self.name.mouseReleaseEvent = self.profile_settings + + self.friends_list = left_column.friendsListWidget + self.friends_list.itemSelectionChanged.connect(self._selected_contact_changed) + self.friends_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.friends_list.customContextMenuRequested.connect(self._friend_right_click) + self.friends_list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.friends_list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.friends_list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.friends_list.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) + + def setup_right_top(self, Form): + Form.resize(650, 75) + self.account_avatar = QtWidgets.QLabel(Form) + self.account_avatar.setGeometry(QtCore.QRect(10, 5, 64, 64)) + self.account_avatar.setScaledContents(False) + self.account_name = DataLabel(Form) + self.account_name.setGeometry(QtCore.QRect(100, 0, 400, 25)) + self.account_name.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) + font = QtGui.QFont() + font.setFamily(self._settings['font']) + font.setPointSize(14) + font.setBold(True) + self.account_name.setFont(font) + self.account_status = DataLabel(Form) + self.account_status.setGeometry(QtCore.QRect(100, 20, 400, 25)) + self.account_status.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) + font.setPointSize(12) + font.setBold(False) + self.account_status.setFont(font) + self.account_status.setObjectName("account_status") + self.callButton = QtWidgets.QPushButton(Form) + self.callButton.setGeometry(QtCore.QRect(550, 5, 50, 50)) + self.callButton.setObjectName("callButton") + self.callButton.clicked.connect(lambda: self._calls_manager.call_click(True)) + self.videocallButton = QtWidgets.QPushButton(Form) + self.videocallButton.setGeometry(QtCore.QRect(550, 5, 50, 50)) + self.videocallButton.setObjectName("videocallButton") + self.videocallButton.clicked.connect(lambda: self._calls_manager.call_click(True, True)) + self.groupMenuButton = QtWidgets.QPushButton(Form) + self.groupMenuButton.setGeometry(QtCore.QRect(470, 10, 50, 50)) + self.groupMenuButton.clicked.connect(self._toggle_gc_peers_list) + self.groupMenuButton.setVisible(False) + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'menu.png')) + icon = QtGui.QIcon(pixmap) + self.groupMenuButton.setIcon(icon) + self.groupMenuButton.setIconSize(QtCore.QSize(45, 60)) + self.update_call_state('call') + self.typing = QtWidgets.QLabel(Form) + self.typing.setGeometry(QtCore.QRect(500, 25, 50, 30)) + pixmap = QtGui.QPixmap(QtCore.QSize(50, 30)) + pixmap.load(util.join_path(util.get_images_directory(), 'typing.png')) + self.typing.setScaledContents(False) + self.typing.setPixmap(pixmap.scaled(50, 30, QtCore.Qt.KeepAspectRatio)) + self.typing.setVisible(False) + QtCore.QMetaObject.connectSlotsByName(Form) + + def setup_right_center(self, widget): + self.messages = QtWidgets.QListWidget(widget) + self.messages.setGeometry(0, 0, 620, 310) + self.messages.setObjectName("messages") + self.messages.setSpacing(1) + self.messages.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.messages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.messages.focusOutEvent = lambda event: self.messages.clearSelection() + self.messages.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) + + def load(pos): + if not pos: + contact = self._contacts_manager.get_curr_contact() + self._history_loader.load_history(contact) + self.messages.verticalScrollBar().setValue(1) + self.messages.verticalScrollBar().valueChanged.connect(load) + self.messages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.messages.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + self.peers_list = QtWidgets.QListWidget(widget) + self.peers_list.setGeometry(0, 0, 0, 0) + self.peers_list.setObjectName("peersList") + self.peers_list.setSpacing(1) + self.peers_list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.peers_list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.peers_list.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) + self.peers_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + + def initUI(self): + self.setMinimumSize(920, 500) + s = self._settings + self.setGeometry(s['x'], s['y'], s['width'], s['height']) + self.setWindowTitle('Toxygen') + menu = QtWidgets.QWidget() + main = QtWidgets.QWidget() + grid = QtWidgets.QGridLayout() + info = QtWidgets.QWidget() + left_column = QtWidgets.QWidget() + messages = QtWidgets.QWidget() + message_buttons = QtWidgets.QWidget() + self.setup_right_center(messages) + self.setup_right_top(info) + self.setup_right_bottom(message_buttons) + self.setup_left_column(left_column) + self.setup_menu(menu) + if not s['mirror_mode']: + grid.addWidget(left_column, 1, 0, 4, 1) + grid.addWidget(messages, 2, 1, 2, 1) + grid.addWidget(info, 1, 1) + grid.addWidget(message_buttons, 4, 1) + grid.setColumnMinimumWidth(1, 500) + grid.setColumnMinimumWidth(0, 270) + else: + grid.addWidget(left_column, 1, 1, 4, 1) + grid.addWidget(messages, 2, 0, 2, 1) + grid.addWidget(info, 1, 0) + grid.addWidget(message_buttons, 4, 0) + grid.setColumnMinimumWidth(0, 500) + grid.setColumnMinimumWidth(1, 270) + + grid.addWidget(menu, 0, 0, 1, 2) + grid.setSpacing(0) + grid.setContentsMargins(0, 0, 0, 0) + grid.setRowMinimumHeight(0, 25) + grid.setRowMinimumHeight(1, 75) + grid.setRowMinimumHeight(2, 25) + grid.setRowMinimumHeight(3, 320) + grid.setRowMinimumHeight(4, 55) + grid.setColumnStretch(1, 1) + grid.setRowStretch(3, 1) + main.setLayout(grid) + self.setCentralWidget(main) + self.messageEdit.setFocus() + self.friend_info = info + self.retranslateUi() + + def closeEvent(self, event): + close_setting = self._settings['close_app'] + if close_setting == 0 or self._settings.closing: + if self._saved: + return + self._saved = True + self._settings['x'] = self.geometry().x() + self._settings['y'] = self.geometry().y() + self._settings['width'] = self.width() + self._settings['height'] = self.height() + self._settings.save() + util_ui.close_all_windows() + event.accept() + elif close_setting == 2 and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + event.ignore() + self.hide() + else: + event.ignore() + self.showMinimized() + + def close_window(self): + self._settings.closing = True + self.close() + + def resizeEvent(self, *args, **kwargs): + width = self.width() - 270 + if not self._should_show_group_peers_list: + self.messages.setGeometry(0, 0, width, self.height() - 155) + self.peers_list.setGeometry(0, 0, 0, 0) + else: + self.messages.setGeometry(0, 0, width * 3 // 4, self.height() - 155) + self.peers_list.setGeometry(width * 3 // 4, 0, width - width * 3 // 4, self.height() - 155) + + invites_button_visible = self.groupInvitesPushButton.isVisible() + self.friends_list.setGeometry(0, 125 if invites_button_visible else 100, + 270, self.height() - 150 if invites_button_visible else self.height() - 125) + + self.videocallButton.setGeometry(QtCore.QRect(self.width() - 330, 10, 50, 50)) + self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 10, 50, 50)) + self.groupMenuButton.setGeometry(QtCore.QRect(self.width() - 450, 10, 50, 50)) + self.typing.setGeometry(QtCore.QRect(self.width() - 450, 20, 50, 30)) + + self.messageEdit.setGeometry(QtCore.QRect(55, 0, self.width() - 395, 55)) + self.menuButton.setGeometry(QtCore.QRect(0, 0, 55, 55)) + self.sendMessageButton.setGeometry(QtCore.QRect(self.width() - 340, 0, 70, 55)) + + self.account_name.setGeometry(QtCore.QRect(100, 15, self.width() - 560, 25)) + self.account_status.setGeometry(QtCore.QRect(100, 35, self.width() - 560, 25)) + self.messageEdit.setFocus() + + def keyPressEvent(self, event): + key, modifiers = event.key(), event.modifiers() + if key == QtCore.Qt.Key_Escape and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + self.hide() + elif key == QtCore.Qt.Key_C and modifiers & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): + rows = list(map(lambda x: self.messages.row(x), self.messages.selectedItems())) + indexes = (rows[0] - self.messages.count(), rows[-1] - self.messages.count()) + s = self._history_loader.export_history(self._contacts_manager.get_curr_friend(), True, indexes) + self.copy_text(s) + elif key == QtCore.Qt.Key_Z and modifiers & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): + self.messages.clearSelection() + elif key == QtCore.Qt.Key_F and modifiers & QtCore.Qt.ControlModifier: + self.show_search_field() + else: + super().keyPressEvent(event) + + # ----------------------------------------------------------------------------------------------------------------- + # Functions which called when user click in menu + # ----------------------------------------------------------------------------------------------------------------- + + def about_program(self): + # TODO: replace with window + text = util_ui.tr('Toxygen is Tox client written on Python.\nVersion: ') + text += '' + '\nGitHub: https://github.com/toxygen-project/toxygen/' + title = util_ui.tr('About') + util_ui.message_box(text, title) + + def network_settings(self): + self._modal_window = self._widget_factory.create_network_settings_window() + self._modal_window.show() + + def plugins_menu(self): + self._modal_window = self._widget_factory.create_plugins_settings_window() + self._modal_window.show() + + def add_contact_triggered(self, _): + self.add_contact() + + def add_contact(self, link=''): + self._modal_window = self._widget_factory.create_add_contact_window(link) + self._modal_window.show() + + def create_gc(self): + self._modal_window = self._widget_factory.create_group_screen_window() + self._modal_window.show() + + def join_gc(self): + self._modal_window = self._widget_factory.create_join_group_screen_window() + self._modal_window.show() + + def profile_settings(self, _): + self._modal_window = self._widget_factory.create_profile_settings_window() + self._modal_window.show() + + def privacy_settings(self): + self._modal_window = self._widget_factory.create_privacy_settings_window() + self._modal_window.show() + + def notification_settings(self): + self._modal_window = self._widget_factory.create_notification_settings_window() + self._modal_window.show() + + def interface_settings(self): + self._modal_window = self._widget_factory.create_interface_settings_window() + self._modal_window.show() + + def audio_settings(self): + self._modal_window = self._widget_factory.create_audio_settings_window() + self._modal_window.show() + + def video_settings(self): + self._modal_window = self._widget_factory.create_video_settings_window() + self._modal_window.show() + + def update_settings(self): + self._modal_window = self._widget_factory.create_update_settings_window() + self._modal_window.show() + + def reload_plugins(self): + if self._plugin_loader is not None: + self._plugin_loader.reload() + + @staticmethod + def import_plugin(): + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugin')) + if directory: + src = directory + '/' + dest = util.get_plugins_directory() + util.copy(src, dest) + util_ui.message_box(util_ui.tr('Plugin will be loaded after restart'), util_ui.tr("Restart Toxygen")) + + def lock_app(self): + if self._toxes.has_password(): + self._settings.locked = True + self.hide() + else: + util_ui.message_box(util_ui.tr('Error. Profile password is not set.'), util_ui.tr("Cannot lock app")) + + def show_menu(self): + if not hasattr(self, 'menu'): + self.menu = DropdownMenu(self) + self.menu.setGeometry(QtCore.QRect(0 if self._settings['mirror_mode'] else 270, + self.height() - 120, + 180, + 120)) + self.menu.show() + + # ----------------------------------------------------------------------------------------------------------------- + # Messages, calls and file transfers + # ----------------------------------------------------------------------------------------------------------------- + + def send_message(self): + self._messenger.send_message() + + def send_file(self): + self.menu.hide() + if self._contacts_manager.is_active_a_friend(): + caption = util_ui.tr('Choose file') + name = util_ui.file_dialog(caption) + if name[0]: + self._file_transfer_handler.send_file(name[0], self._contacts_manager.get_active_number()) + + def send_screenshot(self, hide=False): + self.menu.hide() + if self._contacts_manager.is_active_a_friend(): + self.sw = self._widget_factory.create_screenshot_window(self) + self.sw.show() + if hide: + self.hide() + + def send_smiley(self): + self.menu.hide() + if self._contacts_manager.get_curr_contact() is None: + return + self._smiley_window = self._widget_factory.create_smiley_window(self) + rect = QtCore.QRect(self.menu.x(), + self.menu.y() - self.menu.height(), + self._smiley_window.width(), + self._smiley_window.height()) + self._smiley_window.setGeometry(rect) + self._smiley_window.show() + + def send_sticker(self): + self.menu.hide() + if self._contacts_manager.is_active_a_friend(): + self.sticker = self._widget_factory.create_sticker_window() + self.sticker.setGeometry(QtCore.QRect(self.x() if self._settings['mirror_mode'] else 270 + self.x(), + self.y() + self.height() - 200, + self.sticker.width(), + self.sticker.height())) + self.sticker.show() + + def active_call(self): + self.update_call_state('finish_call') + + def incoming_call(self): + self.update_call_state('incoming_call') + + def call_finished(self): + self.update_call_state('call') + + def update_call_state(self, state): + pixmap = QtGui.QPixmap(os.path.join(util.get_images_directory(), '{}.png'.format(state))) + icon = QtGui.QIcon(pixmap) + self.callButton.setIcon(icon) + self.callButton.setIconSize(QtCore.QSize(50, 50)) + + pixmap = QtGui.QPixmap(os.path.join(util.get_images_directory(), '{}_video.png'.format(state))) + icon = QtGui.QIcon(pixmap) + self.videocallButton.setIcon(icon) + self.videocallButton.setIconSize(QtCore.QSize(35, 35)) + + # ----------------------------------------------------------------------------------------------------------------- + # Functions which called when user open context menu in friends list + # ----------------------------------------------------------------------------------------------------------------- + + def _friend_right_click(self, pos): + item = self.friends_list.itemAt(pos) + number = self.friends_list.indexFromItem(item).row() + contact = self._contacts_manager.get_contact(number) + if contact is None or item is None: + return + generator = contact.get_context_menu_generator() + self.listMenu = generator.generate(self._plugins_loader, self._contacts_manager, self, self._settings, number, + self._groups_service, self._history_loader) + parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) + self.listMenu.move(parent_position + pos) + self.listMenu.show() + + def show_note(self, friend): + note = self._settings['notes'][friend.tox_id] if friend.tox_id in self._settings['notes'] else '' + user = util_ui.tr('Notes about user') + user = '{} {}'.format(user, friend.name) + + def save_note(text): + if friend.tox_id in self._settings['notes']: + del self._settings['notes'][friend.tox_id] + if text: + self._settings['notes'][friend.tox_id] = text + self._settings.save() + self.note = MultilineEdit(user, note, save_note) + self.note.show() + + def set_alias(self, num): + self._contacts_manager.set_alias(num) + + def remove_friend(self, num): + self._contacts_manager.delete_friend(num) + + def block_friend(self, num): + friend = self._contacts_manager.get_contact(num) + self._contacts_manager.block_user(friend.tox_id) + + @staticmethod + def copy_text(text): + util_ui.copy_to_clipboard(text) + + def auto_accept(self, num, value): + tox_id = self._contacts_manager.friend_public_key(num) + if value: + self._settings['auto_accept_from_friends'].append(tox_id) + else: + self._settings['auto_accept_from_friends'].remove(tox_id) + self._settings.save() + + def invite_friend_to_gc(self, friend_number, group_number): + self._contacts_manager.invite_friend(friend_number, group_number) + + def select_contact_row(self, row_index): + self.friends_list.setCurrentRow(row_index) + + # ----------------------------------------------------------------------------------------------------------------- + # Functions which called when user click somewhere else + # ----------------------------------------------------------------------------------------------------------------- + + def _selected_contact_changed(self): + num = self.friends_list.currentRow() + if self._contacts_manager.active_contact != num: + self._contacts_manager.active_contact = num + self.groupMenuButton.setVisible(self._contacts_manager.is_active_a_group()) + + def mouseReleaseEvent(self, event): + pos = self.connection_status.pos() + x, y = pos.x(), pos.y() + 25 + if (x < event.x() < x + 32) and (y < event.y() < y + 32): + self._profile.change_status() + else: + super().mouseReleaseEvent(event) + + def _filtering(self): + index = self.contactsFilterComboBox.currentIndex() + search_text = self.searchLineEdit.text() + self._contacts_manager.filtration_and_sorting(index, search_text) + + def show_search_field(self): + if hasattr(self, 'search_field') and self.search_field.isVisible(): + return + if self._contacts_manager.get_curr_friend() is None: + return + self.search_field = self._widget_factory.create_search_screen(self.messages) + x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40 + self.search_field.setGeometry(x, y, self.messages.width(), 40) + self.messages.setGeometry(x, self.messages.y(), self.messages.width(), self.messages.height() - 40) + if self._should_show_group_peers_list: + self.peers_list.setFixedHeight(self.peers_list.height() - 40) + self.search_field.show() + + def _toggle_gc_peers_list(self): + self._should_show_group_peers_list = not self._should_show_group_peers_list + self.resizeEvent() + if self._should_show_group_peers_list: + self._groups_service.generate_peers_list() + + def _new_contact_selected(self, _): + if self._should_show_group_peers_list: + self._toggle_gc_peers_list() + index = self.friends_list.currentRow() + if self._contacts_manager.active_contact != index: + self.friends_list.setCurrentRow(self._contacts_manager.active_contact) + self.resizeEvent() + + def _open_gc_invites_list(self): + self._modal_window = self._widget_factory.create_group_invites_window() + self._modal_window.show() + + def update_gc_invites_button_state(self): + invites_count = self._groups_service.group_invites_count + self.groupInvitesPushButton.setVisible(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/mainscreen_widgets.py b/toxygen/ui/main_screen_widgets.py similarity index 58% rename from toxygen/mainscreen_widgets.py rename to toxygen/ui/main_screen_widgets.py index 0d1c26b..122561b 100644 --- a/toxygen/mainscreen_widgets.py +++ b/toxygen/ui/main_screen_widgets.py @@ -1,20 +1,27 @@ from PyQt5 import QtCore, QtGui, QtWidgets -from widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit -from profile import Profile -import smileys -import util -import platform +from ui.widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit +import urllib +import re +import utils.util as util +import utils.ui as util_ui +from stickers.stickers import load_stickers class MessageArea(QtWidgets.QPlainTextEdit): """User types messages here""" def __init__(self, parent, form): - super(MessageArea, self).__init__(parent) + super().__init__(parent) + self._messenger = self._contacts_manager = self._file_transfer_handler = None self.parent = form self.setAcceptDrops(True) - self.timer = QtCore.QTimer(self) - self.timer.timeout.connect(lambda: self.parent.profile.send_typing(False)) + self._timer = QtCore.QTimer(self) + self._timer.timeout.connect(lambda: self._messenger.send_typing(False)) + + def set_dependencies(self, messenger, contacts_manager, file_transfer_handler): + self._messenger = messenger + self._contacts_manager = contacts_manager + self._file_transfer_handler = file_transfer_handler def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Paste): @@ -29,22 +36,29 @@ class MessageArea(QtWidgets.QPlainTextEdit): if modifiers & QtCore.Qt.ControlModifier or modifiers & QtCore.Qt.ShiftModifier: self.insertPlainText('\n') else: - if self.timer.isActive(): - self.timer.stop() - self.parent.profile.send_typing(False) - self.parent.send_message() + if self._timer.isActive(): + self._timer.stop() + self._messenger.send_typing(False) + self._messenger.send_message() elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText(): - self.appendPlainText(Profile.get_instance().get_last_message()) - elif event.key() == QtCore.Qt.Key_Tab and not self.parent.profile.is_active_a_friend(): + self.appendPlainText(self._messenger.get_last_message()) + elif event.key() == QtCore.Qt.Key_Tab and self._contacts_manager.is_active_a_group(): text = self.toPlainText() - pos = self.textCursor().position() - self.insertPlainText(Profile.get_instance().get_gc_peer_name(text[:pos])) + text_cursor = self.textCursor() + pos = text_cursor.position() + current_word = re.split("\s+", text[:pos])[-1] + start_index = text.rindex(current_word, 0, pos) + peer_name = self._contacts_manager.get_gc_peer_name(current_word) + self.setPlainText(text[:start_index] + peer_name + text[pos:]) + new_pos = start_index + len(peer_name) + text_cursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor) + self.setTextCursor(text_cursor) else: - self.parent.profile.send_typing(True) - if self.timer.isActive(): - self.timer.stop() - self.timer.start(5000) - super(MessageArea, self).keyPressEvent(event) + self._messenger.send_typing(True) + if self._timer.isActive(): + self._timer.stop() + self._timer.start(5000) + super().keyPressEvent(event) def contextMenuEvent(self, event): menu = create_menu(self.createStandardContextMenu()) @@ -71,21 +85,30 @@ class MessageArea(QtWidgets.QPlainTextEdit): def pasteEvent(self, text=None): text = text or QtWidgets.QApplication.clipboard().text() if text.startswith('file://'): - file_name = self.parse_file_name(text) - self.parent.profile.send_file(file_name) + if not self._contacts_manager.is_active_a_friend(): + return + friend_number = self._contacts_manager.get_active_number() + file_path = self._parse_file_path(text) + self._file_transfer_handler.send_file(file_path, friend_number) else: self.insertPlainText(text) - def parse_file_name(self, file_name): - import urllib + @staticmethod + def _parse_file_path(file_name): if file_name.endswith('\r\n'): file_name = file_name[:-2] file_name = urllib.parse.unquote(file_name) - return file_name[8 if platform.system() == 'Windows' else 7:] + + return file_name[8 if util.get_platform() == 'Windows' else 7:] class ScreenShotWindow(RubberBandWindow): + def __init__(self, file_transfer_handler, contacts_manager, *args): + super().__init__(*args) + self._file_transfer_handler = file_transfer_handler + self._contacts_manager = contacts_manager + def closeEvent(self, *args): if self.parent.isHidden(): self.parent.show() @@ -105,7 +128,8 @@ class ScreenShotWindow(RubberBandWindow): buffer = QtCore.QBuffer(byte_array) buffer.open(QtCore.QIODevice.WriteOnly) p.save(buffer, 'PNG') - Profile.get_instance().send_screenshot(bytes(byte_array.data())) + friend = self._contacts_manager.get_curr_contact() + self._file_transfer_handler.send_screenshot(bytes(byte_array.data()), friend.number) self.close() @@ -114,77 +138,81 @@ class SmileyWindow(QtWidgets.QWidget): Smiley selection window """ - def __init__(self, parent): - super(SmileyWindow, self).__init__() + def __init__(self, parent, smiley_loader): + super().__init__(parent) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - inst = smileys.SmileyLoader.get_instance() - self.data = inst.get_smileys() - count = len(self.data) + self._parent = parent + self._data = smiley_loader.get_smileys() + + count = len(self._data) if not count: self.close() - self.page_size = int(pow(count / 8, 0.5) + 1) * 8 # smileys per page - if count % self.page_size == 0: - self.page_count = count // self.page_size + + self._page_size = int(pow(count / 8, 0.5) + 1) * 8 # smileys per page + if count % self._page_size == 0: + self._page_count = count // self._page_size else: - self.page_count = round(count / self.page_size + 0.5) - self.page = -1 - self.radio = [] - self.parent = parent - for i in range(self.page_count): # buttons with smileys + self._page_count = round(count / self._page_size + 0.5) + self._page = -1 + self._radio = [] + + for i in range(self._page_count): # pages - radio buttons elem = QtWidgets.QRadioButton(self) - elem.setGeometry(QtCore.QRect(i * 20 + 5, 180, 20, 20)) - elem.clicked.connect(lambda c, t=i: self.checked(t)) - self.radio.append(elem) - width = max(self.page_count * 20 + 30, (self.page_size + 5) * 8 // 10) + elem.setGeometry(QtCore.QRect(i * 20 + 5, 160, 20, 20)) + elem.clicked.connect(lambda c, t=i: self._checked(t)) + self._radio.append(elem) + + width = max(self._page_count * 20 + 30, (self._page_size + 5) * 8 // 10) self.setMaximumSize(width, 200) self.setMinimumSize(width, 200) - self.buttons = [] - for i in range(self.page_size): # pages - radio buttons + self._buttons = [] + + for i in range(self._page_size): # buttons with smileys b = QtWidgets.QPushButton(self) b.setGeometry(QtCore.QRect((i // 8) * 20 + 5, (i % 8) * 20, 20, 20)) - b.clicked.connect(lambda c, t=i: self.clicked(t)) - self.buttons.append(b) - self.checked(0) - - def checked(self, pos): # new page opened - self.radio[self.page].setChecked(False) - self.radio[pos].setChecked(True) - self.page = pos - start = self.page * self.page_size - for i in range(self.page_size): - try: - self.buttons[i].setVisible(True) - pixmap = QtGui.QPixmap(self.data[start + i][1]) - icon = QtGui.QIcon(pixmap) - self.buttons[i].setIcon(icon) - except: - self.buttons[i].setVisible(False) - - def clicked(self, pos): # smiley selected - pos += self.page * self.page_size - smiley = self.data[pos][0] - self.parent.messageEdit.insertPlainText(smiley) - self.close() + b.clicked.connect(lambda c, t=i: self._clicked(t)) + self._buttons.append(b) + self._checked(0) def leaveEvent(self, event): self.close() + def _checked(self, pos): # new page opened + self._radio[self._page].setChecked(False) + self._radio[pos].setChecked(True) + self._page = pos + start = self._page * self._page_size + for i in range(self._page_size): + try: + self._buttons[i].setVisible(True) + pixmap = QtGui.QPixmap(self._data[start + i][1]) + icon = QtGui.QIcon(pixmap) + self._buttons[i].setIcon(icon) + except: + self._buttons[i].setVisible(False) + + def _clicked(self, pos): # smiley selected + pos += self._page * self._page_size + smiley = self._data[pos][0] + self._parent.messageEdit.insertPlainText(smiley) + self.close() + class MenuButton(QtWidgets.QPushButton): def __init__(self, parent, enter): - super(MenuButton, self).__init__(parent) + super().__init__(parent) self.enter = enter def enterEvent(self, event): self.enter() - super(MenuButton, self).enterEvent(event) + super().enterEvent(event) class DropdownMenu(QtWidgets.QWidget): def __init__(self, parent): - super(DropdownMenu, self).__init__(parent) + super().__init__(parent) self.installEventFilter(self) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.setMaximumSize(120, 120) @@ -203,30 +231,30 @@ class DropdownMenu(QtWidgets.QWidget): self.stickerButton = QtWidgets.QPushButton(self) self.stickerButton.setGeometry(QtCore.QRect(60, 0, 60, 60)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/file.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'file.png')) icon = QtGui.QIcon(pixmap) self.fileTransferButton.setIcon(icon) self.fileTransferButton.setIconSize(QtCore.QSize(50, 50)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/screenshot.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'screenshot.png')) icon = QtGui.QIcon(pixmap) self.screenshotButton.setIcon(icon) self.screenshotButton.setIconSize(QtCore.QSize(50, 60)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/smiley.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'smiley.png')) icon = QtGui.QIcon(pixmap) self.smileyButton.setIcon(icon) self.smileyButton.setIconSize(QtCore.QSize(50, 50)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/sticker.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'sticker.png')) icon = QtGui.QIcon(pixmap) self.stickerButton.setIcon(icon) self.stickerButton.setIconSize(QtCore.QSize(55, 55)) - self.screenshotButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send screenshot")) - self.fileTransferButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send file")) - self.smileyButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Add smiley")) - self.stickerButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send sticker")) + self.screenshotButton.setToolTip(util_ui.tr("Send screenshot")) + self.fileTransferButton.setToolTip(util_ui.tr("Send file")) + self.smileyButton.setToolTip(util_ui.tr("Add smiley")) + self.stickerButton.setToolTip(util_ui.tr("Send sticker")) self.fileTransferButton.clicked.connect(parent.send_file) self.screenshotButton.clicked.connect(parent.send_screenshot) @@ -246,7 +274,7 @@ class DropdownMenu(QtWidgets.QWidget): class StickerItem(QtWidgets.QWidget): def __init__(self, fl): - super(StickerItem, self).__init__() + super().__init__() self._image_label = QtWidgets.QLabel(self) self.path = fl self.pixmap = QtGui.QPixmap() @@ -260,15 +288,17 @@ class StickerItem(QtWidgets.QWidget): class StickerWindow(QtWidgets.QWidget): """Sticker selection window""" - def __init__(self, parent): - super(StickerWindow, self).__init__() + def __init__(self, file_transfer_handler, contacts_manager): + super().__init__() + self._file_transfer_handler = file_transfer_handler + self._contacts_manager = contacts_manager self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.setMaximumSize(250, 200) self.setMinimumSize(250, 200) self.list = QtWidgets.QListWidget(self) self.list.setGeometry(QtCore.QRect(0, 0, 250, 200)) - self.arr = smileys.sticker_loader() - for sticker in self.arr: + self._stickers = load_stickers() + for sticker in self._stickers: item = StickerItem(sticker) elem = QtWidgets.QListWidgetItem() elem.setSizeHint(QtCore.QSize(250, item.height())) @@ -277,11 +307,11 @@ class StickerWindow(QtWidgets.QWidget): self.list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.list.setSpacing(3) self.list.clicked.connect(self.click) - self.parent = parent def click(self, index): num = index.row() - self.parent.profile.send_sticker(self.arr[num]) + friend = self._contacts_manager.get_curr_contact() + self._file_transfer_handler.send_sticker(self._stickers[num], friend.number) self.close() def leaveEvent(self, event): @@ -290,8 +320,9 @@ class StickerWindow(QtWidgets.QWidget): class WelcomeScreen(CenteredWidget): - def __init__(self): + def __init__(self, settings): super().__init__() + self._settings = settings self.setMaximumSize(250, 200) self.setMinimumSize(250, 200) self.center() @@ -301,51 +332,39 @@ class WelcomeScreen(CenteredWidget): self.text.setOpenExternalLinks(True) self.checkbox = QtWidgets.QCheckBox(self) self.checkbox.setGeometry(QtCore.QRect(5, 170, 240, 30)) - self.checkbox.setText(QtWidgets.QApplication.translate('WelcomeScreen', "Don't show again")) - self.setWindowTitle(QtWidgets.QApplication.translate('WelcomeScreen', 'Tip of the day')) + self.checkbox.setText(util_ui.tr( "Don't show again")) + self.setWindowTitle(util_ui.tr( 'Tip of the day')) import random num = random.randint(0, 10) if num == 0: - text = QtWidgets.QApplication.translate('WelcomeScreen', 'Press Esc if you want hide app to tray.') + text = util_ui.tr('Press Esc if you want hide app to tray.') elif num == 1: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Right click on screenshot button hides app to tray during screenshot.') + text = util_ui.tr('Right click on screenshot button hides app to tray during screenshot.') elif num == 2: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'You can use Tox over Tor. For more info read this post') + text = util_ui.tr('You can use Tox over Tor. For more info read this post') elif num == 3: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Use Settings -> Interface to customize interface.') + text = util_ui.tr('Use Settings -> Interface to customize interface.') elif num == 4: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.') + text = util_ui.tr('Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.') elif num == 5: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Since v0.1.3 Toxygen supports plugins. Read more') + text = util_ui.tr('Since v0.1.3 Toxygen supports plugins. Read more') elif num == 6: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.') + text = util_ui.tr('Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.') elif num == 7: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'New in Toxygen 0.4.1:
Downloading nodes from tox.chat
Bug fixes') + text = util_ui.tr('New in Toxygen 0.4.1:
Downloading nodes from tox.chat
Bug fixes') elif num == 8: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu') + text = util_ui.tr('Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu') elif num == 9: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Use right click on inline image to save it') + text = util_ui.tr( 'Use right click on inline image to save it') else: - text = QtWidgets.QApplication.translate('WelcomeScreen', - 'Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.') + text = util_ui.tr('Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.') self.text.setHtml(text) self.checkbox.stateChanged.connect(self.not_show) QtCore.QTimer.singleShot(1000, self.show) def not_show(self): - import settings - s = settings.Settings.get_instance() - s['show_welcome_screen'] = False - s.save() + self._settings['show_welcome_screen'] = False + self._settings.save() class MainMenuButton(QtWidgets.QPushButton): @@ -373,8 +392,10 @@ class ClickableLabel(QtWidgets.QLabel): class SearchScreen(QtWidgets.QWidget): - def __init__(self, messages, width, *args): + def __init__(self, contacts_manager, history_loader, messages, width, *args): super().__init__(*args) + self._contacts_manager = contacts_manager + self._history_loader = history_loader self.setMaximumSize(width, 40) self.setMinimumSize(width, 40) self._messages = messages @@ -385,7 +406,7 @@ class SearchScreen(QtWidgets.QWidget): self.search_button = ClickableLabel(self) self.search_button.setGeometry(width - 160, 0, 40, 40) pixmap = QtGui.QPixmap() - pixmap.load(util.curr_directory() + '/images/search.png') + pixmap.load(util.join_path(util.get_images_directory(), 'search.png')) self.search_button.setScaledContents(False) self.search_button.setAlignment(QtCore.Qt.AlignCenter) self.search_button.setPixmap(pixmap) @@ -418,31 +439,31 @@ class SearchScreen(QtWidgets.QWidget): self.retranslateUi() def retranslateUi(self): - self.search_text.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "Search")) + self.search_text.setPlaceholderText(util_ui.tr('Search')) def show(self): super().show() self.search_text.setFocus() def search(self): - Profile.get_instance().update() + self._contacts_manager.update() text = self.search_text.text() - friend = Profile.get_instance().get_curr_friend() - if text and friend and util.is_re_valid(text): - index = friend.search_string(text) + contact = self._contacts_manager.get_curr_contact() + if text and contact and util.is_re_valid(text): + index = contact.search_string(text) self.load_messages(index) def prev(self): - friend = Profile.get_instance().get_curr_friend() - if friend is not None: - index = friend.search_prev() + contact = self._contacts_manager.get_curr_contact() + if contact is not None: + index = contact.search_prev() self.load_messages(index) def next(self): - friend = Profile.get_instance().get_curr_friend() + contact = self._contacts_manager.get_curr_contact() text = self.search_text.text() - if friend is not None: - index = friend.search_next() + if contact is not None: + index = contact.search_next() if index is not None: count = self._messages.count() index += count @@ -455,10 +476,9 @@ class SearchScreen(QtWidgets.QWidget): def load_messages(self, index): text = self.search_text.text() if index is not None: - profile = Profile.get_instance() count = self._messages.count() while count + index < 0: - profile.load_history() + self._history_loader.load_history() count = self._messages.count() index += count item = self._messages.item(index) @@ -468,17 +488,9 @@ class SearchScreen(QtWidgets.QWidget): self.not_found(text) def closeEvent(self, *args): - Profile.get_instance().update() self._messages.setGeometry(0, 0, self._messages.width(), self._messages.height() + 40) super().closeEvent(*args) @staticmethod def not_found(text): - mbox = QtWidgets.QMessageBox() - mbox_text = QtWidgets.QApplication.translate("MainWindow", - 'Text "{}" was not found') - - mbox.setText(mbox_text.format(text)) - mbox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", - 'Not found')) - mbox.exec_() + util_ui.message_box(util_ui.tr('Text "{}" was not found').format(text), util_ui.tr('Not found')) diff --git a/toxygen/ui/menu.py b/toxygen/ui/menu.py new file mode 100644 index 0000000..8aec578 --- /dev/null +++ b/toxygen/ui/menu.py @@ -0,0 +1,680 @@ +from PyQt5 import QtCore, QtGui, QtWidgets, uic +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 + + +class AddContact(CenteredWidget): + """Add contact form""" + + def __init__(self, settings, contacts_manager, tox_id=''): + super().__init__() + self._settings = settings + self._contacts_manager = contacts_manager + uic.loadUi(get_views_path('add_contact_screen'), self) + self._update_ui(tox_id) + self._adding = False + + def _update_ui(self, tox_id): + self.toxIdLineEdit = LineEdit(self) + self.toxIdLineEdit.setGeometry(QtCore.QRect(50, 40, 460, 30)) + self.toxIdLineEdit.setText(tox_id) + + self.messagePlainTextEdit.document().setPlainText(util_ui.tr('Hello! Please add me to your contact list.')) + self.addContactPushButton.clicked.connect(self._add_friend) + self._retranslate_ui() + + def _add_friend(self): + if self._adding: + return + self._adding = True + tox_id = self.toxIdLineEdit.text().strip() + if tox_id.startswith('tox:'): + tox_id = tox_id[4:] + message = self.messagePlainTextEdit.toPlainText() + send = self._contacts_manager.send_friend_request(tox_id, message) + self._adding = False + if send is True: + # request was successful + self.close() + else: # print error data + self.errorLabel.setText(send) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Add contact')) + self.addContactPushButton.setText(util_ui.tr('Send request')) + self.toxIdLabel.setText(util_ui.tr('TOX ID:')) + self.messageLabel.setText(util_ui.tr('Message:')) + self.toxIdLineEdit.setPlaceholderText(util_ui.tr('TOX ID or public key of contact')) + + +class NetworkSettings(CenteredWidget): + """Network settings form: UDP, Ipv6 and proxy""" + def __init__(self, settings, reset): + super().__init__() + self._settings = settings + self._reset = reset + uic.loadUi(get_views_path('network_settings_screen'), self) + self._update_ui() + + def _update_ui(self): + self.ipLineEdit = LineEdit(self) + self.ipLineEdit.setGeometry(100, 280, 270, 30) + + self.portLineEdit = LineEdit(self) + self.portLineEdit.setGeometry(100, 325, 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.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._retranslate_ui() + self.proxyCheckBox.stateChanged.connect(lambda x: self._activate_proxy()) + self._activate_proxy() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Network settings")) + self.ipv6CheckBox.setText(util_ui.tr("IPv6")) + self.udpCheckBox.setText(util_ui.tr("UDP")) + self.lanCheckBox.setText(util_ui.tr("LAN")) + self.proxyCheckBox.setText(util_ui.tr("Proxy")) + self.ipLabel.setText(util_ui.tr("IP:")) + self.portLabel.setText(util_ui.tr("Port:")) + 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")) + + def _activate_proxy(self): + bl = self.proxyCheckBox.isChecked() + self.ipLineEdit.setEnabled(bl) + self.portLineEdit.setEnabled(bl) + self.httpProxyRadioButton.setEnabled(bl) + self.socksProxyRadioButton.setEnabled(bl) + self.ipLabel.setEnabled(bl) + self.portLabel.setEnabled(bl) + + def _restart_core(self): + try: + self._settings['ipv6_enabled'] = self.ipv6CheckBox.isChecked() + self._settings['udp_enabled'] = self.udpCheckBox.isChecked() + proxy_enabled = self.proxyCheckBox.isChecked() + 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_list'] = self.downloadNodesCheckBox.isChecked() + self._settings['lan_discovery'] = self.lanCheckBox.isChecked() + self._settings.save() + # recreate tox instance + self._reset() + self.close() + except Exception as ex: + log('Exception in restart: ' + str(ex)) + + +class PrivacySettings(CenteredWidget): + """Privacy settings form: history, typing notifications""" + + def __init__(self, contacts_manager, settings): + """ + :type contacts_manager: ContactsManager + """ + super().__init__() + self._contacts_manager = contacts_manager + self._settings = settings + self.initUI() + self.center() + + def initUI(self): + self.setObjectName("privacySettings") + self.resize(370, 600) + self.setMinimumSize(QtCore.QSize(370, 600)) + self.setMaximumSize(QtCore.QSize(370, 600)) + self.saveHistory = QtWidgets.QCheckBox(self) + self.saveHistory.setGeometry(QtCore.QRect(10, 20, 350, 22)) + self.saveUnsentOnly = QtWidgets.QCheckBox(self) + self.saveUnsentOnly.setGeometry(QtCore.QRect(10, 60, 350, 22)) + + self.fileautoaccept = QtWidgets.QCheckBox(self) + self.fileautoaccept.setGeometry(QtCore.QRect(10, 100, 350, 22)) + + self.typingNotifications = QtWidgets.QCheckBox(self) + self.typingNotifications.setGeometry(QtCore.QRect(10, 140, 350, 30)) + self.inlines = QtWidgets.QCheckBox(self) + self.inlines.setGeometry(QtCore.QRect(10, 180, 350, 30)) + self.auto_path = QtWidgets.QLabel(self) + self.auto_path.setGeometry(QtCore.QRect(10, 230, 350, 30)) + self.path = QtWidgets.QPlainTextEdit(self) + self.path.setGeometry(QtCore.QRect(10, 265, 350, 45)) + self.change_path = QtWidgets.QPushButton(self) + self.change_path.setGeometry(QtCore.QRect(10, 320, 350, 30)) + self.typingNotifications.setChecked(self._settings['typing_notifications']) + self.fileautoaccept.setChecked(self._settings['allow_auto_accept']) + self.saveHistory.setChecked(self._settings['save_history']) + self.inlines.setChecked(self._settings['allow_inline']) + self.saveUnsentOnly.setChecked(self._settings['save_unsent_only']) + self.saveUnsentOnly.setEnabled(self._settings['save_history']) + self.saveHistory.stateChanged.connect(self.update) + self.path.setPlainText(self._settings['auto_accept_path'] or curr_directory()) + self.change_path.clicked.connect(self.new_path) + self.block_user_label = QtWidgets.QLabel(self) + self.block_user_label.setGeometry(QtCore.QRect(10, 360, 350, 30)) + self.block_id = QtWidgets.QPlainTextEdit(self) + self.block_id.setGeometry(QtCore.QRect(10, 390, 350, 30)) + self.block = QtWidgets.QPushButton(self) + self.block.setGeometry(QtCore.QRect(10, 430, 350, 30)) + self.block.clicked.connect(lambda: self._contacts_manager.block_user(self.block_id.toPlainText()) or self.close()) + self.blocked_users_label = QtWidgets.QLabel(self) + self.blocked_users_label.setGeometry(QtCore.QRect(10, 470, 350, 30)) + self.comboBox = QtWidgets.QComboBox(self) + self.comboBox.setGeometry(QtCore.QRect(10, 500, 350, 30)) + self.comboBox.addItems(self._settings['blocked']) + self.unblock = QtWidgets.QPushButton(self) + self.unblock.setGeometry(QtCore.QRect(10, 540, 350, 30)) + self.unblock.clicked.connect(lambda: self.unblock_user()) + self.retranslateUi() + QtCore.QMetaObject.connectSlotsByName(self) + + def retranslateUi(self): + self.setWindowTitle(util_ui.tr("Privacy settings")) + self.saveHistory.setText(util_ui.tr("Save chat history")) + self.fileautoaccept.setText(util_ui.tr("Allow file auto accept")) + self.typingNotifications.setText(util_ui.tr("Send typing notifications")) + self.auto_path.setText(util_ui.tr("Auto accept default path:")) + self.change_path.setText(util_ui.tr("Change")) + self.inlines.setText(util_ui.tr("Allow inlines")) + self.block_user_label.setText(util_ui.tr("Block by public key:")) + self.blocked_users_label.setText(util_ui.tr("Blocked users:")) + self.unblock.setText(util_ui.tr("Unblock")) + self.block.setText(util_ui.tr("Block user")) + self.saveUnsentOnly.setText(util_ui.tr("Save unsent messages only")) + + def update(self, new_state): + self.saveUnsentOnly.setEnabled(new_state) + if not new_state: + self.saveUnsentOnly.setChecked(False) + + def unblock_user(self): + if not self.comboBox.count(): + return + title = util_ui.tr("Add to friend list") + info = util_ui.tr("Do you want to add this user to friend list?") + reply = util_ui.question(info, title) + self._contacts_manager.unblock_user(self.comboBox.currentText(), reply) + self.close() + + def closeEvent(self, event): + self._settings['typing_notifications'] = self.typingNotifications.isChecked() + self._settings['allow_auto_accept'] = self.fileautoaccept.isChecked() + text = util_ui.tr('History will be cleaned! Continue?') + title = util_ui.tr('Chat history') + + if self._settings['save_history'] and not self.saveHistory.isChecked(): # clear history + reply = util_ui.question(text, title) + if reply: + self._history_loader.clear_history() + self._settings['save_history'] = self.saveHistory.isChecked() + else: + self._settings['save_history'] = self.saveHistory.isChecked() + if self.saveUnsentOnly.isChecked() and not self._settings['save_unsent_only']: + reply = util_ui.question(text, title) + if reply: + self._history_loader.clear_history(None, True) + self._settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() + else: + self._settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() + self._settings['auto_accept_path'] = self.path.toPlainText() + self._settings['allow_inline'] = self.inlines.isChecked() + self._settings.save() + + def new_path(self): + directory = util_ui.directory_dialog() + if directory: + self.path.setPlainText(directory) + + +class NotificationsSettings(CenteredWidget): + """Notifications settings form""" + + def __init__(self, setttings): + super().__init__() + self._settings = setttings + uic.loadUi(get_views_path('notifications_settings_screen'), self) + self._update_ui() + self.center() + + def closeEvent(self, *args, **kwargs): + self._settings['notifications'] = self.notificationsCheckBox.isChecked() + self._settings['sound_notifications'] = self.soundNotificationsCheckBox.isChecked() + self._settings['group_notifications'] = self.groupNotificationsCheckBox.isChecked() + self._settings['calls_sound'] = self.callsSoundCheckBox.isChecked() + self._settings.save() + + def _update_ui(self): + self.notificationsCheckBox.setChecked(self._settings['notifications']) + self.soundNotificationsCheckBox.setChecked(self._settings['sound_notifications']) + self.groupNotificationsCheckBox.setChecked(self._settings['group_notifications']) + self.callsSoundCheckBox.setChecked(self._settings['calls_sound']) + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Notifications settings")) + self.notificationsCheckBox.setText(util_ui.tr("Enable notifications")) + self.groupNotificationsCheckBox.setText(util_ui.tr("Notify about all messages in groups")) + self.callsSoundCheckBox.setText(util_ui.tr("Enable call\'s sound")) + self.soundNotificationsCheckBox.setText(util_ui.tr("Enable sound notifications")) + + +class InterfaceSettings(CenteredWidget): + """Interface settings form""" + + def __init__(self, settings, smiley_loader): + super().__init__() + self._settings = settings + self._smiley_loader = smiley_loader + + uic.loadUi(get_views_path('interface_settings_screen'), self) + self._update_ui() + self.center() + + def _update_ui(self): + themes = list(self._settings.built_in_themes().keys()) + self.themeComboBox.addItems(themes) + theme = self._settings['theme'] + if theme in self._settings.built_in_themes().keys(): + index = themes.index(theme) + else: + index = 0 + self.themeComboBox.setCurrentIndex(index) + + supported_languages = sorted(Settings.supported_languages().keys(), reverse=True) + for key in supported_languages: + self.languageComboBox.insertItem(0, key) + if self._settings['language'] == key: + self.languageComboBox.setCurrentIndex(0) + + smiley_packs = self._smiley_loader.get_packs_list() + self.smileysPackComboBox.addItems(smiley_packs) + try: + index = smiley_packs.index(self._settings['smiley_pack']) + except: + 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.compactModeCheckBox.setChecked(self._settings['compact_mode']) + self.showAvatarsCheckBox.setChecked(self._settings['show_avatars']) + self.smileysCheckBox.setChecked(self._settings['smileys']) + + self.importSmileysPushButton.clicked.connect(self._import_smileys) + self.importStickersPushButton.clicked.connect(self._import_stickers) + + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Interface settings")) + self.showAvatarsCheckBox.setText(util_ui.tr("Show avatars in chat")) + self.themeLabel.setText(util_ui.tr("Theme:")) + self.languageLabel.setText(util_ui.tr("Language:")) + self.smileysGroupBox.setTitle(util_ui.tr("Smileys settings")) + self.smileysPackLabel.setText(util_ui.tr("Smiley pack:")) + self.smileysCheckBox.setText(util_ui.tr("Smileys")) + 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.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")) + self.appClosingGroupBox.setTitle(util_ui.tr("App closing settings")) + + @staticmethod + def _import_stickers(): + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with sticker pack')) + if directory: + dest = join_path(get_stickers_directory(), os.path.basename(directory)) + copy(directory, dest) + + @staticmethod + def _import_smileys(): + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with smiley pack')) + if not directory: + return + src = directory + '/' + dest = join_path(get_smileys_directory(), os.path.basename(directory)) + 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]) + with open(styles_path) as fl: + style = fl.read() + 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['compact_mode'] != self.compactModeCheckBox.isChecked(): + self._settings['compact_mode'] = self.compactModeCheckBox.isChecked() + restart = True + + if self._settings['show_avatars'] != self.showAvatarsCheckBox.isChecked(): + self._settings['show_avatars'] = self.showAvatarsCheckBox.isChecked() + restart = True + + self._settings['smiley_pack'] = self.smileysPackComboBox.currentText() + self._smiley_loader.load_pack() + + language = self.languageComboBox.currentText() + 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) + + app_closing_setting = 0 + if self.hideRadioButton.isChecked(): + app_closing_setting = 1 + elif self.closeToTrayRadioButton.isChecked(): + app_closing_setting = 2 + self._settings['close_app'] = app_closing_setting + self._settings.save() + + if restart: + util_ui.message_box(util_ui.tr('Restart app to apply settings'), util_ui.tr('Restart required')) + + +class AudioSettings(CenteredWidget): + """ + Audio calls settings form + """ + + def __init__(self, settings): + super().__init__() + 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() + + def _update_ui(self): + p = pyaudio.PyAudio() + self._in_indexes, self._out_indexes = [], [] + for i in range(p.get_device_count()): + device = p.get_device_info_by_index(i) + if device["maxInputChannels"]: + self.inputDeviceComboBox.addItem(str(device["name"])) + self._in_indexes.append(i) + 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'])) + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Audio settings")) + self.inputDeviceLabel.setText(util_ui.tr("Input device:")) + self.outputDeviceLabel.setText(util_ui.tr("Output device:")) + + +class DesktopAreaSelectionWindow(RubberBandWindow): + + def mouseReleaseEvent(self, event): + if self.rubberband.isVisible(): + self.rubberband.hide() + rect = self.rubberband.geometry() + width, height = rect.width(), rect.height() + if width >= 8 and height >= 8: + self.parent.save(rect.x(), rect.y(), width - (width % 4), height - (height % 4)) + self.close() + + +class VideoSettings(CenteredWidget): + """ + Audio calls settings form + """ + + def __init__(self, settings): + super().__init__() + self._settings = settings + uic.loadUi(get_views_path('video_settings_screen'), self) + self._devices = self._frame_max_sizes = None + self._update_ui() + self.center() + self.desktopAreaSelection = None + + def closeEvent(self, event): + if self.deviceComboBox.currentIndex() == 0: + return + try: + self._settings.video['device'] = self.devices[self.input.currentIndex()] + text = self.resolutionComboBox.currentText() + 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)) + + 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.save() + + def _update_ui(self): + self.deviceComboBox.currentIndexChanged.connect(self._device_changed) + self.selectRegionPushButton.clicked.connect(self._button_clicked) + self._devices = [-1] + screen = QtWidgets.QApplication.primaryScreen() + size = screen.size() + 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) + + 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)) + try: + index = self._devices.index(self._settings.video['device']) + self.deviceComboBox.setCurrentIndex(index) + except: + print('Video devices error!') + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Video settings")) + self.deviceLabel.setText(util_ui.tr("Device:")) + self.selectRegionPushButton.setText(util_ui.tr("Select region")) + + def _button_clicked(self): + self.desktopAreaSelection = DesktopAreaSelectionWindow(self) + + def _device_changed(self): + index = self.deviceComboBox.currentIndex() + self.selectRegionPushButton.setVisible(index == 0) + self.resolutionComboBox.setVisible(index != 0) + width, height = self._frame_max_sizes[index] + self.resolutionComboBox.clear() + dims = [ + (320, 240), + (640, 360), + (640, 480), + (720, 480), + (1280, 720), + (1920, 1080), + (2560, 1440) + ] + for w, h in dims: + if w <= width and h <= height: + self.resolutionComboBox.addItem(str(w) + ' * ' + str(h)) + + +class PluginsSettings(CenteredWidget): + """ + Plugins settings form + """ + + def __init__(self, plugin_loader): + super().__init__() + self._plugin_loader = plugin_loader + self._window = None + self.initUI() + self.center() + self.retranslateUi() + + def initUI(self): + self.resize(400, 210) + self.setMinimumSize(QtCore.QSize(400, 210)) + self.setMaximumSize(QtCore.QSize(400, 210)) + self.comboBox = QtWidgets.QComboBox(self) + self.comboBox.setGeometry(QtCore.QRect(30, 10, 340, 30)) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(30, 40, 340, 90)) + self.label.setWordWrap(True) + self.button = QtWidgets.QPushButton(self) + self.button.setGeometry(QtCore.QRect(30, 130, 340, 30)) + self.button.clicked.connect(self.button_click) + self.open = QtWidgets.QPushButton(self) + self.open.setGeometry(QtCore.QRect(30, 170, 340, 30)) + self.open.clicked.connect(self.open_plugin) + self.update_list() + self.comboBox.currentIndexChanged.connect(self.show_data) + self.show_data() + + def retranslateUi(self): + self.setWindowTitle(util_ui.tr("Plugins")) + 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')) + + def update_list(self): + self.comboBox.clear() + data = self._plugin_loader.get_plugins_list() + self.comboBox.addItems(list(map(lambda x: x[0], data))) + self.data = data + + def show_data(self): + ind = self.comboBox.currentIndex() + if len(self.data): + plugin = self.data[ind] + descr = plugin[2] or util_ui.tr("No description available") + self.label.setText(descr) + if plugin[1]: + self.button.setText(util_ui.tr("Disable plugin")) + else: + self.button.setText(util_ui.tr("Enable plugin")) + else: + self.open.setVisible(False) + self.button.setVisible(False) + self.label.setText(util_ui.tr("No plugins found")) + + def button_click(self): + ind = self.comboBox.currentIndex() + plugin = self.data[ind] + self._plugin_loader.toggle_plugin(plugin[-1]) + plugin[1] = not plugin[1] + if plugin[1]: + self.button.setText(util_ui.tr("Disable plugin")) + else: + self.button.setText(util_ui.tr("Enable plugin")) + + +class UpdateSettings(CenteredWidget): + """ + Updates settings form + """ + + def __init__(self, settings, version): + super().__init__() + self._settings = settings + self._version = version + uic.loadUi(get_views_path('update_settings_screen'), self) + self._update_ui() + self.center() + + def closeEvent(self, event): + self._settings['update'] = self.updateModeComboBox.currentIndex() + self._settings.save() + + def _update_ui(self): + self.updatePushButton.clicked.connect(self._update_client) + self.updateModeComboBox.currentIndexChanged.connect(self._update_mode_changed) + self._retranslate_ui() + self.updateModeComboBox.setCurrentIndex(self._settings['update']) + + def _update_mode_changed(self): + index = self.updateModeComboBox.currentIndex() + self.updatePushButton.setEnabled(index > 0) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Update settings")) + self.updateModeLabel.setText(util_ui.tr("Select update mode:")) + self.updatePushButton.setText(util_ui.tr("Update Toxygen")) + self.updateModeComboBox.addItem(util_ui.tr("Disabled")) + self.updateModeComboBox.addItem(util_ui.tr("Manual")) + self.updateModeComboBox.addItem(util_ui.tr("Auto")) + + def _update_client(self): + if not updater.connection_available(): + util_ui.message_box(util_ui.tr('Problems with internet connection'), util_ui.tr("Error")) + return + if not updater.updater_available(): + util_ui.message_box(util_ui.tr('Updater not found'), util_ui.tr("Error")) + return + version = updater.check_for_updates(self._version, self._settings) + if version is not None: + updater.download(version) + util_ui.close_all_windows() + else: + util_ui.message_box(util_ui.tr('Toxygen is up to date'), util_ui.tr("No updates found")) diff --git a/toxygen/list_items.py b/toxygen/ui/messages_widgets.py similarity index 57% rename from toxygen/list_items.py rename to toxygen/ui/messages_widgets.py index 9b92f2a..8a46fd0 100644 --- a/toxygen/list_items.py +++ b/toxygen/ui/messages_widgets.py @@ -1,20 +1,23 @@ -from toxcore_enums_and_consts import * -from PyQt5 import QtCore, QtGui, QtWidgets -import profile -from file_transfers import TOX_FILE_TRANSFER_STATE, PAUSED_FILE_TRANSFERS, DO_NOT_SHOW_ACCEPT_BUTTON, ACTIVE_FILE_TRANSFERS, SHOW_PROGRESS_BAR -from util import curr_directory, convert_time, curr_time -from widgets import DataLabel, create_menu +from wrapper.toxcore_enums_and_consts import * +import ui.widgets as widgets +import utils.util as util +import ui.menu as menu import html as h -import smileys -import settings import re +from ui.widgets import * +from messenger.messages import MESSAGE_AUTHOR +from file_transfers.file_transfers import * -class MessageEdit(QtWidgets.QTextBrowser): +class MessageBrowser(QtWidgets.QTextBrowser): - def __init__(self, text, width, message_type, parent=None): - super(MessageEdit, self).__init__(parent) + def __init__(self, settings, message_edit, smileys_loader, plugin_loader, text, width, message_type, parent=None): + super().__init__(parent) self.urls = {} + self._message_edit = message_edit + self._smileys_loader = smileys_loader + self._plugin_loader = plugin_loader + self._add_contact = None self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere) @@ -22,7 +25,7 @@ class MessageEdit(QtWidgets.QTextBrowser): self.setOpenExternalLinks(True) self.setAcceptRichText(True) self.setOpenLinks(False) - path = smileys.SmileyLoader.get_instance().get_smileys_path() + path = smileys_loader.get_smileys_path() if path is not None: self.setSearchPaths([path]) self.document().setDefaultStyleSheet('a { color: #306EFF; }') @@ -32,8 +35,8 @@ class MessageEdit(QtWidgets.QTextBrowser): else: self.setHtml(text) font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) - font.setPixelSize(settings.Settings.get_instance()['message_font_size']) + font.setFamily(settings['font']) + font.setPixelSize(settings['message_font_size']) font.setBold(False) self.setFont(font) self.resize(width, self.document().size().height()) @@ -41,45 +44,42 @@ class MessageEdit(QtWidgets.QTextBrowser): self.anchorClicked.connect(self.on_anchor_clicked) def contextMenuEvent(self, event): - menu = create_menu(self.createStandardContextMenu(event.pos())) - quote = menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Quote selected text')) + menu = widgets.create_menu(self.createStandardContextMenu(event.pos())) + quote = menu.addAction(util_ui.tr('Quote selected text')) quote.triggered.connect(self.quote_text) text = self.textCursor().selection().toPlainText() if not text: quote.setEnabled(False) else: - import plugin_support - submenu = plugin_support.PluginLoader.get_instance().get_message_menu(menu, text) - if len(submenu): - plug = menu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins')) - plug.addActions(submenu) + sub_menu = self._plugin_loader.get_message_menu(menu, text) + if len(sub_menu): + plugins_menu = menu.addMenu(util_ui.tr('Plugins')) + plugins_menu.addActions(sub_menu) menu.popup(event.globalPos()) menu.exec_(event.globalPos()) del menu def quote_text(self): text = self.textCursor().selection().toPlainText() - if text: - import mainscreen - window = mainscreen.MainWindow.get_instance() - text = '>' + '\n>'.join(text.split('\n')) - if window.messageEdit.toPlainText(): - text = '\n' + text - window.messageEdit.appendPlainText(text) + if not text: + return + text = '>' + '\n>'.join(text.split('\n')) + if self._message_edit.toPlainText(): + text = '\n' + text + self._message_edit.appendPlainText(text) def on_anchor_clicked(self, url): text = str(url.toString()) if text.startswith('tox:'): - import menu - self.add_contact = menu.AddContact(text[4:]) - self.add_contact.show() + self._add_contact = menu.AddContact(text[4:]) + self._add_contact.show() else: QtGui.QDesktopServices.openUrl(url) self.clearFocus() - def addAnimation(self, url, fileName): + def addAnimation(self, url, file_name): movie = QtGui.QMovie(self) - movie.setFileName(fileName) + movie.setFileName(file_name) self.urls[movie] = url movie.frameChanged[int].connect(lambda x: self.animate(movie)) movie.start() @@ -115,7 +115,7 @@ class MessageEdit(QtWidgets.QTextBrowser): if arr[i].startswith('>'): arr[i] = '' + arr[i][4:] + '' text = '
'.join(arr) - text = smileys.SmileyLoader.get_instance().add_smileys_to_text(text, self) # smileys + text = self._smileys_loader.add_smileys_to_text(text, self) return text @@ -123,35 +123,39 @@ class MessageItem(QtWidgets.QWidget): """ Message in messages list """ - def __init__(self, text, time, user='', sent=True, message_type=TOX_MESSAGE_TYPE['NORMAL'], parent=None): + def __init__(self, text_message, settings, message_browser_factory_method, delete_action, parent=None): QtWidgets.QWidget.__init__(self, parent) - self.name = DataLabel(self) + self._message = text_message + self._delete_action = delete_action + self.name = widgets.DataLabel(self) self.name.setGeometry(QtCore.QRect(2, 2, 95, 23)) self.name.setTextFormat(QtCore.Qt.PlainText) font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) + font.setFamily(settings['font']) font.setPointSize(11) font.setBold(True) - self.name.setFont(font) - self.name.setText(user) + if text_message.author is not None: + self.name.setFont(font) + self.name.setText(text_message.author.name) self.time = QtWidgets.QLabel(self) self.time.setGeometry(QtCore.QRect(parent.width() - 60, 0, 50, 25)) font.setPointSize(10) font.setBold(False) self.time.setFont(font) - self._time = time - if not sent: - movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif') + self._time = text_message.time + if text_message.author and text_message.author.type == MESSAGE_AUTHOR['NOT_SENT']: + movie = QtGui.QMovie(util.join_path(util.get_images_directory(), 'spinner.gif')) self.time.setMovie(movie) movie.start() self.t = True else: - self.time.setText(convert_time(time)) + self.time.setText(util.convert_time(text_message.time)) self.t = False - self.message = MessageEdit(text, parent.width() - 160, message_type, self) - if message_type != TOX_MESSAGE_TYPE['NORMAL']: + self.message = message_browser_factory_method(text_message.text, parent.width() - 160, + text_message.type, self) + if text_message.type != TOX_MESSAGE_TYPE['NORMAL']: self.name.setStyleSheet("QLabel { color: #5CB3FF; }") self.message.setAlignment(QtCore.Qt.AlignCenter) self.time.setStyleSheet("QLabel { color: #5CB3FF; }") @@ -161,19 +165,18 @@ class MessageItem(QtWidgets.QWidget): def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.RightButton and event.x() > self.time.x(): self.listMenu = QtWidgets.QMenu() - delete_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Delete message')) + delete_item = self.listMenu.addAction(util_ui.tr('Delete message')) delete_item.triggered.connect(self.delete) parent_position = self.time.mapToGlobal(QtCore.QPoint(0, 0)) self.listMenu.move(parent_position) self.listMenu.show() def delete(self): - pr = profile.Profile.get_instance() - pr.delete_message(self._time) + self._delete_action(self._message) def mark_as_sent(self): if self.t: - self.time.setText(convert_time(self._time)) + self.time.setText(util.convert_time(self._time)) self.t = False return True return False @@ -212,153 +215,69 @@ class MessageItem(QtWidgets.QWidget): return text -class ContactItem(QtWidgets.QWidget): - """ - Contact in friends list - """ - - def __init__(self, parent=None): - QtWidgets.QWidget.__init__(self, parent) - mode = settings.Settings.get_instance()['compact_mode'] - self.setBaseSize(QtCore.QSize(250, 40 if mode else 70)) - self.avatar_label = QtWidgets.QLabel(self) - size = 32 if mode else 64 - self.avatar_label.setGeometry(QtCore.QRect(3, 4, size, size)) - self.avatar_label.setScaledContents(False) - self.avatar_label.setAlignment(QtCore.Qt.AlignCenter) - self.name = DataLabel(self) - self.name.setGeometry(QtCore.QRect(50 if mode else 75, 3 if mode else 10, 150, 15 if mode else 25)) - font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) - font.setPointSize(10 if mode else 12) - font.setBold(True) - self.name.setFont(font) - self.status_message = DataLabel(self) - self.status_message.setGeometry(QtCore.QRect(50 if mode else 75, 20 if mode else 30, 170, 15 if mode else 20)) - font.setPointSize(10) - font.setBold(False) - self.status_message.setFont(font) - self.connection_status = StatusCircle(self) - self.connection_status.setGeometry(QtCore.QRect(230, -2 if mode else 5, 32, 32)) - self.messages = UnreadMessagesCount(self) - self.messages.setGeometry(QtCore.QRect(20 if mode else 52, 20 if mode else 50, 30, 20)) - - -class StatusCircle(QtWidgets.QWidget): - """ - Connection status - """ - def __init__(self, parent): - QtWidgets.QWidget.__init__(self, parent) - self.setGeometry(0, 0, 32, 32) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) - self.unread = False - - def update(self, status, unread_messages=None): - if unread_messages is None: - unread_messages = self.unread - else: - self.unread = unread_messages - if status == TOX_USER_STATUS['NONE']: - name = 'online' - elif status == TOX_USER_STATUS['AWAY']: - name = 'idle' - elif status == TOX_USER_STATUS['BUSY']: - name = 'busy' - else: - name = 'offline' - if unread_messages: - name += '_notification' - self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) - else: - self.label.setGeometry(QtCore.QRect(2, 0, 32, 32)) - pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(name)) - self.label.setPixmap(pixmap) - - -class UnreadMessagesCount(QtWidgets.QWidget): - - def __init__(self, parent=None): - super(UnreadMessagesCount, self).__init__(parent) - self.resize(30, 20) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(0, 0, 30, 20)) - self.label.setVisible(False) - font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) - font.setPointSize(12) - font.setBold(True) - self.label.setFont(font) - self.label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter) - color = settings.Settings.get_instance()['unread_color'] - self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') - - def update(self, messages_count): - color = settings.Settings.get_instance()['unread_color'] - self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') - if messages_count: - self.label.setVisible(True) - self.label.setText(str(messages_count)) - else: - self.label.setVisible(False) - - class FileTransferItem(QtWidgets.QListWidget): - def __init__(self, file_name, size, time, user, friend_number, file_number, state, width, parent=None): + def __init__(self, transfer_message, file_transfer_handler, settings, width, parent=None): QtWidgets.QListWidget.__init__(self, parent) + self._file_transfer_handler = file_transfer_handler self.resize(QtCore.QSize(width, 34)) - if state == TOX_FILE_TRANSFER_STATE['CANCELLED']: + if transfer_message.state == FILE_TRANSFER_STATE['CANCELLED']: self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') - elif state in PAUSED_FILE_TRANSFERS: + elif transfer_message.state in PAUSED_FILE_TRANSFERS: self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') else: self.setStyleSheet('QListWidget { border: 1px solid green; }') - self.state = state + self.state = transfer_message.state self.name = DataLabel(self) self.name.setGeometry(QtCore.QRect(3, 7, 95, 25)) self.name.setTextFormat(QtCore.Qt.PlainText) font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) + font.setFamily(settings['font']) font.setPointSize(11) font.setBold(True) self.name.setFont(font) - self.name.setText(user) + self.name.setText(transfer_message.author.name) self.time = QtWidgets.QLabel(self) self.time.setGeometry(QtCore.QRect(width - 60, 7, 50, 25)) font.setPointSize(10) font.setBold(False) self.time.setFont(font) - self.time.setText(convert_time(time)) + self.time.setText(util.convert_time(transfer_message.time)) self.cancel = QtWidgets.QPushButton(self) self.cancel.setGeometry(QtCore.QRect(width - 125, 2, 30, 30)) - pixmap = QtGui.QPixmap(curr_directory() + '/images/decline.png') + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'decline.png')) icon = QtGui.QIcon(pixmap) self.cancel.setIcon(icon) self.cancel.setIconSize(QtCore.QSize(30, 30)) - self.cancel.setVisible(state in ACTIVE_FILE_TRANSFERS) - self.cancel.clicked.connect(lambda: self.cancel_transfer(friend_number, file_number)) + self.cancel.setVisible(transfer_message.state in ACTIVE_FILE_TRANSFERS or + transfer_message.state == FILE_TRANSFER_STATE['UNSENT']) + self.cancel.clicked.connect( + lambda: self.cancel_transfer(transfer_message.friend_number, transfer_message.file_number)) self.cancel.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none;}') self.accept_or_pause = QtWidgets.QPushButton(self) self.accept_or_pause.setGeometry(QtCore.QRect(width - 170, 2, 30, 30)) - if state == TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: + if transfer_message.state == FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: self.accept_or_pause.setVisible(True) self.button_update('accept') - elif state in DO_NOT_SHOW_ACCEPT_BUTTON: + elif transfer_message.state in DO_NOT_SHOW_ACCEPT_BUTTON: self.accept_or_pause.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: # setup for continue + elif transfer_message.state == FILE_TRANSFER_STATE['PAUSED_BY_USER']: # setup for continue self.accept_or_pause.setVisible(True) self.button_update('resume') + elif transfer_message.state == FILE_TRANSFER_STATE['UNSENT']: + self.accept_or_pause.setVisible(False) + self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') else: # pause self.accept_or_pause.setVisible(True) self.button_update('pause') - self.accept_or_pause.clicked.connect(lambda: self.accept_or_pause_transfer(friend_number, file_number, size)) + self.accept_or_pause.clicked.connect( + lambda: self.accept_or_pause_transfer(transfer_message.friend_number, transfer_message.file_number, + transfer_message.size)) self.accept_or_pause.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none}') @@ -366,64 +285,60 @@ class FileTransferItem(QtWidgets.QListWidget): self.pb.setGeometry(QtCore.QRect(100, 7, 100, 20)) self.pb.setValue(0) self.pb.setStyleSheet('QProgressBar { background-color: #302F2F; }') - self.pb.setVisible(state in SHOW_PROGRESS_BAR) + self.pb.setVisible(transfer_message.state in SHOW_PROGRESS_BAR) self.file_name = DataLabel(self) self.file_name.setGeometry(QtCore.QRect(210, 7, width - 420, 20)) font.setPointSize(12) self.file_name.setFont(font) - file_size = size // 1024 + file_size = transfer_message.size // 1024 if not file_size: - file_size = '{}B'.format(size) + file_size = '{}B'.format(transfer_message.size) elif file_size >= 1024: file_size = '{}MB'.format(file_size // 1024) else: file_size = '{}KB'.format(file_size) - file_data = '{} {}'.format(file_size, file_name) + file_data = '{} {}'.format(file_size, transfer_message.file_name) self.file_name.setText(file_data) - self.file_name.setToolTip(file_name) - self.saved_name = file_name + self.file_name.setToolTip(transfer_message.file_name) + self.saved_name = transfer_message.file_name self.time_left = QtWidgets.QLabel(self) self.time_left.setGeometry(QtCore.QRect(width - 92, 7, 30, 20)) font.setPointSize(10) self.time_left.setFont(font) - self.time_left.setVisible(state == TOX_FILE_TRANSFER_STATE['RUNNING']) + self.time_left.setVisible(transfer_message.state == FILE_TRANSFER_STATE['RUNNING']) self.setFocusPolicy(QtCore.Qt.NoFocus) self.paused = False def cancel_transfer(self, friend_number, file_number): - pr = profile.Profile.get_instance() - pr.cancel_transfer(friend_number, file_number) + self._file_transfer_handler.cancel_transfer(friend_number, file_number) self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') self.cancel.setVisible(False) self.accept_or_pause.setVisible(False) self.pb.setVisible(False) def accept_or_pause_transfer(self, friend_number, file_number, size): - if self.state == TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - directory = QtWidgets.QFileDialog.getExistingDirectory(self, - QtWidgets.QApplication.translate("MainWindow", 'Choose folder'), - curr_directory(), - QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) + if self.state == FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: + directory = util_ui.directory_dialog(util_ui.tr('Choose folder')) self.pb.setVisible(True) if directory: - pr = profile.Profile.get_instance() - pr.accept_transfer(self, directory + '/' + self.saved_name, friend_number, file_number, size) + self._file_transfer_handler.accept_transfer(directory + '/' + self.saved_name, + friend_number, file_number, size) self.button_update('pause') - elif self.state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: # resume + elif self.state == FILE_TRANSFER_STATE['PAUSED_BY_USER']: # resume self.paused = False - profile.Profile.get_instance().resume_transfer(friend_number, file_number) + self._file_transfer_handler.resume_transfer(friend_number, file_number) self.button_update('pause') - self.state = TOX_FILE_TRANSFER_STATE['RUNNING'] + self.state = FILE_TRANSFER_STATE['RUNNING'] else: # pause self.paused = True - self.state = TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER'] - profile.Profile.get_instance().pause_transfer(friend_number, file_number) + self.state = FILE_TRANSFER_STATE['PAUSED_BY_USER'] + self._file_transfer_handler.pause_transfer(friend_number, file_number) self.button_update('resume') self.accept_or_pause.clearFocus() def button_update(self, path): - pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(path)) + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), '{}.png'.format(path))) icon = QtGui.QIcon(pixmap) self.accept_or_pause.setIcon(icon) self.accept_or_pause.setIconSize(QtCore.QSize(30, 30)) @@ -434,31 +349,31 @@ class FileTransferItem(QtWidgets.QListWidget): m, s = divmod(time, 60) self.time_left.setText('{0:02d}:{1:02d}'.format(m, s)) if self.state != state and self.state in ACTIVE_FILE_TRANSFERS: - if state == TOX_FILE_TRANSFER_STATE['CANCELLED']: + if state == FILE_TRANSFER_STATE['CANCELLED']: self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') self.cancel.setVisible(False) self.accept_or_pause.setVisible(False) self.pb.setVisible(False) self.state = state self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['FINISHED']: + elif state == FILE_TRANSFER_STATE['FINISHED']: self.accept_or_pause.setVisible(False) self.pb.setVisible(False) self.cancel.setVisible(False) self.setStyleSheet('QListWidget { border: 1px solid green; }') self.state = state self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']: + elif state == FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']: self.accept_or_pause.setVisible(False) self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') self.state = state self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: + elif state == FILE_TRANSFER_STATE['PAUSED_BY_USER']: self.button_update('resume') # setup button continue self.setStyleSheet('QListWidget { border: 1px solid green; }') self.state = state self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']: + elif state == FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']: self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') self.accept_or_pause.setVisible(False) self.time_left.setVisible(False) @@ -471,31 +386,27 @@ class FileTransferItem(QtWidgets.QListWidget): self.state = state self.time_left.setVisible(True) - def mark_as_sent(self): - return False - class UnsentFileItem(FileTransferItem): - def __init__(self, file_name, size, user, time, width, parent=None): - super(UnsentFileItem, self).__init__(file_name, size, time, user, -1, -1, - TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'], width, parent) + def __init__(self, transfer_message, file_transfer_handler, settings, width, parent=None): + super().__init__(transfer_message, file_transfer_handler, settings, width, parent) self._time = time - self.pb.setVisible(False) - movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif') + movie = QtGui.QMovie(util.join_path(util.get_images_directory(), 'spinner.gif')) self.time.setMovie(movie) movie.start() + self._message_id = transfer_message.message_id + self._friend_number = transfer_message.friend_number def cancel_transfer(self, *args): - pr = profile.Profile.get_instance() - pr.cancel_not_started_transfer(self._time) + self._file_transfer_handler.cancel_not_started_transfer(self._friend_number, self._message_id) class InlineImageItem(QtWidgets.QScrollArea): - def __init__(self, data, width, elem): + def __init__(self, data, width, elem, parent=None): - QtWidgets.QScrollArea.__init__(self) + QtWidgets.QScrollArea.__init__(self, parent) self.setFocusPolicy(QtCore.Qt.NoFocus) self._elem = elem self._image_label = QtWidgets.QLabel(self) @@ -532,14 +443,7 @@ class InlineImageItem(QtWidgets.QScrollArea): self._full_size = not self._full_size self._elem.setSizeHint(QtCore.QSize(self.width(), self.height())) elif event.button() == QtCore.Qt.RightButton: # save inline - directory = QtWidgets.QFileDialog.getExistingDirectory(self, - QtWidgets.QApplication.translate("MainWindow", - 'Choose folder'), - curr_directory(), - QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) + directory = util_ui.directory_dialog(util_ui.tr('Choose folder')) if directory: - fl = QtCore.QFile(directory + '/toxygen_inline_' + curr_time().replace(':', '_') + '.png') + fl = QtCore.QFile(directory + '/toxygen_inline_' + util.curr_time().replace(':', '_') + '.png') self._pixmap.save(fl, 'PNG') - - def mark_as_sent(self): - return False diff --git a/toxygen/passwordscreen.py b/toxygen/ui/password_screen.py similarity index 72% rename from toxygen/passwordscreen.py rename to toxygen/ui/password_screen.py index ca721e5..bbae7ff 100644 --- a/toxygen/passwordscreen.py +++ b/toxygen/ui/password_screen.py @@ -1,25 +1,27 @@ -from widgets import CenteredWidget, LineEdit +from ui.widgets import CenteredWidget, LineEdit, DialogWithResult from PyQt5 import QtCore, QtWidgets +import utils.ui as util_ui class PasswordArea(LineEdit): def __init__(self, parent): - super(PasswordArea, self).__init__(parent) - self.parent = parent + super().__init__(parent) + self._parent = parent self.setEchoMode(QtWidgets.QLineEdit.Password) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Return: - self.parent.button_click() + self._parent.button_click() else: - super(PasswordArea, self).keyPressEvent(event) + super().keyPressEvent(event) -class PasswordScreenBase(CenteredWidget): +class PasswordScreenBase(CenteredWidget, DialogWithResult): def __init__(self, encrypt): - super(PasswordScreenBase, self).__init__() + CenteredWidget.__init__(self) + DialogWithResult.__init__(self) self._encrypt = encrypt self.initUI() @@ -36,7 +38,7 @@ class PasswordScreenBase(CenteredWidget): self.button = QtWidgets.QPushButton(self) self.button.setGeometry(QtCore.QRect(30, 90, 300, 30)) - self.button.setText('OK') + self.button.setText(util_ui.tr('OK')) self.button.clicked.connect(self.button_click) self.warning = QtWidgets.QLabel(self) @@ -58,28 +60,27 @@ class PasswordScreenBase(CenteredWidget): super(PasswordScreenBase, self).keyPressEvent(event) def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("pass", "Enter password")) - self.enter_pass.setText(QtWidgets.QApplication.translate("pass", "Password:")) - self.warning.setText(QtWidgets.QApplication.translate("pass", "Incorrect password")) + self.setWindowTitle(util_ui.tr('Enter password')) + self.enter_pass.setText(util_ui.tr('Password:')) + self.warning.setText(util_ui.tr('Incorrect password')) class PasswordScreen(PasswordScreenBase): def __init__(self, encrypt, data): - super(PasswordScreen, self).__init__(encrypt) + super().__init__(encrypt) self._data = data def button_click(self): if self.password.text(): try: self._encrypt.set_password(self.password.text()) - new_data = self._encrypt.pass_decrypt(self._data[0]) + new_data = self._encrypt.pass_decrypt(self._data) except Exception as ex: self.warning.setVisible(True) print('Decryption error:', ex) else: - self._data[0] = new_data - self.close() + self.close_with_result(new_data) class UnlockAppScreen(PasswordScreenBase): @@ -129,16 +130,15 @@ class SetProfilePasswordScreen(CenteredWidget): self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("PasswordScreen", "Profile password")) + self.setWindowTitle(util_ui.tr('Profile password')) self.password.setPlaceholderText( - QtWidgets.QApplication.translate("PasswordScreen", "Password (at least 8 symbols)")) + util_ui.tr('Password (at least 8 symbols)')) self.confirm_password.setPlaceholderText( - QtWidgets.QApplication.translate("PasswordScreen", "Confirm password")) + util_ui.tr('Confirm password')) self.set_password.setText( - QtWidgets.QApplication.translate("PasswordScreen", "Set password")) - self.not_match.setText(QtWidgets.QApplication.translate("PasswordScreen", "Passwords do not match")) - self.warning.setText( - QtWidgets.QApplication.translate("PasswordScreen", "There is no way to recover lost passwords")) + util_ui.tr('Set password')) + self.not_match.setText(util_ui.tr('Passwords do not match')) + self.warning.setText(util_ui.tr('There is no way to recover lost passwords')) def new_password(self): if self.password.text() == self.confirm_password.text(): @@ -146,9 +146,8 @@ class SetProfilePasswordScreen(CenteredWidget): self._encrypt.set_password(self.password.text()) self.close() else: - self.not_match.setText( - QtWidgets.QApplication.translate("PasswordScreen", "Password must be at least 8 symbols")) + self.not_match.setText(util_ui.tr('Password must be at least 8 symbols')) self.not_match.setVisible(True) else: - self.not_match.setText(QtWidgets.QApplication.translate("PasswordScreen", "Passwords do not match")) + self.not_match.setText(util_ui.tr('Passwords do not match')) self.not_match.setVisible(True) diff --git a/toxygen/ui/peer_screen.py b/toxygen/ui/peer_screen.py new file mode 100644 index 0000000..8f2d5ba --- /dev/null +++ b/toxygen/ui/peer_screen.py @@ -0,0 +1,111 @@ +from ui.widgets import CenteredWidget +from PyQt5 import uic +import utils.util as util +import utils.ui as util_ui +from ui.contact_items import * +import wrapper.toxcore_enums_and_consts as consts + + +class PeerScreen(CenteredWidget): + + def __init__(self, contacts_manager, groups_service, group, peer_id): + super().__init__() + self._contacts_manager = contacts_manager + self._groups_service = groups_service + self._group = group + self._peer = group.get_peer_by_id(peer_id) + + self._roles = { + TOX_GROUP_ROLE['FOUNDER']: util_ui.tr('Administrator'), + TOX_GROUP_ROLE['MODERATOR']: util_ui.tr('Moderator'), + TOX_GROUP_ROLE['USER']: util_ui.tr('User'), + TOX_GROUP_ROLE['OBSERVER']: util_ui.tr('Observer') + } + + uic.loadUi(util.get_views_path('peer_screen'), self) + self._update_ui() + + def _update_ui(self): + self.statusCircle = StatusCircle(self) + self.statusCircle.setGeometry(50, 15, 30, 30) + + self.statusCircle.update(self._peer.status) + self.peerNameLabel.setText(self._peer.name) + self.ignorePeerCheckBox.setChecked(self._peer.is_muted) + self.ignorePeerCheckBox.clicked.connect(self._toggle_ignore) + self.sendPrivateMessagePushButton.clicked.connect(self._send_private_message) + self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) + self.roleNameLabel.setText(self._get_role_name()) + can_change_role_or_ban = self._can_change_role_or_ban() + 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.kickPushButton.clicked.connect(self._kick_peer) + + self._retranslate_ui() + + self.rolesComboBox.currentIndexChanged.connect(self._role_set) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Peer details')) + self.ignorePeerCheckBox.setText(util_ui.tr('Ignore peer')) + 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.kickPushButton.setText(util_ui.tr('Kick peer')) + self.banGroupBox.setTitle(util_ui.tr('Ban peer')) + self.ipBanRadioButton.setText(util_ui.tr('IP')) + self.nickBanRadioButton.setText(util_ui.tr('Nickname')) + self.pkBanRadioButton.setText(util_ui.tr('Public key')) + + self.rolesComboBox.clear() + index = self._group.get_self_peer().role + roles = list(self._roles.values()) + for role in roles[index + 1:]: + self.rolesComboBox.addItem(role) + self.rolesComboBox.setCurrentIndex(self._peer.role - index - 1) + + def _can_change_role_or_ban(self): + self_peer = self._group.get_self_peer() + if self_peer.role > TOX_GROUP_ROLE['MODERATOR']: + return False + + return self_peer.role < self._peer.role + + def _role_set(self): + index = self.rolesComboBox.currentIndex() + all_roles_count = len(self._roles) + diff = all_roles_count - self.rolesComboBox.count() + self._groups_service.set_new_peer_role(self._group, self._peer, index + diff) + + def _get_role_name(self): + return self._roles[self._peer.role] + + def _toggle_ignore(self): + ignore = self.ignorePeerCheckBox.isChecked() + self._groups_service.toggle_ignore_peer(self._group, self._peer, ignore) + + def _send_private_message(self): + self._contacts_manager.add_group_peer(self._group, self._peer) + self.close() + + def _copy_public_key(self): + util_ui.copy_to_clipboard(self._peer.public_key) + + def _ban_peer(self): + ban_type = self._get_ban_type() + self._groups_service.ban_peer(self._group, self._peer.id, ban_type) + self.close() + + def _kick_peer(self): + self._groups_service.kick_peer(self._group, self._peer.id) + self.close() + + def _get_ban_type(self): + if self.ipBanRadioButton.isChecked(): + return consts.TOX_GROUP_BAN_TYPE['IP_PORT'] + elif self.nickBanRadioButton.isChecked(): + return consts.TOX_GROUP_BAN_TYPE['NICK'] + return consts.TOX_GROUP_BAN_TYPE['PUBLIC_KEY'] diff --git a/toxygen/ui/profile_settings_screen.py b/toxygen/ui/profile_settings_screen.py new file mode 100644 index 0000000..2e55d3d --- /dev/null +++ b/toxygen/ui/profile_settings_screen.py @@ -0,0 +1,157 @@ +from ui.widgets import CenteredWidget +import utils.ui as util_ui +from utils.util import join_path, get_images_directory, get_views_path +from user_data.settings import Settings +from PyQt5 import QtGui, QtCore, uic + + +class ProfileSettings(CenteredWidget): + """Form with profile settings such as name, status, TOX ID""" + def __init__(self, profile, profile_manager, settings, toxes): + super().__init__() + self._profile = profile + self._profile_manager = profile_manager + self._settings = settings + self._toxes = toxes + self._auto = False + + uic.loadUi(get_views_path('profile_settings_screen'), self) + + self._init_ui() + self.center() + + def closeEvent(self, event): + self._profile.set_name(self.nameLineEdit.text()) + self._profile.set_status_message(self.statusMessageLineEdit.text()) + self._profile.set_status(self.statusComboBox.currentIndex()) + + def _init_ui(self): + 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.defaultProfilePushButton.clicked.connect(self._toggle_auto_profile) + self.copyToxIdPushButton.clicked.connect(self._copy_tox_id) + self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) + self.changePasswordPushButton.clicked.connect(self._save_password) + self.exportProfilePushButton.clicked.connect(self._export_profile) + self.newNoSpamPushButton.clicked.connect(self._set_new_no_spam) + self.newAvatarPushButton.clicked.connect(self._set_avatar) + self.resetAvatarPushButton.clicked.connect(self._reset_avatar) + + self.invalidPasswordsLabel.setVisible(False) + + self._retranslate_ui() + + if self._profile.status is not None: + self.statusComboBox.setCurrentIndex(self._profile.status) + else: + self.statusComboBox.setVisible(False) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Profile settings")) + + self.exportProfilePushButton.setText(util_ui.tr("Export profile")) + self.nameLabel.setText(util_ui.tr("Name:")) + self.statusLabel.setText(util_ui.tr("Status:")) + self.toxIdTitleLabel.setText(util_ui.tr("TOX ID:")) + self.copyToxIdPushButton.setText(util_ui.tr("Copy TOX ID")) + self.newAvatarPushButton.setText(util_ui.tr("New avatar")) + self.resetAvatarPushButton.setText(util_ui.tr("Reset avatar")) + self.newNoSpamPushButton.setText(util_ui.tr("New NoSpam")) + self.profilePasswordLabel.setText(util_ui.tr("Profile password")) + self.passwordLineEdit.setPlaceholderText(util_ui.tr("Password (at least 8 symbols)")) + self.confirmPasswordLineEdit.setPlaceholderText(util_ui.tr("Confirm password")) + self.changePasswordPushButton.setText(util_ui.tr("Set password")) + self.invalidPasswordsLabel.setText(util_ui.tr("Passwords do not match")) + self.emptyPasswordLabel.setText(util_ui.tr("Leaving blank will reset current password")) + self.warningLabel.setText(util_ui.tr("There is no way to recover lost passwords")) + 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._set_default_profile_button_text() + + def _toggle_auto_profile(self): + if self._auto: + Settings.reset_auto_profile() + else: + Settings.set_auto_profile(self._profile_manager.get_path()) + self._auto = not self._auto + self._set_default_profile_button_text() + + def _set_default_profile_button_text(self): + if self._auto: + self.defaultProfilePushButton.setText(util_ui.tr("Mark as not default profile")) + else: + self.defaultProfilePushButton.setText(util_ui.tr("Mark as default profile")) + + def _save_password(self): + password = self.passwordLineEdit.text() + confirm_password = self.confirmPasswordLineEdit.text() + if password == confirm_password: + if not len(password) or len(password) >= 8: + self._toxes.set_password(password) + self.close() + else: + self.invalidPasswordsLabel.setText( + util_ui.tr("Password must be at least 8 symbols")) + self.invalidPasswordsLabel.setVisible(True) + else: + self.invalidPasswordsLabel.setText(util_ui.tr("Passwords do not match")) + self.invalidPasswordsLabel.setVisible(True) + + def _copy_tox_id(self): + util_ui.copy_to_clipboard(self._profile.tox_id) + + icon = self._get_accept_icon() + self.copyToxIdPushButton.setIcon(icon) + self.copyToxIdPushButton.setIconSize(QtCore.QSize(10, 10)) + + def _copy_public_key(self): + util_ui.copy_to_clipboard(self._profile.tox_id[:64]) + + icon = self._get_accept_icon() + self.copyPublicKeyPushButton.setIcon(icon) + self.copyPublicKeyPushButton.setIconSize(QtCore.QSize(10, 10)) + + def _set_new_no_spam(self): + self.toxIdLabel.setText(self._profile.set_new_nospam()) + + def _reset_avatar(self): + self._profile.reset_avatar(self._settings['identicons']) + + def _set_avatar(self): + choose = util_ui.tr("Choose avatar") + name = util_ui.file_dialog(choose, 'Images (*.png)') + if not name[0]: + return + bitmap = QtGui.QPixmap(name[0]) + bitmap.scaled(QtCore.QSize(128, 128), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + + byte_array = QtCore.QByteArray() + buffer = QtCore.QBuffer(byte_array) + buffer.open(QtCore.QIODevice.WriteOnly) + bitmap.save(buffer, 'PNG') + + self._profile.set_avatar(bytes(byte_array.data())) + + def _export_profile(self): + directory = util_ui.directory_dialog() + if not directory: + return + + 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) + + @staticmethod + def _get_accept_icon(): + pixmap = QtGui.QPixmap(join_path(get_images_directory(), 'accept.png')) + + return QtGui.QIcon(pixmap) + diff --git a/toxygen/ui/self_peer_screen.py b/toxygen/ui/self_peer_screen.py new file mode 100644 index 0000000..cf252d3 --- /dev/null +++ b/toxygen/ui/self_peer_screen.py @@ -0,0 +1,66 @@ +from ui.widgets import CenteredWidget, LineEdit +from PyQt5 import uic +import utils.util as util +import utils.ui as util_ui +from ui.contact_items import * + + +class SelfPeerScreen(CenteredWidget): + + def __init__(self, contacts_manager, groups_service, group): + super().__init__() + self._contacts_manager = contacts_manager + self._groups_service = groups_service + self._group = group + self._peer = group.get_self_peer() + self._roles = { + TOX_GROUP_ROLE['FOUNDER']: util_ui.tr('Administrator'), + TOX_GROUP_ROLE['MODERATOR']: util_ui.tr('Moderator'), + TOX_GROUP_ROLE['USER']: util_ui.tr('User'), + TOX_GROUP_ROLE['OBSERVER']: util_ui.tr('Observer') + } + + uic.loadUi(util.get_views_path('self_peer_screen'), self) + self._update_ui() + + def _update_ui(self): + self.lineEdit = LineEdit(self) + self.lineEdit.setGeometry(140, 40, 400, 30) + self.lineEdit.setText(self._peer.name) + self.lineEdit.textChanged.connect(self._nick_changed) + + self.savePushButton.clicked.connect(self._save) + self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) + + self._retranslate_ui() + + self.statusComboBox.setCurrentIndex(self._peer.status) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Change credentials in group')) + self.lineEdit.setPlaceholderText(util_ui.tr('Your nickname in group')) + self.nameLabel.setText(util_ui.tr('Name:')) + self.roleLabel.setText(util_ui.tr('Role:')) + self.statusLabel.setText(util_ui.tr('Status:')) + self.copyPublicKeyPushButton.setText(util_ui.tr('Copy public key')) + self.savePushButton.setText(util_ui.tr('Save')) + self.roleNameLabel.setText(self._get_role_name()) + self.statusComboBox.addItem(util_ui.tr('Online')) + self.statusComboBox.addItem(util_ui.tr('Away')) + self.statusComboBox.addItem(util_ui.tr('Busy')) + + def _get_role_name(self): + return self._roles[self._peer.role] + + def _nick_changed(self): + nick = self.lineEdit.text() + self.savePushButton.setEnabled(bool(nick)) + + def _save(self): + nick = self.lineEdit.text() + status = self.statusComboBox.currentIndex() + self._groups_service.set_self_info(self._group, nick, status) + self.close() + + def _copy_public_key(self): + util_ui.copy_to_clipboard(self._peer.public_key) diff --git a/toxygen/ui/tray.py b/toxygen/ui/tray.py new file mode 100644 index 0000000..3bfc7f3 --- /dev/null +++ b/toxygen/ui/tray.py @@ -0,0 +1,111 @@ +from PyQt5 import QtWidgets, QtGui, QtCore +from utils.ui import tr +from utils.util import * +from ui.password_screen import UnlockAppScreen +import os.path + + +class SystemTrayIcon(QtWidgets.QSystemTrayIcon): + + leftClicked = QtCore.pyqtSignal() + + def __init__(self, icon, parent=None): + super().__init__(icon, parent) + self.activated.connect(self.icon_activated) + + def icon_activated(self, reason): + if reason == QtWidgets.QSystemTrayIcon.Trigger: + self.leftClicked.emit() + + +class Menu(QtWidgets.QMenu): + + def __init__(self, settings, profile, *args): + super().__init__(*args) + self._settings = settings + self._profile = profile + + def new_status(self, status): + if not self._settings.locked: + self._profile.set_status(status) + self.about_to_show_handler() + self.hide() + + def about_to_show_handler(self): + status = self._profile.status + act = self.act + if status is None or self._settings.locked: + self.actions()[1].setVisible(False) + else: + self.actions()[1].setVisible(True) + act.actions()[0].setChecked(False) + act.actions()[1].setChecked(False) + act.actions()[2].setChecked(False) + act.actions()[status].setChecked(True) + self.actions()[2].setVisible(not self._settings.locked) + + def languageChange(self, *args, **kwargs): + self.actions()[0].setText(tr('Open Toxygen')) + self.actions()[1].setText(tr('Set status')) + self.actions()[2].setText(tr('Exit')) + self.act.actions()[0].setText(tr('Online')) + self.act.actions()[1].setText(tr('Away')) + self.act.actions()[2].setText(tr('Busy')) + + +def init_tray(profile, settings, main_screen, toxes): + icon = os.path.join(get_images_directory(), 'icon.png') + tray = SystemTrayIcon(QtGui.QIcon(icon)) + + menu = Menu(settings, profile) + show = menu.addAction(tr('Open Toxygen')) + sub = menu.addMenu(tr('Set status')) + online = sub.addAction(tr('Online')) + away = sub.addAction(tr('Away')) + busy = sub.addAction(tr('Busy')) + online.setCheckable(True) + away.setCheckable(True) + busy.setCheckable(True) + menu.act = sub + exit = menu.addAction(tr('Exit')) + + def show_window(): + def show(): + if not main_screen.isActiveWindow(): + main_screen.setWindowState( + main_screen.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + main_screen.activateWindow() + main_screen.show() + if not settings.locked: + show() + else: + def correct_pass(): + show() + settings.locked = False + settings.unlockScreen = False + if not settings.unlockScreen: + settings.unlockScreen = True + show_window.screen = UnlockAppScreen(toxes, correct_pass) + show_window.screen.show() + + def tray_activated(reason): + if reason == QtWidgets.QSystemTrayIcon.DoubleClick: + show_window() + + def close_app(): + if not settings.locked: + settings.closing = True + main_screen.close() + + show.triggered.connect(show_window) + exit.triggered.connect(close_app) + menu.aboutToShow.connect(menu.about_to_show_handler) + online.triggered.connect(lambda: menu.new_status(0)) + away.triggered.connect(lambda: menu.new_status(1)) + busy.triggered.connect(lambda: menu.new_status(2)) + + tray.setContextMenu(menu) + tray.show() + tray.activated.connect(tray_activated) + + return tray diff --git a/toxygen/ui/views/add_contact_screen.ui b/toxygen/ui/views/add_contact_screen.ui new file mode 100644 index 0000000..0f26a25 --- /dev/null +++ b/toxygen/ui/views/add_contact_screen.ui @@ -0,0 +1,99 @@ + + + Form + + + + 0 + 0 + 560 + 320 + + + + + 560 + 320 + + + + + 560 + 320 + + + + Form + + + + + 50 + 10 + 150 + 20 + + + + TextLabel + + + + + + 50 + 70 + 150 + 30 + + + + TextLabel + + + + + + 50 + 110 + 460 + 150 + + + + + + + 50 + 270 + 460 + 30 + + + + PushButton + + + + + true + + + + 220 + 10 + 321 + 31 + + + + Qt::NoContextMenu + + + + + + + + + diff --git a/toxygen/ui/views/audio_settings_screen.ui b/toxygen/ui/views/audio_settings_screen.ui new file mode 100644 index 0000000..a404592 --- /dev/null +++ b/toxygen/ui/views/audio_settings_screen.ui @@ -0,0 +1,87 @@ + + + Form + + + + 0 + 0 + 315 + 218 + + + + + 315 + 218 + + + + + 315 + 218 + + + + Form + + + + + 30 + 10 + 261 + 30 + + + + + 16 + + + + TextLabel + + + + + + 30 + 100 + 261 + 30 + + + + + 16 + + + + TextLabel + + + + + + 30 + 50 + 255 + 41 + + + + + + + 30 + 140 + 255 + 41 + + + + + + + diff --git a/toxygen/ui/views/bans_list_screen.ui b/toxygen/ui/views/bans_list_screen.ui new file mode 100644 index 0000000..16339d8 --- /dev/null +++ b/toxygen/ui/views/bans_list_screen.ui @@ -0,0 +1,29 @@ + + + Form + + + + 0 + 0 + 500 + 375 + + + + Form + + + + + 0 + 0 + 500 + 375 + + + + + + + diff --git a/toxygen/ui/views/create_group_screen.ui b/toxygen/ui/views/create_group_screen.ui new file mode 100644 index 0000000..3a3358a --- /dev/null +++ b/toxygen/ui/views/create_group_screen.ui @@ -0,0 +1,127 @@ + + + Form + + + + 0 + 0 + 640 + 300 + + + + Form + + + + false + + + + 20 + 250 + 601 + 41 + + + + + + + + + + 150 + 20 + 470 + 35 + + + + + + + 150 + 80 + 470 + 35 + + + + + + + 20 + 20 + 121 + 31 + + + + TextLabel + + + + + + 20 + 80 + 121 + 31 + + + + TextLabel + + + + + + 20 + 200 + 111 + 17 + + + + TextLabel + + + + + + 20 + 150 + 111 + 17 + + + + TextLabel + + + + + + 150 + 140 + 470 + 35 + + + + + + + 150 + 190 + 470 + 35 + + + + + + + diff --git a/toxygen/ui/views/create_profile_screen.ui b/toxygen/ui/views/create_profile_screen.ui new file mode 100644 index 0000000..bfffee5 --- /dev/null +++ b/toxygen/ui/views/create_profile_screen.ui @@ -0,0 +1,128 @@ + + + Form + + + + 0 + 0 + 400 + 340 + + + + + 400 + 340 + + + + + 400 + 340 + + + + Form + + + + + 30 + 270 + 341 + 51 + + + + PushButton + + + + + + 30 + 170 + 341 + 41 + + + + QLineEdit::Password + + + + + + 30 + 120 + 341 + 41 + + + + QLineEdit::Password + + + + + + 30 + 80 + 330 + 20 + + + + TextLabel + + + + + + 30 + 10 + 330 + 23 + + + + RadioButton + + + true + + + + + + 30 + 40 + 330 + 23 + + + + RadioButton + + + + + + 30 + 220 + 341 + 30 + + + + + + + Qt::AlignCenter + + + + + + diff --git a/toxygen/ui/views/gc_ban_item.ui b/toxygen/ui/views/gc_ban_item.ui new file mode 100644 index 0000000..a57d0e1 --- /dev/null +++ b/toxygen/ui/views/gc_ban_item.ui @@ -0,0 +1,58 @@ + + + Form + + + + 0 + 0 + 500 + 100 + + + + Form + + + + + 330 + 30 + 161 + 41 + + + + PushButton + + + + + + 15 + 20 + 305 + 20 + + + + TextLabel + + + + + + 15 + 50 + 305 + 20 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/gc_invite_item.ui b/toxygen/ui/views/gc_invite_item.ui new file mode 100644 index 0000000..6eddbeb --- /dev/null +++ b/toxygen/ui/views/gc_invite_item.ui @@ -0,0 +1,71 @@ + + + Form + + + + 0 + 0 + 600 + 150 + + + + Form + + + + + 250 + 30 + 300 + 21 + + + + TextLabel + + + + + + 250 + 70 + 300 + 21 + + + + TextLabel + + + + + + 140 + 30 + 60 + 60 + + + + TextLabel + + + + + + 40 + 50 + 20 + 23 + + + + + + + + + + diff --git a/toxygen/ui/views/gc_settings_screen.ui b/toxygen/ui/views/gc_settings_screen.ui new file mode 100644 index 0000000..526c156 --- /dev/null +++ b/toxygen/ui/views/gc_settings_screen.ui @@ -0,0 +1,83 @@ + + + Form + + + + 0 + 0 + 400 + 220 + + + + + 400 + 220 + + + + + 400 + 220 + + + + Form + + + + + 10 + 20 + 380 + 20 + + + + TextLabel + + + + + + 10 + 60 + 380 + 40 + + + + PushButton + + + + + + 10 + 120 + 380 + 20 + + + + TextLabel + + + + + + 10 + 160 + 380 + 20 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/group_invites_screen.ui b/toxygen/ui/views/group_invites_screen.ui new file mode 100644 index 0000000..183f801 --- /dev/null +++ b/toxygen/ui/views/group_invites_screen.ui @@ -0,0 +1,113 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 0 + 150 + 600 + 25 + + + + TextLabel + + + Qt::AlignCenter + + + + + + 0 + 0 + 600 + 341 + + + + + + + 10 + 360 + 350 + 35 + + + + + + + 10 + 410 + 350 + 35 + + + + + + + 390 + 390 + 200 + 35 + + + + + + + 40 + 460 + 201 + 31 + + + + PushButton + + + + + + 360 + 460 + 201 + 31 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/group_management_screen.ui b/toxygen/ui/views/group_management_screen.ui new file mode 100644 index 0000000..859754b --- /dev/null +++ b/toxygen/ui/views/group_management_screen.ui @@ -0,0 +1,110 @@ + + + Form + + + + 0 + 0 + 658 + 238 + + + + Form + + + + + 180 + 20 + 450 + 41 + + + + + + + 20 + 30 + 145 + 20 + + + + TextLabel + + + + + + 20 + 80 + 145 + 20 + + + + TextLabel + + + + + + 180 + 70 + 450 + 40 + + + + 2 + + + 9999 + + + 512 + + + + + + 20 + 130 + 145 + 20 + + + + TextLabel + + + + + + 180 + 120 + 450 + 40 + + + + + + + 20 + 180 + 611 + 41 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/interface_settings_screen.ui b/toxygen/ui/views/interface_settings_screen.ui new file mode 100644 index 0000000..fb0bcf1 --- /dev/null +++ b/toxygen/ui/views/interface_settings_screen.ui @@ -0,0 +1,253 @@ + + + Form + + + + 0 + 0 + 552 + 847 + + + + Form + + + + + + Qt::ScrollBarAsNeeded + + + true + + + + + 0 + 0 + 532 + 827 + + + + + + 30 + 140 + 67 + 17 + + + + TextLabel + + + + + + 20 + 180 + 471 + 31 + + + + + + + 20 + 60 + 471 + 31 + + + + + + + 30 + 20 + 67 + 17 + + + + TextLabel + + + + + + 30 + 220 + 461 + 23 + + + + CheckBox + + + + + + 30 + 280 + 461 + 221 + + + + GroupBox + + + + + 30 + 40 + 92 + 23 + + + + CheckBox + + + + + + 30 + 80 + 411 + 17 + + + + TextLabel + + + + + + 30 + 120 + 411 + 31 + + + + + + + + 30 + 250 + 461 + 23 + + + + CheckBox + + + + + + 30 + 750 + 471 + 40 + + + + PushButton + + + + + + 30 + 690 + 471 + 40 + + + + PushButton + + + + + + 30 + 520 + 461 + 23 + + + + CheckBox + + + + + + 30 + 550 + 471 + 131 + + + + GroupBox + + + + + 30 + 30 + 421 + 23 + + + + RadioButton + + + + + + 30 + 60 + 431 + 23 + + + + RadioButton + + + + + + 30 + 90 + 421 + 23 + + + + RadioButton + + + + + + + + + + + diff --git a/toxygen/ui/views/join_group_screen.ui b/toxygen/ui/views/join_group_screen.ui new file mode 100644 index 0000000..077a332 --- /dev/null +++ b/toxygen/ui/views/join_group_screen.ui @@ -0,0 +1,139 @@ + + + Form + + + + 0 + 0 + 740 + 320 + + + + + 740 + 320 + + + + + 740 + 320 + + + + Form + + + + + 30 + 30 + 67 + 17 + + + + TextLabel + + + + + + 30 + 90 + 67 + 17 + + + + TextLabel + + + + + false + + + + 30 + 260 + 680 + 51 + + + + + + + + + + 190 + 20 + 520 + 41 + + + + + + + 190 + 80 + 520 + 41 + + + + + + + 30 + 150 + 67 + 17 + + + + TextLabel + + + + + + 30 + 210 + 67 + 17 + + + + TextLabel + + + + + + 190 + 140 + 520 + 41 + + + + + + + 190 + 200 + 520 + 41 + + + + + + + diff --git a/toxygen/ui/views/login_screen.ui b/toxygen/ui/views/login_screen.ui new file mode 100644 index 0000000..50ca1e0 --- /dev/null +++ b/toxygen/ui/views/login_screen.ui @@ -0,0 +1,136 @@ + + + loginScreen + + + + 0 + 0 + 400 + 200 + + + + + 400 + 200 + + + + + 400 + 200 + + + + Form + + + + + 0 + 5 + 401 + 30 + + + + + Garuda + 16 + 75 + true + + + + Toxygen + + + Qt::AlignCenter + + + + + + 10 + 40 + 180 + 150 + + + + GroupBox + + + Qt::AlignCenter + + + + + 10 + 110 + 160 + 27 + + + + PushButton + + + + + + + 210 + 40 + 180 + 150 + + + + GroupBox + + + Qt::AlignCenter + + + + + 10 + 40 + 160 + 27 + + + + + + + 10 + 75 + 160 + 27 + + + + CheckBox + + + + + + 10 + 110 + 160 + 27 + + + + PushButton + + + + + + + diff --git a/toxygen/ui/views/ms_left_column.ui b/toxygen/ui/views/ms_left_column.ui new file mode 100644 index 0000000..ffbff71 --- /dev/null +++ b/toxygen/ui/views/ms_left_column.ui @@ -0,0 +1,94 @@ + + + Form + + + + 0 + 0 + 270 + 500 + + + + PointingHandCursor + + + Form + + + + + 5 + 5 + 64 + 64 + + + + PointingHandCursor + + + TextLabel + + + + + + 0 + 75 + 150 + 25 + + + + + + + 150 + 75 + 120 + 25 + + + + + + + 0 + 77 + 20 + 20 + + + + TextLabel + + + + + + 0 + 100 + 270 + 400 + + + + + + + 0 + 100 + 270 + 30 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/network_settings_screen.ui b/toxygen/ui/views/network_settings_screen.ui new file mode 100644 index 0000000..aacf1e0 --- /dev/null +++ b/toxygen/ui/views/network_settings_screen.ui @@ -0,0 +1,180 @@ + + + Form + + + + 0 + 0 + 400 + 500 + + + + + 400 + 500 + + + + + 400 + 500 + + + + Form + + + + + 30 + 20 + 150 + 30 + + + + CheckBox + + + + + + 210 + 20 + 150 + 30 + + + + CheckBox + + + + + + 30 + 140 + 150 + 30 + + + + CheckBox + + + + + + 30 + 190 + 150 + 25 + + + + RadioButton + + + + + + 30 + 230 + 150 + 25 + + + + RadioButton + + + + + + 30 + 100 + 150 + 30 + + + + CheckBox + + + + + + 30 + 280 + 60 + 20 + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 330 + 60 + 20 + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 370 + 340 + 40 + + + + PushButton + + + + + + 30 + 60 + 340 + 30 + + + + CheckBox + + + + + + 30 + 420 + 340 + 65 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/notifications_settings_screen.ui b/toxygen/ui/views/notifications_settings_screen.ui new file mode 100644 index 0000000..67e2dc6 --- /dev/null +++ b/toxygen/ui/views/notifications_settings_screen.ui @@ -0,0 +1,71 @@ + + + Form + + + + 0 + 0 + 320 + 201 + + + + Form + + + + + 20 + 20 + 271 + 41 + + + + CheckBox + + + + + + 20 + 60 + 271 + 41 + + + + CheckBox + + + + + + 20 + 100 + 271 + 41 + + + + CheckBox + + + + + + 20 + 140 + 271 + 41 + + + + CheckBox + + + + + + diff --git a/toxygen/ui/views/peer_screen.ui b/toxygen/ui/views/peer_screen.ui new file mode 100644 index 0000000..e8e9e31 --- /dev/null +++ b/toxygen/ui/views/peer_screen.ui @@ -0,0 +1,200 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 110 + 10 + 431 + 40 + + + + TextLabel + + + + + + 50 + 140 + 500 + 50 + + + + PushButton + + + + + + 50 + 100 + 500 + 23 + + + + CheckBox + + + + + + 50 + 300 + 500 + 161 + + + + GroupBox + + + + + 380 + 50 + 101 + 41 + + + + PushButton + + + + + + 40 + 40 + 251 + 23 + + + + RadioButton + + + true + + + + + + 40 + 80 + 251 + 23 + + + + RadioButton + + + + + + 40 + 120 + 251 + 23 + + + + RadioButton + + + + + + 380 + 100 + 101 + 41 + + + + PushButton + + + + + + + 50 + 60 + 67 + 20 + + + + TextLabel + + + + + + 130 + 60 + 411 + 20 + + + + TextLabel + + + + + + 50 + 210 + 500 + 50 + + + + PushButton + + + + + + 130 + 55 + 291 + 30 + + + + + + + diff --git a/toxygen/ui/views/profile_settings_screen.ui b/toxygen/ui/views/profile_settings_screen.ui new file mode 100644 index 0000000..ece0083 --- /dev/null +++ b/toxygen/ui/views/profile_settings_screen.ui @@ -0,0 +1,280 @@ + + + Form + + + + 0 + 0 + 900 + 702 + + + + Form + + + + + 30 + 10 + 161 + 31 + + + + TextLabel + + + + + + 30 + 90 + 161 + 31 + + + + TextLabel + + + + + + 30 + 50 + 421 + 31 + + + + + + + 30 + 130 + 421 + 31 + + + + + + + 520 + 30 + 311 + 31 + + + + + + + 40 + 180 + 131 + 21 + + + + TextLabel + + + + + + 40 + 210 + 831 + 61 + + + + TextLabel + + + true + + + + + + 40 + 280 + 371 + 31 + + + + PushButton + + + + + + 440 + 280 + 371 + 31 + + + + PushButton + + + + + + 520 + 80 + 321 + 35 + + + + PushButton + + + + + + 520 + 130 + 321 + 35 + + + + PushButton + + + + + + 60 + 380 + 161 + 31 + + + + TextLabel + + + + + + 50 + 420 + 421 + 31 + + + + + + + 50 + 470 + 421 + 31 + + + + + + + 500 + 420 + 381 + 21 + + + + TextLabel + + + + + + 60 + 580 + 381 + 21 + + + + TextLabel + + + + + + 40 + 630 + 831 + 35 + + + + PushButton + + + + + + 50 + 520 + 421 + 35 + + + + PushButton + + + + + + 500 + 470 + 381 + 21 + + + + TextLabel + + + + + + 40 + 330 + 371 + 35 + + + + PushButton + + + + + + 440 + 330 + 371 + 35 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/self_peer_screen.ui b/toxygen/ui/views/self_peer_screen.ui new file mode 100644 index 0000000..38e1f88 --- /dev/null +++ b/toxygen/ui/views/self_peer_screen.ui @@ -0,0 +1,119 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 50 + 120 + 67 + 20 + + + + TextLabel + + + + + + 50 + 250 + 500 + 50 + + + + PushButton + + + + + + 140 + 110 + 400 + 40 + + + + + + + 50 + 40 + 67 + 20 + + + + TextLabel + + + + + + 50 + 190 + 67 + 20 + + + + TextLabel + + + + + + 140 + 190 + 411 + 20 + + + + TextLabel + + + + + + 50 + 330 + 500 + 50 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/update_settings_screen.ui b/toxygen/ui/views/update_settings_screen.ui new file mode 100644 index 0000000..76e7c57 --- /dev/null +++ b/toxygen/ui/views/update_settings_screen.ui @@ -0,0 +1,67 @@ + + + Form + + + + 0 + 0 + 400 + 120 + + + + + 400 + 120 + + + + + 400 + 120 + + + + Form + + + + + 25 + 5 + 350 + 20 + + + + TextLabel + + + + + + 25 + 30 + 350 + 30 + + + + + + + 25 + 70 + 350 + 30 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/video_settings_screen.ui b/toxygen/ui/views/video_settings_screen.ui new file mode 100644 index 0000000..cfa36fb --- /dev/null +++ b/toxygen/ui/views/video_settings_screen.ui @@ -0,0 +1,77 @@ + + + Form + + + + 0 + 0 + 400 + 120 + + + + + 400 + 120 + + + + + 400 + 120 + + + + Form + + + + + 25 + 5 + 350 + 20 + + + + TextLabel + + + + + + 25 + 30 + 350 + 30 + + + + + + + 25 + 70 + 350 + 30 + + + + PushButton + + + + + + 25 + 70 + 350 + 30 + + + + + + + diff --git a/toxygen/widgets.py b/toxygen/ui/widgets.py similarity index 75% rename from toxygen/widgets.py rename to toxygen/ui/widgets.py index b63deb0..e7fe623 100644 --- a/toxygen/widgets.py +++ b/toxygen/ui/widgets.py @@ -1,4 +1,5 @@ from PyQt5 import QtCore, QtGui, QtWidgets +import utils.ui as util_ui class DataLabel(QtWidgets.QLabel): @@ -22,7 +23,8 @@ class ComboBox(QtWidgets.QComboBox): class CenteredWidget(QtWidgets.QWidget): def __init__(self): - super(CenteredWidget, self).__init__() + super().__init__() + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.center() def center(self): @@ -32,10 +34,26 @@ class CenteredWidget(QtWidgets.QWidget): self.move(qr.topLeft()) +class DialogWithResult(QtWidgets.QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self._result = None + + def get_result(self): + return self._result + + result = property(get_result) + + def close_with_result(self, result): + self._result = result + self.close() + + class LineEdit(QtWidgets.QLineEdit): def __init__(self, parent=None): - super(LineEdit, self).__init__(parent) + super().__init__(parent) def contextMenuEvent(self, event): menu = create_menu(self.createStandardContextMenu()) @@ -50,20 +68,20 @@ class QRightClickButton(QtWidgets.QPushButton): rightClicked = QtCore.pyqtSignal() - def __init__(self, parent): - super(QRightClickButton, self).__init__(parent) + def __init__(self, parent=None): + super().__init__(parent) def mousePressEvent(self, event): if event.button() == QtCore.Qt.RightButton: self.rightClicked.emit() else: - super(QRightClickButton, self).mousePressEvent(event) + super().mousePressEvent(event) class RubberBand(QtWidgets.QRubberBand): def __init__(self): - super(RubberBand, self).__init__(QtWidgets.QRubberBand.Rectangle, None) + super().__init__(QtWidgets.QRubberBand.Rectangle, None) self.setPalette(QtGui.QPalette(QtCore.Qt.transparent)) self.pen = QtGui.QPen(QtCore.Qt.blue, 4) self.pen.setStyle(QtCore.Qt.SolidLine) @@ -121,21 +139,21 @@ def create_menu(menu): text = action.text() if 'Link Location' in text: text = text.replace('Copy &Link Location', - QtWidgets.QApplication.translate("MainWindow", "Copy link location")) + util_ui.tr("Copy link location")) elif '&Copy' in text: - text = text.replace('&Copy', QtWidgets.QApplication.translate("MainWindow", "Copy")) + text = text.replace('&Copy', util_ui.tr("Copy")) elif 'All' in text: - text = text.replace('Select All', QtWidgets.QApplication.translate("MainWindow", "Select all")) + text = text.replace('Select All', util_ui.tr("Select all")) elif 'Delete' in text: - text = text.replace('Delete', QtWidgets.QApplication.translate("MainWindow", "Delete")) + text = text.replace('Delete', util_ui.tr("Delete")) elif '&Paste' in text: - text = text.replace('&Paste', QtWidgets.QApplication.translate("MainWindow", "Paste")) + text = text.replace('&Paste', util_ui.tr("Paste")) elif 'Cu&t' in text: - text = text.replace('Cu&t', QtWidgets.QApplication.translate("MainWindow", "Cut")) + text = text.replace('Cu&t', util_ui.tr("Cut")) elif '&Undo' in text: - text = text.replace('&Undo', QtWidgets.QApplication.translate("MainWindow", "Undo")) + text = text.replace('&Undo', util_ui.tr("Undo")) elif '&Redo' in text: - text = text.replace('&Redo', QtWidgets.QApplication.translate("MainWindow", "Redo")) + text = text.replace('&Redo', util_ui.tr("Redo")) else: menu.removeAction(action) continue @@ -156,7 +174,7 @@ class MultilineEdit(CenteredWidget): self.edit.setText(text) self.button = QtWidgets.QPushButton(self) self.button.setGeometry(QtCore.QRect(0, 150, 350, 50)) - self.button.setText(QtWidgets.QApplication.translate("MainWindow", "Save")) + self.button.setText(util_ui.tr("Save")) self.button.clicked.connect(self.button_click) self.center() self.save = save @@ -164,3 +182,16 @@ class MultilineEdit(CenteredWidget): def button_click(self): self.save(self.edit.toPlainText()) self.close() + + +class LineEditWithEnterSupport(LineEdit): + + def __init__(self, enter_action, parent=None): + super().__init__(parent) + self._action = enter_action + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Return: + self._action() + else: + super().keyPressEvent(event) diff --git a/toxygen/ui/widgets_factory.py b/toxygen/ui/widgets_factory.py new file mode 100644 index 0000000..128e85e --- /dev/null +++ b/toxygen/ui/widgets_factory.py @@ -0,0 +1,97 @@ +from ui.main_screen_widgets import * +from ui.menu import * +from ui.groups_widgets import * +from ui.peer_screen import * +from ui.self_peer_screen import * +from ui.group_invites_widgets import * +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, + plugin_loader, toxes, version, groups_service, history, contacts_provider): + self._settings = settings + self._profile = profile + self._profile_manager = profile_manager + self._contacts_manager = contacts_manager + self._file_transfer_handler = file_transfer_handler + self._smiley_loader = smiley_loader + self._plugin_loader = plugin_loader + self._toxes = toxes + self._version = version + self._groups_service = groups_service + self._history = history + self._contacts_provider = contacts_provider + + def create_screenshot_window(self, *args): + return ScreenShotWindow(self._file_transfer_handler, self._contacts_manager, *args) + + def create_welcome_window(self): + return WelcomeScreen(self._settings) + + def create_profile_settings_window(self): + return ProfileSettings(self._profile, self._profile_manager, self._settings, self._toxes) + + def create_network_settings_window(self): + return NetworkSettings(self._settings, self._profile.restart) + + def create_audio_settings_window(self): + return AudioSettings(self._settings) + + def create_video_settings_window(self): + return VideoSettings(self._settings) + + def create_update_settings_window(self): + return UpdateSettings(self._settings, self._version) + + def create_plugins_settings_window(self): + return PluginsSettings(self._plugin_loader) + + def create_add_contact_window(self, tox_id): + return AddContact(self._settings, self._contacts_manager, tox_id) + + def create_privacy_settings_window(self): + return PrivacySettings(self._contacts_manager, self._settings) + + def create_interface_settings_window(self): + return InterfaceSettings(self._settings, self._smiley_loader) + + def create_notification_settings_window(self): + return NotificationsSettings(self._settings) + + def create_smiley_window(self, parent): + return SmileyWindow(parent, self._smiley_loader) + + def create_sticker_window(self): + return StickerWindow(self._file_transfer_handler, self._contacts_manager) + + def create_group_screen_window(self): + return CreateGroupScreen(self._groups_service, self._profile) + + def create_join_group_screen_window(self): + return JoinGroupScreen(self._groups_service, self._profile) + + def create_search_screen(self, messages): + return SearchScreen(self._contacts_manager, self._history, messages, messages.parent()) + + def create_peer_screen_window(self, group, peer_id): + return PeerScreen(self._contacts_manager, self._groups_service, group, peer_id) + + def create_self_peer_screen_window(self, group): + return SelfPeerScreen(self._contacts_manager, self._groups_service, group) + + def create_group_invites_window(self): + return GroupInvitesScreen(self._groups_service, self._profile, self._contacts_provider) + + def create_group_management_screen(self, group): + return GroupManagementScreen(self._groups_service, group) + + @staticmethod + def create_group_settings_screen(group): + return GroupSettingsScreen(group) + + def create_groups_bans_screen(self, group): + return GroupBansScreen(self._groups_service, group) diff --git a/toxygen/updater/__init__.py b/toxygen/updater/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/updater.py b/toxygen/updater/updater.py similarity index 75% rename from toxygen/updater.py rename to toxygen/updater/updater.py index 762892a..329353c 100644 --- a/toxygen/updater.py +++ b/toxygen/updater/updater.py @@ -1,6 +1,6 @@ -import util +import utils.util as util +import utils.ui as util_ui import os -import settings import platform import urllib from PyQt5 import QtNetwork, QtCore @@ -24,12 +24,11 @@ def updater_available(): return os.path.exists(util.curr_directory() + '/toxygen_updater') -def check_for_updates(): - current_version = util.program_version +def check_for_updates(current_version, settings): major, minor, patch = list(map(lambda x: int(x), current_version.split('.'))) versions = generate_versions(major, minor, patch) for version in versions: - if send_request(version): + if send_request(version, settings): return version return None # no new version was found @@ -79,14 +78,13 @@ def download(version): util.log('Exception: running updater failed with ' + str(ex)) -def send_request(version): - s = settings.Settings.get_instance() +def send_request(version, settings): netman = QtNetwork.QNetworkAccessManager() proxy = QtNetwork.QNetworkProxy() - if s['proxy_type']: - proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) - proxy.setHostName(s['proxy_host']) - proxy.setPort(s['proxy_port']) + if settings['proxy_type']: + 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) url = test_url(version) try: @@ -108,3 +106,19 @@ def generate_versions(major, minor, patch): new_minor = '.'.join([str(major), str(minor + 1), '0']) new_patch = '.'.join([str(major), str(minor), str(patch + 1)]) return new_major, new_minor, new_patch + + +def start_update_if_needed(version, settings): + updating = False + if settings['update'] and updater_available() and connection_available(): # auto update + version = check_for_updates(version, settings) + if version is not None: + if settings['update'] == 2: + download(version) + updating = True + else: + reply = util_ui.question(util_ui.tr('Update for Toxygen was found. Download and install it?')) + if reply: + download(version) + updating = True + return updating diff --git a/toxygen/user_data/__init__.py b/toxygen/user_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/user_data/backup_service.py b/toxygen/user_data/backup_service.py new file mode 100644 index 0000000..bb0cef9 --- /dev/null +++ b/toxygen/user_data/backup_service.py @@ -0,0 +1,40 @@ +import os.path +from utils.util import get_profile_name_from_path, join_path + + +class BackupService: + + def __init__(self, settings, profile_manager): + self._settings = settings + self._profile_name = get_profile_name_from_path(profile_manager.get_path()) + + settings.settings_saved_event.add_callback(self._settings_saved) + profile_manager.profile_saved_event.add_callback(self._profile_saved) + + def _settings_saved(self, data): + if not self._check_if_should_save_backup(): + return + + file_path = join_path(self._get_backup_directory(), self._profile_name + '.json') + + with open(file_path, 'wt') as fl: + fl.write(data) + + def _profile_saved(self, data): + if not self._check_if_should_save_backup(): + return + + file_path = join_path(self._get_backup_directory(), self._profile_name + '.tox') + + with open(file_path, 'wb') as fl: + fl.write(data) + + def _check_if_should_save_backup(self): + backup_directory = self._get_backup_directory() + if backup_directory is None: + return False + + return os.path.exists(backup_directory) and os.path.isdir(backup_directory) + + def _get_backup_directory(self): + return self._settings['backup_directory'] diff --git a/toxygen/user_data/profile_manager.py b/toxygen/user_data/profile_manager.py new file mode 100644 index 0000000..05e2f2d --- /dev/null +++ b/toxygen/user_data/profile_manager.py @@ -0,0 +1,90 @@ +import utils.util as util +import os +from user_data.settings import Settings +from common.event import Event + + +class ProfileManager: + """ + Class with methods for search, load and save profiles + """ + def __init__(self, toxes, path): + self._toxes = toxes + self._path = path + self._directory = os.path.dirname(path) + self._profile_saved_event = Event() + # create /avatars if not exists: + avatars_directory = util.join_path(self._directory, 'avatars') + if not os.path.exists(avatars_directory): + os.makedirs(avatars_directory) + + # ----------------------------------------------------------------------------------------------------------------- + # Properties + # ----------------------------------------------------------------------------------------------------------------- + + def get_profile_saved_event(self): + return self._profile_saved_event + + profile_saved_event = property(get_profile_saved_event) + + # ----------------------------------------------------------------------------------------------------------------- + # Public methods + # ----------------------------------------------------------------------------------------------------------------- + + def open_profile(self): + with open(self._path, 'rb') as fl: + data = fl.read() + if data: + return data + else: + raise IOError('Save file has zero size!') + + def get_dir(self): + return self._directory + + def get_path(self): + return self._path + + def save_profile(self, data): + if self._toxes.has_password(): + data = self._toxes.pass_encrypt(data) + with open(self._path, 'wb') as fl: + fl.write(data) + print('Profile saved successfully') + + 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() + with open(path, 'wb') as fout: + fout.write(data) + print('Profile exported successfully') + util.copy(self._directory + 'avatars', new_path + 'avatars') + if use_new_path: + self._path = new_path + os.path.basename(self._path) + self._directory = new_path + settings.update_path(new_path) + + @staticmethod + def find_profiles(): + """ + Find available tox profiles + """ + path = Settings.get_default_path() + result = [] + # check default path + if not os.path.exists(path): + os.makedirs(path) + for fl in os.listdir(path): + if fl.endswith('.tox'): + name = fl[:-4] + result.append((path, name)) + path = util.get_base_directory(__file__) + # check current directory + for fl in os.listdir(path): + if fl.endswith('.tox'): + name = fl[:-4] + result.append((path + '/', name)) + return result diff --git a/toxygen/settings.py b/toxygen/user_data/settings.py similarity index 51% rename from toxygen/settings.py rename to toxygen/user_data/settings.py index 101f372..71422c2 100644 --- a/toxygen/settings.py +++ b/toxygen/user_data/settings.py @@ -1,38 +1,34 @@ -from platform import system import json -import os -from util import Singleton, curr_directory, log, copy, append_slash +from utils.util import * import pyaudio -from toxes import ToxES -import smileys +from common.event import Event -class Settings(dict, Singleton): +class Settings(dict): """ Settings of current profile + global app settings """ - def __init__(self, name): - Singleton.__init__(self) - self.path = ProfileHelper.get_path() + str(name) + '.json' - self.name = name - if os.path.isfile(self.path): - with open(self.path, 'rb') as fl: + def __init__(self, toxes, path): + self._path = path + self._profile_path = path.replace('.json', '.tox') + self._toxes = toxes + self._settings_saved_event = Event() + if os.path.isfile(path): + with open(path, 'rb') as fl: data = fl.read() - inst = ToxES.get_instance() try: - if inst.is_data_encrypted(data): - data = inst.pass_decrypt(data) + if toxes.is_data_encrypted(data): + data = toxes.pass_decrypt(data) info = json.loads(str(data, 'utf-8')) except Exception as ex: info = Settings.get_default_settings() log('Parsing settings error: ' + str(ex)) - super(Settings, self).__init__(info) - self.upgrade() + super().__init__(info) + self._upgrade() else: - super(Settings, self).__init__(Settings.get_default_settings()) - self.save() - smileys.SmileyLoader(self) + super().__init__(Settings.get_default_settings()) + self.save() self.locked = False self.closing = False self.unlockScreen = False @@ -49,22 +45,78 @@ class Settings(dict, Singleton): 'enabled': input_devices and output_devices} self.video = {'device': -1, 'width': 640, 'height': 480, 'x': 0, 'y': 0} + # ----------------------------------------------------------------------------------------------------------------- + # Properties + # ----------------------------------------------------------------------------------------------------------------- + + def get_settings_saved_event(self): + return self._settings_saved_event + + settings_saved_event = property(get_settings_saved_event) + + # ----------------------------------------------------------------------------------------------------------------- + # Public methods + # ----------------------------------------------------------------------------------------------------------------- + + def save(self): + text = json.dumps(self) + if self._toxes.has_password(): + text = bytes(self._toxes.pass_encrypt(bytes(text, 'utf-8'))) + else: + text = bytes(text, 'utf-8') + with open(self._path, 'wb') as fl: + fl.write(text) + + self._settings_saved_event(text) + + def close(self): + path = self._profile_path + '.lock' + if os.path.isfile(path): + os.remove(path) + + def set_active_profile(self): + """ + Mark current profile as active + """ + path = self._profile_path + '.lock' + with open(path, 'w') as fl: + fl.write('active') + + def export(self, path): + text = json.dumps(self) + name = os.path.basename(self._path) + with open(join_path(path, str(name)), 'w') as fl: + fl.write(text) + + def update_path(self, new_path): + self._path = new_path + self.save() + + # ----------------------------------------------------------------------------------------------------------------- + # Static methods + # ----------------------------------------------------------------------------------------------------------------- + @staticmethod def get_auto_profile(): p = Settings.get_global_settings_path() - if os.path.isfile(p): - with open(p) as fl: - data = fl.read() + if not os.path.isfile(p): + return None + with open(p) as fl: + data = fl.read() + try: auto = json.loads(data) - if 'path' in auto and 'name' in auto: - path = str(auto['path']) - name = str(auto['name']) - if os.path.isfile(append_slash(path) + name + '.tox'): - return path, name - return '', '' + except Exception as ex: + log(str(ex)) + auto = {} + if 'profile_path' in auto: + path = str(auto['profile_path']) + if not os.path.isabs(path): + path = join_path(path, curr_directory(__file__)) + if os.path.isfile(path): + return path @staticmethod - def set_auto_profile(path, name): + def set_auto_profile(path): p = Settings.get_global_settings_path() if os.path.isfile(p): with open(p) as fl: @@ -72,8 +124,7 @@ class Settings(dict, Singleton): data = json.loads(data) else: data = {} - data['path'] = str(path) - data['name'] = str(name) + data['profile_path'] = str(path) with open(p, 'w') as fl: fl.write(json.dumps(data)) @@ -86,16 +137,14 @@ class Settings(dict, Singleton): data = json.loads(data) else: data = {} - if 'path' in data: - del data['path'] - del data['name'] + if 'profile_path' in data: + del data['profile_path'] with open(p, 'w') as fl: fl.write(json.dumps(data)) @staticmethod - def is_active_profile(path, name): - path = path + name + '.lock' - return os.path.isfile(path) + def is_active_profile(profile_path): + return os.path.isfile(profile_path + '.lock') @staticmethod def get_default_settings(): @@ -141,12 +190,16 @@ class Settings(dict, Singleton): 'unread_color': 'red', 'save_unsent_only': False, 'compact_mode': False, + 'identicons': True, 'show_welcome_screen': True, - 'close_to_tray': False, + 'close_app': 0, 'font': 'Times New Roman', 'update': 1, 'group_notifications': True, - 'download_nodes_list': False + 'download_nodes_list': False, + 'notify_all_gc': False, + 'lan_discovery': True, + 'backup_directory': None } @staticmethod @@ -161,133 +214,31 @@ class Settings(dict, Singleton): @staticmethod def built_in_themes(): return { - 'dark': '/styles/dark_style.qss', - 'default': '/styles/style.qss' + 'dark': 'dark_style.qss', + 'default': 'style.qss' } - def upgrade(self): + @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/' + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _upgrade(self): default = Settings.get_default_settings() for key in default: if key not in self: print(key) self[key] = default[key] - self.save() - - def save(self): - text = json.dumps(self) - inst = ToxES.get_instance() - if inst.has_password(): - text = bytes(inst.pass_encrypt(bytes(text, 'utf-8'))) - else: - text = bytes(text, 'utf-8') - with open(self.path, 'wb') as fl: - fl.write(text) - - def close(self): - profile_path = ProfileHelper.get_path() - path = str(profile_path + str(self.name) + '.lock') - if os.path.isfile(path): - os.remove(path) - - def set_active_profile(self): - """ - Mark current profile as active - """ - profile_path = ProfileHelper.get_path() - path = str(profile_path + str(self.name) + '.lock') - with open(path, 'w') as fl: - fl.write('active') - - def export(self, path): - text = json.dumps(self) - with open(path + str(self.name) + '.json', 'w') as fl: - fl.write(text) - - def update_path(self): - self.path = ProfileHelper.get_path() + self.name + '.json' - - @staticmethod - def get_global_settings_path(): - return curr_directory() + '/toxygen.json' - - @staticmethod - def get_default_path(): - 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/' - - -class ProfileHelper(Singleton): - """ - Class with methods for search, load and save profiles - """ - def __init__(self, path, name): - Singleton.__init__(self) - path = append_slash(path) - self._path = path + name + '.tox' - self._directory = path - # create /avatars if not exists: - directory = path + 'avatars' - if not os.path.exists(directory): - os.makedirs(directory) - - def open_profile(self): - with open(self._path, 'rb') as fl: - data = fl.read() - if data: - return data - else: - raise IOError('Save file has zero size!') - - def get_dir(self): - return self._directory - - def save_profile(self, data): - inst = ToxES.get_instance() - if inst.has_password(): - data = inst.pass_encrypt(data) - with open(self._path, 'wb') as fl: - fl.write(data) - print('Profile saved successfully') - - def export_profile(self, new_path, use_new_path): - path = new_path + os.path.basename(self._path) - with open(self._path, 'rb') as fin: - data = fin.read() - with open(path, 'wb') as fout: - fout.write(data) - print('Profile exported successfully') - copy(self._directory + 'avatars', new_path + 'avatars') - if use_new_path: - self._path = new_path + os.path.basename(self._path) - self._directory = new_path - Settings.get_instance().update_path() - - @staticmethod - def find_profiles(): - """ - Find available tox profiles - """ - path = Settings.get_default_path() - result = [] - # check default path - if not os.path.exists(path): - os.makedirs(path) - for fl in os.listdir(path): - if fl.endswith('.tox'): - name = fl[:-4] - result.append((path, name)) - path = curr_directory() - # check current directory - for fl in os.listdir(path): - if fl.endswith('.tox'): - name = fl[:-4] - result.append((path + '/', name)) - return result - - @staticmethod - def get_path(): - return ProfileHelper.get_instance().get_dir() diff --git a/toxygen/user_data/toxes.py b/toxygen/user_data/toxes.py new file mode 100644 index 0000000..982f287 --- /dev/null +++ b/toxygen/user_data/toxes.py @@ -0,0 +1,24 @@ + +class ToxES: + + def __init__(self, tox_encrypt_save): + self._tox_encrypt_save = tox_encrypt_save + self._password = None + + def set_password(self, password): + self._password = password + + def has_password(self): + return bool(self._password) + + def is_password(self, password): + return self._password == password + + def is_data_encrypted(self, data): + return len(data) > 0 and self._tox_encrypt_save.is_data_encrypted(data) + + def pass_encrypt(self, data): + return self._tox_encrypt_save.pass_encrypt(data, self._password) + + def pass_decrypt(self, data): + return self._tox_encrypt_save.pass_decrypt(data, self._password) diff --git a/toxygen/util.py b/toxygen/util.py deleted file mode 100644 index d862d56..0000000 --- a/toxygen/util.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -import time -import shutil -import sys -import re - - -program_version = '0.4.2' - - -def cached(func): - saved_result = None - - def wrapped_func(): - nonlocal saved_result - if saved_result is None: - saved_result = func() - - return saved_result - - return wrapped_func - - -def log(data): - try: - with open(curr_directory() + '/logs.log', 'a') as fl: - fl.write(str(data) + '\n') - except: - pass - - -@cached -def curr_directory(): - return os.path.dirname(os.path.realpath(__file__)) - - -def curr_time(): - return time.strftime('%H:%M') - - -def copy(src, dest): - if not os.path.exists(dest): - os.makedirs(dest) - src_files = os.listdir(src) - for file_name in src_files: - full_file_name = os.path.join(src, file_name) - if os.path.isfile(full_file_name): - shutil.copy(full_file_name, dest) - else: - copy(full_file_name, os.path.join(dest, file_name)) - - -def remove(folder): - if os.path.isdir(folder): - shutil.rmtree(folder) - - -def convert_time(t): - offset = time.timezone + time_offset() * 60 - sec = int(t) - offset - m, s = divmod(sec, 60) - h, m = divmod(m, 60) - d, h = divmod(h, 24) - return '%02d:%02d' % (h, m) - - -@cached -def time_offset(): - hours = int(time.strftime('%H')) - minutes = int(time.strftime('%M')) - sec = int(time.time()) - time.timezone - m, s = divmod(sec, 60) - h, m = divmod(m, 60) - d, h = divmod(h, 24) - result = hours * 60 + minutes - h * 60 - m - return result - - -def append_slash(s): - if len(s) and s[-1] not in ('\\', '/'): - s += '/' - return s - - -@cached -def is_64_bit(): - return sys.maxsize > 2 ** 32 - - -def is_re_valid(regex): - try: - re.compile(regex) - except re.error: - return False - else: - return True - - -class Singleton: - _instance = None - - def __init__(self): - self.__class__._instance = self - - @classmethod - def get_instance(cls): - return cls._instance diff --git a/toxygen/utils/__init__.py b/toxygen/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/utils/ui.py b/toxygen/utils/ui.py new file mode 100644 index 0000000..d2d7122 --- /dev/null +++ b/toxygen/utils/ui.py @@ -0,0 +1,54 @@ +from PyQt5 import QtWidgets +import utils.util as util + + +def tr(s): + return QtWidgets.QApplication.translate('Toxygen', s) + + +def question(text, title=None): + reply = QtWidgets.QMessageBox.question(None, title or 'Toxygen', text, + QtWidgets.QMessageBox.Yes, + QtWidgets.QMessageBox.No) + return reply == QtWidgets.QMessageBox.Yes + + +def message_box(text, title=None): + m_box = QtWidgets.QMessageBox() + m_box.setText(tr(text)) + m_box.setWindowTitle(title or 'Toxygen') + m_box.exec_() + + +def text_dialog(text, title='', default_value=''): + text, ok = QtWidgets.QInputDialog.getText(None, title, text, QtWidgets.QLineEdit.Normal, default_value) + + return text, ok + + +def directory_dialog(caption=''): + return QtWidgets.QFileDialog.getExistingDirectory(None, caption, util.curr_directory(), + QtWidgets.QFileDialog.DontUseNativeDialog) + + +def file_dialog(caption, file_filter=None): + return QtWidgets.QFileDialog.getOpenFileName(None, caption, util.curr_directory(), file_filter, + options=QtWidgets.QFileDialog.DontUseNativeDialog) + + +def save_file_dialog(caption, filter=None): + return QtWidgets.QFileDialog.getSaveFileName(None, caption, util.curr_directory(), + filter=filter, + options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) + + +def close_all_windows(): + QtWidgets.QApplication.closeAllWindows() + + +def copy_to_clipboard(text): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(text) + + +# TODO: all dialogs diff --git a/toxygen/utils/util.py b/toxygen/utils/util.py new file mode 100644 index 0000000..5bd5c3a --- /dev/null +++ b/toxygen/utils/util.py @@ -0,0 +1,170 @@ +import os +import time +import shutil +import sys +import re +import platform +import datetime + + +def cached(func): + saved_result = None + + def wrapped_func(): + nonlocal saved_result + if saved_result is None: + saved_result = func() + + return saved_result + + return wrapped_func + + +def log(data): + try: + with open(join_path(curr_directory(), 'logs.log'), 'a') as fl: + fl.write(str(data) + '\n') + except Exception as ex: + print(ex) + + +def curr_directory(current_file=None): + return os.path.dirname(os.path.realpath(current_file or __file__)) + + +def get_base_directory(current_file=None): + return os.path.dirname(curr_directory(current_file or __file__)) + + +@cached +def get_images_directory(): + return get_app_directory('images') + + +@cached +def get_styles_directory(): + return get_app_directory('styles') + + +@cached +def get_sounds_directory(): + return get_app_directory('sounds') + + +@cached +def get_stickers_directory(): + return get_app_directory('stickers') + + +@cached +def get_smileys_directory(): + return get_app_directory('smileys') + + +@cached +def get_translations_directory(): + return get_app_directory('translations') + + +@cached +def get_plugins_directory(): + return get_app_directory('plugins') + + +@cached +def get_libs_directory(): + return get_app_directory('libs') + + +def get_app_directory(directory_name): + return os.path.join(get_base_directory(), directory_name) + + +def get_profile_name_from_path(path): + return os.path.basename(path)[:-4] + + +def get_views_path(view_name): + ui_folder = os.path.join(get_base_directory(), 'ui') + views_folder = os.path.join(ui_folder, 'views') + + return os.path.join(views_folder, view_name + '.ui') + + +def curr_time(): + return time.strftime('%H:%M') + + +def get_unix_time(): + return int(time.time()) + + +def join_path(a, b): + return os.path.join(a, b) + + +def file_exists(file_path): + return os.path.exists(file_path) + + +def copy(src, dest): + if not os.path.exists(dest): + os.makedirs(dest) + src_files = os.listdir(src) + for file_name in src_files: + full_file_name = os.path.join(src, file_name) + if os.path.isfile(full_file_name): + shutil.copy(full_file_name, dest) + else: + copy(full_file_name, os.path.join(dest, file_name)) + + +def remove(folder): + if os.path.isdir(folder): + shutil.rmtree(folder) + + +def convert_time(t): + offset = time.timezone + time_offset() * 60 + sec = int(t) - offset + m, s = divmod(sec, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + return '%02d:%02d' % (h, m) + + +@cached +def time_offset(): + hours = int(time.strftime('%H')) + minutes = int(time.strftime('%M')) + sec = int(time.time()) - time.timezone + m, s = divmod(sec, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + result = hours * 60 + minutes - h * 60 - m + return result + + +def unix_time_to_long_str(unix_time): + date_time = datetime.datetime.utcfromtimestamp(unix_time) + + return date_time.strftime('%Y-%m-%d %H:%M:%S') + + +@cached +def is_64_bit(): + return sys.maxsize > 2 ** 32 + + +def is_re_valid(regex): + try: + re.compile(regex) + except re.error: + return False + else: + return True + + +@cached +def get_platform(): + return platform.system() diff --git a/toxygen/wrapper/__init__.py b/toxygen/wrapper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/wrapper/libtox.py b/toxygen/wrapper/libtox.py new file mode 100644 index 0000000..01d41f1 --- /dev/null +++ b/toxygen/wrapper/libtox.py @@ -0,0 +1,61 @@ +from ctypes import CDLL +import utils.util as util + + +class LibToxCore: + + def __init__(self): + platform = util.get_platform() + if platform == 'Windows': + self._libtoxcore = CDLL(util.join_path(util.get_libs_directory(), 'libtox.dll')) + elif platform == 'Darwin': + self._libtoxcore = CDLL('libtoxcore.dylib') + else: + # libtoxcore and libsodium must be installed in your os + try: + self._libtoxcore = CDLL('libtoxcore.so') + except: + self._libtoxcore = CDLL(util.join_path(util.get_libs_directory(), 'libtoxcore.so')) + + def __getattr__(self, item): + return self._libtoxcore.__getattr__(item) + + +class LibToxAV: + + def __init__(self): + platform = util.get_platform() + if platform == 'Windows': + # on Windows av api is in libtox.dll + self._libtoxav = CDLL(util.join_path(util.get_libs_directory(), 'libtox.dll')) + elif platform == 'Darwin': + self._libtoxav = CDLL('libtoxcore.dylib') + else: + # /usr/lib/libtoxcore.so must exists + try: + self._libtoxav = CDLL('libtoxcore.so') + except: + self._libtoxav = CDLL(util.join_path(util.get_libs_directory(), 'libtoxcore.so')) + + def __getattr__(self, item): + return self._libtoxav.__getattr__(item) + + +class LibToxEncryptSave: + + def __init__(self): + platform = util.get_platform() + if platform == 'Windows': + # on Windows profile encryption api is in libtox.dll + self._lib_tox_encrypt_save = CDLL(util.join_path(util.get_libs_directory(), 'libtox.dll')) + elif platform == 'Darwin': + self._lib_tox_encrypt_save = CDLL('libtoxcore.dylib') + else: + # /usr/lib/libtoxcore.so must exists + try: + self._lib_tox_encrypt_save = CDLL('libtoxcore.so') + except: + self._lib_tox_encrypt_save = CDLL(util.join_path(util.get_libs_directory(), 'libtoxcore.so')) + + def __getattr__(self, item): + return self._lib_tox_encrypt_save.__getattr__(item) diff --git a/toxygen/tox.py b/toxygen/wrapper/tox.py similarity index 64% rename from toxygen/tox.py rename to toxygen/wrapper/tox.py index ef4e44c..21b0ebc 100644 --- a/toxygen/tox.py +++ b/toxygen/wrapper/tox.py @@ -1,22 +1,35 @@ +# -*- coding: utf-8 -*- from ctypes import * -from toxcore_enums_and_consts import * -from toxav import ToxAV -from libtox import LibToxCore +from wrapper.toxcore_enums_and_consts import * +from wrapper.toxav import ToxAV +from wrapper.libtox import LibToxCore class ToxOptions(Structure): _fields_ = [ ('ipv6_enabled', c_bool), ('udp_enabled', c_bool), + ('local_discovery_enabled', c_bool), ('proxy_type', c_int), ('proxy_host', c_char_p), ('proxy_port', c_uint16), ('start_port', c_uint16), ('end_port', c_uint16), ('tcp_port', c_uint16), + ('hole_punching_enabled', c_bool), ('savedata_type', c_int), ('savedata_data', c_char_p), - ('savedata_length', c_size_t) + ('savedata_length', c_size_t), + ('log_callback', c_void_p), + ('log_user_data', c_void_p) + ] + + +class GroupChatSelfPeerInfo(Structure): + _fields_ = [ + ('nick', c_char_p), + ('nick_length', c_uint8), + ('user_status', c_int) ] @@ -30,9 +43,8 @@ def bin_to_string(raw_id, length): class Tox: - libtoxcore = LibToxCore() - + def __init__(self, tox_options=None, tox_pointer=None): """ Creates and initialises a new Tox instance with the options passed. @@ -47,8 +59,9 @@ class Tox: self._tox_pointer = tox_pointer else: tox_err_new = c_int() - Tox.libtoxcore.tox_new.restype = POINTER(c_void_p) - self._tox_pointer = Tox.libtoxcore.tox_new(tox_options, byref(tox_err_new)) + f = Tox.libtoxcore.tox_new + f.restype = POINTER(c_void_p) + self._tox_pointer = f(tox_options, byref(tox_err_new)) tox_err_new = tox_err_new.value if tox_err_new == TOX_ERR_NEW['NULL']: raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') @@ -90,15 +103,25 @@ class Tox: self.file_recv_chunk_cb = None self.friend_lossy_packet_cb = None self.friend_lossless_packet_cb = None - self.group_namelist_change_cb = None - self.group_title_cb = None - self.group_action_cb = None - self.group_message_cb = None + self.group_moderation_cb = None + self.group_join_fail_cb = None + self.group_self_join_cb = None self.group_invite_cb = None + self.group_custom_packet_cb = None + self.group_private_message_cb = None + self.group_message_cb = None + self.group_password_cb = None + self.group_peer_limit_cb = None + self.group_privacy_state_cb = None + self.group_topic_cb = None + self.group_peer_status_cb = None + self.group_peer_name_cb = None + self.group_peer_exit_cb = None + self.group_peer_join_cb = None self.AV = ToxAV(self._tox_pointer) - def __del__(self): + def kill(self): del self.AV Tox.libtoxcore.tox_kill(self._tox_pointer) @@ -196,6 +219,7 @@ class Tox: :param public_key: The long term public key of the bootstrap node (TOX_PUBLIC_KEY_SIZE bytes). :return: True on success. """ + address = bytes(address, 'utf-8') tox_err_bootstrap = c_int() result = Tox.libtoxcore.tox_bootstrap(self._tox_pointer, c_char_p(address), c_uint16(port), string_to_bin(public_key), byref(tox_err_bootstrap)) @@ -222,6 +246,7 @@ class Tox: :param public_key: The long term public key of the TCP relay (TOX_PUBLIC_KEY_SIZE bytes). :return: True on success. """ + address = bytes(address, 'utf-8') tox_err_bootstrap = c_int() result = Tox.libtoxcore.tox_add_tcp_relay(self._tox_pointer, c_char_p(address), c_uint16(port), string_to_bin(public_key), byref(tox_err_bootstrap)) @@ -245,7 +270,7 @@ class Tox: """ return Tox.libtoxcore.tox_self_get_connection_status(self._tox_pointer) - def callback_self_connection_status(self, callback, user_data): + def callback_self_connection_status(self, callback): """ Set the callback for the `self_connection_status` event. Pass None to unset. @@ -256,12 +281,11 @@ class Tox: :param callback: Python function. Should take pointer (c_void_p) to Tox object, TOX_CONNECTION (c_int), pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_int, c_void_p) self.self_connection_status_cb = c_callback(callback) Tox.libtoxcore.tox_callback_self_connection_status(self._tox_pointer, - self.self_connection_status_cb, user_data) + self.self_connection_status_cb) def iteration_interval(self): """ @@ -270,11 +294,13 @@ class Tox: """ return Tox.libtoxcore.tox_iteration_interval(self._tox_pointer) - def iterate(self): + def iterate(self, user_data=None): """ The main loop that needs to be run in intervals of tox_iteration_interval() milliseconds. """ - Tox.libtoxcore.tox_iterate(self._tox_pointer) + if user_data is not None: + user_data = c_char_p(user_data) + Tox.libtoxcore.tox_iterate(self._tox_pointer, user_data) # ----------------------------------------------------------------------------------------------------------------- # Internal client information (Tox address/id) @@ -350,6 +376,7 @@ class Tox: :return: True on success. """ tox_err_set_info = c_int() + name = bytes(name, 'utf-8') result = Tox.libtoxcore.tox_self_set_name(self._tox_pointer, c_char_p(name), c_size_t(len(name)), byref(tox_err_set_info)) tox_err_set_info = tox_err_set_info.value @@ -398,6 +425,7 @@ class Tox: :return: True on success. """ tox_err_set_info = c_int() + status_message = bytes(status_message, 'utf-8') result = Tox.libtoxcore.tox_self_set_status_message(self._tox_pointer, c_char_p(status_message), c_size_t(len(status_message)), byref(tox_err_set_info)) tox_err_set_info = tox_err_set_info.value @@ -699,7 +727,7 @@ class Tox: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: raise ArgumentError('The friend_number did not designate a valid friend.') - def callback_friend_name(self, callback, user_data): + def callback_friend_name(self, callback): """ Set the callback for the `friend_name` event. Pass None to unset. @@ -710,11 +738,10 @@ class Tox: A byte array (c_char_p) containing the same data as tox_friend_get_name would write to its `name` parameter, A value (c_size_t) equal to the return value of tox_friend_get_name_size, pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p) self.friend_name_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_name(self._tox_pointer, self.friend_name_cb, user_data) + Tox.libtoxcore.tox_callback_friend_name(self._tox_pointer, self.friend_name_cb) def friend_get_status_message_size(self, friend_number): """ @@ -763,7 +790,7 @@ class Tox: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: raise ArgumentError('The friend_number did not designate a valid friend.') - def callback_friend_status_message(self, callback, user_data): + def callback_friend_status_message(self, callback): """ Set the callback for the `friend_status_message` event. Pass NULL to unset. @@ -775,12 +802,11 @@ class Tox: `status_message` parameter, A value (c_size_t) equal to the return value of tox_friend_get_status_message_size, pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p) self.friend_status_message_cb = c_callback(callback) Tox.libtoxcore.tox_callback_friend_status_message(self._tox_pointer, - self.friend_status_message_cb, c_void_p(user_data)) + self.friend_status_message_cb) def friend_get_status(self, friend_number): """ @@ -804,7 +830,7 @@ class Tox: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: raise ArgumentError('The friend_number did not designate a valid friend.') - def callback_friend_status(self, callback, user_data): + def callback_friend_status(self, callback): """ Set the callback for the `friend_status` event. Pass None to unset. @@ -818,7 +844,7 @@ class Tox: """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) self.friend_status_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_status(self._tox_pointer, self.friend_status_cb, c_void_p(user_data)) + Tox.libtoxcore.tox_callback_friend_status(self._tox_pointer, self.friend_status_cb) def friend_get_connection_status(self, friend_number): """ @@ -843,7 +869,7 @@ class Tox: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: raise ArgumentError('The friend_number did not designate a valid friend.') - def callback_friend_connection_status(self, callback, user_data): + def callback_friend_connection_status(self, callback): """ Set the callback for the `friend_connection_status` event. Pass NULL to unset. @@ -856,12 +882,11 @@ class Tox: The friend number (c_uint32) of the friend whose connection status changed, The result of calling tox_friend_get_connection_status (TOX_CONNECTION) on the passed friend_number, pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) self.friend_connection_status_cb = c_callback(callback) Tox.libtoxcore.tox_callback_friend_connection_status(self._tox_pointer, - self.friend_connection_status_cb, c_void_p(user_data)) + self.friend_connection_status_cb) def friend_get_typing(self, friend_number): """ @@ -883,7 +908,7 @@ class Tox: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: raise ArgumentError('The friend_number did not designate a valid friend.') - def callback_friend_typing(self, callback, user_data): + def callback_friend_typing(self, callback): """ Set the callback for the `friend_typing` event. Pass NULL to unset. @@ -893,11 +918,10 @@ class Tox: The friend number (c_uint32) of the friend who started or stopped typing, The result of calling tox_friend_get_typing (c_bool) on the passed friend_number, pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_bool, c_void_p) self.friend_typing_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_typing(self._tox_pointer, self.friend_typing_cb, c_void_p(user_data)) + Tox.libtoxcore.tox_callback_friend_typing(self._tox_pointer, self.friend_typing_cb) # ----------------------------------------------------------------------------------------------------------------- # Sending private messages @@ -962,7 +986,7 @@ class Tox: elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['EMPTY']: raise ArgumentError('Attempted to send a zero-length message.') - def callback_friend_read_receipt(self, callback, user_data): + def callback_friend_read_receipt(self, callback): """ Set the callback for the `friend_read_receipt` event. Pass None to unset. @@ -978,13 +1002,13 @@ class Tox: c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) self.friend_read_receipt_cb = c_callback(callback) Tox.libtoxcore.tox_callback_friend_read_receipt(self._tox_pointer, - self.friend_read_receipt_cb, c_void_p(user_data)) + self.friend_read_receipt_cb) # ----------------------------------------------------------------------------------------------------------------- # Receiving private messages and friend requests # ----------------------------------------------------------------------------------------------------------------- - def callback_friend_request(self, callback, user_data): + def callback_friend_request(self, callback): """ Set the callback for the `friend_request` event. Pass None to unset. @@ -999,9 +1023,9 @@ class Tox: """ c_callback = CFUNCTYPE(None, c_void_p, POINTER(c_uint8), c_char_p, c_size_t, c_void_p) self.friend_request_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_request(self._tox_pointer, self.friend_request_cb, c_void_p(user_data)) + Tox.libtoxcore.tox_callback_friend_request(self._tox_pointer, self.friend_request_cb) - def callback_friend_message(self, callback, user_data): + def callback_friend_message(self, callback): """ Set the callback for the `friend_message` event. Pass None to unset. @@ -1013,11 +1037,10 @@ class Tox: The message data (c_char_p) they sent, The size (c_size_t) of the message byte array. pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_char_p, c_size_t, c_void_p) self.friend_message_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_message(self._tox_pointer, self.friend_message_cb, c_void_p(user_data)) + Tox.libtoxcore.tox_callback_friend_message(self._tox_pointer, self.friend_message_cb) # ----------------------------------------------------------------------------------------------------------------- # File transmission: common between sending and receiving @@ -1075,7 +1098,7 @@ class Tox: elif tox_err_file_control == TOX_ERR_FILE_CONTROL['SENDQ']: raise RuntimeError('Packet queue is full.') - def callback_file_recv_control(self, callback, user_data): + def callback_file_recv_control(self, callback): """ Set the callback for the `file_recv_control` event. Pass NULL to unset. @@ -1090,12 +1113,11 @@ class Tox: The friend-specific file number (c_uint32) the data received is associated with. The file control (TOX_FILE_CONTROL) command received. pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_void_p) self.file_recv_control_cb = c_callback(callback) Tox.libtoxcore.tox_callback_file_recv_control(self._tox_pointer, - self.file_recv_control_cb, user_data) + self.file_recv_control_cb) def file_seek(self, friend_number, file_number, position): """ @@ -1265,7 +1287,7 @@ class Tox: elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['WRONG_POSITION']: raise ArgumentError('Position parameter was wrong.') - def callback_file_chunk_request(self, callback, user_data): + def callback_file_chunk_request(self, callback): """ Set the callback for the `file_chunk_request` event. Pass None to unset. @@ -1291,17 +1313,16 @@ class Tox: The file or stream position (c_uint64) from which to continue reading. The number of bytes (c_size_t) requested for the current chunk. pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, c_size_t, c_void_p) self.file_chunk_request_cb = c_callback(callback) - self.libtoxcore.tox_callback_file_chunk_request(self._tox_pointer, self.file_chunk_request_cb, user_data) + self.libtoxcore.tox_callback_file_chunk_request(self._tox_pointer, self.file_chunk_request_cb) # ----------------------------------------------------------------------------------------------------------------- # File transmission: receiving # ----------------------------------------------------------------------------------------------------------------- - def callback_file_recv(self, callback, user_data): + def callback_file_recv(self, callback): """ Set the callback for the `file_recv` event. Pass None to unset. @@ -1321,13 +1342,12 @@ class Tox: send request. Size in bytes (c_size_t) of the filename. pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint32, c_uint64, c_char_p, c_size_t, c_void_p) self.file_recv_cb = c_callback(callback) - self.libtoxcore.tox_callback_file_recv(self._tox_pointer, self.file_recv_cb, user_data) + self.libtoxcore.tox_callback_file_recv(self._tox_pointer, self.file_recv_cb) - def callback_file_recv_chunk(self, callback, user_data): + def callback_file_recv_chunk(self, callback): """ Set the callback for the `file_recv_chunk` event. Pass NULL to unset. @@ -1348,11 +1368,10 @@ class Tox: A byte array (c_char_p) containing the received chunk. The length (c_size_t) of the received chunk. pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, POINTER(c_uint8), c_size_t, c_void_p) self.file_recv_chunk_cb = c_callback(callback) - self.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer, self.file_recv_chunk_cb, user_data) + self.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer, self.file_recv_chunk_cb) # ----------------------------------------------------------------------------------------------------------------- # Low-level custom packet sending and receiving @@ -1433,7 +1452,7 @@ class Tox: elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']: raise RuntimeError('Packet queue is full.') - def callback_friend_lossy_packet(self, callback, user_data): + def callback_friend_lossy_packet(self, callback): """ Set the callback for the `friend_lossy_packet` event. Pass NULL to unset. @@ -1443,13 +1462,12 @@ class Tox: A byte array (c_uint8 array) containing the received packet data, length (c_size_t) - The length of the packet data byte array, pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_void_p) self.friend_lossy_packet_cb = c_callback(callback) - self.libtoxcore.tox_callback_friend_lossy_packet(self._tox_pointer, self.friend_lossy_packet_cb, user_data) + self.libtoxcore.tox_callback_friend_lossy_packet(self._tox_pointer, self.friend_lossy_packet_cb) - def callback_friend_lossless_packet(self, callback, user_data): + def callback_friend_lossless_packet(self, callback): """ Set the callback for the `friend_lossless_packet` event. Pass NULL to unset. @@ -1459,12 +1477,10 @@ class Tox: A byte array (c_uint8 array) containing the received packet data, length (c_size_t) - The length of the packet data byte array, pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_void_p) self.friend_lossless_packet_cb = c_callback(callback) - self.libtoxcore.tox_callback_friend_lossless_packet(self._tox_pointer, self.friend_lossless_packet_cb, - user_data) + self.libtoxcore.tox_callback_friend_lossless_packet(self._tox_pointer, self.friend_lossless_packet_cb) # ----------------------------------------------------------------------------------------------------------------- # Low-level network information @@ -1515,87 +1531,1002 @@ class Tox: raise RuntimeError('The instance was not bound to any port.') # ----------------------------------------------------------------------------------------------------------------- - # Group chats + # Group chat instance management # ----------------------------------------------------------------------------------------------------------------- - def del_groupchat(self, groupnumber): - result = Tox.libtoxcore.tox_del_groupchat(self._tox_pointer, c_int(groupnumber), None) + def group_new(self, privacy_state, group_name, nick, status): + """ + Creates a new group chat. + + This function creates a new group chat object and adds it to the chats array. + + The client should initiate its peer list with self info after calling this function, as + the peer_join callback will not be triggered. + + :param privacy_state: The privacy state of the group. If this is set to TOX_GROUP_PRIVACY_STATE_PUBLIC, + the group will attempt to announce itself to the DHT and anyone with the Chat ID may join. + Otherwise a friend invite will be required to join the group. + :param group_name: The name of the group. The name must be non-NULL. + + :return group number on success, UINT32_MAX on failure. + """ + + error = c_int() + peer_info = self.group_self_peer_info_new() + nick = bytes(nick, 'utf-8') + group_name = group_name.encode('utf-8') + peer_info.contents.nick = c_char_p(nick) + peer_info.contents.nick_length = len(nick) + peer_info.contents.user_status = status + result = Tox.libtoxcore.tox_group_new(self._tox_pointer, privacy_state, group_name, + len(group_name), peer_info, byref(error)) return result - def group_peername(self, groupnumber, peernumber): - buffer = create_string_buffer(TOX_MAX_NAME_LENGTH) - result = Tox.libtoxcore.tox_group_peername(self._tox_pointer, c_int(groupnumber), c_int(peernumber), - buffer, None) - return str(buffer[:result], 'utf-8') + def group_join(self, chat_id, password, nick, status): + """ + Joins a group chat with specified Chat ID. - def invite_friend(self, friendnumber, groupnumber): - result = Tox.libtoxcore.tox_invite_friend(self._tox_pointer, c_int(friendnumber), - c_int(groupnumber), None) + This function creates a new group chat object, adds it to the chats array, and sends + a DHT announcement to find peers in the group associated with chat_id. Once a peer has been + found a join attempt will be initiated. + + :param chat_id: The Chat ID of the group you wish to join. This must be TOX_GROUP_CHAT_ID_SIZE bytes. + :param password: The password required to join the group. Set to NULL if no password is required. + + :return group_number on success, UINT32_MAX on failure. + """ + + error = c_int() + peer_info = self.group_self_peer_info_new() + nick = bytes(nick, 'utf-8') + peer_info.contents.nick = c_char_p(nick) + peer_info.contents.nick_length = len(nick) + peer_info.contents.user_status = status + result = Tox.libtoxcore.tox_group_join(self._tox_pointer, string_to_bin(chat_id), + password, + len(password) if password is not None else 0, + peer_info, + byref(error)) return result - def join_groupchat(self, friendnumber, data): - result = Tox.libtoxcore.tox_join_groupchat(self._tox_pointer, - c_int(friendnumber), c_char_p(data), c_uint16(len(data)), None) + def group_reconnect(self, group_number): + """ + Reconnects to a group. + + This function disconnects from all peers in the group, then attempts to reconnect with the group. + The caller's state is not changed (i.e. name, status, role, chat public key etc.) + + :param group_number: The group number of the group we wish to reconnect to. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_reconnect(self._tox_pointer, group_number, byref(error)) return result - def group_message_send(self, groupnumber, message): - result = Tox.libtoxcore.tox_group_message_send(self._tox_pointer, c_int(groupnumber), c_char_p(message), - c_uint16(len(message)), None) + def group_is_connected(self, group_number): + error = c_int() + result = Tox.libtoxcore.tox_group_is_connected(self._tox_pointer, group_number, byref(error)) return result - def group_action_send(self, groupnumber, action): - result = Tox.libtoxcore.tox_group_action_send(self._tox_pointer, - c_int(groupnumber), c_char_p(action), - c_uint16(len(action)), None) + def group_disconnect(self, group_number): + error = c_int() + result = Tox.libtoxcore.tox_group_disconnect(self._tox_pointer, group_number, byref(error)) return result - def group_set_title(self, groupnumber, title): - result = Tox.libtoxcore.tox_group_set_title(self._tox_pointer, c_int(groupnumber), - c_char_p(title), c_uint8(len(title)), None) + def group_leave(self, group_number, message=''): + """ + Leaves a group. + + This function sends a parting packet containing a custom (non-obligatory) message to all + peers in a group, and deletes the group from the chat array. All group state information is permanently + lost, including keys and role credentials. + + :param group_number: The group number of the group we wish to leave. + :param message: The parting message to be sent to all the peers. Set to NULL if we do not wish to + send a parting message. + + :return True if the group chat instance was successfully deleted. + """ + + error = c_int() + f = Tox.libtoxcore.tox_group_leave + f.restype = c_bool + result = f(self._tox_pointer, group_number, message, + len(message) if message is not None else 0, byref(error)) return result - def group_get_title(self, groupnumber): - buffer = create_string_buffer(TOX_MAX_NAME_LENGTH) - result = Tox.libtoxcore.tox_group_get_title(self._tox_pointer, - c_int(groupnumber), buffer, - c_uint32(TOX_MAX_NAME_LENGTH), None) - return str(buffer[:result], 'utf-8') + # ----------------------------------------------------------------------------------------------------------------- + # Group user-visible client information (nickname/status/role/public key) + # ----------------------------------------------------------------------------------------------------------------- - def group_number_peers(self, groupnumber): - result = Tox.libtoxcore.tox_group_number_peers(self._tox_pointer, c_int(groupnumber), None) + def group_self_set_name(self, group_number, name): + """ + Set the client's nickname for the group instance designated by the given group number. + + Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is equal to zero or name is a NULL + pointer, the function call will fail. + + :param name: A byte array containing the new nickname. + + :return True on success. + """ + + error = c_int() + name = bytes(name, 'utf-8') + result = Tox.libtoxcore.tox_group_self_set_name(self._tox_pointer, group_number, name, len(name), byref(error)) return result - def add_av_groupchat(self): - result = self.AV.libtoxav.toxav_add_av_groupchat(self._tox_pointer, None, None) + def group_self_get_name_size(self, group_number): + """ + Return the length of the client's current nickname for the group instance designated + by group_number as passed to tox_group_self_set_name. + + If no nickname was set before calling this function, the name is empty, + and this function returns 0. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_name_size(self._tox_pointer, group_number, byref(error)) return result - def join_av_groupchat(self, friendnumber, data): - result = self.AV.libtoxav.toxav_join_av_groupchat(self._tox_pointer, c_int32(friendnumber), - c_char_p(data), c_uint16(len(data)), - None, None) + def group_self_get_name(self, group_number): + """ + Write the nickname set by tox_group_self_set_name to a byte array. + + If no nickname was set before calling this function, the name is empty, + and this function has no effect. + + Call tox_group_self_get_name_size to find out how much memory to allocate for the result. + :return nickname + """ + + error = c_int() + size = self.group_self_get_name_size(group_number) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_self_get_name(self._tox_pointer, group_number, name, byref(error)) + return str(name[:size], 'utf-8') + + def group_self_set_status(self, group_number, status): + + """ + Set the client's status for the group instance. Status must be a TOX_USER_STATUS. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_set_status(self._tox_pointer, group_number, status, byref(error)) return result - def callback_group_invite(self, callback, user_data=None): - c_callback = CFUNCTYPE(None, c_void_p, c_int32, c_uint8, POINTER(c_uint8), c_uint16, c_void_p) - self.group_invite_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, self.group_invite_cb, user_data) + def group_self_get_status(self, group_number): + """ + returns the client's status for the group instance on success. + return value is unspecified on failure. + """ - def callback_group_message(self, callback, user_data=None): - c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_char_p, c_uint16, c_void_p) + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_status(self._tox_pointer, group_number, byref(error)) + return result + + def group_self_get_role(self, group_number): + """ + returns the client's role for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_role(self._tox_pointer, group_number, byref(error)) + return result + + def group_self_get_peer_id(self, group_number): + """ + returns the client's peer id for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_peer_id(self._tox_pointer, group_number, byref(error)) + return result + + def group_self_get_public_key(self, group_number): + """ + Write the client's group public key designated by the given group number to a byte array. + + This key will be permanently tied to the client's identity for this particular group until + the client explicitly leaves the group or gets kicked/banned. This key is the only way for + other peers to reliably identify the client across client restarts. + + `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + + :return public key + """ + + error = c_int() + key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + result = Tox.libtoxcore.tox_group_self_get_public_key(self._tox_pointer, group_number, + key, byref(error)) + return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + + # ----------------------------------------------------------------------------------------------------------------- + # Peer-specific group state queries. + # ----------------------------------------------------------------------------------------------------------------- + + def group_peer_get_name_size(self, group_number, peer_id): + """ + Return the length of the peer's name. If the group number or ID is invalid, the + return value is unspecified. + + The return value is equal to the `length` argument received by the last + `group_peer_name` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_name_size(self._tox_pointer, group_number, peer_id, byref(error)) + return result + + def group_peer_get_name(self, group_number, peer_id): + """ + Write the name of the peer designated by the given ID to a byte + array. + + Call tox_group_peer_get_name_size to determine the allocation size for the `name` parameter. + + The data written to `name` is equal to the data received by the last + `group_peer_name` callback. + + :param group_number: The group number of the group we wish to query. + :param peer_id: The ID of the peer whose name we want to retrieve. + + :return name. + """ + error = c_int() + size = self.group_peer_get_name_size(group_number, peer_id) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_peer_get_name(self._tox_pointer, group_number, peer_id, name, byref(error)) + return str(name[:], 'utf-8') + + def group_peer_get_status(self, group_number, peer_id): + """ + Return the peer's user status (away/busy/...). If the ID or group number is + invalid, the return value is unspecified. + + The status returned is equal to the last status received through the + `group_peer_status` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_status(self._tox_pointer, group_number, peer_id, byref(error)) + return result + + def group_peer_get_role(self, group_number, peer_id): + """ + Return the peer's role (user/moderator/founder...). If the ID or group number is + invalid, the return value is unspecified. + + The role returned is equal to the last role received through the + `group_moderation` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_role(self._tox_pointer, group_number, peer_id, byref(error)) + return result + + def group_peer_get_public_key(self, group_number, peer_id): + """ + Write the group public key with the designated peer_id for the designated group number to public_key. + + This key will be permanently tied to a particular peer until they explicitly leave the group or + get kicked/banned, and is the only way to reliably identify the same peer across client restarts. + + `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + + :return public key + """ + + error = c_int() + key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + result = Tox.libtoxcore.tox_group_peer_get_public_key(self._tox_pointer, group_number, peer_id, + key, byref(error)) + return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + + def callback_group_peer_name(self, callback, user_data): + """ + Set the callback for the `group_peer_name` event. Pass NULL to unset. + This event is triggered when a peer changes their nickname. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_peer_name_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_name(self._tox_pointer, self.group_peer_name_cb, user_data) + + def callback_group_peer_status(self, callback, user_data): + """ + Set the callback for the `group_peer_status` event. Pass NULL to unset. + This event is triggered when a peer changes their status. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_void_p) + self.group_peer_status_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_status(self._tox_pointer, self.group_peer_status_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat state queries and events. + # ----------------------------------------------------------------------------------------------------------------- + + def group_set_topic(self, group_number, topic): + """ + Set the group topic and broadcast it to the rest of the group. + + topic length cannot be longer than TOX_GROUP_MAX_TOPIC_LENGTH. If length is equal to zero or + topic is set to NULL, the topic will be unset. + + :return True on success. + """ + + error = c_int() + topic = bytes(topic, 'utf-8') + result = Tox.libtoxcore.tox_group_set_topic(self._tox_pointer, group_number, topic, len(topic), byref(error)) + return result + + def group_get_topic_size(self, group_number): + """ + Return the length of the group topic. If the group number is invalid, the + return value is unspecified. + + The return value is equal to the `length` argument received by the last + `group_topic` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_topic_size(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_topic(self, group_number): + """ + Write the topic designated by the given group number to a byte array. + Call tox_group_get_topic_size to determine the allocation size for the `topic` parameter. + The data written to `topic` is equal to the data received by the last + `group_topic` callback. + + :return topic + """ + + error = c_int() + size = self.group_get_topic_size(group_number) + topic = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_topic(self._tox_pointer, group_number, topic, byref(error)) + return str(topic[:size], 'utf-8') + + def group_get_name_size(self, group_number): + """ + Return the length of the group name. If the group number is invalid, the + return value is unspecified. + """ + error = c_int() + result = Tox.libtoxcore.tox_group_get_name_size(self._tox_pointer, group_number, byref(error)) + return int(result) + + def group_get_name(self, group_number): + """ + Write the name of the group designated by the given group number to a byte array. + Call tox_group_get_name_size to determine the allocation size for the `name` parameter. + :return true on success. + """ + + error = c_int() + size = self.group_get_name_size(group_number) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_name(self._tox_pointer, group_number, + name, byref(error)) + return str(name[:size], 'utf-8') + + def group_get_chat_id(self, group_number): + """ + Write the Chat ID designated by the given group number to a byte array. + `chat_id` should have room for at least TOX_GROUP_CHAT_ID_SIZE bytes. + :return chat id. + """ + + error = c_int() + buff = create_string_buffer(TOX_GROUP_CHAT_ID_SIZE) + result = Tox.libtoxcore.tox_group_get_chat_id(self._tox_pointer, group_number, + buff, byref(error)) + return bin_to_string(buff, TOX_GROUP_CHAT_ID_SIZE) + + def group_get_number_groups(self): + """ + Return the number of groups in the Tox chats array. + """ + + result = Tox.libtoxcore.tox_group_get_number_groups(self._tox_pointer) + return result + + def groups_get_list(self): + groups_list_size = self.group_get_number_groups() + groups_list = create_string_buffer(sizeof(c_uint32) * groups_list_size) + groups_list = POINTER(c_uint32)(groups_list) + Tox.libtoxcore.tox_groups_get_list(self._tox_pointer, groups_list) + return groups_list[0:groups_list_size] + + def group_get_privacy_state(self, group_number): + """ + Return the privacy state of the group designated by the given group number. If group number + is invalid, the return value is unspecified. + + The value returned is equal to the data received by the last + `group_privacy_state` callback. + + see the `Group chat founder controls` section for the respective set function. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_privacy_state(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_peer_limit(self, group_number): + """ + Return the maximum number of peers allowed for the group designated by the given group number. + If the group number is invalid, the return value is unspecified. + + The value returned is equal to the data received by the last + `group_peer_limit` callback. + + see the `Group chat founder controls` section for the respective set function. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_peer_limit(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_password_size(self, group_number): + """ + Return the length of the group password. If the group number is invalid, the + return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_password_size(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_password(self, group_number): + """ + Write the password for the group designated by the given group number to a byte array. + + Call tox_group_get_password_size to determine the allocation size for the `password` parameter. + + The data received is equal to the data received by the last + `group_password` callback. + + see the `Group chat founder controls` section for the respective set function. + + :return password + """ + + error = c_int() + size = self.group_get_password_size(group_number) + password = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_password(self._tox_pointer, group_number, + password, byref(error)) + return str(password[:size], 'utf-8') + + def callback_group_topic(self, callback, user_data): + """ + Set the callback for the `group_topic` event. Pass NULL to unset. + This event is triggered when a peer changes the group topic. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_topic_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_topic(self._tox_pointer, self.group_topic_cb, user_data) + + def callback_group_privacy_state(self, callback, user_data): + """ + Set the callback for the `group_privacy_state` event. Pass NULL to unset. + This event is triggered when the group founder changes the privacy state. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) + self.group_privacy_state_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_privacy_state(self._tox_pointer, self.group_privacy_state_cb, user_data) + + def callback_group_peer_limit(self, callback, user_data): + """ + Set the callback for the `group_peer_limit` event. Pass NULL to unset. + This event is triggered when the group founder changes the maximum peer limit. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) + self.group_peer_limit_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_limit(self._tox_pointer, self.group_peer_limit_cb, user_data) + + def callback_group_password(self, callback, user_data): + """ + Set the callback for the `group_password` event. Pass NULL to unset. + This event is triggered when the group founder changes the group password. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_password_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_password(self._tox_pointer, self.group_password_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group message sending + # ----------------------------------------------------------------------------------------------------------------- + + def group_send_custom_packet(self, group_number, lossless, data): + """ + Send a custom packet to the group. + + If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + to TCP (reliability, arrive in order) but with packets instead of a stream. + + If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + meaning they might never reach the other side or might arrive more than once (if someone + is messing with the connection) or might arrive in the wrong order. + + Unless latency is an issue or message reliability is not important, it is recommended that you use + lossless custom packets. + + :param group_number: The group number of the group the message is intended for. + :param lossless: True if the packet should be lossless. + :param data A byte array containing the packet data. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_custom_packet(self._tox_pointer, group_number, lossless, data, + len(data), byref(error)) + return result + + def group_send_private_message(self, group_number, peer_id, message_type, message): + """ + Send a text chat message to the specified peer in the specified group. + + This function creates a group private message packet and pushes it into the send + queue. + + The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages + must be split by the client and sent as separate messages. Other clients can + then reassemble the fragments. Messages may not be empty. + + :param group_number: The group number of the group the message is intended for. + :param peer_id: The ID of the peer the message is intended for. + :param message: A non-NULL pointer to the first element of a byte array containing the message text. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_private_message(self._tox_pointer, group_number, peer_id, + message_type, message, + len(message), byref(error)) + return result + + def group_send_message(self, group_number, type, message): + """ + Send a text chat message to the group. + + This function creates a group message packet and pushes it into the send + queue. + + The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages + must be split by the client and sent as separate messages. Other clients can + then reassemble the fragments. Messages may not be empty. + + :param group_number: The group number of the group the message is intended for. + :param type: Message type (normal, action, ...). + :param message: A non-NULL pointer to the first element of a byte array containing the message text. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_message(self._tox_pointer, group_number, type, message, len(message), + byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group message receiving + # ----------------------------------------------------------------------------------------------------------------- + + def callback_group_message(self, callback, user_data): + """ + Set the callback for the `group_message` event. Pass NULL to unset. + This event is triggered when the client receives a group message. + + Callback: python function with params: + tox Tox* instance + group_number The group number of the group the message is intended for. + peer_id The ID of the peer who sent the message. + type The type of message (normal, action, ...). + message The message data. + length The length of the message. + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_char_p, c_size_t, c_void_p) self.group_message_cb = c_callback(callback) Tox.libtoxcore.tox_callback_group_message(self._tox_pointer, self.group_message_cb, user_data) - def callback_group_action(self, callback, user_data=None): - c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_char_p, c_uint16, c_void_p) - self.group_action_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_group_action(self._tox_pointer, self.group_action_cb, user_data) + def callback_group_private_message(self, callback, user_data): + """ + Set the callback for the `group_private_message` event. Pass NULL to unset. + This event is triggered when the client receives a private message. + """ - def callback_group_title(self, callback, user_data=None): - c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_char_p, c_uint8, c_void_p) - self.group_title_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_group_title(self._tox_pointer, self.group_title_cb, user_data) + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint8, c_char_p, c_size_t, c_void_p) + self.group_private_message_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_private_message(self._tox_pointer, self.group_private_message_cb, user_data) - def callback_group_namelist_change(self, callback, user_data=None): - c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_uint8, c_void_p) - self.group_namelist_change_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_group_namelist_change(self._tox_pointer, self.group_namelist_change_cb, user_data) + def callback_group_custom_packet(self, callback, user_data): + """ + Set the callback for the `group_custom_packet` event. Pass NULL to unset. + + This event is triggered when the client receives a custom packet. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, POINTER(c_uint8), c_void_p) + self.group_custom_packet_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_custom_packet(self._tox_pointer, self.group_custom_packet_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat inviting and join/part events + # ----------------------------------------------------------------------------------------------------------------- + + def group_invite_friend(self, group_number, friend_number): + """ + Invite a friend to a group. + + This function creates an invite request packet and pushes it to the send queue. + + :param group_number: The group number of the group the message is intended for. + :param friend_number: The friend number of the friend the invite is intended for. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_invite_friend(self._tox_pointer, group_number, friend_number, byref(error)) + return result + + @staticmethod + def group_self_peer_info_new(): + error = c_int() + f = Tox.libtoxcore.tox_group_self_peer_info_new + f.restype = POINTER(GroupChatSelfPeerInfo) + result = f(byref(error)) + + return result + + def group_invite_accept(self, invite_data, friend_number, nick, status, password=None): + """ + Accept an invite to a group chat that the client previously received from a friend. The invite + is only valid while the inviter is present in the group. + + :param invite_data: The invite data received from the `group_invite` event. + :param password: The password required to join the group. Set to NULL if no password is required. + :return the group_number on success, UINT32_MAX on failure. + """ + + error = c_int() + f = Tox.libtoxcore.tox_group_invite_accept + f.restype = c_uint32 + peer_info = self.group_self_peer_info_new() + nick = bytes(nick, 'utf-8') + peer_info.contents.nick = c_char_p(nick) + peer_info.contents.nick_length = len(nick) + peer_info.contents.user_status = status + result = f(self._tox_pointer, friend_number, invite_data, len(invite_data), password, + len(password) if password is not None else 0, peer_info, byref(error)) + print('Invite accept. Result:', result, 'Error:', error.value) + return result + + def callback_group_invite(self, callback, user_data): + """ + Set the callback for the `group_invite` event. Pass NULL to unset. + + This event is triggered when the client receives a group invite from a friend. The client must store + invite_data which is used to join the group via tox_group_invite_accept. + + Callback: python function with params: + tox - Tox* + friend_number The friend number of the contact who sent the invite. + invite_data The invite data. + length The length of invite_data. + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, + POINTER(c_uint8), c_size_t, c_void_p) + self.group_invite_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, self.group_invite_cb, user_data) + + def callback_group_peer_join(self, callback, user_data): + """ + Set the callback for the `group_peer_join` event. Pass NULL to unset. + + This event is triggered when a peer other than self joins the group. + Callback: python function with params: + tox - Tox* + group_number - group number + peer_id - peer id + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) + self.group_peer_join_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_join(self._tox_pointer, self.group_peer_join_cb, user_data) + + def callback_group_peer_exit(self, callback, user_data): + """ + Set the callback for the `group_peer_exit` event. Pass NULL to unset. + + This event is triggered when a peer other than self exits the group. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_peer_exit_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_exit(self._tox_pointer, self.group_peer_exit_cb, user_data) + + def callback_group_self_join(self, callback, user_data): + """ + Set the callback for the `group_self_join` event. Pass NULL to unset. + + This event is triggered when the client has successfully joined a group. Use this to initialize + any group information the client may need. + Callback: python fucntion with params: + tox - *Tox + group_number - group number + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_void_p) + self.group_self_join_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_self_join(self._tox_pointer, self.group_self_join_cb, user_data) + + def callback_group_join_fail(self, callback, user_data): + """ + Set the callback for the `group_join_fail` event. Pass NULL to unset. + + This event is triggered when the client fails to join a group. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) + self.group_join_fail_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_join_fail(self._tox_pointer, self.group_join_fail_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat founder controls (these only work for the group founder) + # ----------------------------------------------------------------------------------------------------------------- + + def group_founder_set_password(self, group_number, password): + """ + Set or unset the group password. + + This function sets the groups password, creates a new group shared state including the change, + and distributes it to the rest of the group. + + :param group_number: The group number of the group for which we wish to set the password. + :param password: The password we want to set. Set password to NULL to unset the password. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_password(self._tox_pointer, group_number, password, + len(password), byref(error)) + return result + + def group_founder_set_privacy_state(self, group_number, privacy_state): + """ + Set the group privacy state. + + This function sets the group's privacy state, creates a new group shared state + including the change, and distributes it to the rest of the group. + + If an attempt is made to set the privacy state to the same state that the group is already + in, the function call will be successful and no action will be taken. + + :param group_number: The group number of the group for which we wish to change the privacy state. + :param privacy_state: The privacy state we wish to set the group to. + + :return true on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_privacy_state(self._tox_pointer, group_number, privacy_state, + byref(error)) + return result + + def group_founder_set_peer_limit(self, group_number, max_peers): + """ + Set the group peer limit. + + This function sets a limit for the number of peers who may be in the group, creates a new + group shared state including the change, and distributes it to the rest of the group. + + :param group_number: The group number of the group for which we wish to set the peer limit. + :param max_peers: The maximum number of peers to allow in the group. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_peer_limit(self._tox_pointer, group_number, + max_peers, byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat moderation + # ----------------------------------------------------------------------------------------------------------------- + + def group_toggle_ignore(self, group_number, peer_id, ignore): + """ + Ignore or unignore a peer. + + :param group_number: The group number of the group the in which you wish to ignore a peer. + :param peer_id: The ID of the peer who shall be ignored or unignored. + :param ignore: True to ignore the peer, false to unignore the peer. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_toggle_ignore(self._tox_pointer, group_number, peer_id, ignore, byref(error)) + return result + + def group_mod_set_role(self, group_number, peer_id, role): + """ + Set a peer's role. + + This function will first remove the peer's previous role and then assign them a new role. + It will also send a packet to the rest of the group, requesting that they perform + the role reassignment. Note: peers cannot be set to the founder role. + + :param group_number: The group number of the group the in which you wish set the peer's role. + :param peer_id: The ID of the peer whose role you wish to set. + :param role: The role you wish to set the peer to. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_set_role(self._tox_pointer, group_number, peer_id, role, byref(error)) + return result + + def group_mod_remove_peer(self, group_number, peer_id): + """ + Kick/ban a peer. + + This function will remove a peer from the caller's peer list and optionally add their IP address + to the ban list. It will also send a packet to all group members requesting them + to do the same. + + :param group_number: The group number of the group the ban is intended for. + :param peer_id: The ID of the peer who will be kicked and/or added to the ban list. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_remove_peer(self._tox_pointer, group_number, peer_id, + byref(error)) + return result + + def group_mod_ban_peer(self, group_number, peer_id, ban_type): + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_ban_peer(self._tox_pointer, group_number, peer_id, + ban_type, byref(error)) + return result + + def group_mod_remove_ban(self, group_number, ban_id): + """ + Removes a ban. + + This function removes a ban entry from the ban list, and sends a packet to the rest of + the group requesting that they do the same. + + :param group_number: The group number of the group in which the ban is to be removed. + :param ban_id: The ID of the ban entry that shall be removed. + + :return True on success + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_remove_ban(self._tox_pointer, group_number, ban_id, byref(error)) + return result + + def callback_group_moderation(self, callback, user_data): + """ + Set the callback for the `group_moderation` event. Pass NULL to unset. + + This event is triggered when a moderator or founder executes a moderation event. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint32, c_int, c_void_p) + self.group_moderation_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_moderation(self._tox_pointer, self.group_moderation_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat ban list queries + # ----------------------------------------------------------------------------------------------------------------- + + def group_ban_get_list_size(self, group_number): + """ + Return the number of entries in the ban list for the group designated by + the given group number. If the group number is invalid, the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_list_size(self._tox_pointer, group_number, byref(error)) + return result + + def group_ban_get_list(self, group_number): + """ + Copy a list of valid ban list ID's into an array. + + Call tox_group_ban_get_list_size to determine the number of elements to allocate. + return true on success. + """ + + error = c_int() + bans_list_size = self.group_ban_get_list_size(group_number) + bans_list = create_string_buffer(sizeof(c_uint32) * bans_list_size) + bans_list = POINTER(c_uint32)(bans_list) + result = Tox.libtoxcore.tox_group_ban_get_list(self._tox_pointer, group_number, bans_list, byref(error)) + return bans_list[:bans_list_size] + + def group_ban_get_type(self, group_number, ban_id): + """ + Return the type for the ban list entry designated by ban_id, in the + group designated by the given group number. If either group_number or ban_id is invalid, + the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_type(self._tox_pointer, group_number, ban_id, byref(error)) + return result + + def group_ban_get_target_size(self, group_number, ban_id): + """ + Return the length of the name for the ban list entry designated by ban_id, in the + group designated by the given group number. If either group_number or ban_id is invalid, + the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_target_size(self._tox_pointer, group_number, ban_id, byref(error)) + return result + + def group_ban_get_target(self, group_number, ban_id): + """ + Write the name of the ban entry designated by ban_id in the group designated by the + given group number to a byte array. + + Call tox_group_ban_get_name_size to find out how much memory to allocate for the result. + + :return name + """ + + error = c_int() + size = self.group_ban_get_target_size(group_number, ban_id) + target = create_string_buffer(size) + target_type = self.group_ban_get_type(group_number, ban_id) + + result = Tox.libtoxcore.tox_group_ban_get_target(self._tox_pointer, group_number, ban_id, + target, byref(error)) + if target_type == TOX_GROUP_BAN_TYPE['PUBLIC_KEY']: + return bin_to_string(target, size) + return str(target[:size], 'utf-8') + + def group_ban_get_time_set(self, group_number, ban_id): + """ + Return a time stamp indicating the time the ban was set, for the ban list entry + designated by ban_id, in the group designated by the given group number. + If either group_number or ban_id is invalid, the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_time_set(self._tox_pointer, group_number, ban_id, byref(error)) + return result diff --git a/toxygen/toxav.py b/toxygen/wrapper/toxav.py similarity index 98% rename from toxygen/toxav.py rename to toxygen/wrapper/toxav.py index 0ab891c..98e1c73 100644 --- a/toxygen/toxav.py +++ b/toxygen/wrapper/toxav.py @@ -1,7 +1,7 @@ from ctypes import c_int, POINTER, c_void_p, byref, ArgumentError, c_uint32, CFUNCTYPE, c_size_t, c_uint8, c_uint16 from ctypes import c_char_p, c_int32, c_bool, cast -from libtox import LibToxAV -from toxav_enums import * +from wrapper.libtox import LibToxAV +from wrapper.toxav_enums import * class ToxAV: @@ -24,8 +24,9 @@ class ToxAV: """ self.libtoxav = LibToxAV() toxav_err_new = c_int() - self.libtoxav.toxav_new.restype = POINTER(c_void_p) - self._toxav_pointer = self.libtoxav.toxav_new(tox_pointer, byref(toxav_err_new)) + f = self.libtoxav.toxav_new + f.restype = POINTER(c_void_p) + self._toxav_pointer = f(tox_pointer, byref(toxav_err_new)) toxav_err_new = toxav_err_new.value if toxav_err_new == TOXAV_ERR_NEW['NULL']: raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') @@ -40,7 +41,7 @@ class ToxAV: self.video_receive_frame_cb = None self.call_cb = None - def __del__(self): + def kill(self): """ Releases all resources associated with the A/V session. diff --git a/toxygen/toxav_enums.py b/toxygen/wrapper/toxav_enums.py similarity index 100% rename from toxygen/toxav_enums.py rename to toxygen/wrapper/toxav_enums.py diff --git a/toxygen/wrapper/toxcore_enums_and_consts.py b/toxygen/wrapper/toxcore_enums_and_consts.py new file mode 100644 index 0000000..b34e272 --- /dev/null +++ b/toxygen/wrapper/toxcore_enums_and_consts.py @@ -0,0 +1,954 @@ +TOX_USER_STATUS = { + 'NONE': 0, + 'AWAY': 1, + 'BUSY': 2, +} + +TOX_MESSAGE_TYPE = { + 'NORMAL': 0, + 'ACTION': 1, +} + +TOX_PROXY_TYPE = { + 'NONE': 0, + 'HTTP': 1, + 'SOCKS5': 2, +} + +TOX_SAVEDATA_TYPE = { + 'NONE': 0, + 'TOX_SAVE': 1, + 'SECRET_KEY': 2, +} + +TOX_ERR_OPTIONS_NEW = { + 'OK': 0, + 'MALLOC': 1, +} + +TOX_ERR_NEW = { + 'OK': 0, + 'NULL': 1, + 'MALLOC': 2, + 'PORT_ALLOC': 3, + 'PROXY_BAD_TYPE': 4, + 'PROXY_BAD_HOST': 5, + 'PROXY_BAD_PORT': 6, + 'PROXY_NOT_FOUND': 7, + 'LOAD_ENCRYPTED': 8, + 'LOAD_BAD_FORMAT': 9, +} + +TOX_ERR_BOOTSTRAP = { + 'OK': 0, + 'NULL': 1, + 'BAD_HOST': 2, + 'BAD_PORT': 3, +} + +TOX_CONNECTION = { + 'NONE': 0, + 'TCP': 1, + 'UDP': 2, +} + +TOX_ERR_SET_INFO = { + 'OK': 0, + 'NULL': 1, + 'TOO_LONG': 2, +} + +TOX_ERR_FRIEND_ADD = { + 'OK': 0, + 'NULL': 1, + 'TOO_LONG': 2, + 'NO_MESSAGE': 3, + 'OWN_KEY': 4, + 'ALREADY_SENT': 5, + 'BAD_CHECKSUM': 6, + 'SET_NEW_NOSPAM': 7, + 'MALLOC': 8, +} + +TOX_ERR_FRIEND_DELETE = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_BY_PUBLIC_KEY = { + 'OK': 0, + 'NULL': 1, + 'NOT_FOUND': 2, +} + +TOX_ERR_FRIEND_GET_PUBLIC_KEY = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_GET_LAST_ONLINE = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_QUERY = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, +} + +TOX_ERR_SET_TYPING = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_SEND_MESSAGE = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'SENDQ': 4, + 'TOO_LONG': 5, + 'EMPTY': 6, +} + +TOX_FILE_KIND = { + 'DATA': 0, + 'AVATAR': 1, +} + +TOX_FILE_CONTROL = { + 'RESUME': 0, + 'PAUSE': 1, + 'CANCEL': 2, +} + +TOX_ERR_FILE_CONTROL = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, + 'FRIEND_NOT_CONNECTED': 2, + 'NOT_FOUND': 3, + 'NOT_PAUSED': 4, + 'DENIED': 5, + 'ALREADY_PAUSED': 6, + 'SENDQ': 7, +} + +TOX_ERR_FILE_SEEK = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, + 'FRIEND_NOT_CONNECTED': 2, + 'NOT_FOUND': 3, + 'DENIED': 4, + 'INVALID_POSITION': 5, + 'SENDQ': 6, +} + +TOX_ERR_FILE_GET = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'NOT_FOUND': 3, +} + +TOX_ERR_FILE_SEND = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'NAME_TOO_LONG': 4, + 'TOO_MANY': 5, +} + +TOX_ERR_FILE_SEND_CHUNK = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'NOT_FOUND': 4, + 'NOT_TRANSFERRING': 5, + 'INVALID_LENGTH': 6, + 'SENDQ': 7, + 'WRONG_POSITION': 8, +} + +TOX_ERR_FRIEND_CUSTOM_PACKET = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'INVALID': 4, + 'EMPTY': 5, + 'TOO_LONG': 6, + 'SENDQ': 7, +} + +TOX_ERR_GET_PORT = { + 'OK': 0, + 'NOT_BOUND': 1, +} + +TOX_GROUP_PRIVACY_STATE = { + + # + # The group is considered to be public. Anyone may join the group using the Chat ID. + # + # If the group is in this state, even if the Chat ID is never explicitly shared + # with someone outside of the group, information including the Chat ID, IP addresses, + # and peer ID's (but not Tox ID's) is visible to anyone with access to a node + # storing a DHT entry for the given group. + # + 'PUBLIC': 0, + + # + # The group is considered to be private. The only way to join the group is by having + # someone in your contact list send you an invite. + # + # If the group is in this state, no group information (mentioned above) is present in the DHT; + # the DHT is not used for any purpose at all. If a public group is set to private, + # all DHT information related to the group will expire shortly. + # + 'PRIVATE': 1 +} + +TOX_GROUP_ROLE = { + + # + # May kick and ban all other peers as well as set their role to anything (except founder). + # Founders may also set the group password, toggle the privacy state, and set the peer limit. + # + 'FOUNDER': 0, + + # + # May kick, ban and set the user and observer roles for peers below this role. + # May also set the group topic. + # + 'MODERATOR': 1, + + # + # May communicate with other peers normally. + # + 'USER': 2, + + # + # May observe the group and ignore peers; may not communicate with other peers or with the group. + # + 'OBSERVER': 3 +} + +TOX_ERR_GROUP_NEW = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_NEW_OK': 0, + + # + # The group name exceeded TOX_GROUP_MAX_GROUP_NAME_LENGTH. + # + 'TOX_ERR_GROUP_NEW_TOO_LONG': 1, + + # + # group_name is NULL or length is zero. + # + 'TOX_ERR_GROUP_NEW_EMPTY': 2, + + # + # TOX_GROUP_PRIVACY_STATE is an invalid type. + # + 'TOX_ERR_GROUP_NEW_PRIVACY': 3, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_NEW_INIT': 4, + + # + # The group state failed to initialize. This usually indicates that something went wrong + # related to cryptographic signing. + # + 'TOX_ERR_GROUP_NEW_STATE': 5, + + # + # The group failed to announce to the DHT. This indicates a network related error. + # + 'TOX_ERR_GROUP_NEW_ANNOUNCE': 6, +} + +TOX_ERR_GROUP_JOIN = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_JOIN_OK': 0, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_JOIN_INIT': 1, + + # + # The chat_id pointer is set to NULL or a group with chat_id already exists. This usually + # happens if the client attempts to create multiple sessions for the same group. + # + 'TOX_ERR_GROUP_JOIN_BAD_CHAT_ID': 2, + + # + # Password length exceeded TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_JOIN_TOO_LONG': 3, +} + +TOX_ERR_GROUP_RECONNECT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_RECONNECT_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND': 1, +} + +TOX_ERR_GROUP_LEAVE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_LEAVE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_GROUP_MAX_PART_LENGTH. + # + 'TOX_ERR_GROUP_LEAVE_TOO_LONG': 2, + + # + # The parting packet failed to send. + # + 'TOX_ERR_GROUP_LEAVE_FAIL_SEND': 3, + + # + # The group chat instance failed to be deleted. This may occur due to memory related errors. + # + 'TOX_ERR_GROUP_LEAVE_DELETE_FAIL': 4, +} + +TOX_ERR_GROUP_SELF_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND': 1, +} + + +TOX_ERR_GROUP_SELF_NAME_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND': 1, + + # + # Name length exceeded 'TOX_MAX_NAME_LENGTH. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG': 2, + + # + # The length given to the set function is zero or name is a NULL pointer. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_INVALID': 3, + + # + # The name is already taken by another peer in the group. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_TAKEN': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_SELF_STATUS_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND': 1, + + # + # An invalid type was passed to the set function. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_INVALID': 2, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND': 3 +} + +TOX_ERR_GROUP_PEER_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_PEER_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND': 2 +} + +TOX_ERR_GROUP_STATE_QUERIES = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_STATE_QUERIES_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND': 1 +} + + +TOX_ERR_GROUP_TOPIC_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_TOPIC_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND': 1, + + # + # Topic length exceeded 'TOX_GROUP_MAX_TOPIC_LENGTH. + # + 'TOX_ERR_GROUP_TOPIC_SET_TOO_LONG': 2, + + # + # The caller does not have the required permissions to set the topic. + # + 'TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS': 3, + + # + # The packet could not be created. This error is usually related to cryptographic signing. + # + 'TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_SEND_MESSAGE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG': 2, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_EMPTY': 3, + + # + # The message type is invalid. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE': 4, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS': 5, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND': 6 +} + +TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND': 2, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG': 3, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY': 4, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS': 5, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND': 6 +} + +TOX_ERR_GROUP_SEND_CUSTOM_PACKET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG': 2, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY': 3, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS': 4 +} + +TOX_ERR_GROUP_INVITE_FRIEND = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND': 1, + + # + # The friend number passed did not designate a valid friend. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND': 2, + + # + # Creation of the invite packet failed. This indicates a network related error. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL': 3, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_INVITE_ACCEPT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_OK': 0, + + # + # The invite data is not in the expected format. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE': 1, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED': 2, + + # + # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG': 3 +} + +TOX_GROUP_JOIN_FAIL = { + + # + # You are using the same nickname as someone who is already in the group. + # + 'TOX_GROUP_JOIN_FAIL_NAME_TAKEN': 0, + + # + # The group peer limit has been reached. + # + 'TOX_GROUP_JOIN_FAIL_PEER_LIMIT': 1, + + # + # You have supplied an invalid password. + # + 'TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD': 2, + + # + # The join attempt failed due to an unspecified error. This often occurs when the group is + # not found in the DHT. + # + 'TOX_GROUP_JOIN_FAIL_UNKNOWN': 3 +} + +TOX_ERR_GROUP_FOUNDER_SET_PASSWORD = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions to set the password. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS': 2, + + # + # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND': 1, + + # + # 'TOX_GROUP_PRIVACY_STATE is an invalid type. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_INVALID': 2, + + # + # The caller does not have the required permissions to set the privacy state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS': 3, + + # + # The privacy state could not be set. This may occur due to an error related to + # cryptographic signing of the new shared state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions to set the peer limit. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS': 2, + + # + # The peer limit could not be set. This may occur due to an error related to + # cryptographic signing of the new shared state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_TOGGLE_IGNORE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_PEER_NOT_FOUND': 2 +} + +TOX_ERR_GROUP_MOD_SET_ROLE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. Note: you cannot set your own role. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND': 2, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS': 3, + + # + # The role assignment is invalid. This will occur if you try to set a peer's role to + # the role they already have. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT': 4, + + # + # The role was not successfully set. This may occur if something goes wrong with role setting': , + # or if the packet fails to send. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION': 5 +} + +TOX_ERR_GROUP_MOD_REMOVE_PEER = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PEER_NOT_FOUND': 2, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PERMISSIONS': 3, + + # + # The peer could not be removed from the group. + # + # If a ban was set': , this error indicates that the ban entry could not be created. + # This is usually due to the peer's IP address already occurring in the ban list. It may also + # be due to the entry containing invalid peer information': , or a failure to cryptographically + # authenticate the entry. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_ACTION': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_MOD_REMOVE_BAN = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_PERMISSIONS': 2, + + # + # The ban entry could not be removed. This may occur if ban_id does not designate + # a valid ban entry. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_ACTION': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_SEND': 4 +} + +TOX_GROUP_MOD_EVENT = { + + # + # A peer has been kicked from the group. + # + 'KICK': 0, + + # + # A peer has been banned from the group. + # + 'BAN': 1, + + # + # A peer as been given the observer role. + # + 'OBSERVER': 2, + + # + # A peer has been given the user role. + # + 'USER': 3, + + # + # A peer has been given the moderator role. + # + 'MODERATOR': 4, +} + +TOX_ERR_GROUP_BAN_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_BAN_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_BAN_QUERY_GROUP_NOT_FOUND': 1, + + # + # The ban_id does not designate a valid ban list entry. + # + 'TOX_ERR_GROUP_BAN_QUERY_BAD_ID': 2, +} + + +TOX_GROUP_BAN_TYPE = { + + 'IP_PORT': 0, + + 'PUBLIC_KEY': 1, + + 'NICK': 2 +} + +TOX_PUBLIC_KEY_SIZE = 32 + +TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6 + +TOX_MAX_FRIEND_REQUEST_LENGTH = 1016 + +TOX_MAX_MESSAGE_LENGTH = 1372 + +TOX_GROUP_MAX_TOPIC_LENGTH = 512 + +TOX_GROUP_MAX_PART_LENGTH = 128 + +TOX_GROUP_MAX_GROUP_NAME_LENGTH = 48 + +TOX_GROUP_MAX_PASSWORD_SIZE = 32 + +TOX_GROUP_CHAT_ID_SIZE = 32 + +TOX_GROUP_PEER_PUBLIC_KEY_SIZE = 32 + +TOX_MAX_NAME_LENGTH = 128 + +TOX_MAX_STATUS_MESSAGE_LENGTH = 1007 + +TOX_SECRET_KEY_SIZE = 32 + +TOX_FILE_ID_LENGTH = 32 + +TOX_HASH_LENGTH = 32 + +TOX_MAX_CUSTOM_PACKET_SIZE = 1373 diff --git a/toxygen/toxencryptsave.py b/toxygen/wrapper/toxencryptsave.py similarity index 97% rename from toxygen/toxencryptsave.py rename to toxygen/wrapper/toxencryptsave.py index b579e21..31de085 100644 --- a/toxygen/toxencryptsave.py +++ b/toxygen/wrapper/toxencryptsave.py @@ -1,6 +1,6 @@ -import libtox +from wrapper import libtox from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool -from toxencryptsave_enums_and_consts import * +from wrapper.toxencryptsave_enums_and_consts import * class ToxEncryptSave: diff --git a/toxygen/toxencryptsave_enums_and_consts.py b/toxygen/wrapper/toxencryptsave_enums_and_consts.py similarity index 100% rename from toxygen/toxencryptsave_enums_and_consts.py rename to toxygen/wrapper/toxencryptsave_enums_and_consts.py