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