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..5eb2fa6 100644
--- a/docs/contact.md
+++ b/docs/contact.md
@@ -1,5 +1,6 @@
# Contact us:
-1) Using GitHub - open issue
+1) https://git.plastiras.org/emdee/toxygen/issues
-2) Use Toxygen Tox Group - add bot kalina@toxme.io (or 12EDB939AA529641CE53830B518D6EB30241868EE0E5023C46A372363CAEC91C2C948AEFE4EB)
+2) Use Toxygen Tox Group (NGC) -
+ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA
diff --git a/docs/contributing.md b/docs/contributing.md
index 93292aa..b2cebf4 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -7,12 +7,15 @@ Help us find all bugs in Toxygen! Please provide following info:
- Toxygen executable info - python executable (.py), precompiled binary, from package etc.
- Steps to reproduce the bug
-Want to see new feature in Toxygen? [Ask for it!](https://github.com/toxygen-project/toxygen/issues)
+Want to see new feature in Toxygen?
+[Ask for it!](https://git.plastiras.org/emdee/toxygen/issues)
# Pull requests
Developer? Feel free to open pull request. Our dev team is small so we glad to get help.
-Don't know what to do? Improve UI, fix [issues](https://github.com/toxygen-project/toxygen/issues) or implement features from our TODO list.
+Don't know what to do? Improve UI, fix
+[issues](https://git.plastiras.org/emdee/toxygen/issues)
+or implement features from our TODO list.
You can find our TODO's in code, issues list and [here](/README.md). Also you can implement [plugins](/docs/plugins.md) for Toxygen.
Note that we have a lot of branches for different purposes. Master branch is for stable versions (releases) only, so I recommend to open PR's to develop branch. Development of next Toxygen version usually goes there. Other branches used for implementing different tasks such as file transfers improvements or audio calls implementation etc.
diff --git a/docs/install.md b/docs/install.md
index 00f8188..b3c5457 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -1,33 +1,15 @@
# How to install Toxygen
-## Use precompiled binary (recommended for users):
-[Check our releases page](https://github.com/toxygen-project/toxygen/releases)
-
-## Using pip3
-
-### Windows
-
-``pip install toxygen``
-
-Run app using ``toxygen`` command.
-
### Linux
-1. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/)
+1. Install [c-toxcore](https://github.com/TokTok/c-toxcore/)
2. Install PortAudio:
``sudo apt-get install portaudio19-dev``
3. For 32-bit Linux install PyQt5: ``sudo apt-get install python3-pyqt5``
4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python``
-5. Install toxygen:
-``sudo pip3 install toxygen``
+5. Install [toxygen](https://git.plastiras.org/emdee/toxygen/)
6. Run toxygen using ``toxygen`` command.
-## Packages
-
-Arch Linux: [AUR](https://aur.archlinux.org/packages/toxygen-git/)
-
-Debian/Ubuntu: [tox.chat](https://tox.chat/download.html#gnulinux)
-
## From source code (recommended for developers)
### Windows
@@ -44,27 +26,17 @@ Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly r
8. Download latest libtox.dll build, download latest libsodium.a build, put it into \toxygen\libs\
9. Run \toxygen\main.py.
-Optional: install toxygen using setup.py: ``python setup.py install``
-
-[libtox.dll for 32-bit Python](https://build.tox.chat/view/libtoxcore/job/libtoxcore_build_windows_x86_shared_release/lastSuccessfulBuild/artifact/libtoxcore_build_windows_x86_shared_release.zip)
-
-[libtox.dll for 64-bit Python](https://build.tox.chat/view/libtoxcore/job/libtoxcore_build_windows_x86-64_shared_release/lastSuccessfulBuild/artifact/libtoxcore_build_windows_x86-64_shared_release.zip)
-
-[libsodium.a for 32-bit Python](https://build.tox.chat/view/libsodium/job/libsodium_build_windows_x86_static_release/lastSuccessfulBuild/artifact/libsodium_build_windows_x86_static_release.zip)
-
-[libsodium.a for 64-bit Python](https://build.tox.chat/view/libsodium/job/libsodium_build_windows_x86-64_static_release/lastSuccessfulBuild/artifact/libsodium_build_windows_x86-64_static_release.zip)
-
### Linux
1. Install latest Python3:
``sudo apt-get install python3``
2. Install PyQt5: ``sudo apt-get install python3-pyqt5`` or ``sudo pip3 install pyqt5``
-3. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/)
+3. Install [toxcore](https://github.com/TokTok/c-toxcore) with toxav support)
4. Install PyAudio:
``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``sudo pip3 install pyaudio``)
5. Install NumPy: ``sudo pip3 install numpy``
6. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python``
-7. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip)
+7. [Download toxygen](https://git.plastiras.org/emdee/toxygen/)
8. Unpack archive
9. Run app:
``python3 main.py``
diff --git a/docs/plugin_api.md b/docs/plugin_api.md
index d549e68..9eb30a4 100644
--- a/docs/plugin_api.md
+++ b/docs/plugin_api.md
@@ -1,6 +1,6 @@
# Plugins API
-In Toxygen plugin is single python (supported Python 3.4 - 3.6) module (.py file) and directory with data associated with it.
+In Toxygen plugin is single python module (.py file) and directory with data associated with it.
Every module must contain one class derived from PluginSuperClass defined in [plugin_super_class.py](/src/plugins/plugin_super_class.py). Instance of this class will be created by PluginLoader class (defined in [plugin_support.py](/src/plugin_support.py) ). This class can enable/disable plugins and send data to it.
Every plugin has its own full name and unique short name (1-5 symbols). Main app can get it using special methods.
diff --git a/docs/ubuntu.png b/docs/ubuntu.png
old mode 100755
new mode 100644
index cd80444..67951a5
Binary files a/docs/ubuntu.png and b/docs/ubuntu.png differ
diff --git a/docs/windows.png b/docs/windows.png
old mode 100755
new mode 100644
index d4ed323..f13f4c0
Binary files a/docs/windows.png and b/docs/windows.png differ
diff --git a/setup.py b/setup.py
index 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